ClickHouse 云:使用连接表引擎实现快速、可更新的查找
本文字数:3941;估计阅读时间:10 分钟
作者:Hellmar Becker
当你将数据从事务型或事件型数据源迁移到 ClickHouse 这样的分析型数据库时,很可能会考虑根据 Kimball 方法论 建立维度模型。
维度建模 总是运用事实 (facts,即度量) 和维度 (dimensions,即上下文) 的概念。事实通常(但并非总是)是可以聚合的数值,而维度则是定义事实的层级结构和描述信息。
由此可知,事实表通常是不可变的,数据以追加方式写入;而维度表则较小,会发生(不频繁的)更新(即缓慢变化的维度)。当你执行分析查询时,需要将维度表与事实表进行关联查询。
在 ClickHouse 中,实现上述目标的一种常见方法是在字典 (Dictionary) 中将维度数据维护在内存中。这种方法支持 Direct Joins,并被推荐用于优化 Join 性能。
字典的设置需要指定 SOURCE 和 LIFETIME 等属性。ClickHouse 会从源端拉取最新数据,并使用 LIFETIME 来决定刷新字典的频率。但有些客户问我:字典是否能像常规表一样进行更新?确实,通过使用另一种特殊的表引擎,可以实现这一目标。
Join 表引擎 正是此处所需的解决方案。它是一种内存结构,旨在存储需要在表定义中明确声明的特定类型 Join 操作所需的数据,并由持久化层提供支持。配置 Join 表时,需要指定以下参数:
• Join 严格性 (join strictness)
• Join 类型 (join type)
• 用于 Join 操作的 键列 (key column(s)) 。
Join 严格性 (Join strictness)
其值可为 ANY 或 ALL。当设置为 ALL 时,Join 表中所有匹配的行都会被获取;而 ANY 则只获取最新的一行。
这意味着,对于 ANY 类型,一个 INSERT 操作会通过键转变为一个 UPSERT 操作: 你可以通过插入一行具有相同键的新行来更新一个维度行。
Join 类型
ClickHouse 的 join 类型之一,例如 INNER、LEFT 或 RIGHT。在维度建模中,你大多会使用 LEFT。
尽管 Join 表可以使用 SELECT 语句像普通表一样进行查询,但它还有另外两种使用方式:
1. 如果你在 JOIN 查询中引用 Join 表,并且连接参数与该表的定义一致,ClickHouse 将自动启用直接 Join 算法。
2. 你可以使用 joinGet 函数,通过给定键来查找对应的值。它的工作原理与对 Dictionary 使用 dictGet 类似。
那么,一个带有 ANY LEFT 连接条件的 Join 表不正是实现 缓慢变化维度类型 1 (Type 1 Slowly Changing Dimension) 的理想方案吗?你可以更新给定键的值,同时获得高性能的连接。既然如此,我们为什么不一直使用它呢?
事实证明,开源 ClickHouse 中 Join 表引擎的实现存在一些不足之处,使其不那么适用于此类场景:
1. Join 表不是分布式的 ;每个集群节点都必须维护该表独立的副本或版本。
2. 持久化层并未针对频繁的插入/更新操作进行优化 。 Join 表引擎会将数据以压缩的 Native-format.bin 文件形式持久化到磁盘上表的数据目录中(每个 INSERT 批处理都会生成一个文件)。在服务器启动时,这些文件会被顺序读取,并从中重建内存中的 HashJoin 哈希表。这意味着每次更新都会创建一个新的、带编号的 .bin 文件。由于缺乏后台合并压缩机制,文件不会自动合并。随着时间的推移,这最终会导致性能下降。
这些问题在 ClickHouse Cloud 中得到了巧妙且优雅的解决。 在 ClickHouse Cloud 中,Join 表实际上被透明地实现为 SharedJoin 表,其底层是 MergeTree 家族的表:
• 对于 ALL 连接,它是一个 MergeTree 表
• 对于 ANY 连接,它是一个 ReplacingMergeTree 表。
你可以在 system.tables 中找到这些表。底层表的命名遵循如下约定:.inner_id.SharedJoin.<Join 表的 uuid>。
注意: 设置 join_any_take_last_row不会生效。
内存表的数据来源于持久化的底层表。这发生于两种情况:一是向 Join 表插入数据时(此时会执行查询,对于 ANY 连接还会包含 FINAL 子句,并带有一个筛选器以只选择最新数据);二是表在启动时加载时。
或许,最具有意义的用例是利用 ANY LEFT 连接进行数据扩充或维度建模。为了更好地说明这个具体场景,我们将在 ClickHouse 文档中的示例基础上稍作修改:
-- Create the fact table and insert some data
CREATE OR REPLACE TABLE id_val (
`id` UInt32,
`val` UInt32
) ENGINE = MergeTree
ORDER BY (id);
INSERT INTO id_val VALUES
(1, 11), (2, 12), (3, 13);
-- Creating the right-side Join table:
CREATE OR REPLACE TABLE id_val_join (
`id` UInt32,
`val` UInt8
) ENGINE = Join(ANY, LEFT, id);
-- Insert some values
INSERT INTO id_val_join VALUES
(1, 21), (1, 22), (3, 23);
-- Enrichment query
SELECT *
FROM id_val
ANY LEFT JOIN id_val_join USING (id);
查询结果如下所示:
┌─id─┬─val─┬─id_val_join.val─┐
1. │ 1 │ 11 │ 22 │
2. │ 2 │ 12 │ 0 │
3. │ 3 │ 13 │ 23 │
└────┴─────┴─────────────────┘
接下来,我们来了解一下当对键 1 的数据进行 upsert 操作时,Join 表和底层表会发生什么。
-- And another insert
INSERT INTO id_val_join VALUES (1,42);
查看底层表:
SELECT database, name, uuid, engine
FROM system.tables
WHERE name = 'id_val_join'
FORMAT Vertical;
查询结果如下所示:
database: default
name: id_val_join
uuid: 64f169ee-977d-46c2-b067-580fdf8c1d4b
engine: SharedJoin
Join 表会进行去重,并且只保留最新的条目:
SELECT * FROM id_val_join;
查询结果如下所示:
┌─id─┬─val─┐
1. │ 3 │ 23 │
2. │ 1 │ 42 │
└────┴─────┘
从 UUID 来看,底层 ReplacingMergeTree 表会在合并操作完成之前保留重复数据:
SELECT * FROM default.\`.inner_id.SharedJoin.64f169ee-977d-46c2-b067-580fdf8c1d4b\`;
查询结果如下所示:
┌─id─┬─val─┐
1. │ 1 │ 22 │
2. │ 3 │ 23 │
3. │ 1 │ 42 │
└────┴─────┘
最后,再次运行数据扩充查询,我们可以看到更新后的维度条目是如何体现在结果中的:
SELECT *
FROM id_val
ANY LEFT JOIN id_val_join USING (id);
查询结果如下所示:
┌─id─┬─val─┬─id_val_join.val─┐
1. │ 1 │ 11 │ 42 │
2. │ 2 │ 12 │ 0 │
3. │ 3 │ 13 │ 23 │
└────┴─────┴─────────────────┘
每当您插入新行或一组行时:
• 数据会被插入到底层的 ReplacingMergeTree 表中。
• 内存中的数据表示会在两种情况下更新:一是向 Join 表插入数据时(此时会基于块 ID 进行筛选以只选择最新数据);二是表在启动时加载时。
• 该查询还会应用 FINAL 子句,因此内存中的 Join 表将永远不会有重复数据。
• join_any_take_last_row 设置会被忽略。您总是会获取最新的条目。
• ClickHouse 中的 Join 表引擎提供了一个预计算的哈希映射,可用于加速 JOIN 操作。
• Join 表类似于字典,数据存储在内存中,但它由持久化层(即保存在文件中的数据)提供支持。
• 在 ClickHouse Cloud 中, Join 表会自动进行集群化,并由完整的 MergeTree 表提供支持,使其非常适合处理频繁更新的场景。
• 特别是在 ClickHouse Cloud 中进行维度建模时,使用 Join(ANY, LEFT, id) —— upsert 操作、数据去重和数据压缩都将由底层的 ReplacingMergeTree 自动处理!
/END/
试用阿里云 ClickHouse企业版
轻松节省30%云资源成本?阿里云数据库ClickHouse 云原生架构全新升级,首次购买ClickHouse企业版计算和存储资源组合,首月消费不超过99.58元(包含最大16CCU+450G OSS用量)了解详情:https://t.aliyun.com/Kz5Z0q9G
征稿启示
面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:[email protected]