为什么选择 ClickHouse Cloud 而非 BigQuery?
将 BigQuery 数据加载到 ClickHouse Cloud
数据集
post、vote、user、comment 和 badge 数据。该数据的 BigQuery schema 如下所示:
对于希望将此数据集导入 BigQuery instance 以测试迁移步骤的用户,我们已在一个 GCS 存储桶中提供了这些表的 Parquet 格式数据,并在 这里 提供了用于在 BigQuery 中创建和加载这些表的 DDL 命令。
迁移数据
- 初始批量加载并定期更新 - 需要先迁移初始数据集,再按固定时间间隔 (例如每天) 定期更新。这里的更新通过重新发送已发生变化的行来完成——这些变化可通过某个可用于比对的列 (例如日期列) 来识别。删除操作则通过定期完整重载整个数据集来处理。
- 实时复制或 CDC (变更数据捕获) - 需要先迁移初始数据集。之后,对该数据集的变更必须近实时地同步到 ClickHouse,且只允许数秒的延迟。实际上,这就是一个 变更数据捕获 (CDC (变更数据捕获) ) 流程:BigQuery 中的表必须与 ClickHouse 保持同步,也就是说,BigQuery 表中的插入、更新和删除都必须应用到 ClickHouse 中对应的表。
通过 Google Cloud Storage (GCS) 进行批量加载
- 将这 7 个表导出到 GCS。相关命令可在此处查看。
-
将数据导入 ClickHouse Cloud。为此,可以使用 gcs 表函数。DDL 和导入查询可在此处查看。请注意,由于 ClickHouse Cloud 实例由多个计算节点组成,因此这里使用的是 s3Cluster 表函数,而不是
gcs表函数。该函数同样适用于 gcs 存储桶,并且会利用 ClickHouse Cloud 服务的所有节点并行加载数据。
- BigQuery 的导出功能支持通过过滤器仅导出部分数据。
- BigQuery 支持导出为 Parquet、Avro、JSON 和 CSV 格式,以及多种压缩类型;而这些都受到 ClickHouse 支持。
- GCS 支持对象生命周期管理,允许在指定时间后删除那些已从 BigQuery 导出并导入到 ClickHouse 的数据。
- Google 允许每天最多免费向 GCS 导出 50TB 数据。用户只需为 GCS 存储付费。
- 导出会自动生成多个文件,并将每个文件的表数据大小限制在最多 1GB。这对 ClickHouse 很有利,因为这样可以并行导入。
通过计划查询实现实时复制或 CDC
设计 schema
posts 表上。其 BigQuery schema 如下所示:
优化类型
INSERT INTO SELECT 来填充这张表,使用 gcs table function 从 gcs 读取导出的数据。请注意,在 ClickHouse Cloud 上,你也可以使用与 gcs 兼容的 s3Cluster table function,以便在多个节点间并行加载:
ClickHouse 主键有何不同?
- 对于 ClickHouse 常见的使用规模而言,内存和磁盘效率至关重要。数据会以称为 parts 的块写入 ClickHouse 表,并按照规则在后台合并这些 parts。在 ClickHouse 中,每个 part 都有自己的主索引。当 parts 被合并时,合并后 part 的主索引也会一并合并。需要注意的是,这些索引并不是为每一行单独构建的。相反,一个 part 的主索引会为每组行保存一个索引条目——这种技术称为稀疏索引。
- 稀疏索引之所以可行,是因为 ClickHouse 会按指定键对 part 中的行排序后存储到磁盘上。稀疏主索引不会像基于 B-Tree 的索引那样直接定位到单行,而是让 ClickHouse 能够快速地 (通过对索引条目进行 binary search) 识别出可能匹配查询的行组。随后,这些可能匹配的行组会被并行流式传输到 ClickHouse engine 中,以查找真正匹配的行。这种索引设计使主索引可以非常小 (可完全装入主内存) ,同时仍能显著提升查询执行速度,尤其适合数据分析场景中常见的范围查询。更多细节,建议阅读这篇深入指南。
表中的所有列都会根据指定排序键的值进行排序,无论这些列本身是否包含在该键中。比如,如果使用CreationDate作为键,那么其他所有列中的值顺序都会与CreationDate列中的值顺序保持一致。也可以指定多个排序键——其排序语义与SELECT查询中的ORDER BY子句相同。
如何选择排序键
数据建模技术
分区
PARTITION BY 子句指定的。该子句可以包含基于任意列的 SQL 表达式,其结果将决定某一行会被发送到哪个分区。
磁盘上的数据分区片段在逻辑上都隶属于各自的分区,并且可以单独查询。在下面的示例中,我们使用表达式 toYear(CreationDate) 按年份对 posts 表进行分区。随着行被插入 ClickHouse,系统会对每一行计算该表达式的结果——随后,这些行会以属于相应分区的新数据分区片段的形式被写入对应分区。
应用场景
- 数据管理 - 在 ClickHouse 中,应主要将分区视为一种数据管理功能,而不是查询优化技术。通过基于某个键对数据进行逻辑划分,每个分区都可以独立操作,例如删除。这使你能够按时间高效地在存储层级之间移动分区及其子集,或让数据过期/从集群中高效删除数据。例如,在下面的示例中,我们会删除 2008 年的帖子数据:
- 查询优化 - 虽然分区可以帮助提升查询性能,但效果在很大程度上取决于访问模式。如果查询只涉及少量分区 (理想情况下仅一个) ,性能可能会有所提升。通常只有在分区键不在主键中,且查询会按该键进行过滤时,这才比较有意义。不过,如果查询需要覆盖很多分区,性能反而可能比不分区更差 (因为分区可能会导致产生更多 parts) 。如果分区键本身已经是主键中靠前的字段,那么只命中单个分区带来的收益会进一步降低,甚至几乎没有。若每个分区中的值都是唯一的,分区还可用于优化
GROUP BY查询。不过,一般来说,你应优先确保主键设计已优化,只有在少数特殊场景下,才应将分区作为查询优化手段来考虑——即访问模式会稳定地访问某个可预测的特定时间子集,例如按天分区,而大多数查询都集中在最近一天。
建议
ORDER BY 表达式中的第一列。
在内部,ClickHouse 会为插入的数据创建 parts。随着插入数据不断增多,parts 的数量也会增加。为防止 parts 数量过多而降低查询性能 (因为需要读取更多文件) ,系统会在后台通过异步过程将 parts 合并。如果 parts 的数量超过了预先配置的限制,ClickHouse 就会在插入时抛出异常,并报出“parts 过多”错误。正常运行时不应出现这种情况;它通常只会在 ClickHouse 配置不当或使用方式不正确时发生,例如存在大量小批量插入。由于 parts 是按分区彼此独立创建的,分区数量增加也会导致 parts 的数量增加,也就是说,parts 数量会随分区数成倍增长。因此,高基数分区键可能会导致此错误,应尽量避免。
materialized view 与投影
ORDER BY 子句。
在 ClickHouse 数据建模 中,我们探讨了如何在 ClickHouse 中使用 materialized view 预先计算聚合、转换行,以及针对不同的访问模式优化查询。对于后者,我们给出了一个示例:materialized view 会将行发送到一个目标表,而该表使用的排序键与接收插入的原始表不同。
例如,考虑以下查询:
UserId
不是排序键。此前,我们通过一个充当 PostId 查找表的 materialized view
来解决这个问题。这个问题同样也可以用投影来解决。
下面的命令会添加一个使用 ORDER BY user_id 的投影。
ALTER 命令创建的,那么在执行 MATERIALIZE PROJECTION 命令后,
创建过程会异步进行。你可以使用以下查询来确认此操作的进度,
并等待 is_done=1。
EXPLAIN 命令,我们还可以确认此查询使用了该投影:
何时使用投影
- 需要对数据进行完全重排序。虽然投影中的表达式理论上可以使用
GROUP BY,,但 materialized view 在维护聚合方面更高效。查询优化器也更可能利用采用简单重排序的投影,即SELECT * ORDER BY x。您可以在该表达式中只选择部分列,以减少存储占用。 - 用户能够接受随之而来的存储占用增加,以及数据被写入两次带来的开销。请测试其对插入速度的影响,并评估存储开销。
在 ClickHouse 中改写 BigQuery 查询
ClickHouse
聚合函数
argMax 函数 为例,说明如何计算每年浏览量最高的问题。
BigQuery
ClickHouse
条件判断与数组
HAVING 和 SELECT 子句中复用别名的能力,下面的 ClickHouse 查询写得非常简洁。
BigQuery
ClickHouse