从 BigQuery 迁移到 ClickHouse Cloud
为什么选择 ClickHouse Cloud 而不是 BigQuery?
简而言之:在现代数据分析场景中,ClickHouse 比 BigQuery 更快、更便宜,也更强大:

从 BigQuery 向 ClickHouse Cloud 加载数据
数据集
作为展示从 BigQuery 迁移到 ClickHouse Cloud 典型流程的示例数据集,我们使用了 Stack Overflow 数据集,相关说明文档见此处。该数据集包含自 2008 年至 2024 年 4 月期间出现在 Stack Overflow 上的每条 post、vote、user、comment 和 badge。该数据在 BigQuery 中的 schema 如下所示:

对于希望在自己的 BigQuery 实例中填充该数据集以测试迁移步骤的用户,我们在一个 GCS bucket 中提供了这些表的 Parquet 格式数据,并提供了在 BigQuery 中创建并加载这些表的 DDL 命令,详见此处。
数据迁移
在 BigQuery 和 ClickHouse Cloud 之间迁移数据,大致可以分为两类主要工作负载模式:
- 初始批量加载与周期性更新 - 首先需要迁移一个初始数据集,随后按照固定周期(例如每天)执行更新。这里的更新通过重新发送已发生变更的行来处理——这些行通常通过某个可用于比较的列(例如日期列)来识别。删除操作则通过周期性地对整个数据集执行完全重新加载来处理。
- 实时复制或 CDC - 首先需要迁移一个初始数据集。随后对该数据集的变更必须在 ClickHouse 中实现近实时反映,只允许数秒级的延迟。这实质上是一个CDC(变更数据捕获)流程,即 BigQuery 中的表必须与 ClickHouse 中的表保持同步,也就是说,BigQuery 表中的插入、更新和删除必须应用到 ClickHouse 中对应的表上。
通过 Google Cloud Storage(GCS)进行批量加载
BigQuery 支持将数据导出到 Google 的对象存储服务(GCS)。针对我们的示例数据集:
-
将这 7 张表导出到 GCS。相关命令可在此处获取。
-
将数据导入到 ClickHouse Cloud。为此我们可以使用 gcs 表函数。对应的 DDL 语句和导入查询可在此处获取。请注意,由于一个 ClickHouse Cloud 实例由多个计算节点组成,我们并未直接使用
gcs表函数,而是使用了 s3Cluster 表函数。该函数同样支持 GCS bucket,并且利用 ClickHouse Cloud 服务中的所有节点以并行方式加载数据。

该方法具有如下优势:
- BigQuery 的导出功能支持通过过滤条件只导出数据子集。
- BigQuery 支持导出为 Parquet、Avro、JSON 和 CSV 格式,以及多种压缩类型——这些均受 ClickHouse 支持。
- GCS 支持对象生命周期管理,允许在数据被导出并导入 ClickHouse 之后,在指定时间段后自动删除这些数据。
- Google 允许每天最多免费向 GCS 导出 50TB 数据,用户只需为 GCS 存储付费。
- 导出会自动生成多个文件,每个文件最多包含 1GB 的表数据。这对 ClickHouse 有利,因为这样可以将导入过程并行化。
在尝试以下示例之前,我们建议用户先查看导出所需权限和数据位置方面的建议,以最大化导出与导入性能。
通过计划查询实现实时复制或 CDC
CDC(变更数据捕获)是指在两个数据库之间保持表数据同步的过程。如果需要在近实时场景下处理更新和删除操作,这会变得更加复杂。一种方法是简单地利用 BigQuery 的计划查询功能定期导出数据。只要能够接受数据在插入 ClickHouse 时存在一定延迟,这种方法就非常容易实现和维护。示例见这篇博客文章。
模式设计
Stack Overflow 数据集包含许多相关的表。我们建议优先迁移主表。这不一定是最大的那张表,而是您预计会收到最多分析查询的那张表。这样可以帮助您熟悉 ClickHouse 的核心概念。随着后续添加更多表,为了充分利用 ClickHouse 的特性并获得最佳性能,可能需要对该表进行重新建模。我们在数据建模文档中对这一建模过程进行了探讨。
遵循这一原则,我们重点关注主表 posts。其在 BigQuery 中的模式如下所示:
优化数据类型
按照此处所述的流程进行后,将得到如下模式:
我们可以使用一个简单的 INSERT INTO SELECT,通过 gcs 表函数 从 GCS 读取导出的数据来填充此表。请注意,在 ClickHouse Cloud 上还可以使用与 GCS 兼容的 s3Cluster 表函数,在多个节点上并行加载数据:
在我们的新 schema 中不会保留任何 null 值。上面的 insert 语句会将这些值隐式转换为其各自类型的默认值——整数为 0,字符串为空值。ClickHouse 还会自动将所有数值转换为其目标精度。
ClickHouse 主键有何不同?
如此处所述,与 BigQuery 一样,ClickHouse 不会对表主键列的取值强制唯一性。
与 BigQuery 中的分簇(clustering)类似,ClickHouse 表的数据在磁盘上按照主键列的顺序进行存储。查询优化器会利用这一排序来避免重新排序、减少用于连接的内存占用,并支持对 limit 子句进行短路执行。 与 BigQuery 不同,ClickHouse 会基于主键列值自动创建(稀疏)主索引。该索引用于加速所有包含主键列过滤条件的查询。具体来说:
- 内存和磁盘效率对于 ClickHouse 常见的使用规模至关重要。数据以称为 part 的数据块写入 ClickHouse 表,并在后台根据一定规则对 part 进行合并。在 ClickHouse 中,每个 part 都有自己的主索引。当 part 被合并时,合并后 part 的主索引也会被合并。请注意,这些索引并不是为每一行构建的,而是一个 part 的主索引是每一组行对应一个索引条目——这种技术称为稀疏索引。
- 稀疏索引之所以可行,是因为 ClickHouse 会按照指定的键在磁盘上存储一个 part 的行。稀疏主索引并不是像基于 B-Tree 的索引那样直接定位单行数据,而是通过对索引条目做二分查找,快速定位可能匹配查询的行组。然后,这些被定位出的潜在匹配行组会并行地以流式方式送入 ClickHouse 引擎,以查找真正的匹配。这样的索引设计使主索引可以保持较小(完全常驻于主内存中),同时显著加速查询执行时间,尤其是数据分析场景中常见的范围查询。更多细节请参考这篇深入指南。

在 ClickHouse 中,所选择的主键不仅决定索引本身,还会决定数据写入磁盘的顺序。由于这一点,它会显著影响压缩率,进而影响查询性能。使大多数字段的值以连续顺序写入的排序键,将有利于所选压缩算法(以及编码器)更高效地压缩数据。
表中的所有列都会根据指定排序键的值进行排序,而不论这些列本身是否包含在排序键中。例如,如果使用
CreationDate作为排序键,那么所有其他列中的取值顺序将与CreationDate列中的值顺序保持一致。可以指定多个排序键——其排序语义与SELECT查询中的ORDER BY子句相同。
选择排序键
有关选择排序键时的考量因素和具体步骤,并以 posts 表为例进行说明,请参见此处。
数据建模技术
我们建议从 BigQuery 迁移的用户阅读在 ClickHouse 中进行数据建模的指南。该指南使用相同的 Stack Overflow 数据集,并结合 ClickHouse 的特性来探索多种建模方法。
分区
BigQuery 用户应该已经熟悉表分区的概念:通过将表拆分为更小、更易管理的部分(称为分区),来提升大型数据库的性能和可管理性。分区可以通过在指定列(例如日期)上使用范围分区、定义列表分区,或者基于键的哈希分区来实现。这使得管理员可以根据特定条件(例如日期范围或地理位置)来组织数据。
分区有助于通过分区裁剪和更高效的索引来提升查询性能。它还可以简化备份和数据清理等维护任务,因为这些操作可以针对单个分区执行,而不必作用于整张表。此外,通过将负载分布到多个分区,分区还能显著提升 BigQuery 数据库的可扩展性。
在 ClickHouse 中,分区是在创建表时通过 PARTITION BY 子句指定的。该子句可以包含基于任意列的 SQL 表达式,其结果将决定每一行被写入到哪个分区。

数据片段在磁盘上与各个分区逻辑关联,并且可以被单独查询。对于下面的示例,我们使用表达式 toYear(CreationDate) 按年份对 posts 表进行分区。当行被插入到 ClickHouse 时,该表达式会对每一行进行计算——然后这些行会被路由到对应的分区,并以属于该分区的新数据片段形式存储。
应用场景
ClickHouse 中的分区与 BigQuery 有类似的应用场景,但存在一些细微差异。更具体地说:
- 数据管理 - 在 ClickHouse 中,用户原则上应将分区视为一种数据管理特性,而不是查询优化技术。通过基于某个键对数据进行逻辑划分,可以对每个分区独立执行操作,例如删除。这样,用户便可以根据时间在存储层级之间高效地迁移分区(也就是对应的数据子集),或者使数据过期或高效地从集群中删除数据。例如,在下面的示例中,我们删除 2008 年的帖子:
- 查询优化 - 虽然分区可以帮助提高查询性能,但这在很大程度上取决于访问模式。如果查询只会命中少量分区(理想情况下是一个),则性能可能会得到提升。这通常只在分区键不在主键中且你按该键进行过滤时才有用。然而,需要扫描许多分区的查询,其性能可能会比不使用分区时更差(因为分区可能会导致存在更多的 part)。如果分区键已经是主键中靠前的字段,那么针对单个分区的好处会明显减弱,甚至几乎不存在。如果每个分区中的值是唯一的,分区也可以用于优化
GROUP BY查询。但是,总体来说,用户应首先确保主键已得到优化,仅在少数特殊场景下才将分区视作查询优化手段,例如访问模式稳定且只访问一天内某个可预测的时间子区间时(例如按天分区,而大多数查询只访问最近一天的数据)。
建议
用户应将分区视为一种数据管理技术。在处理时序数据并需要让数据从集群中过期时,它是理想的选择,例如可以直接删除最老的分区。
重要:确保你的分区键表达式不会产生高基数集合,即应避免创建超过 100 个分区。例如,不要按客户端标识符或名称等高基数列对数据进行分区。相反,应将客户端标识符或名称设为 ORDER BY 表达式中的第一列。
在内部,ClickHouse 会为插入的数据创建 part。随着更多数据被插入,part 的数量会增加。为了避免 part 数量过多(这会降低查询性能,因为需要读取的文件更多),这些 part 会在后台异步合并。如果 part 的数量超过了预先配置的上限,ClickHouse 会在插入时抛出一个"too many parts" 错误。在正常运行下这不应发生,只会在 ClickHouse 配置错误或使用不当(例如大量小批量插入)时出现。由于 part 是在每个分区内独立创建的,增加分区数量会导致 part 数量增加,即 part 数量是分区数量的倍数。因此,高基数分区键可能会导致该错误,应当避免。
物化视图与投影
ClickHouse 的投影(projection)概念允许用户为同一张表指定多个 ORDER BY 子句。
在 ClickHouse 数据建模 中,我们探讨了如何在 ClickHouse 中使用物化视图 预先计算聚合、转换行,以及针对不同访问模式优化查询。对于后者,我们给出了一个示例,其中 物化视图会将行写入一个目标表,该表的排序键与接收插入的原始表不同。
例如,考虑以下查询:
此查询需要扫描全部 9000 万行数据(尽管速度很快),因为 UserId 并不是排序键。之前,我们通过使用一个充当 PostId 查找索引的物化视图来解决这个问题。使用投影(projection)也可以解决同样的问题。下面的命令添加了一个使用 ORDER BY user_id 的投影。
请注意,我们必须先创建该 projection,然后再对其进行物化。 第二个命令会导致数据在磁盘上按照两种不同的顺序各存储一份。 projection 也可以在创建数据时定义,如下所示,并且会在插入数据时自动维护。
如果通过 ALTER 命令创建投影,那么在执行 MATERIALIZE PROJECTION 命令时,该投影的创建是异步进行的。
用户可以通过以下查询来确认该操作的进度,并等待 is_done=1。
如果我们重复执行上述查询,可以看到性能有了显著提升,但代价是增加了额外的存储开销。
借助 EXPLAIN 命令,我们还能确认该查询确实使用了这个 projection:
┌─explain─────────────────────────────────────────────┐
- │ 表达式 ((投影 + ORDER BY 之前)) │
- │ 聚合 │
- │ 过滤 │
- │ ReadFromMergeTree (comments_user_id) │
- │ 索引: │
- │ PrimaryKey │
- │ Keys: │
- │ UserId │
- │ 条件: (UserId in [8592047, 8592047]) │
- │ 数据部分: 2/2 │
- │ 数据粒度: 2/11360 │ └─────────────────────────────────────────────────────┘
11 行结果。耗时: 0.004 秒。
在 ClickHouse 中重写 BigQuery 查询
下文给出了 BigQuery 与 ClickHouse 的对比查询示例。该列表旨在演示如何利用 ClickHouse 的特性来大幅简化查询。这里的示例使用完整的 Stack Overflow 数据集(截至 2024 年 4 月)。
收到最多浏览量的用户(提问数超过 10 个):
BigQuery

ClickHouse
哪些标签的浏览量最高:
BigQuery

ClickHouse
聚合函数
在条件允许的情况下,用户应尽可能利用 ClickHouse 的聚合函数。下面我们展示如何使用 argMax 函数 来计算每一年浏览次数最多的问题。
BigQuery


ClickHouse
条件和数组
条件和数组函数可以显著简化查询。下面的查询会计算在 2022 年到 2023 年间,出现次数超过 10000 次的标签中,百分比增幅最大的那些标签。请注意,得益于条件函数、数组函数以及在 HAVING 和 SELECT 子句中重复使用别名的能力,下面的 ClickHouse 查询非常简洁。
BigQuery

ClickHouse
至此,我们完成了本基础指南,面向从 BigQuery 迁移到 ClickHouse 的用户。我们建议正在从 BigQuery 迁移的用户阅读 在 ClickHouse 中建模数据 文档,以进一步了解 ClickHouse 的高级功能。