比较规范化与反规范化 schema
一种由 NoSQL 方案推广的常见技术,是在缺少
JOIN 支持时对数据进行反规范化,本质上是将所有统计信息或相关行作为列和嵌套对象存储在父行上。例如,在一个博客的示例 schema 中,我们可以将所有 Comments 作为对象 Array 存储在各自的帖子上。
何时使用反规范化
- 对于变更不频繁的表,或者能够容忍数据在可用于分析查询前存在一定延迟的场景,可以进行反规范化。也就是说,这类数据可以按批次整体重新加载。
- 避免对多对多关系进行反规范化。如果单个源行发生变化,可能会导致需要更新大量行。
- 避免对高基数关系进行反规范化。如果一个表中的每一行在另一张表中都有数千个关联条目,就需要将这些条目表示为
Array,其元素可以是基本类型,也可以是元组。通常不建议使用包含超过 1000 个元组的数组。 - 与其将所有列都反规范化为嵌套对象,不如考虑借助 materialized view 仅反规范化某个统计值 (见下文) 。
避免对频繁更新的数据进行反规范化
- 当表中的某一行发生变化时,需要触发正确的 JOIN 语句。理想情况下,这不应导致该 JOIN 涉及的所有对象都被更新,而应该只更新受影响的对象。要调整这些 JOIN,使其能够高效过滤到正确的行,并在高吞吐量下实现这一点,通常需要借助外部工具或额外工程实现。
- ClickHouse 中的行更新需要谨慎管理,这会带来额外的复杂性。
因此,更常见的做法是采用批次更新流程,定期重新加载所有反规范化后的对象。
反规范化的实际场景
Posts 表,其中已包含 AnswerCount 和 CommentCount 等统计信息,也就是说源数据本身就是以这种反规范化的形式提供的。实际上,我们可能反而希望将这些信息规范化,因为这类数据很可能经常发生变化。其中许多列也可以通过其他表获取,例如某篇帖子的评论可以通过 PostId 列和 Comments 表获得。为了便于举例,假设帖子会通过批次处理重新加载。
这里我们只考虑将其他表反规范化到 Posts 上,因为我们将其视为分析中的主表。对于某些查询,反过来做反规范化也同样合适,并且上述考虑因素依然适用。
对于下面的每个示例,假设都存在一条查询,需要通过 join 同时使用这两个表。
Posts and Votes
Score 列就是这样一个统计值,即总赞成票减去反对票。理想情况下,我们只需在查询时通过一次简单查找即可获取这些统计信息 (参见字典) 。
Users 和 Badges
Users 和 Badges:
首先使用以下命令插入数据:
我们可能希望将 Badges 中的统计信息反规范化到 Users 上,例如 badge 的数量。我们会在对该数据集使用字典并在写入时处理时讨论这样一个示例。
Posts 和 PostLinks
PostLinks 用于关联用户认为彼此相关或重复的 Posts。以下查询展示了 schema 和加载命令:
简单统计示例
INSERT INTO SELECT,将重复统计信息与帖子表进行连接。
利用复杂类型处理一对多关系
- 命名元组 - 可将相关结构表示为一组列。
- Array(Tuple) 或 Nested - 由命名元组构成的数组,也称为 Nested,其中每个 entry 表示一个对象。适用于一对多关系。
PostLinks 反规范化到 Posts 为例进行说明。
如前文 PostLinks schema 所示,每篇帖子都可能包含若干指向其他帖子的链接。作为 Nested 类型,我们可以按如下方式表示这些关联帖子和重复帖子:
请注意,这里使用了 flatten_nested=0 设置。我们建议关闭嵌套数据的扁平化。
我们可以通过带有 OUTER JOIN 的 INSERT INTO SELECT 查询来完成这一反规范化:
请注意这里的耗时。我们在大约 2 分钟内就完成了对 6600 万行数据的反规范化。正如稍后会看到的,这项操作可以调度执行。注意这里使用了
groupArray 函数:在执行连接之前,先将 PostLinks 按每个 PostId 聚合为一个数组。然后再将该数组过滤成两个子列表:LinkedPosts 和 DuplicatePosts,同时排除外连接产生的所有空结果。
我们可以选择一些行,来查看新的反规范化结构:
反规范化的编排与调度
批处理
INSERT INTO SELECT 加载数据后,使用 ClickHouse 执行这种转换。这适用于周期性的批处理转换。
如果可以接受周期性的批处理加载过程,用户在 ClickHouse 中有多种方式来编排这一过程:
- Refreshable Materialized Views - 可刷新 materialized views 可用于按周期调度查询,并将结果发送到目标表。在查询执行时,该视图会确保目标表以原子方式更新。这为调度此类工作提供了一种 ClickHouse 原生方式。
- 外部工具 - 使用 dbt 和 Airflow 等工具来周期性调度转换。dbt 的 ClickHouse 集成 可确保该过程以原子方式执行:先创建目标表的新版本,再通过 EXCHANGE 命令与当前接收查询的版本进行原子交换。