上章介绍了驱动 Data Cloud 的创新架构,以及它如何开启传统数据库从未实现的可能性。本章将深入讲解 Snowflake 客户在建模过程中会用到的数据库对象。诸如**表(tables)与视图(views)**之类的对象,对有 SQL 经验的人来说并不陌生;但即便在这些“老朋友”身上,Snowflake 的独特特性也能在物理模型设计中释放额外的效率,因此用户应当熟悉它们。
由于采用可变支出(variable spend)的计费模式,Snowflake 的数据建模需要用户充分了解其支持对象及其参数在成本/性能上的权衡。对从未接触过 Snowflake 的人而言,诸如 streams 与**外部表(external tables)**等专属功能可能不太熟悉。但要做出良好的设计决策,必须对 Snowflake 的完整建模工具箱有牢固而全面的掌握。
本章主要涵盖以下主题:
- Stages 与文件格式对象(file formats) :用于将数据加载到 Snowflake
- 各类物理表类型:用于存储数据
- 元数据表:用于查看已暂存(staged)的文件
- 视图及其相关的成本与性能收益
- 物化视图及其使用场景
- Streams:用于数据变更跟踪
- Tasks:用于自动化持续性的 ETL 流水线
Stages
在数据进入表之前,必须先通过 stage 加载到 Snowflake。Stage 是一种逻辑对象,用来抽象云端文件系统,使其可以用统一方式为 Snowflake 加载数据。可定义两类 stage:外部(external)与内部(internal) 。
- 外部 stage:建立在 Snowflake 之外受支持的云存储位置之上,用于从外部源系统加载数据。
- 内部 stage:在 Snowflake 账户内创建,因此使用托管云厂商的存储类型;用于暂存来源于该 Snowflake 账户内部的文件,不能用于加载外部数据。
外部 stage 支持的云存储服务包括:
- Amazon S3 存储桶
- Google Cloud Storage 存储桶
- Microsoft Azure 容器
下图展示了外部 stage 与各类内部 stage 的关系布局:
图 4.1 —— Snowflake 中的外部与内部 stage
与存储在表中的数据不同(表数据会被微分区并采用内部列式格式),stage 可存放任意类型文件。尽管 Snowflake 仅支持将特定的结构化与半结构化文件类型加载进表,但当它与外部表与目录表(本章稍后讨论)配合时,stage 中非结构化文件的可见性也很重要。
stage 可以存储各种文件与类型;但从 stage 访问数据并进一步加载到表时,必须指定文件格式(file format) 。
File formats(文件格式)
可将 file format 理解为读/写文件时对文件类型的“模板”描述。文件格式定义属性,如分隔符(逗号、制表符等)、日期/时间格式以及编码(如 Hex、Base64、UTF-8)。
Snowflake 预置多种结构化与半结构化文件格式(如 CSV、JSON、Parquet),并提供常用默认值。但用户也可在装载/卸载特定类型文件时自定义命名的文件格式。在使用 COPY INTO 命令时,也可以在运行时指定文件格式属性。
关于受支持文件类型与装载文件格式的说明,参见官方文档:
docs.snowflake.com/en/user-gui…
有关包括流式在内的各类数据装载的完整概览,参见:
docs.snowflake.com/en/user-gui…
了解了如何通过 stage + file format 将数据加载进 Snowflake,接下来认识用于存储这些数据的表。
Tables(表)
在 Snowflake 中,数据存储在表里,表是数据建模的基础组件之一。在进入建模语境之前,先了解 Snowflake 中存在的不同表类型及其成本差异。
上一章介绍了 Snowflake 的 Time Travel:它允许恢复已删除的对象,或查询过去某一时刻的数据。不过,Time Travel 带来相应的存储成本,可用的 Time Travel 天数(保留期)取决于表类型(稍后详述)。
Snowflake 还提供一种托管型的历史保护:Fail-safe。所有永久表(permanent)都有 7 天的 Fail-safe 期。与用户可直接访问的 Time Travel 不同,Fail-safe 由 Snowflake 统一管理,仅在系统故障、数据泄露等灾难场景下用于保护用户数据;如需从 Fail-safe 恢复数据,用户需联系 Snowflake 官方。
对于同时具备 Time Travel 与 Fail-safe 的表,7 天 Fail-safe 期会在 Time Travel 期结束后立即开始。下图展示了 Snowflake 表提供的数据保护类型与持续时间:
图 4.2 —— 连续数据保护生命周期
不同用例需要不同的备份策略:事务型数据可能需要保存数天历史,而暂存表(staging)可能只是临时需要。Snowflake 提供多种物理表类型,其保留策略(以及对应成本)各不相同,包括:
- Permanent(永久表)
- Transient(短暂表)
- Temporary(临时表)
- Hybrid Unistore 表
此外,Snowflake 还提供若干基于 stage 内容的元数据表构造。它们是建立在 stage 之上的语义对象,使用户可以像查询普通表一样查询文件内容与元数据(如文件名)。这些对象为只读,形式包括:
- External tables(外部表)
- Directory tables(目录表)
下面详解各类表的独特属性。
物理表(Physical tables)
在 Snowflake 中,数据存储在按schema 逻辑分组的物理表中。表由列组成,列具有名称、数据类型以及可选的约束与属性(如是否可空、默认值、主键)。
用于创建表的 DDL 命令是 CREATE TABLE,在关系建模场景下用于按给定结构实例化一个空表。
在变换式(transformational)场景中,还有若干 CREATE TABLE 变体,可基于现有对象创建、套用模式或克隆表,并在适用时包含对应数据。这些操作之所以称为“变换式”,是因为它们需要既有的关系模型/模式作为前提(例如通过克隆现有表来创建新表)。常见变体包括:
- CREATE TABLE AS SELECT(CTAS) :以查询结果创建表(并据此推断数据类型)。
- CREATE TABLE USING TEMPLATE:从半结构化文件推导列定义创建空表(目前支持 Parquet/Avro/ORC)。
- CREATE TABLE LIKE:基于现有表的列定义创建空表,同时复制列属性、默认值与约束。
- CREATE TABLE CLONE:与 LIKE 类似,但借助零拷贝克隆同时“包含”源表的全部数据(实际上并不复制)。
CLONE可结合 Time Travel 在历史时点进行克隆。克隆表在其结构或内容被修改(无论在克隆表还是源对象)之前不占用存储。
下面分别介绍 Snowflake 提供的不同物理表类型。
Permanent tables(永久表)
Snowflake 的默认表类型是 permanent。严格说这有点名不副实,因为除临时表以外,所有 Snowflake 表都会持续存在,直到被显式删除。永久表自带备份能力:包括不可修改/禁用的 7 天 Fail-safe,以及默认 1 天的 Time Travel。在 Enterprise 版本上,永久表的 Time Travel 可关闭或延长至最多 90 天。
由于具备较强的可恢复性,永久表非常适合存放关键业务数据——这些数据一旦丢失将难以或无法重建。
创建永久表的命令:
CREATE TABLE <table_name>
并非所有表都需要备份或较长的 Time Travel。在这类场景下,可考虑 transient 表。
Transient tables(短暂表)
与永久表不同,transient 表没有 Fail-safe,但默认有 1 天 Time Travel。用户可以将 Time Travel 设为 0(关闭),但无法延长至超过 1 天,即便在 Enterprise 版本也不行。
当备份与恢复不是高优先级(如暂存表),或在开发/测试环境中(可在schema 与database 层设定 transient 默认)时,transient 表是不错的选择。
创建短暂表的命令:
CREATE TRANSIENT TABLE <table_name>
虽然 transient 表非常适合暂存与开发环境,Snowflake 还提供一种只在会话期间存在的临时表。
Temporary tables(临时表)
临时表的数据保留属性与 transient 表相同,但有一个关键差异:临时表会在用户会话结束时被删除。会话可以理解为一次活跃登录。尽管可以并行开启多个会话(如多个 worksheet),但它们都会随连接/登录的结束而终止。
鉴于其短暂性,临时表用于在同一会话内保存流程的中间结果。在开发环境中,临时表也适合用于“一次性测试”,免去手动清理。
创建临时表的命令:
CREATE TEMPORARY TABLE <table_name>
关于临时表命名
尽管临时表与其他数据库对象一样必须隶属于某个 schema,但由于其按会话持久的特性,它不会与其他表类型发生重名冲突:临时表可以与非临时表同名,并在该会话内优先于后者(相当于“遮蔽”非临时表)。
还有一种物理表类型,它的意义不在于保留时间,而在于对 Snowflake 所能支持的负载类型带来了颠覆性变化——这就是 Hybrid Unistore 表。
Hybrid Unistore tables(混合型 Unistore 表)
Snowflake 在 Summit 2022 上宣布了 Hybrid Unistore,并以私测形式提供访问。当它 GA(预计 2023 年)后,有望解锁全新能力,因此在此预先介绍其潜在变革。
Hybrid 表在一个语义对象下透明地管理两张表:一张标准(永久)OLAP 表与一张 OLTP 表,由 Snowflake 服务层同步且透明地维护。这种 OLAP + OLTP 的组合即 HTAP(混合事务/分析处理) ,可兼得二者之长。
-
OLAP 表:采用前述永久表的列式存储,提供 Snowflake 著名的分析性能特性。
-
OLTP 表:提供 OLAP 表所不具备的独占能力(下一章详述),包括:
- 约束强制(constraint enforcement)
- 索引支持(indexes)
- **行级锁(row-level locking)**以进行变更与更新
当向 Hybrid 表加载数据时,Snowflake 会同步更新其 OLAP 与 OLTP 两张表;随后查询优化器会将 DML/DQL 路由到最适合该操作的那张表。
尽管 Hybrid 表的完整文档尚未公开,CREATE 命令已对外分享,且沿用现有约束声明语法,例如:
CREATE HYBRID TABLE <table_name>
在为表选择合适的备份策略与负载形态方面,Snowflake 为用户提供了充足选项。下面对物理表类型及其属性做一张汇总表。
在表与保留期方面,Snowflake 提供了丰富选择。我们已了解三类 OLAP 风格物理表与新近发布的 Hybrid HTAP 表,值得将其属性汇总对照:
图 4.3 —— 永久表的保留期(示意)
我们已详细讨论 Time Travel 与恢复,但还值得一提的是,物理表还可启用另一类“历史”——变更历史(change history) 。与 Time Travel 提供某一历史快照不同,变更历史记录表的逐条变更并支持 CDC(变更数据捕获) 。相关功能将在本章后文详细介绍。
物理表是 Snowflake 用户最常直接接触、用于存储与处理信息的对象,但还可以定义其他表类型:外部表(external tables)与目录表(directory tables) ——它们是建立在 stage 对象之上的元数据表,并可按关系范式进行建模。下面我们更详细地探讨这些元数据对象。
Stage 元数据表(Stage metadata tables)
在 Snowflake 中,外部表(external tables)与目录表(directory tables)让用户像查询常规表一样访问已暂存(staged)文件中的数据。此处之所以重点提到外部表,是因为它们支持主键与外键,可以按标准关系模型进行建模。目录表与外部表相似,但不允许创建列或约束,为内容完整起见一并介绍。
External tables(外部表)
外部表是建立在 stage 之上的元数据对象,允许用户像在数据库内部一样查询外部暂存文件中的数据。Stage 支持的文件格式,同样可通过外部表来访问数据。所有外部表都包含以下列:
VALUE:VARIANT类型列,表示外部文件中的单行记录METADATA$FILENAME:标识外部表中每个暂存数据文件的文件名及其在 stage 中的路径METADATA$FILE_ROW_NUMBER:显示暂存数据文件中每条记录的行号
尽管只读,外部表允许用户基于 VALUE 或 METADATA 列创建虚拟列(表达式列)。通过外部表查询数据通常慢于查询原生表,因此更适合查看文件内容或元数据,而非作为稳定的数据源依赖。
创建外部表的命令:
CREATE EXTERNAL TABLE <table_name>
当仅需要文件目录信息时,可使用目录表(directory tables) 。
Directory tables(目录表)
从概念上看,目录表与外部表相似,但有若干关键差异。与外部表相比,目录表具有以下不同点:
- 不是独立的数据库对象,而是作为 stage 的一个可启用功能
- 提供文件元数据(路径、大小、URL 等),不提供文件内容
- 不允许创建虚拟列(或任何约束)
- 可在内部与外部 stage 上创建
外部表让用户以与常规物理表一致的方式访问外部云存储中的数据;而目录表仅展示文件元数据。目录表更像 LIST 函数(返回某个 stage 下文件列表),而非真正的“表”。使用目录表代替 LIST 的好处是:可以用熟悉的 SELECT 语法来获取这些信息。
启用目录表功能的示例:
CREATE STAGE <stage_name>
...
DIRECTORY = (ENABLE = TRUE);
Snowflake 视图(Snowflake views)
Snowflake 的视图(view)与大多数关系型数据库类似:将对物理对象的 SELECT 语句作为对象存放在某个 schema 中。将 SELECT 以可共享的数据库对象形式存储,带来多项优势:
- 需要时无需每次重写
SELECT,可复用的模块化查询为数据管道与分析带来省时与易维护性 - 向所有有权访问视图的人统一过滤条件与业务逻辑
- 视图与其引用的数据源相互独立,可设置不同的权限,从而避免暴露全部底层数据
- 可联合多个来源同时取数
- 与物化结果不同,视图总是显示源表的最新数据,无需刷新
- 零存储成本:数据不做物理复制,在调用视图时实时读取
创建视图的命令:
CREATE VIEW <view_name>
这些好处以性能为代价:每次查询视图,系统都需要从底层表读取数据,并计算视图中的全部逻辑与公式,消耗计算资源与 credits。好消息是:上一章介绍的 Snowflake 缓存机制同样适用于视图,从而缓解性能与成本问题。
Caching(缓存)
视图在执行查询时,会尽可能利用 Snowflake 的各类缓存来降低成本与提升性能:
- 结果缓存(results cache) :由服务层管理,保存系统中的所有查询结果,包括对视图执行的查询。只要底层数据未变化,且后续查询在语法上等价,则可瞬时从缓存返回结果,且不消耗 compute credits。
- 若无法命中结果缓存,Snowflake 会尝试利用仓库缓存(warehouse cache) :通过视图引用表数据时,之前从云存储读取过的数据会在仓库保持激活期间留在内存/SSD,从而缩短再次访问的取数时间。
Security(安全)
如前所述,视图可作为可授权的独立对象来强化数据安全。对包含敏感数据的表(无论行级还是列级),可通过视图限制可见列与过滤条件,并为视图赋予单独的访问角色。
Snowflake 还提供安全视图(secure view) ,其创建方式与普通视图相同,只是在 CREATE 语句中加入 SECURE。安全视图与普通视图的主要区别在于:视图定义(DDL)对消费者不可见,仅视图拥有者角色可见。为避免暴露底层数据,安全视图会禁用某些内部优化,但总体性能可与普通视图相当。
创建安全视图的命令:
CREATE SECURE VIEW <view_name>
了解了表与视图之后,接下来介绍物化视图(materialized views) ——一种兼具两类对象优势的 Snowflake 对象。
物化视图(Materialized views)
物化视图既不像普通视图那样“只是定义”,也不完全等同于表;它位于二者的交汇处,带来有趣的收益,但也有一个重要的注意点。
顾名思义,物化视图实际上是物理表,用于存储视图查询的结果。当底层数据发生变化时,结果会自动再物化(刷新) 。这意味着:物化视图兼具表的性能与已缓存查询结果的优势,同时仍保有常规视图的安全性收益。
唯一的权衡是成本:因为物化视图将结果存放在物理表中,所以会产生存储费用;此外,物化视图在刷新数据时也会消耗 credits。
与常规视图相比,物化视图对 SELECT 语句可用的逻辑有一定限制。其中最重要的一点是:物化视图只能建立在单一数据源之上(允许 stage),因此不能使用 JOIN。这使得物化视图更适合预计算的表结果或聚合。好处是,Snowflake 的优化器足够智能,在可能时会自动将表查询重定向到对应的物化视图。
当满足以下全部条件时,用户应考虑采用物化视图;若有任一不满足,则应谨慎或放弃:
- 查询结果变动不频繁
- 视图结果的使用频率远高于数据变更频率
- 查询计算资源消耗很大
创建物化视图的命令:
CREATE MATERIALIZED VIEW <view_name>
Streams
在介绍完多种数据源选项之后,下面聚焦于检测数据变更并自动化它们之间的变换型流水线。
Stream 是一种逻辑对象,用于捕获底层源(包括前文提到的物理表、视图、外部表与目录表)的数据变更。每当源对象发生 DML 操作,stream 就会跟踪插入、删除以及更新的前/后镜像。其实现方式是偏移量存储:在逻辑上先对数据进行一次快照,然后通过元数据列持续跟踪增量变化。虽然可以像表一样对 stream 进行查询,但它本身不是独立数据对象,且不存放表数据。
创建 stream 后,会在源对象上附加若干元数据列,开始追踪变更。下图给出了这些元数据字段及其含义:
图 4.4 —— Stream 元数据列
在表上创建 stream 的命令:
CREATE STREAM <stream_name> ON TABLE <table_name>
此后每次 DML 操作,stream 都会保存变更记录的前镜像与后镜像,并结合标识 DML 类型(insert/update/delete)的相关元数据,使下游对象能够实现精确的 CDC。
从 Streams 加载(Loading from streams)
Snowflake 通过一种优雅的消费(consumption)机制,让 stream 可控且不会无限膨胀:当使用 stream 的内容成功加载或更新至下游来源后,stream 会被清空,从而无需人工定期清理,能立即接收后续 DML 变更。仅查询(SELECT)或失败的消费尝试不会清空 stream。
下图展示了在各种 DML 操作下的实际流程。本例假设每次源表变更后,stream 都会被**消费(清空)**一次。
图 4.5 —— 在表发生变更后消费 stream 中的数据
根据被跟踪数据的特性,stream 有两种类型:
- 标准(Standard / delta) :记录插入、删除与更新。该类型适用于表、目录表与视图。
- 追加 / 仅插入(Append / insert-only) :只跟踪插入,忽略更新与删除。对于物理表、目录表与视图提供 append-only;对于外部表提供 insert-only。
能够无需手工对比版本就追踪变更,大幅简化了下游 DML 的编排,因为我们可以预判需要处理的变更类型。基于此,stream 常用于事实表与**缓慢变化维(SCD)**的建模与更新(这些对象及维护技巧将在后续章节详述)。
Stream 旨在被定期消费。其默认保留期为 14 天。虽然可以延长,但 Snowflake 不保证超过该时长后的新鲜度。若需要不因消费而消失、波动性更小的替代方案,可考虑下一节的变更跟踪(change tracking) ,它既可与 stream 配合使用,也可在 CDC 中作为替代方案。
变更跟踪(Change tracking)
与作为独立对象(建在某个 schema 下)的 stream 不同,变更跟踪是直接在表上启用的,使 Snowflake 用户能够查询 CDC(变更数据捕获)元数据。变更跟踪使用与 stream 相同的元数据字段,但将其直接附加到表上。与 stream 不同的是:即使这些变更已被用于更新下游对象,变更记录也不会被清空;相反,它会在表的数据保留期内一直存在。
下面示例展示了如何在表上启用并查询变更跟踪。本例在启用变更跟踪的表中插入三条记录,其后对一条记录进行更新、对另一条记录进行删除:
create or replace table myTable (
myTable_id varchar(10),
my_date date
);
-- Enable change tracking
alter table myTable set change_tracking = true;
-- Initialize a session variable for the current
--create timestamps
set cDts = (select current_timestamp());
set cDate = (select current_date());
--perform 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';
-- Query the change tracking metadata to observe the delta
-- from the timestamp till now
select * from myTable
changes(information => default)
at(timestamp => $cDts);
对表的变更日志进行查询后返回的结果,展示了记录的累积效果(最新版本) 。注意:yesterday 与 today 两条记录在设置时间戳之前并不存在于当前状态中,因此不会出现在结果里。
图 4.6 —— 使用默认方式查询表变更跟踪的结果
当以 append-only 模式查询同一变更日志时,看看会发生什么:
-- Query the change tracking metadata to observe
-- only inserts from the timestamp till now
select * from myTable
changes(information => append_only)
at(timestamp => $cDts);
该结果只返回插入到表中的记录,不管之后是否被删除或更新。可以看到,today 与 tomorrow 两条记录被返回,而 good day(一次更新)未出现在结果中。
图 4.7 —— 以追加模式查询表变更跟踪的结果
在一定限制条件下,只要底层表也启用了该功能,视图同样可以启用变更跟踪。由于变更跟踪元数据会在整个表的保留期内存在,且不会因被消费而清空,它并不适合用于自动化 CDC 流水线,而更适合用于分析变更本身。
说到自动化数据流水线,本章最后一个对象正是为此而生,并且能与 streams/CDC 良好配合。
任务(Tasks)
Snowflake 使用 tasks 来调度与自动化数据加载与变换。尽管数据搬运不属于关系建模的范畴,但它是变换式建模不可或缺的一部分,这里为了完整性一并介绍。
Tasks 通过串行或并行步骤执行 SQL 来自动化数据流水线。Tasks 可以与 streams 组合,构建持续的 ELT 工作流以处理最近变更的表行。执行既可以是无服务器的(由 Snowflake 托管、可自动伸缩、无需激活仓库),也可以使用专用的用户自定义仓库。
创建 task 的代码如下:
CREATE TASK <task_name>
...
[ AFTER <parent_task_1> [ , <parent_task_2> , ... ] ]
[ WHEN <boolean_expr> ]
AS <sql>
Task 的概念很直观:它会在计划时间或作为父任务链的一部分被调用时,执行一条 SQL(或存储过程)。下图展示了如何将 tasks 串并联成依赖关系,组装成数据流水线。
图 4.8 —— 串行与并行依赖构成的任务树
存储过程与脚本(STORED PROCEDURES AND SCRIPTING)
存储过程与 Snowflake Scripting 允许使用循环、分支等过程式构造,这超出本书范围。相关资料请参阅官方文档:
- Stored Procedures:docs.snowflake.com/en/sql-refe…
- Snowflake Scripting:docs.snowflake.com/en/develope…
任务与 Streams 的组合(Combining tasks and streams)
Tasks 与 streams 可谓“天作之合”,它们允许在执行前检查是否存在变更。Task 定义中包含 WHEN <boolean_expr> 子句,用于设置执行前必须满足的逻辑条件。该条件需返回布尔值,但可运行任意函数或子查询。
恰到好处的是,streams 提供了一个系统函数:当存在 CDC 记录时返回 TRUE,否则返回 FALSE。调用方式如下(以 stream 名称为参数):
SYSTEM$STREAM_HAS_DATA('<stream_name>')
在 task 的开头设置这一条件,就可以把 task 调度为按变更发生的可能频率运行,但只有在确有记录需要处理时才会真正执行。
小结(Summary)
本章展示了:Snowflake 的各类对象蕴含了大量特性,哪怕是大家熟悉的表与视图也不例外。
- 在 Snowflake 中,一张表不仅能存数据——依据其设置,还可保留长达数月的历史与灾备备份,并提供用于 CDC 的偏移式变更跟踪。
- 视图同样超出常规预期,既能启用变更跟踪,又支持自动再物化。
我们看到 stage 是外部数据进入 Snowflake 表的入口。它还提供了实用能力:通过外部表直接读取文件内容,而无需先拷贝进物理表。
最后,为了编排入库数据、建立自动化 ELT 流水线、并简化 CDC,Snowflake 将 tasks 与 streams 搭配使用,为用户提供全托管或无服务器的控制能力,把 stages、tables、views 以及所有变换逻辑串联成体系。
在了解了各对象的优势与能力之后,下一章将从数据建模的基石讲起,并将其映射到相应的 Snowflake 对象:列、表、约束等。这些基础将为本书后续对四种建模类型的深入展开打下地基。