ClickHouseInc

ClickHouse 云:使用连接表引擎实现快速、可更新的查找

图片

本文字数:3941;估计阅读时间:10 分钟

作者:Hellmar Becker

Image
Image
ClickHouse 中的字典 (Dictionaries)

当你将数据从事务型或事件型数据源迁移到 ClickHouse 这样的分析型数据库时,很可能会考虑根据 Kimball 方法论 建立维度模型。

维度建模 总是运用事实 (facts,即度量) 和维度 (dimensions,即上下文) 的概念。事实通常(但并非总是)是可以聚合的数值,而维度则是定义事实的层级结构和描述信息。

由此可知,事实表通常是不可变的,数据以追加方式写入;而维度表则较小,会发生(不频繁的)更新(即缓慢变化的维度)。当你执行分析查询时,需要将维度表与事实表进行关联查询。

在 ClickHouse 中,实现上述目标的一种常见方法是在字典 (Dictionary) 中将维度数据维护在内存中。这种方法支持 Direct Joins,并被推荐用于优化 Join 性能。

字典的设置需要指定 SOURCE 和 LIFETIME 等属性。ClickHouse 会从源端拉取最新数据,并使用 LIFETIME 来决定刷新字典的频率。但有些客户问我:字典是否能像常规表一样进行更新?确实,通过使用另一种特殊的表引擎,可以实现这一目标。

Image
Join 表引擎 (Join table engine)

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。

Image
查询 Join 表

尽管 Join 表可以使用 SELECT 语句像普通表一样进行查询,但它还有另外两种使用方式:

1. 如果你在 JOIN 查询中引用 Join 表,并且连接参数与该表的定义一致,ClickHouse 将自动启用直接 Join 算法。

2. 你可以使用 joinGet 函数,通过给定键来查找对应的值。它的工作原理与对 Dictionary 使用 dictGet 类似。

Image
Image

那么,一个带有 ANY LEFT 连接条件的 Join 表不正是实现 缓慢变化维度类型 1 (Type 1 Slowly Changing Dimension) 的理想方案吗?你可以更新给定键的值,同时获得高性能的连接。既然如此,我们为什么不一直使用它呢?

事实证明,开源 ClickHouse 中 Join 表引擎的实现存在一些不足之处,使其不那么适用于此类场景:

1. Join 表不是分布式的 ;每个集群节点都必须维护该表独立的副本或版本。

2. 持久化层并未针对频繁的插入/更新操作进行优化 。 Join 表引擎会将数据以压缩的 Native-format.bin 文件形式持久化到磁盘上表的数据目录中(每个 INSERT 批处理都会生成一个文件)。在服务器启动时,这些文件会被顺序读取,并从中重建内存中的 HashJoin 哈希表。这意味着每次更新都会创建一个新的、带编号的 .bin 文件。由于缺乏后台合并压缩机制,文件不会自动合并。随着时间的推移,这最终会导致性能下降。

Image
ClickHouse Cloud 中的实现

这些问题在 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 子句,并带有一个筛选器以只选择最新数据);二是表在启动时加载时。

Image
示例:数据扩充

或许,最具有意义的用例是利用 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 设置会被忽略。您总是会获取最新的条目。

Image
结论

• 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]

图片图片