这是关于从 PostgreSQL 迁移到 ClickHouse 的指南的第 3 部分。本部分通过一个实际示例,说明如果从 PostgreSQL 迁移到 ClickHouse,应如何进行数据建模。我们建议从 Postgres 迁移的用户阅读ClickHouse 数据建模指南。该指南使用相同的 Stack Overflow 数据集,并探讨了利用 ClickHouse 功能进行建模的多种方法。
来自 OLTP 数据库的用户,往往会在 ClickHouse 中寻找对应的概念。看到 ClickHouse 支持
PRIMARY KEY 语法后,用户可能会倾向于沿用源 OLTP 数据库中的相同键来定义表 schema。但这并不合适。
ClickHouse 主键有何不同?
- 按定义,Postgres 主键在每一行中都是唯一的。借助 B-tree structures,可以通过该键高效查找单行数据。虽然 ClickHouse 也可以针对单个行值查找进行优化,但分析型工作负载通常需要读取少量列,却要处理大量行。更常见的情况是,过滤器需要识别出一部分行,再对其执行聚合。
- 对于 ClickHouse 常见的使用规模来说,内存和磁盘效率至关重要。数据会以称为 parts 的块写入 ClickHouse 表,并按规则在后台对这些 parts 进行合并。在 ClickHouse 中,每个 part 都有自己的主索引。当 parts 被合并时,合并后 part 的主索引也会一并合并。与 Postgres 不同,这些索引并不是为每一行构建的。相反,一个 part 的主索引是每组行对应一条索引条目——这种技术称为稀疏索引。
- 之所以能够使用稀疏索引,是因为 ClickHouse 会按照指定的键,将一个 part 中的行按顺序存储到磁盘上。与基于 B-Tree 的索引直接定位单行不同,稀疏主索引可以快速地 (通过对索引条目执行 binary search) 识别出可能匹配查询的行组。随后,这些可能匹配的行组会被并行流式传入 ClickHouse 引擎,以找出实际匹配项。这种索引设计使主索引体积很小 (可完全放入主内存) ,同时仍能显著提升查询执行速度,尤其适用于数据分析场景中常见的范围查询。
表中的所有列都会根据指定排序键的值进行排序,无论这些列本身是否包含在该键中。例如,如果使用CreationDate作为键,那么其他所有列中的值顺序都会与CreationDate列中的值顺序保持一致。也可以指定多个排序键——其排序语义与SELECT查询中的ORDER BY子句相同。
选择排序键
分区
PARTITION BY 子句指定的。该子句可以包含基于任意列的 SQL 表达式,其结果将决定某一行会被写入哪个分区。
数据分区片段在磁盘上会按分区进行逻辑归属,并且可以单独查询。在下面的示例中,我们使用表达式 toYear(CreationDate) 按年份对 posts 表进行分区。当行被插入 ClickHouse 时,系统会对每一行计算该表达式,并将其写入对应的结果分区 (如果某一年份的第一行数据到来时该分区尚不存在,则会创建该分区) 。
分区的用途
- 数据管理 - 在 ClickHouse 中,你首先应将分区视为一种数据管理功能,而不是查询优化技术。通过按某个键在逻辑上拆分数据,每个分区都可以独立操作,例如删除。这使你能够按时间高效地在存储层级之间移动分区及其对应的数据子集,或让数据过期/从集群中高效删除。例如,下面我们会删除 2008 年的 posts。
- 查询优化 - 虽然分区有助于提升查询性能,但效果在很大程度上取决于访问模式。如果查询只涉及少量分区 (理想情况下仅一个) ,性能可能会有所提升。通常,只有当分区键不在主键中,并且查询按该分区键进行过滤时,这样做才有意义。不过,如果查询需要覆盖很多分区,性能反而可能比不使用分区更差 (因为分区可能会导致产生更多的 parts) 。如果分区键本身已经在主键中靠前的位置,那么只命中单个分区带来的收益就会更不明显,甚至几乎没有。如果每个分区中的值都是唯一的,分区还可用于优化 GROUP BY 查询。不过,总体来说,你应先确保主键已经过优化,只有在极少数特殊情况下,才应将分区视为一种查询优化手段——也就是访问模式会稳定地访问某个可预测的特定数据子集时,例如按天分区,而大多数查询都集中在最近一天的数据。
分区建议
在内部,ClickHouse 会为插入的数据创建 parts。随着插入的数据越来越多,parts 的数量也会增加。为了避免 parts 数量过多而导致查询性能下降 (因为需要读取更多文件) ,系统会在后台通过异步过程将 parts 合并。如果 parts 数量超过预先配置的限制,ClickHouse 就会在 insert 时抛出异常,即报出“parts 过多”错误。这种情况在正常运行中不应出现,通常只会发生在 ClickHouse 配置不当或使用方式不正确时,例如存在大量小批量 insert。
由于 parts 是按分区彼此独立创建的,因此分区数量增加时,parts 的数量也会随之增加,也就是说,它与分区数量成倍数关系。因此,高基数分区键可能会导致此错误,应当避免。
materialized views 与 PROJECTION
ORDER BY 子句。
在 ClickHouse 的数据建模文档中,我们探讨了如何使用 ClickHouse 中的 materialized view 来预计算聚合、转换行,以及针对不同的访问模式优化查询。
对于后者,我们提供了一个示例:materialized view 会将行发送到目标表,而该目标表的排序键不同于接收插入操作的原始表。
例如,考虑以下查询:
UserId 并不是排序键。
此前,我们通过让 materialized view 充当 PostId 的查找表来解决这个问题。这个问题同样也可以通过 PROJECTION 解决。下面的命令会为
ORDER BY user_id 添加一个 PROJECTION。
ALTER 创建的,那么在发出 MATERIALIZE PROJECTION 命令后,创建过程会异步执行。你可以使用以下查询来确认此操作的进度,并等待 is_done=1。
EXPLAIN 命令,我们还可以确认该投影已用于执行此查询:
何时使用PROJECTION
- 需要对数据进行完全重排序。虽然PROJECTION中的表达式
理论上可以使用
GROUP BY,,但 materialized view 在维护聚合方面 更高效。查询优化器也更可能 利用采用简单重排序的PROJECTION,即SELECT * ORDER BY x。 你可以在该表达式中只选择部分列,以减少存储占用。 - 用户能够接受随之增加的存储占用,以及 数据被写入两次所带来的开销。请测试其对插入速度的影响,并 评估存储开销。
从 25.5 版本开始,ClickHouse 在
PROJECTION中支持虚拟列
_part_offset。这带来了一种存储PROJECTION时更节省空间的方式。更多详情,请参见”PROJECTION”