上一章介绍了支持 Data Cloud 的创新架构,以及它在其他数据库中从未实现过的可能性。在本章中,我们将探讨 Snowflake 客户在建模过程中会使用到的数据库对象。对于大多数有 SQL 经验的用户来说,表和视图等对象无疑是熟悉的。然而,即便如此,Snowflake 的独特功能在设计物理模型时仍能释放潜在的效率,因此用户需要深入了解这些对象。
由于采用了可变支出定价模式,Snowflake 的数据建模要求用户熟悉其支持对象的成本/性能权衡及其参数。对于从未接触过 Snowflake 的人来说,诸如流(streams)和外部表(external tables)等独特功能可能仍然陌生。但要做出良好的设计决策,用户需要全面掌握和彻底理解 Snowflake 的完整建模工具集。
本章将涵盖以下主要主题:
- 用于将数据加载到 Snowflake 的阶段(stages)和文件格式对象
- 用于存储数据的各种物理表类型
- 用于查看已阶段文件的元数据表
- 视图及其相关的成本和性能优势
- 物化视图及其适用场景
- 用于数据变更跟踪的流(streams)
- 用于自动化连续 ETL 管道的任务(tasks)
阶段(Stages)
在数据进入表之前,必须通过阶段将其加载到 Snowflake 中。阶段是逻辑对象,用于抽象云文件系统,使其可以以标准方式将数据加载到 Snowflake 中。阶段可以定义为两种类型:外部阶段和内部阶段。
- 外部阶段:可以在 Snowflake 之外的云存储位置(支持的云存储服务)上创建,用于从外部源系统加载数据。
- 内部阶段:在 Snowflake 账户内创建,因此使用托管提供商的存储类型。内部阶段用于阶段性存储来自 Snowflake 账户内部的文件,不能用于加载外部数据。
支持外部阶段的云存储服务包括:
- Amazon S3 存储桶
- Google Cloud 存储桶
- Microsoft Azure 容器
以下图示展示了外部阶段与内部阶段类型之间的关系布局:
与存储在表中的数据不同,表中的数据是微分区化的并使用内部列式格式,而阶段(stages) 可以存储任何类型的文件。尽管 Snowflake 仅支持特定的结构化和半结构化文件类型用于加载数据,但阶段中的非结构化文件可见性在与外部表和目录表(本章稍后讨论)结合使用时是一个重要的考虑因素。
阶段可以存储多种文件及其类型。然而,在从阶段访问数据以随后加载到 Snowflake 表时,必须指定文件格式。
文件格式(File Formats)
文件格式可以被视为描述文件类型的模板,用于读取或写入文件中的数据。文件格式定义了属性,例如:
- 分隔符类型(例如逗号、制表符或其他);
- 日期/时间格式;
- 编码(例如 Hex、Base64 和 UTF-8)。
Snowflake 提供了几种结构化和半结构化的文件格式,例如 CSV、JSON 和 Parquet,并且包含常用的默认设置。然而,用户可以根据需要创建自己的命名文件格式,用于加载或卸载指定类型的文件。在使用 COPY INTO 命令时,也可以在运行时指定文件格式属性。
关于支持的数据加载文件类型和文件格式的详细信息,可参考 Snowflake 文档。
有关所有类型数据加载(包括流式加载)的完整概述,请参阅 Snowflake 网站上的指南。
表(Tables)
数据在 Snowflake 中存储于表中,表是数据建模的基本组成部分之一。然而,在从建模上下文中探索表之前,我们应先了解 Snowflake 中的各种表类型及其成本。
上一章介绍了 Snowflake 的时间旅行(Time Travel) 功能,该功能允许恢复已删除的对象或查询过去时间点的数据。然而,时间旅行附带存储成本,其可用的时间旅行天数(称为保留期)取决于表的类型。我们将在后面详细回顾这些内容。
此外,Snowflake 提供了一种托管类型的时间旅行功能,称为故障保护(Fail-safe) 。所有永久表都有 7 天的故障保护期。与用户可访问的时间旅行不同,故障保护由 Snowflake 管理,仅在系统故障或数据泄露等灾难中供 Snowflake 访问以保护用户数据。要恢复存储在故障保护中的数据,用户需要直接联系 Snowflake。
对于同时提供时间旅行和故障保护的表,在时间旅行期结束后,7 天的故障保护期立即开始。以下图示说明了 Snowflake 表提供的数据保护类型及其持续时间:
不同的使用场景需要不同的备份策略。例如,事务数据可能需要数天的历史记录,而阶段表(staging table)可能只需要临时存在。Snowflake 提供了多种物理表类型,不同表类型具有不同的保留期(以及相关的成本)选项,包括:
- 永久表(Permanent)
- 瞬态表(Transient)
- 临时表(Temporary)
- Hybrid Unistore 表
此外,Snowflake 还提供了几种针对阶段内容的元数据表构造。这些是存在于阶段之上的语义对象,使用户可以像查询常规表一样查询文件内容和元数据(如文件名)。这些对象是只读的,支持以下格式:
- 外部表(External Tables)
- 目录表(Directory Tables)
以下部分将详细介绍每种表类型的独特属性。
物理表
在 Snowflake 中,数据存储在按模式(schemas)逻辑分组的物理表中。表由具有名称、数据类型以及可选约束和属性(如是否为空、默认值和主键)的列组成。
用于创建表的 DDL 命令是 CREATE TABLE,它在关系建模场景中用于实例化具有给定结构的空表。
在转换场景中,CREATE TABLE 的其他变体用于基于现有对象创建、模式化或克隆表(如适用),这些操作被称为转换型操作,因为它们需要预先存在的关系模型/模式。例如,通过克隆现有表创建一个表。支持的变体包括:
- CREATE TABLE AS SELECT (CTAS) :通过查询返回的数据创建一个表(并使用隐含的数据类型)。
- CREATE TABLE USING TEMPLATE:通过从半结构化文件中派生的列定义创建一个空表(目前支持 Parquet、Avro 和 ORC 格式)。
- CREATE TABLE LIKE:使用现有表的列定义创建一个空表,同时复制列属性、默认值和约束。
- CREATE TABLE CLONE:与 LIKE 类似,但使用零复制克隆(Zero-Copy Cloning) ,还包括源表中的所有数据(实际上并未复制)。CLONE 变体可以与时间旅行(Time Travel)结合使用,以克隆过去某一时间点的数据。克隆表不会消耗存储,直到克隆对象或源对象的结构或内容发生更改。
接下来,让我们了解 Snowflake 提供的不同物理表类型。
永久表(Permanent Tables)
Snowflake 的默认表类型是永久表。尽管如此,这个名称稍显不准确,因为除了临时表外,所有 Snowflake 表在被显式删除之前都会持久存在。永久表具有内置的备份功能,包括:
- 7 天的故障保护期(Fail-safe) :无法修改或禁用。
- 默认 1 天的时间旅行(Time Travel) :可以在 Snowflake 企业版计划中禁用或扩展到最多 90 天。
由于其广泛的可恢复性,永久表是存储关键业务信息的理想选择,尤其是在发生灾难时重新生成这些数据代价高昂或不可能的情况下。
创建永久表的命令如下:
CREATE TABLE <table_name>;
然而,并非所有表都需要备份或扩展的时间旅行期。在这种情况下,可以考虑使用瞬态表(Transient Tables) 。
瞬态表(Transient Tables)
与永久表不同,瞬态表没有故障保护期,但默认具有 1 天的时间旅行。用户可以通过将时间旅行设置为 0 来关闭它,但即使在企业版计划中也不能将其扩展到超过 1 天。
当备份和恢复不是高优先级时,例如用于阶段性表(staging tables) ,瞬态表是一个不错的选择。同样,在开发和测试环境中,可以在模式和数据库级别设置瞬态表的默认值(稍后在本章中会详细描述)。
创建瞬态表的命令如下:
CREATE TRANSIENT TABLE <table_name>;
虽然瞬态表是阶段性数据和开发环境的理想选择,但 Snowflake 还提供了一种仅在用户会话期间持久化的临时表(Temporary Tables) 。
临时表(Temporary Tables)
临时表的数据保留属性与瞬态表相同,除了一个关键区别:临时表会在用户会话结束时被删除。一个会话可以视为一次活动的登录。虽然多个会话(例如使用多个工作表时)可以并行运行,但它们都会在连接或登录结束时终止。
由于其非永久性,临时表适用于在同一会话期间保存中间结果的过程。它们还可以用于开发环境中的一次性测试,省去了显式删除的步骤。
创建临时表的命令如下:
CREATE TEMPORARY TABLE <table_name>;
临时表命名注意事项
尽管临时表必须像所有数据库对象一样分配到一个模式中,但其基于会话的持久性使其免受与其他表类型的命名冲突。在同一会话中,临时表可以与非临时表同名,并且优先于后者(从而隐藏非临时表)。
Hybrid Unistore 表
Snowflake 在 2022 年峰会上宣布了 Hybrid Unistore 表,目前提供私人预览访问。预计在 2023 年发布正式版本,这里将其包含在内,以展望其可能带来的变革性机会。
Hybrid 表本质上是两个表,在一个单一的语义对象下由 Snowflake 的服务层透明地管理:
- 一个标准的(永久)OLAP 表。
- 一个 OLTP 表。
这种 OLAP 和 OLTP 的结合称为混合事务与分析处理(HTAP) ,提供了两者的最佳性能。
OLAP 和 OLTP 表特性
-
OLAP 表:使用前述永久表的列式格式,并提供 Snowflake 著名的所有分析性能功能。
-
OLTP 表:提供独特的功能,不在 OLAP 表中可用,包括:
- 约束强制(Constraint Enforcement)
- 支持索引(Support for Indexes)
- 行级锁定(Row-Level Locking)
在将数据加载到 Hybrid 表时,Snowflake 会同步更新 OLAP 和 OLTP 表。查询优化器会将 DML 或 DQL 查询重定向到最适合所需操作的表。
虽然 Hybrid 表的完整文档尚未公开,但 CREATE 命令的语法已经分享,如下所示:
CREATE HYBRID TABLE <table_name>;
表类型总结
Snowflake 为用户提供了多种表类型和保留期选项。现在我们已经熟悉了三种物理 OLAP 风格的表以及新发布的混合 HTAP 表,以下将它们的属性总结在一个表格中:
我们已经详细讨论了时间旅行(Time Travel)和恢复功能,但值得一提的是,另一种历史记录类型也可以针对物理表启用:变更历史(Change History) 。与时间旅行不同,时间旅行允许用户访问表的历史快照,而变更历史记录表的每次更改,并支持变更数据捕获(Change Data Capture, CDC) 。这一功能将在本章后面详细介绍。
物理表是 Snowflake 用户最常用来存储和操作信息的表类型,但还可以定义其他类型的表。例如,**外部表(External Tables)和目录表(Directory Tables)**是建立在阶段(stages)对象之上的元数据表,且可以在关系模型中定义和使用。接下来,我们详细探讨这些元数据对象。
阶段元数据表(Stage Metadata Tables)
外部表和目录表允许用户像查询常规表一样访问阶段文件中的数据。
- 外部表允许创建主键和外键,因此可以在标准关系上下文中建模。
- 目录表与外部表类似,但不允许创建列或约束,主要是为了完整性而包含在内。
外部表(External Tables)
外部表是建立在阶段之上的元数据对象,允许用户像查询数据库内部数据一样查询外部阶段文件中的数据。阶段支持的文件格式同样适用于通过外部表访问数据。所有外部表都包含以下列:
- VALUE:一个
VARIANT类型的列,表示外部文件中的单行。 - METADATA$FILENAME:标识外部表中每个阶段数据文件的名称,包括其路径。
- METADATA$FILE_ROW_NUMBER:显示阶段数据文件中每条记录的行号。
尽管是只读的,外部表允许用户通过 VALUE 或 METADATA 列创建虚拟列作为表达式。通过外部表查询数据可能比原生表慢,因此外部表适合用于查看文件内容或元数据,而不是作为数据源。
创建外部表的命令如下:
CREATE EXTERNAL TABLE <table_name>;
外部表使用户能够像查询常规表一样访问阶段文件的内容。然而,当只需要文件目录信息时,可以使用目录表(Directory Tables) 。
目录表(Directory Tables)
从概念上讲,目录表类似于外部表,但有以下显著区别:
- 不是单独的数据库对象,而是作为阶段的一部分启用。
- 提供文件元数据(如路径、大小和 URL),但不提供文件内容。
- 不允许创建虚拟列或约束。
- 可以在内部阶段和外部阶段上创建。
外部表允许用户使用与常规物理表相同的约定访问存储在外部云存储中的数据,而目录表仅显示文件元数据。目录表更类似于 LIST 函数(返回阶段中的文件列表),而不是实际的表。使用目录表代替 LIST 函数,可以让用户使用熟悉的 SELECT 语法访问信息。
创建目录表的命令如下:
CREATE STAGE <stage_name>
...
DIRECTORY = (ENABLE = TRUE);
现在我们已经覆盖了 Snowflake 所有形式的表类型,接下来我们来看**视图(Views)**的内容。
Snowflake 视图(Views)
Snowflake 的视图与大多数关系数据库中的视图行为类似:它们将基于物理对象的 SELECT 语句存储为模式中的一个对象。将 SELECT 语句存储为可共享的数据库对象,为系统用户提供了多项优势:
- 节省时间和提高可维护性:
不需要每次使用时从头编写查询语句,通过可复用的模块化代码实现数据管道和分析的效率提升。 - 业务逻辑一致性:
所有有权访问视图的用户都可以使用一致的过滤器和业务逻辑。 - 独立权限管理:
视图是独立于其引用的数据源的数据库对象,因此可以分配不同的权限,而无需暴露所有底层数据。 - 多数据源查询:
视图可以同时从多个数据源中选择数据。 - 实时数据:
始终显示源表中的最新数据,而无需刷新(与物化结果存储为物理表相对)。 - 零存储成本:
数据不会被物理复制,而是在调用视图时实时读取。
创建视图的命令如下:
CREATE VIEW <view_name>;
性能权衡
尽管视图带来了上述诸多好处,但也有一个主要的性能权衡:每次调用视图时,必须从底层表中读取数据,并使用计算资源来计算视图中的所有逻辑和公式,从而消耗信用。然而,这并不像听起来那么令人担忧,因为 Snowflake 的缓存功能(在上一章中描述)同样适用于视图。
缓存(Caching)
Snowflake 的视图利用其多种缓存机制来缓解执行用户查询时的性能和成本问题:
- 结果缓存:
由服务层管理,存储系统中所有查询的结果,包括视图上的查询结果。只要底层数据没有更改,后续语法等效的查询将从缓存中立即返回结果,无需消耗计算信用。 - 仓库缓存:
如果结果缓存不可用,Snowflake 会尝试在通过视图引用表数据时利用仓库缓存。只要仓库保持活动状态,从云存储读取的表数据将保留在内存中,从而减少视图访问数据时的检索时间。
安全性(Security)
正如前文所述,视图可以用来增强数据安全性,因为它们是独立的可安全管理的数据库对象。包含敏感数据的表(无论是行级还是列级)可以通过视图限制公共访问。视图可以限制选定的列和应用的过滤器,并分配单独的访问角色。
Snowflake 提供了额外的安全功能,用户可以创建安全视图(Secure Views) 。安全视图的声明方式与普通视图类似,只需在 CREATE 语句中添加 secure。安全视图与非安全视图的主要区别在于:
- 视图定义(DDL)不可见:只有视图的拥有角色可以查看定义,其他消费者无法访问。
- 性能影响:为了避免暴露底层数据,某些内部优化被禁用,但性能通常与非安全视图相当。
创建安全视图的命令如下:
CREATE SECURE VIEW <view_name>;
物化视图(Materialized Views)
物化视图既不是完全的视图,也不是完全的表,而是位于这两种对象类型的交汇点,提供了一些有趣的优势,同时伴随一个重要的权衡。
特性与优势
正如其名称所示,物化视图本质上是存储了视图结果的物理表。当底层数据发生变化时,结果会自动重新物化。这意味着物化视图结合了表的性能与缓存查询结果的优势,同时保留了普通视图的所有安全特性。
成本权衡
唯一的权衡点在于成本:
- 由于物化视图将查询结果存储在物理表中,因此会产生相关的存储成本。
- 当数据刷新时,物化视图会消耗计算信用(credits) 。
限制
相比普通视图,物化视图对 SELECT 语句的逻辑有一定限制,其中最主要的是:
- 物化视图只能基于单一数据源构建(支持阶段),因此不能使用 JOIN 操作。
这种限制将物化视图的使用场景缩小到预计算的表结果或聚合的用途。不过 Snowflake 优化器足够智能,在可能的情况下会自动将表查询重定向到物化视图以提高性能。
适用条件
Snowflake 用户可以在以下条件均满足时考虑使用物化视图,如果任何条件不满足,则应谨慎选择:
- 查询结果不会频繁改变。
- 视图结果的使用频率显著高于数据更改的频率。
- 查询消耗了大量的计算资源。
创建物化视图
创建物化视图的命令如下:
CREATE MATERIALIZED VIEW <view_name>;
现在我们已经介绍了 Snowflake 的各种数据源选项,接下来将关注如何检测数据的变化,并在这些数据源之间实现自动化转换管道。
流(Streams)
流是逻辑对象,用于捕获底层源对象中的数据变化,包括前文提到的对象(如物理表、视图、外部表和目录表)。当源对象中发生 DML 操作时,流会跟踪这些变化(包括插入、删除以及更新的前/后镜像)。
流通过一种偏移存储技术实现其功能:逻辑上对数据进行初始快照,然后通过元数据列跟踪变化。尽管流可以像表一样被查询,但它并不是一个独立的对象,也不包含表数据本身。
以下命令在表上创建一个流:
CREATE STREAM <stream_name> ON TABLE <table_name>
对于每个后续的DML操作,流将保存被更改记录的前后图像,并通过将它们与标识DML操作类型(插入、更新或删除)的相关元数据配对,使得下游对象能够准确地进行CDC(变更数据捕获)。
从流加载
Snowflake通过一种优雅的消费方式使得流保持可管理,并防止其过度增长。消费流内容(即成功地将流数据加载或更新到下游源)将清空流,使其准备好吸收未来的DML更改,而无需手动定期修剪。选择操作和失败的消费尝试不会清空流。
在下面的图示中,我们将看到如何在实际操作中处理各种DML操作。这个示例假设每次对源表进行更改后,流会被消费(清空)。
根据跟踪数据的性质,流分为两种类型:
- 标准流(或增量流) :记录插入、删除和更新操作。这种类型的流适用于表、目录表和视图。
- 追加/仅插入流:仅跟踪插入的记录,忽略任何更新或删除操作。这种流类型适用于物理表、目录表和视图,作为仅追加流,对于外部表,作为仅插入流。
能够在没有任何手动跟踪和比较版本的处理开销的情况下跟踪更改,简化了下游的DML操作,因为它能够预测需要更新的更改类型。因此,流通常用于建模事实表和缓慢变化的维度(SCD),并保持其更新(这些对象及其维护技术将在后续章节中介绍)。
流旨在定期消费。流中更改的默认保留期为14天。虽然这个期限可以延长,但Snowflake不能保证在此期间之后流的有效性。然而,存在一种不太易变的替代方案,它在被消费时不会消失。
以下部分描述了更改跟踪,以及它如何与流结合使用或作为CDC的替代方案。
更改跟踪
虽然流作为模式中的独立对象创建,但更改跟踪直接在表上启用,允许Snowflake用户查询CDC元数据。更改跟踪使用与流中相同的元数据字段,但将其直接附加到表中。与流不同,如果更改用于更新下游对象,这些更改不会被消除;相反,更改跟踪会在表的数据保留期内持续存在。
以下是如何启用并查询表的更改跟踪的示例。在此示例中,三条记录插入到启用了更改跟踪的表中。随后,一条记录被更新,另一条记录被删除:
create or replace table myTable (
myTable_id varchar(10),
my_date date
);
-- 启用更改跟踪
alter table myTable set change_tracking = true;
-- 初始化当前时间戳的会话变量
set cDts = (select current_timestamp());
set cDate = (select current_date());
-- 执行DML操作
insert into myTable (myTable_id, my_date) values
('yesterday', $cDate-1 ),
('today' , $cDate+0 ),
('tomorrow' , $cDate+1 );
delete from myTable where myTable_id = 'yesterday';
update myTable set my_date = '1970-01-01'
where myTable_id = 'today';
-- 查询更改跟踪元数据,观察从时间戳到现在的增量
select * from myTable
changes(information => default)
at(timestamp => $cDts);
查询表的更改日志返回的结果显示了记录的累积效应(最新版本)。请注意,昨天和今天的记录在结果中缺失,因为它们在设置时间戳之前并不存在。
但是,观察在仅追加模式下查询相同更改日志时会发生什么:
-- 查询更改跟踪元数据,观察
-- 从时间戳到现在的仅插入记录
select * from myTable
changes(information => append_only)
at(timestamp => $cDts);
该查询仅返回插入到表中的记录,无论后续是否进行了删除或更新。注意,今天和明天的记录已经返回,而“好日子”这一更新记录则缺失。
除了一些限制外,只要底层的表也启用了更改跟踪,视图上也可以启用更改跟踪。由于更改跟踪元数据会在整个表的数据保留期内持续存在,并且在消费时无法清除,因此它不适用于自动化的CDC管道,而是用于分析更改本身。
说到自动化数据管道,本章最后讨论的对象是专门用于处理这一关键任务,并且与流和CDC检测配合得非常好。
任务
Snowflake使用任务来调度和自动化数据加载与转换。虽然在关系模型中没有跟踪数据迁移,但它是转换模型的重要组成部分,因此这里也包含了这一内容。
任务通过执行SQL语句的串行或并行步骤来自动化数据管道。任务可以与流结合,用于处理最近更改的表行,从而实现持续的ELT工作流。这可以在无服务器模式下完成(使用自动可扩展的Snowflake托管计算集群,不需要活动仓库),或者使用专用的用户定义仓库。
创建任务的代码如下:
CREATE TASK <task_name>
...
[ AFTER <parent_task_1> [ , <parent_task_2> , ... ] ]
[ WHEN <boolean_expr> ]
AS <sql>
任务易于理解——它们按照计划或作为父任务的一部分调用时执行SQL命令(或执行存储过程)。以下图示展示了任务如何串联和并行地形成数据管道的依赖关系。
存储过程和Snowflake脚本允许您使用循环和分支结构编写过程化代码,这超出了本书的范围。有关如何处理过程逻辑的信息,请参阅Snowflake文档中的存储过程(docs.snowflake.com/en/sql-refe…)和Snowflake脚本(docs.snowflake.com/en/develope…)。
现在我们理解了如何在Snowflake中将任务串联起来形成数据管道,我们可以通过将任务与流结合进一步扩展它们的应用。
任务与流的结合
任务与流非常契合,可以在执行之前检查是否有变化。作为任务定义的一部分,任务有一个 WHEN <boolean_expr> 子句,用于设置执行前必须满足的逻辑条件。该条件必须返回布尔值,但可以运行用户希望的任何函数或子查询。恰如其分,流提供了一个系统函数,当存在CDC记录时返回TRUE,否则返回FALSE。可以通过如下方式调用该函数,并引用流的名称:
SYSTEM$STREAM_HAS_DATA('<stream_name>')
在任务开始时设置此条件意味着它可以根据预期变化的频率调度运行,但只有在有记录需要处理时才会执行。
让我们总结一下本章所学内容。
总结
在本章中,我们展示了Snowflake对象的丰富功能,即使是像表和视图这样常见的对象也包含了许多额外的特性。在Snowflake中,表不仅可以存储数据——根据其设置,它还可以存储数月的历史数据和灾难恢复备份,并为CDC提供偏移变化跟踪。视图同样超出了预期,提供了变更跟踪和自动重新物化功能。
我们了解了阶段(stages)如何标记数据从外部源到Snowflake表的入口点。阶段还提供了有用的功能,例如允许通过外部表访问文件内容,而无需将其提前复制到表中。
最后,为了协调传入的数据、建立自动化的ELT管道并简化CDC,Snowflake将任务与流结合使用,赋予用户完全的无服务器或托管控制——将阶段、表、视图以及所有连接的转换逻辑结合在一起。
理解了每个对象的优势和功能后,我们将在下一章讨论数据建模的构建模块,并将它们与Snowflake对象的对应部分联系起来:列、表、约束等。这些基础将为后续章节详细探讨四种建模类型奠定基础。
参考资料
您可以参考以下资源获取更多信息:
- 《理解与使用时间旅行》(Snowflake文档):docs.snowflake.com/en/user-gui…。访问时间:2022年11月16日。