在其最纯粹的形式中,关系建模(经规范化的表 + 严格执行的物理约束)最常见于 OLTP(联机事务处理) 数据库。事务型数据库存储的是业务信息的**最新(as-is)版本;而数据仓库则保存历史快照,并随时间跟踪信息的变化,从而在时间维度上进行额外的按时点(as-at)**分析。
不过,这并不意味着关系建模的理念不适用于 OLAP(联机分析处理) 数据库——恰恰相反。数据仓库不仅需要复制事务系统中已有的实体与关系,还必须完成一项额外任务:将来自其他来源的维度进行一致化(conforming) ,并在下游的变换与分析中把它们关联起来。
掌握建模“通用语言”的另一个理由,是上一章介绍的 Hybrid Unistore 表:它有望模糊事务系统与数据仓库的边界,并解锁其他架构前所未有的用例(例如在同一平台上既作为业务系统又作为仓库,对事务数据进行实时、无需 ELT 的分析访问)。
本章将重点讨论以下主题:
- 通过理解实体如何表示为表以及 Snowflake 如何管理底层存储,来确保高效设计
- 探索为属性选择正确数据类型的收益
- 理解数据库约束并在 Snowflake 中管理它们
- 学习如何利用标识符与主键(primary keys)
- 在存在多个主键候选时使用备选键(alternate keys)
- 通过外键关系将模型各部分连接起来
- 指定并检查必填列(mandatory columns)
让我们先回顾前几章的建模基础概念,并将其映射到相应的 Snowflake 对象。
实体即表(Entities as tables)
在进入数据库细节之前,先回忆业务层面的“实体”概念:与企业相关、需要被维护信息的人、事物、地点、事件或概念。换言之,实体是具有共同属性的业务相关概念。识别与命名实体的经验法则是使用英语单数名词,如 customer、item、reservation。
在 Snowflake 中,存储与维护信息的显然候选是表(table) 。借助 SQL,表为用户提供了一种标准而熟悉的方式来访问与操作实体细节。正如上一章所示,Snowflake 的表有多种“口味”,提供不同的备份与恢复选项。除了选择能提供合适 Time Travel 与 Fail-safe 的表类型外,Snowflake 的表还兑现了“近乎零运维”的承诺——无需手工维护索引、表空间或分区。
用户能控制的存储层面仅有两点:Snowflake 用于以内部列式格式存储数据的微分区(micro-partitions) ,以及它们的**聚簇(clustering)**方式。下面分别展开。
Snowflake 如何存储数据(How Snowflake stores data)
Snowflake 表中的数据以列式格式存放在称为微分区的连续存储单元中。传统数据库通常将大型表拆分为物理分区以提升性能与可扩展性,但物理分区需要维护,并会导致数据倾斜(部分分区增长更快)。
Snowflake 通过保持微分区足够小(未压缩约 50–500 MB),并由服务层透明管理,来规避这些限制。微分区是不可变的,因此 DML 操作更加高效,总是通过新建分区体现变更。由于分区较小,系统可以为每个微分区内的各列维护细粒度统计信息,并依据数据类型进行最优压缩。与传统分区不同,微分区的取值范围可以重叠(但单条记录绝不会跨多个分区)。
微分区按照数据装载进表时的自然顺序创建。比如,每天向 SALES 表加载新交易,则 transaction_date 会形成表的自然分区;若表改为按客户排序,则会形成按客户的分区,而 transaction_date 的取值会发生重叠。
微分区的核心收益在于:其元数据(取值范围、去重计数等)允许 Snowflake 的查询优化器进行剪枝(pruning) 。剪枝可以跳过那些已知不可能包含过滤值的微分区。即使 SALES 表增长到数百万行、跨越上千个微分区,优化器也能通过跳过无关分区、仅扫描满足条件的分区,在毫秒级返回结果。
用一个简化的概念性示例来看微分区的工作方式。下表包含超级英雄及其创作者的数据,用户看到的是这样的逻辑视图:
图 5.1 —— Snowflake 表数据的逻辑视图
在内部,表会被划分为两个微分区,数据以列式压缩存储:
图 5.2 —— 被划分为两个微分区的 Snowflake 表
由于表按 Date Debuted 排序,当服务层将其拆分为微分区时,该列的取值会自然聚簇。本例仅演示两个分区;实际中,Snowflake 表可根据规模由数百万个微分区组成。
借助(图 5.2 所示的部分)分区元数据(如列取值范围),Snowflake 的查询优化器可根据查询过滤条件对分区进行剪枝或忽略。
在下例中,优化器可以安全跳过分区 1,因为其值域范围(由元数据得知)与过滤条件不相符:
图 5.3 —— 成功的查询剪枝
若优化器无法基于值域范围排除任何微分区,就必须读取所有分区以查找匹配记录。
在下例中,尽管创作者 Paul Gustavson 只存在于分区 2,但就值域范围而言两个分区都匹配搜索条件,因此无法进行剪枝:
图 5.4 —— 给定过滤条件无法进行查询剪枝
Snowflake 如何管理微分区,取决于数据装载到表中的顺序。数据可以在插入时通过 ORDER BY 显式排序,或遵循自然的装载模式(如 CREATED_DATE 或 LOAD_DATE)。当分区方式与查询过滤相匹配时,可通过剪枝显著提升性能。比如,在一张拥有数百万行的表中,数据按 LOAD_DATE 装载,且查询条件是 LOAD_DATE = CURRENT_DATE,其性能可与“只包含当日数据”的表等同。但如果忽略该过滤条件或搜索其他属性,则可能无法受益于剪枝。
如上例所示,微分区会将数据划分为多个段,列取值存在不同程度的重叠。此种数据分段即所谓的聚簇(clustering) ,可以被策略性地利用来组织表结构、提升查询性能。下面我们继续了解如何将其转化为优势。
聚簇(Clustering)
当表数据被切分为微分区(micro-partitions)时,列值会自然地聚集成若干数据段。列值在各微分区之间的差异程度与重叠程度称为聚簇。分区之间重叠越少,优化器能够提供的查询剪枝与列剪枝就越强。
除了微分区级别的列元数据之外,Snowflake 的服务层还会存储如下聚簇统计信息:
- 每张表的微分区总数
- 在(指定的一组表列上)彼此取值区间发生重叠的微分区数量
- 重叠的深度(overlap depth)
下面这个(简化的)示例展示了三段微分区在聚簇范围上的重叠情形。实际表可能包含更多分区与列;想要做到完全分离往往不现实也没必要,只要能改进查询性能即可。
图 5.5 —— 分区重叠与聚簇深度
当表规模达到相当体量时,应综合考虑数据如何装载与如何被查询,并关注聚簇深度/重叠度(越小越有利于剪枝与性能)。尽管对表按一维或多维进行排序可能通过剪枝改进性能,但排序本身的成本可能抵消这些收益。
通常,即便没有对行进行任何排序或定序,即便表有数百万行,也能获得足够的性能。但如果装载模式与查询模式不匹配(例如数据按 TRANSACTION_DATE 装载,却经常按 CLIENT 查询),当表达到某个规模后,查询性能可能下降。
用户可以使用 Snowflake 可视化查询分析器(visual query profiler)评估查询剪枝情况:界面会显示微分区总数以及本次查询访问的分区数。
下面的截图显示扫描分区数 = 分区总数——意味着优化器无法进行剪枝。
图 5.6 —— 通过查询计划评估剪枝
需要强调的是:仅仅缺少剪枝并不等于性能必然很差。查询时长受缓存、查询复杂度、数据量等多因素影响。然而,当查询性能达不到预期、且如图 5.6 所示存在高比例的表扫描并伴随缺少剪枝时,可能需要考虑对表进行重新聚簇(reclustering) 。重聚簇既可通过重建表并在创建时进行排序的手工方式完成,也可交由 Snowflake 自动处理。
自动聚簇(Automatic clustering)
Snowflake 会根据数据的自然装载模式自动处理聚簇。但对超大表(TB 级及以上)而言,持续的 DML 可能会弱化目标列上的聚簇质量,从而影响查询性能。
由于表很大,手工对整表重新排序的代价高昂,Snowflake 提供了基于聚簇键(clustering key)的自动重聚簇选项。聚簇键是一个或多个列(或表达式) ,Snowflake 会据此自动排序给定表中的数据,并在必要时透明地重新排序。
定义聚簇键的收益包括:
- 通过剪枝跳过不匹配过滤条件的数据,从而提升查询性能
- 更好的列压缩与更低的存储成本
- 由 Snowflake 执行、免维护的自动重聚簇
但请牢记:自动重聚簇与手工重聚簇一样,都会产生处理成本。因此,仅当性能优先于成本,或聚簇带来的性能收益足以抵消为维护聚簇所需的 credits 时,才考虑设置聚簇键。以下情形有助于判断后者更可能成立:
-
该表包含多个 TB的数据,且拥有大量微分区;
-
查询能利用聚簇。通常意味着以下至少一条为真:
- 查询只需读取很小比例的表行;
- 查询会对数据排序(例如使用
ORDER BY);
-
高比例的查询能从同一组少数列作为聚簇键中受益(即多数查询都在选择或排序这些列)。
关于聚簇深度的更多信息,参见 Snowflake 文档:
docs.snowflake.com/en/user-gui…。
实体是关系模型的核心要素。本节展示了如何将业务实体映射为 Snowflake 的物理表,以及为获得最佳性能与存储效率需要权衡的设计点。下一节我们将通过为实体添加属性来继续扩展这一主题。
将属性映射为列(Attributes as columns)
回顾上一节:实体(entity)是企业希望维护其信息的、与业务相关的概念。属性(attribute)是在概念建模阶段与业务团队共同定义,或在 ETL 过程中从既有源数据加载得到的,用来描述实体的特性,最终以列(column)的形式存储。属性既可以是描述性的(如 NAME、ADDRESS、QUANTITY),也可以是元数据(如 ETL_SOURCE、LOAD_DATE)。
属性的性质——数值、字符串、日期或其他类型——既是概念层理解业务需求的关键细节,也是物理层选择合适数据类型的依据。Snowflake 既提供其他数据库常见的基础类型(如 VARCHAR、DATE、INTEGER),也提供较少见但很有建模与处理潜力的类型(如 VARIANT、GEOGRAPHY)。
下面了解 Snowflake 的数据类型及其特性,以便为实体属性合理选型。
Snowflake 数据类型(Snowflake data types)
Snowflake 遵循 ANSI,支持存储字符串、日期与数字的通用 SQL 数据类型。此外,Snowflake 还提供用于半结构化与地理空间数据的高级类型。前文谈到微分区时提到:按数据类型进行列压缩是性能与成本管理的关键因素之一。除此之外,正确使用数据类型还能在查询层面提升可用性。
继续之前,若你还不熟悉,请先浏览官方类型清单:
docs.snowflake.com/en/sql-refe…
有些数据类型支持在建表时设置附加属性,如排序规则(collation)与自增(autoincrement/identity) 。排序规则用于控制字符串比较的大小写、重音敏感、空格裁剪等;对数值类型而言,identity/autoincrement 可生成唯一序列,便于用作代理键(surrogate keys) (后文介绍)。
除存储与压缩外,对属性列进行恰当的类型化还能在数据加载时确保格式一致(例如数值列不会混入“脏字符”,日期列格式正确),并解锁与该类型相关的内置函数——如日期的 DATEDIFF(计算两个日期之差)、字符串的 STRTOK(分词并返回指定部分)。完整函数参考见:
docs.snowflake.com/en/sql-refe…
在 Snowflake 中存储字符串
Snowflake 使用 VARCHAR 存储字符串。VARCHAR 的最大长度为 16 MB(即 16,777,216 个字符/字节)。16 MB 有多大?托尔斯泰《战争与和平》(约 58.7 万词)装下四遍不成问题。需要注意:16 MB 也是默认长度。别紧张——Snowflake 会按实际内容长度高效压缩,不会按 16 MB 上限计费。不过,在某些场景下显式限制长度更能向使用者传达意图(例如 ISO 币种代码用 VARCHAR(3))。
下面从结构化列转向半结构化数据类型。
存储半结构化数据(Storing semi-structured data)
Snowflake 以其处理半结构化数据的易用与敏捷著称。为此提供了三个专用类型:VARIANT、ARRAY、OBJECT。除各自独有的函数(稍后讨论)外,它们都支持用原生 SQL 直接查询内容,无需事先转换或扁平化。半结构化数据的查询将在后续章节与官方文档中详述:
docs.snowflake.com/en/user-gui…
在同一张表中,可以混用结构化与半结构化类型,并在一次 SELECT 中原生查询。但这三种半结构化类型的使用方式与适用场景存在差异,需分清:
VARIANT:用于层次化半结构化数据的推荐类型,可无需显式定义结构就加载/查询 JSON、Avro、ORC、Parquet 等。VARIANT最大 16 MB,可存除VARIANT自身外的任意类型值。常见用例:邮件日志、网站行为、事件数据。OBJECT:类似 JSON object 或其他语言中的字典/映射。其本质是键值对:键为VARCHAR,值为VARIANT。当键名本身有语义时(如以 ISO 货币代码为键的国家列表)适合使用。最大 16 MB,值可为任意类型(含OBJECT)。ARRAY:与其他语言中的数组相似,包含 0 个或多个元素(长度动态),按位置引用。与按键名访问元素的OBJECT不同,ARRAY按下标访问,适合按自然/时间顺序遍历处理。最大 16 MB,元素可为任意类型(含ARRAY);但位置引用本身也计入 16 MB 限制,因而有效可用空间更小。
以上仅是对 Snowflake 结构化与半结构化类型及其函数的概览。更系统的分节参考见:
docs.snowflake.com/en/sql-refe…
理解了用于定义属性列的数据类型后,接下来介绍可在其上定义的各类约束(constraints) 。不过在深入约束细节之前,需要先强调 Snowflake 对约束管理的一项关键特点。
约束与执⾏(Constraints and enforcement)
本章余下部分将讨论表约束,因此有必要先明确它们是什么,并说明在 Snowflake 中使用它们的若干重要细节。按照 ANSI-SQL 标准,约束用于为表中数据定义完整性与一致性规则。Snowflake 支持四种约束类型:
- PRIMARY KEY
- UNIQUE
- FOREIGN KEY
- NOT NULL
至于每类约束的具体作用,会在本章后文分别展开;本节仅聚焦它们的执⾏(enforcement) 。
数据库层面的执⾏,指的是当对表执行 DML 操作时,数据库主动监控并校验给定约束的完整性规则。启用执⾏后,一旦发生约束违例,数据库会抛出错误,并阻止该 DML 操作完成。
例如,某列上的 NOT NULL 约束表示该列不能包含 NULL。若启用执⾏,当插入或更新操作试图将 NULL 写入该列时,Snowflake 会报错。
之所以以 NOT NULL 为例,是因为在上述四类约束中,只有它在(传统)Snowflake 表上是被执⾏的。其余三类在常规表上是提示性(informational/orientative) :即不强制执⾏,即便被违反也不会报错;但它们为数据库使用者提供了有价值的元数据信息(详见本章后文)。——至少在 Hybrid Unistore 表发布之前是如此。
正如上一章所述,Hybrid Unistore 表采用完全不同的 HTAP 架构,使 Snowflake 能够对四类约束全部实施执⾏。
了解这一点后,下面来看 PRIMARY KEY 约束,以便理解该约束在启用执⾏与未执⾏两种情形下分别起到的作用。
标识符与主键(Identifiers as primary keys)
表通过相应数据类型的属性来存放业务实体的信息。CUSTOMER 表中的一行代表一个客户;ORDERS 表中的一行代表一张订单——真的是这样吗? 在某些业务中,一张订单可能包含多个产品,并在表中跨越多行。要确定实体的唯一实例,需要一个标识符——在物理数据库层面即主键(PK) 。
PK 是一列或若干列的组合,其取值能唯一确定一个实体实例。每张表只能定义一个主键。从业务视角看,PK 表示单一的实体实例。回到前文示例:一张订单是否就是“包含一个产品的一行”?抑或“允许一张订单包含多个产品(跨多行)”?PK 将这种业务现实清晰地体现在表层面。
下图展示了一个虚构 ORDERS 表的示例数据。
图 5.7 —— ORDERS 表的示例数据
看起来 O_ORDERKEY 列没有重复值,但这只是数百万行大表中的小样本。仅凭样本无法判断一张订单是否仅由 O_ORDERKEY 唯一标识,还是需要 O_ORDERKEY + O_PRODKEY 的组合作为标识,或是其他列组合(当 PK 由多列组成时,称为复合/组合主键)。
图 5.8 —— 在 ORDERS 表上定义单列或组合主键
即便把全表数据都检查一遍未发现重复,也不能保证后续不会出现重复——若业务与数据模型实际上属于图 5.8 场景 2 的配置。只有在概念层明确标识符,并在对应的 Snowflake 表中物理定义它,用户才能明确如何在该表中定位一个唯一的实体实例。
理解主键用于识别唯一记录之后,下面讨论使用主键的实际收益。
主键(PK)的收益(Benefits of a PK)
支持使用主键(PK)的最有力论据,并非来自教材,而是出自苏斯博士(Dr. Seuss)的童诗《太多的戴夫》(Too Many Daves)。诗中讲到麦凯夫太太(Mrs. McCave)做出的糟糕生活选择:她生了 23 个孩子,并且都取名叫 Dave。缺少唯一标识符的后果是,麦凯夫太太无法定位某一个特定的 Dave——这对许多数据库使用者来说再熟悉不过(诗见:allpoetry.com/poem/115758…)。
除了能“锁定你要找的那个 Dave”,定义 PK 还有许多令人信服的理由。
确定粒度(Determining granularity)
当数据进入表后,紧随其后的需求往往是查询单个实体,或对实体进行汇总统计。那么一条记录究竟对应多少行? 一张订单是否存放在单行中?抑或每包含一个商品就增加一行?换句话说,最低明细层级是什么?
PK 通过定义表的粒度来回答该问题——告诉用户表中数据的最低层级是什么,以及如何唯一识别一个实体实例。
了解表的 PK,数据库使用者便可在正确的列和值上精确检索,避免歧义(比如出现多个“Dave”)。
对聚合同样如此:在对表做计数与度量汇总时,Snowflake 提供了 COUNT、SUM、MAX 等函数,但它们要求用户在 GROUP BY 中指明实体的唯一标识(或其他分组维度) 。若错误指定(或想当然假设)了表的有效标识,查询就会返回错误的计数与汇总。
确保正确的连接结果(Ensuring correct join results)
查询经常需要把一张表的数据与另一张表的信息关联并富化。连接(JOIN)类型很多(LEFT、INNER、FULL 等),但用户往往回避笛卡尔/交叉连接(CROSS JOIN),因为它会把一张表的行按另一张表的匹配行数成倍放大。而发生笛卡尔连接的最常见原因,正是无法在参与连接的一方或双方正确识别唯一记录。
在苏斯博士的例子中,歧义让结果从 1 行膨胀为 23 行。在数据库里,查询可能作用在数百万记录上,意外的膨胀会导致数十亿额外行,最终结果也毫无意义。此类查询的运行时间会相对正确连接条件呈指数级增长。在任何数据库中,这都是在浪费时间;而在 Snowflake 中,还会白白花掉 credits。
避免主键重复(仅 Hybrid Unistore)(Avoiding duplicate values, Hybrid Unistore)
PK 用于唯一标识实体,因此 PK 列出现重复值应被禁止。不过,只有 Hybrid Unistore 表能够强制这一规则:它们可通过索引在执行 DML 时高效扫描并阻止插入重复值。
如果使用的是常规 Snowflake 表,则需要在 DML 前后通过临时校验检测重复。
苏斯博士在诗的结尾写道:如果麦凯夫太太给孩子起不同的名字,就能避免许多麻烦,“但她没有,而且现在太晚了”。——所幸在数据库里,给表指定 PK 永远不算晚。下面看看 Snowflake 提供的三种指定 PK 的方式。
指定主键(Specifying a PK)
在创建 Snowflake 表时,可以用两种方式指定 PK:
- 行内(inline) :在
CREATE TABLE的相应列旁边直接声明(仅适用于单列主键); - 行外(out of line) :在
CREATE TABLE语句的末尾统一声明(支持多列复合主键)。此外,也可在已存在的表上通过ALTER TABLE添加行外主键。
1)新表·行内方式(仅单列 PK) :
CREATE TABLE ORDERS
(
O_ORDERKEY number(38,0) CONSTRAINT my_pk_name PRIMARY KEY,
O_PRODUCTKEY number(38,0) NOT NULL,
O_CUSTKEY number(38,0) NOT NULL,
-- < rest of columns>
O_COMMENT varchar(200) COMMENT 'Order details'
);
2)新表·行外方式(可多列复合 PK) :
CREATE TABLE ORDERS
(
O_ORDERKEY number(38,0),
O_PRODUCTKEY number(38,0),
O_CUSTKEY number(38,0) NOT NULL,
-- < rest of columns>
O_COMMENT varchar(200) COMMENT 'Order details',
CONSTRAINT my_pk_name PRIMARY KEY ( O_ORDERKEY, O_PRODUCTKEY )
);
3)已有表·行外方式(通过 ALTER TABLE 添加 PK) :
ALTER TABLE ORDERS
ADD CONSTRAINT my_pk_name PRIMARY KEY (O_ORDERKEY, O_PRODUCTKEY);
了解了如何定义 PK,接下来我们将讨论不同类型的键及其在建模中的用法。
键的分类学(Keys taxonomy)
前文已看到 主键(PK) 在定义与查询数据表中的关键作用。接下来需要深入了解各类“键”的分类及其在表设计中的角色。
尽管 PK 的职能始终不变——在一张表中唯一标识一条记录,但在建模语言中,可依据键列的性质加以区分。先从最常见的一类开始:业务键。
业务键(Business key)
业务键是指其取值在组织内部具有业务含义或重要性的主键。只要能保证唯一,任意字符组合都可以充当实体实例的标识;但业务键的不同之处在于:键值本身就有意义。
本章先前的示例都属于业务键。在图 5.7 的例子中,O_ORDERKEY 与 O_PRODKEY 的取值在组织的运营团队及其系统中广为人知,并在 CRM 或其他相关表中被反复引用。再如,若把**社会保障号(SSN)**或 ISO 国家代码用作 PK,它们也属于业务键,因为其含义在更广泛的外部世界同样成立。
在为包含业务键的列建模时,常用前/后缀 BKEY 以示区分。
当然,一个键即使没有业务含义,也可能在表设计中非常有用——这就是代理键。
代理键(Surrogate key)
与具备业务意义的业务键不同,代理键是没有业务含义的主键。代理键通常由随机字符、列哈希或自增整数生成。它们满足唯一性,但其取值不承载实质意义。
很多表的 PK 由多列构成(复合/组合主键),在连接等操作中书写繁琐。代理键天然是单列、且格式统一(如哈希或序列),可以缓解这一不便。一些数据建模体系(例如稍后章节会讨论的 Data Vault 2.0)依赖代理键来确保规范的命名与取值模式,使表设计可复用且一致。
在为包含代理键的列建模时,常用前/后缀 SKEY 以示区分。
序列(Sequences)
当需要生成顺序型代理键时,Snowflake 提供了序列(sequence)对象。序列是独立的数据库对象,用于生成递增(不保证无间隙)的整数,确保列值唯一。创建序列时可指定起始值与步长。
作为独立对象,一个序列可以被多张表共享,也可以通过函数调用进行操作。
在建表时,可将序列设为列默认值以充当代理键:使用 nextval 作为默认值,例如:
create or replace sequence seq1;
create or replace table foo (k number default seq1.nextval, v number);
下面的示例展示了在不同 nextval 使用情形下插入新记录的行为:
-- 使用 seq1 生成的唯一键,显式给出 v
insert into foo (v) values (100);
insert into foo values (default, 101);
-- 复用已有 v 值,但为每行生成新的唯一 k
insert into foo (v) select v from foo;
-- 两列都显式赋值
insert into foo values (200, 202);
select * from foo;
+------+------+
| K | V |
|------+------|
| 1 | 100 |
| 2 | 101 |
| 3 | 100 |
| 4 | 101 |
| 200 | 202 |
+------+------+
或者,也可在建表时使用 AUTOINCREMENT / IDENTITY 让 Snowflake 为特定表透明地创建与管理序列对象:
CREATE TABLE FOO
(
k number DEFAULT seq1.nextval,
v number
);
CREATE TABLE BAR
(
k number NOT NULL AUTOINCREMENT START 10 INCREMENT 5,
v number NOT NULL
);
更多关于序列的用法,参见官方文档:
docs.snowflake.com/en/user-gui…。
我们已理解了 PK 的分类:具业务意义的业务键与仅为唯一性服务的代理键。但请记住:一张表只能定义一个 PK。那么,当存在多列都能唯一标识实体实例时,会怎样处理呢?
备选键与唯一约束(Alternate keys as unique constraints)
设想我们在建模一张 EMPLOYEE 表,其中包含一个 EMPLOYEE_ID 列(组织内部的唯一业务标识)以及 社会保障号(Social Security number) (政府颁发的个人标识)。两者任一都能满足 EMPLOYEE 表唯一标识一条记录的主键(PK)要求;但请记住:一张表只能有一个 PK。为了在已经存在 PK 的情况下仍然告知数据库使用者“另一些列也满足 PK 条件”,可以定义备选键(AK,Alternate Key)/ 唯一约束(UNIQUE) 。
在上述示例中,EMPLOYEE 表有两个有效的 PK 候选:EMPLOYEE_ID 与 SOCIAL_SECURITY_ID。在 OLTP 数据库中,应将组织的业务键对应的列设为主键;而在数据仓库中,可能会加载来自多个源系统的业务键,通常会使用代理键作为 PK。按这一惯例,EMPLOYEE 表可建模为:EMPLOYEE_ID 作为 PK,SOCIAL_SECURITY_ID 作为 AK(UNIQUE) 。
示例:
CREATE TABLE EMPLOYEE
(
EMPLOYEE_ID varchar(10),
SOCIAL_SECURITY_ID number(9),
-- < rest of columns>
NAME varchar,
CONSTRAINT pk_employee PRIMARY KEY ( EMPLOYEE_ID ),
CONSTRAINT ak_employee UNIQUE (SOCIAL_SECURITY_ID)
);
与 PK 不同,一张表可以有任意多个 AK。AK 能向数据库使用者传达表及其多列的粒度信息,并在功能上享有与 PK 相同的诸多收益。需要注意:与 PK 一样,UNIQUE 约束只有在 Hybrid Unistore 表中才被强制执行;在标准表上它们仅具提示性。
通过外键建立关系(Relationships as foreign keys)
任何业务都可以分解为实体及其交互。例如:客户(customers)向供应商(suppliers)提供的商品(items)下单,同时可能应用营销活动的优惠码。这只是典型组织活动的一小部分。前文一直聚焦于实体本身(订单、商品、供应商等),现在该关注交互——在建模术语中称为关系(relationships) ,如“下订单”“提供商品”“应用促销”。
当业务实体存在关系时,其对应的表也必须能记录该交互的细节。客户购买商品时,订单明细需要记录是谁(客户)以及买了什么(商品)。回忆:PK 唯一标识表中的一条记录。因此,当两张表存在关系时,一方的 PK 必须出现在另一方,以存储该交互的细节。在数据库中,这种关系通过一种表级约束来建立,称为外键(FK) 。
当一张表(父表)的 PK 列被包含到另一张表(子表)中时,可以在子表上声明 FK 约束以形式化这种关系。FK 告诉数据库与使用者:两张表之所以共享若干列并非巧合,而是因为它们在业务上存在联系。
在图上,FK 用一条从父到子的连线表示。回想第 1 章“释放建模力量”中的 PERSON 与 ACCOUNT 示例:每个账户必须分配给某个人,一个人可以拥有 0 到多张账户。若确定 PERSON_ID 能唯一标识一个 PERSON(其 PK),则该列必须包含在 ACCOUNT 表中,并在其中声明为 FK。在创建 子表 ACCOUNT 时,需要同时定义其 PK 与 FK。
示例 DDL:
CREATE TABLE PERSON
(
PERSON_ID number(8,0) NOT NULL,
SOCIAL_SERCURITY_NUM number(9,0) NOT NULL,
DRIVERS_LICENSE varchar(10) NOT NULL,
NAME varchar NOT NULL,
BIRTH_DATE date NOT NULL,
CONSTRAINT PK_1 PRIMARY KEY ( PERSON_ID )
);
CREATE TABLE ACCOUNT
(
PERSON_ID number(8,0) NOT NULL,
ACCOUNT_ID varchar(12) NOT NULL,
ACCOUNT_TYPE varchar(3) NOT NULL,
IS_ACTIVE boolean NOT NULL,
OPEN_DATE date NOT NULL,
CLOSE_DATE date,
CONSTRAINT PK_2 PRIMARY KEY ( PERSON_ID, ACCOUNT_ID ),
CONSTRAINT FK_1 FOREIGN KEY ( PERSON_ID ) REFERENCES PERSON ( PERSON_ID )
);
对应的关系图(便于人类理解)如第 1 章所示:
图 5.9 —— 两张存在关系的表
上述 DDL(以及图 5.10 的表达)确保了开户相关的业务规则被固化在数据库设计中。
下图给出了这两张表的示例数据,展示该模型可能包含的数据形态:
图 5.10 —— PERSON 与 ACCOUNT 的示例值
在数据库中,FK → PK 引用是最常见的关系形式,但并非唯一。FK 也可以引用 AK。实践中,一张表可能以代理键作为 PK,并将多个业务键声明为 AK。由于代理键是技术性产物、不具业务意义,关联表往往会引用备选的业务键,如下例所示:
图 5.11 —— FK 引用 AK 的示意
从语法上看,FK 引用 AK 与 FK 引用 PK 的写法完全相同:
CREATE OR REPLACE TABLE employee
(
employee_skey integer NOT NULL AUTOINCREMENT START 1 INCREMENT 1,
employee_bkey varchar(10) NOT NULL,
name varchar NOT NULL,
social_security_id number(8,0) NOT NULL,
healthcare_id integer NOT NULL,
birth_date date NOT NULL,
CONSTRAINT pk_employee_skey PRIMARY KEY ( employee_skey ),
CONSTRAINT ak_employee_bkey UNIQUE ( employee_bkey ),
CONSTRAINT ak_healthcare_id UNIQUE ( healthcare_id ),
CONSTRAINT ak_ss_id UNIQUE ( social_security_id )
);
CREATE OR REPLACE TABLE employee_of_the_month
(
month date NOT NULL,
employee_bkey varchar(10) NOT NULL,
awarded_for varchar NOT NULL,
comments varchar NOT NULL,
CONSTRAINT pk_employee_of_the_month_month PRIMARY KEY ( month ),
CONSTRAINT fk_ref_employee FOREIGN KEY ( employee_bkey )
REFERENCES employee ( employee_bkey )
);
了解了 FK 的定义,下面继续认识它所带来的收益。
外键(FK)的收益(Benefits of an FK)
声明 FK 能将两张表之间的业务关系形式化。这些元数据成为数据模型的一部分,供所有数据库使用者参考。其带来的优势包括:
可视化数据模型(Visualizing the data model)
人类依赖可视化来增强认知。上一节展示了:一个简单的图示,往往能比代码更快、更直观地传达同样的信息。想象把视野从两张表扩展到整个模式(schema) :若没有用于映射实体与关系的可视化指引,在缺乏先验知识的情况下,要弄清所有表及其内容将十分困难。
图 5.12 —— 从两张表扩展到整套模式的示意
在企业系统中,数据模型动辄数百乃至上千张表,此时图示对于理解业务/数据版图至关重要。
指导编写连接(Informing joins)
定义 FK 时,必须明确父表的 PK 列与子表对应列之间的引用关系。也就是说,两表之间的联系被“烙”进了数据模型,供用户在进行分析或变换时据此连接取数。
下图展示了 FK 关系如何突出两表间的公共列(PERSON_ID) :
图 5.13 —— 高亮 FK 列以推断连接条件
据此,用户可以利用该元数据,通过 PERSON_ID 连接 PERSON 与 ACCOUNT 以检索数据。
不仅对人有用,BI 工具也会利用这些信息。更重要的是,Snowflake 也能借助这些细节,在不需要连接时避免执行 JOIN、提升性能(详见第 12 章关于 RELY 属性与变换建模实践)。
在 BI 工具中自动化功能(Automating functionality in BI tools)
许多 BI 工具会利用 FK 来提升易用性、节省时间。例如 Tableau、Power BI 等看板/分析工具会检测 FK,并自动创建表间连接,无需用户显式指定。
建模工具与部分 SQL IDE 还能通过读取数据库 DDL 生成 ER 图。但尽管每个对象都有对应的 CREATE 语句,若对象之间没有通过 FK 建立关系,生成的 ER 图也难言有用。
强制参照完整性(仅 Hybrid Unistore)(Enforcing referential integrity)
接下来这一收益依赖于强制执行 FK,因此仅适用于 Hybrid Unistore 表,但它在 Snowflake 平台上解锁了强大的新可能。
如前所述,FK 关系不仅是提示性的,它通过数据库约定固化业务规则。回想之前的规则:没有 PERSON_ID 就不能开立账户。那么当创建新账户时,数据库如何确保已提供 PERSON_ID,并且它确实对应 PERSON 表中的一条有效记录?答案是——强制执行 FK 约束。
当数据库强制执行 FK 时,会检查子表中的标识符是否存在于父表——这被称为参照完整性。参照完整性保证子表 FK 值对应父表的有效记录。就像不应为数据库中不存在的人开户一样,参照完整性检查可防止 DML 导致错误或异常数据。
与强制执行 PK 类似,FK 执行依赖索引以快速完成参照检查,因此仅在 Hybrid Unistore 表(或你手动实现检查)中可行。
无论是否强制,使用 FK 的理由都很充分。接下来要介绍的是所有 Snowflake 表(不仅限于 Hybrid Unistore)都会强制执行的最后一种约束——让我们看看它如何工作。
必填列与 NOT NULL 约束(Mandatory columns as NOT NULL constraints)
在为某个实体定义属性时,哪些是必填、哪些是可选几乎总会被提及。与大多数建模决策一样,答案更多取决于业务语境,而非数据库的技术属性。同一属性(例如 CUSTOMER 的电子邮箱)在不同场景中的必填性可能不同:对网店来说它或许是必需(没有邮箱可能无法登录网站);对线下零售而言则可能是可选(没有邮箱只是收不到促销邮件)。
从概念模型落地到 Snowflake 的物理设计时,可通过 NOT NULL 约束来定义必填列。NOT NULL 以**行内(inline)方式紧挨对应列进行声明,无需命名;因此无法以行外(out of line)**方式声明 NOT NULL。
为列添加 NOT NULL 的语法格式如下:
<col1_name> <col1_type> [ NOT NULL ]
在前面的示例(如 ACCOUNT 表)中我们已看到 NOT NULL 的用法。下面可见,在 ACCOUNT 表中,只有 CLOSE_DATE 不是必填,允许为 NULL:
CREATE TABLE ACCOUNT
(
PERSON_ID number(8,0) NOT NULL,
ACCOUNT_ID varchar(12) NOT NULL,
ACCOUNT_TYPE varchar(3) NOT NULL,
IS_ACTIVE boolean NOT NULL,
OPEN_DATE date NOT NULL,
CLOSE_DATE date,
CONSTRAINT PK_2 PRIMARY KEY ( PERSON_ID, ACCOUNT_ID ),
CONSTRAINT FK_1 FOREIGN KEY ( PERSON_ID ) REFERENCES PERSON ( PERSON_ID )
);
与其他约束不同,执行 NOT NULL 不需要查找或引用其他表——只需在当前行检查目标列是否有值即可。正因如此,NOT NULL 在所有 Snowflake 表上都会被强制执行,且在 DML 操作期间不会带来性能开销。
小结(Summary)
本章讨论了如何将逻辑建模概念映射为 Snowflake 的物理对象。在此过程中,我们了解了 Snowflake 如何通过将超大表拆分为可管理的**微分区(micro-partitions)来处理近乎无限规模的数据,以及如何利用聚簇(clustering)**优化查询与 DML 性能。
随后,我们通过认识 Snowflake 的数据类型及其特性来学习如何定义属性列。Snowflake 提供了丰富的函数以提升各数据类型的易用性与性能,更不用说其对半结构化数据的强大支持。
在深入各类约束之前,我们先理解了什么是数据库约束,以及 Snowflake 如何根据表类型来组织与执行这些约束。
我们看到,唯一标识符在表定义中至关重要,Snowflake 通过 PK 约束来管理这一点。PK 帮助用户明确表的最低明细粒度,并保障精确查询与正确连接。当存在多个可充当 PK 的候选列时,可通过 UNIQUE 定义 AK(备选键) ,并获得与 PK 相同的很多益处。
接着,我们了解了如何通过定义 FK 约束在物理数据库中形式化关系。FK 将表间的业务关系固化为元数据,使得可视化与导航复杂数据版图成为可能——这些信息既被人类用户利用,也常被 BI 工具使用。
最后,我们学习了如何声明必填列并通过 NOT NULL 约束强制执行该规则,以确保数据质量。
本章及其他章节多次借助关系图来澄清示例、解释抽象概念;但我们尚未系统说明其建模标记法与可视化工具箱中的诸要素。现在我们已在逻辑与物理层面理解了数据模型的核心元素,下一章将聚焦建模标记法,以支持在任意规模上设计或可视化数据模型。