从 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 模式如下所示

对于希望将此数据集填充到 BigQuery 实例以测试迁移步骤的用户,我们已在 GCS bucket 中以 Parquet 格式提供了这些表的数据,并且用于在 BigQuery 中创建和加载表的 DDL 命令在此处 提供。
迁移数据
在 BigQuery 和 ClickHouse Cloud 之间迁移数据主要分为两种工作负载类型
- 初始批量加载与定期更新 - 必须迁移初始数据集,并按设定的时间间隔(例如,每天)进行定期更新。此处的更新通过重新发送已更改的行来处理 - 通过可用于比较的列(例如,日期)来识别。删除通过数据集的完整定期重新加载来处理。
- 实时复制或 CDC - 必须迁移初始数据集。对此数据集的更改必须近乎实时地反映在 ClickHouse 中,仅可接受几秒的延迟。这实际上是一个 变更数据捕获 (CDC) 过程,其中 BigQuery 中的表必须与 ClickHouse 同步,即 BigQuery 表中的插入、更新和删除必须应用于 ClickHouse 中的等效表。
通过 Google Cloud Storage (GCS) 批量加载
BigQuery 支持将数据导出到 Google 的对象存储 (GCS)。对于我们的示例数据集
-
将数据导入 ClickHouse Cloud。为此,我们可以使用 gcs 表函数。DDL 和导入查询在此处 提供。请注意,由于 ClickHouse Cloud 实例由多个计算节点组成,因此我们使用的是 s3Cluster 表函数 而不是
gcs
表函数。此函数也适用于 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 模式如下所示
CREATE TABLE stackoverflow.posts (
id INTEGER,
posttypeid INTEGER,
acceptedanswerid STRING,
creationdate TIMESTAMP,
score INTEGER,
viewcount INTEGER,
body STRING,
owneruserid INTEGER,
ownerdisplayname STRING,
lasteditoruserid STRING,
lasteditordisplayname STRING,
lasteditdate TIMESTAMP,
lastactivitydate TIMESTAMP,
title STRING,
tags STRING,
answercount INTEGER,
commentcount INTEGER,
favoritecount INTEGER,
conentlicense STRING,
parentid STRING,
communityowneddate TIMESTAMP,
closeddate TIMESTAMP
);
优化类型
应用 此处描述 的过程将得到以下模式
CREATE TABLE stackoverflow.posts
(
`Id` Int32,
`PostTypeId` Enum('Question' = 1, 'Answer' = 2, 'Wiki' = 3, 'TagWikiExcerpt' = 4, 'TagWiki' = 5, 'ModeratorNomination' = 6, 'WikiPlaceholder' = 7, 'PrivilegeWiki' = 8),
`AcceptedAnswerId` UInt32,
`CreationDate` DateTime,
`Score` Int32,
`ViewCount` UInt32,
`Body` String,
`OwnerUserId` Int32,
`OwnerDisplayName` String,
`LastEditorUserId` Int32,
`LastEditorDisplayName` String,
`LastEditDate` DateTime,
`LastActivityDate` DateTime,
`Title` String,
`Tags` String,
`AnswerCount` UInt16,
`CommentCount` UInt8,
`FavoriteCount` UInt8,
`ContentLicense`LowCardinality(String),
`ParentId` String,
`CommunityOwnedDate` DateTime,
`ClosedDate` DateTime
)
ENGINE = MergeTree
ORDER BY tuple()
COMMENT 'Optimized types'
我们可以使用简单的 INSERT INTO SELECT
填充此表,使用 gcs
表函数 从 gcs 读取导出的数据。请注意,在 ClickHouse Cloud 上,您还可以使用与 gcs 兼容的 s3Cluster
表函数 在多个节点上并行加载。
INSERT INTO stackoverflow.posts SELECT * FROM gcs( 'gs://clickhouse-public-datasets/stackoverflow/parquet/posts/*.parquet', NOSIGN);
我们在新模式中不保留任何 null 值。上面的插入操作会将这些值隐式转换为其各自类型的默认值 - 整数为 0,字符串为空值。ClickHouse 还会自动将任何数值转换为其目标精度。
ClickHouse 的主键有何不同?
正如 此处 所述,与 BigQuery 一样,ClickHouse 不强制表的主键列值的唯一性。
与 BigQuery 中的聚簇类似,ClickHouse 表的数据在磁盘上按主键列排序存储。查询优化器利用此排序顺序来防止重新排序,最大限度地减少连接的内存使用,并为 limit 子句启用短路。与 BigQuery 相比,ClickHouse 基于主键列值自动创建 (稀疏)主索引。此索引用于加速所有包含主键列过滤器的查询。具体来说
- 内存和磁盘效率对于 ClickHouse 经常使用的规模至关重要。数据以称为“parts”的块写入 ClickHouse 表,并应用规则在后台合并 parts。在 ClickHouse 中,每个 part 都有自己的主索引。当 parts 合并时,合并后的 part 的主索引也会合并。请注意,这些索引不是为每行构建的。相反,part 的主索引每个行组只有一个索引条目 - 此技术称为稀疏索引。
- 稀疏索引之所以可行,是因为 ClickHouse 将 part 的行按指定的键排序后存储在磁盘上。稀疏主索引不是直接定位单行(如基于 B 树的索引),而是允许快速(通过对索引条目进行二分搜索)识别可能与查询匹配的行组。然后,将定位到的可能匹配的行组并行流式传输到 ClickHouse 引擎中,以查找匹配项。这种索引设计使主索引可以很小(完全适合主内存),同时仍然显着加快查询执行时间,特别是对于数据分析用例中常见的范围查询。有关更多详细信息,我们建议阅读 这份深入指南。

在 ClickHouse 中选择的主键不仅决定索引,还决定数据写入磁盘的顺序。因此,它可能会极大地影响压缩级别,进而影响查询性能。使大多数列的值以连续顺序写入的排序键将使所选的压缩算法(和编解码器)能够更有效地压缩数据。
表中的所有列都将根据指定的排序键的值进行排序,无论它们是否包含在键本身中。例如,如果使用
CreationDate
作为键,则所有其他列中的值顺序将与CreationDate
列中的值顺序相对应。可以指定多个排序键 - 这将以与SELECT
查询中的ORDER BY
子句相同的语义进行排序。
选择排序键
有关选择排序键的注意事项和步骤,以 posts 表为例,请参阅 此处。
数据建模技术
我们建议从 BigQuery 迁移的用户阅读 ClickHouse 数据建模指南。本指南使用相同的 Stack Overflow 数据集,并探讨了使用 ClickHouse 功能的多种方法。
分区
BigQuery 用户将熟悉表分区的概念,它通过将表划分为更小、更易于管理的部分(称为分区)来增强大型数据库的性能和可管理性。可以使用指定列上的范围(例如,日期)、定义的列表或基于键的哈希来实现此分区。这使管理员可以根据特定标准(如日期范围或地理位置)组织数据。
分区通过分区裁剪和更高效的索引实现更快的数据访问,从而有助于提高查询性能。它还有助于备份和数据清除等维护任务,因为它允许对单个分区而不是整个表进行操作。此外,分区可以通过在多个分区之间分配负载来显着提高 BigQuery 数据库的可扩展性。
在 ClickHouse 中,分区是在最初通过 PARTITION BY
子句定义表时指定的。此子句可以包含任何列上的 SQL 表达式,其结果将定义行发送到哪个分区。

数据 parts 在逻辑上与磁盘上的每个分区关联,并且可以单独查询。对于下面的示例,我们使用表达式 toYear(CreationDate)
按年份对 posts 表进行分区。当行插入到 ClickHouse 中时,将针对每行评估此表达式 – 然后将行以属于该分区的新数据 parts 的形式路由到结果分区。
CREATE TABLE posts
(
`Id` Int32 CODEC(Delta(4), ZSTD(1)),
`PostTypeId` Enum8('Question' = 1, 'Answer' = 2, 'Wiki' = 3, 'TagWikiExcerpt' = 4, 'TagWiki' = 5, 'ModeratorNomination' = 6, 'WikiPlaceholder' = 7, 'PrivilegeWiki' = 8),
`AcceptedAnswerId` UInt32,
`CreationDate` DateTime64(3, 'UTC'),
...
`ClosedDate` DateTime64(3, 'UTC')
)
ENGINE = MergeTree
ORDER BY (PostTypeId, toDate(CreationDate), CreationDate)
PARTITION BY toYear(CreationDate)
应用
ClickHouse 中的分区与 BigQuery 中的分区具有类似的应用,但存在一些细微的差异。更具体地说
- 数据管理 - 在 ClickHouse 中,用户应主要将分区视为数据管理功能,而不是查询优化技术。通过基于键在逻辑上分离数据,可以独立操作每个分区,例如删除。这允许用户按时有效地在 存储层 之间移动分区以及子集,或使数据过期/从集群中有效删除数据。在下面的示例中,我们删除了 2008 年的 posts
SELECT DISTINCT partition
FROM system.parts
WHERE `table` = 'posts'
┌─partition─┐
│ 2008 │
│ 2009 │
│ 2010 │
│ 2011 │
│ 2012 │
│ 2013 │
│ 2014 │
│ 2015 │
│ 2016 │
│ 2017 │
│ 2018 │
│ 2019 │
│ 2020 │
│ 2021 │
│ 2022 │
│ 2023 │
│ 2024 │
└───────────┘
17 rows in set. Elapsed: 0.002 sec.
ALTER TABLE posts
(DROP PARTITION '2008')
Ok.
0 rows in set. Elapsed: 0.103 sec.
- 查询优化 - 虽然分区可以帮助提高查询性能,但这很大程度上取决于访问模式。如果查询仅针对少量分区(理想情况下为一个),则性能可能会提高。这通常仅在分区键不在主键中且您按其进行过滤时才有用。但是,需要覆盖许多分区的查询的性能可能比不使用分区时更差(因为分区可能会导致更多 parts)。如果分区键已经是主键中的早期条目,则针对单个分区的好处将变得更不明显甚至不存在。如果每个分区中的值都是唯一的,则分区还可以用于优化
GROUP BY
查询。但是,总的来说,用户应确保主键已优化,并且仅在访问模式访问一天中特定的可预测子集(例如,按天分区,大多数查询都在最后一天)的特殊情况下才考虑将分区作为查询优化技术。
建议
用户应将分区视为一种数据管理技术。当需要从集群中删除数据时,尤其是在处理时间序列数据时,分区是理想的选择,例如,最旧的分区可以 简单地删除。
重要提示:确保您的分区键表达式不会导致高基数集,即应避免创建超过 100 个分区。例如,不要按高基数列(如客户端标识符或名称)对数据进行分区。相反,请将客户端标识符或名称作为 ORDER BY
表达式中的第一列。
在内部,ClickHouse 为插入的数据创建 parts。随着插入更多数据,parts 的数量会增加。为了防止 parts 数量过多(这会降低查询性能,因为需要读取更多文件),parts 在后台异步进程中合并在一起。如果 parts 的数量超过 预配置的限制,则 ClickHouse 将在插入时抛出异常,提示 “parts 过多”错误。这在正常操作下不应发生,仅在 ClickHouse 配置错误或使用不当(例如,许多小插入)时才会发生。由于 parts 是在每个分区中隔离创建的,因此增加分区数量会导致 parts 数量增加,即它是分区数量的倍数。因此,高基数分区键可能会导致此错误,应避免使用。
物化视图与投影
ClickHouse 的投影概念允许用户为表指定多个 ORDER BY
子句。
在 ClickHouse 数据建模 中,我们探讨了如何在 ClickHouse 中使用物化视图来预计算聚合、转换行以及针对不同的访问模式优化查询。对于后者,我们 提供了一个示例,其中物化视图将行发送到目标表,该目标表的排序键与接收插入的原始表不同。
例如,考虑以下查询
SELECT avg(Score)
FROM comments
WHERE UserId = 8592047
┌──────────avg(Score)─┐
│ 0.18181818181818182 │
└────────────────────┘
1 row in set. Elapsed: 0.040 sec. Processed 90.38 million rows, 361.59 MB (2.25 billion rows/s., 9.01 GB/s.)
Peak memory usage: 201.93 MiB.
此查询需要扫描所有 9 千万行(诚然速度很快),因为 UserId
不是排序键。以前,我们使用物化视图作为 PostId
的查找来解决此问题。同样的问题可以使用投影来解决。以下命令为 ORDER BY user_id
添加投影。
ALTER TABLE comments ADD PROJECTION comments_user_id (
SELECT * ORDER BY UserId
)
ALTER TABLE comments MATERIALIZE PROJECTION comments_user_id
请注意,我们必须首先创建投影,然后将其物化。后一个命令会导致数据以两种不同的顺序在磁盘上存储两次。投影也可以在创建数据时定义,如下所示,并且将在插入数据时自动维护。
CREATE TABLE comments
(
`Id` UInt32,
`PostId` UInt32,
`Score` UInt16,
`Text` String,
`CreationDate` DateTime64(3, 'UTC'),
`UserId` Int32,
`UserDisplayName` LowCardinality(String),
PROJECTION comments_user_id
(
SELECT *
ORDER BY UserId
)
)
ENGINE = MergeTree
ORDER BY PostId
如果投影是通过 ALTER
命令创建的,则在发出 MATERIALIZE PROJECTION
命令时,创建是异步的。用户可以使用以下查询确认此操作的进度,等待 is_done=1
。
SELECT
parts_to_do,
is_done,
latest_fail_reason
FROM system.mutations
WHERE (`table` = 'comments') AND (command LIKE '%MATERIALIZE%')
┌─parts_to_do─┬─is_done─┬─latest_fail_reason─┐
1. │ 1 │ 0 │ │
└─────────────┴─────────┴────────────────────┘
1 row in set. Elapsed: 0.003 sec.
如果我们重复上面的查询,我们可以看到性能得到了显着提高,但代价是额外的存储。
SELECT avg(Score)
FROM comments
WHERE UserId = 8592047
┌──────────avg(Score)─┐
1. │ 0.18181818181818182 │
└─────────────────────┘
1 row in set. Elapsed: 0.008 sec. Processed 16.36 thousand rows, 98.17 KB (2.15 million rows/s., 12.92 MB/s.)
Peak memory usage: 4.06 MiB.
使用 EXPLAIN
命令,我们还可以确认投影用于服务此查询
EXPLAIN indexes = 1
SELECT avg(Score)
FROM comments
WHERE UserId = 8592047
┌─explain─────────────────────────────────────────────┐
1. │ Expression ((Projection + Before ORDER BY)) │
2. │ Aggregating │
3. │ Filter │
4. │ ReadFromMergeTree (comments_user_id) │
5. │ Indexes: │
6. │ PrimaryKey │
7. │ Keys: │
8. │ UserId │
9. │ Condition: (UserId in [8592047, 8592047]) │
10. │ Parts: 2/2 │
11. │ Granules: 2/11360 │
└─────────────────────────────────────────────────────┘
11 rows in set. Elapsed: 0.004 sec.
何时使用投影
对于新用户来说,投影是一个很有吸引力的功能,因为它们会在插入数据时自动维护。此外,查询只需发送到单个表,在可能的情况下,可以利用投影来加快响应时间。

这与物化视图形成对比,在物化视图中,用户必须根据过滤器选择适当的优化目标表或重写其查询。这更加强调了用户应用程序,并增加了客户端复杂性。
尽管有这些优点,但投影也存在一些固有的局限性,用户应注意这些局限性,因此应谨慎部署
- 投影不允许为源表和(隐藏的)- 目标表使用不同的 TTL。物化视图允许不同的 TTL。
- 投影目前不支持(隐藏的)目标表的
optimize_read_in_order
。 - 对于具有投影的表,不支持轻量级更新和删除。
- 物化视图可以链接:一个物化视图的目标表可以是另一个物化视图的源表,依此类推。投影无法实现这一点。
- 投影不支持连接;物化视图支持。
- 投影不支持过滤器 (
WHERE
子句);物化视图支持。
我们建议在以下情况下使用投影
- 需要完全重新排序数据。虽然理论上投影中的表达式可以使用
GROUP BY
,但物化视图更有效地维护聚合。查询优化器也更可能利用使用简单重新排序的投影,即SELECT * ORDER BY x
。用户可以在此表达式中选择列的子集以减少存储占用。 - 用户可以接受相关的存储占用增加以及两次写入数据的开销。测试对插入速度的影响并评估存储开销。
在 ClickHouse 中重写 BigQuery 查询
以下提供比较 BigQuery 和 ClickHouse 的示例查询。此列表旨在演示如何利用 ClickHouse 功能来显着简化查询。此处的示例使用完整的 Stack Overflow 数据集(截至 2024 年 4 月)。
收到最多浏览量的用户(提问超过 10 个)
BigQuery

ClickHouse
SELECT
OwnerDisplayName,
sum(ViewCount) AS total_views
FROM stackoverflow.posts
WHERE (PostTypeId = 'Question') AND (OwnerDisplayName != '')
GROUP BY OwnerDisplayName
HAVING count() > 10
ORDER BY total_views DESC
LIMIT 5
┌─OwnerDisplayName─┬─total_views─┐
1. │ Joan Venge │ 25520387 │
2. │ Ray Vega │ 21576470 │
3. │ anon │ 19814224 │
4. │ Tim │ 19028260 │
5. │ John │ 17638812 │
└──────────────────┴─────────────┘
5 rows in set. Elapsed: 0.076 sec. Processed 24.35 million rows, 140.21 MB (320.82 million rows/s., 1.85 GB/s.)
Peak memory usage: 323.37 MiB.
哪些标签收到最多浏览量
BigQuery

ClickHouse
-- ClickHouse
SELECT
arrayJoin(arrayFilter(t -> (t != ''), splitByChar('|', Tags))) AS tags,
sum(ViewCount) AS views
FROM stackoverflow.posts
GROUP BY tags
ORDER BY views DESC
LIMIT 5
┌─tags───────┬──────views─┐
1. │ javascript │ 8190916894 │
2. │ python │ 8175132834 │
3. │ java │ 7258379211 │
4. │ c# │ 5476932513 │
5. │ android │ 4258320338 │
└────────────┴────────────┘
5 rows in set. Elapsed: 0.318 sec. Processed 59.82 million rows, 1.45 GB (188.01 million rows/s., 4.54 GB/s.)
Peak memory usage: 567.41 MiB.
聚合函数
在可能的情况下,用户应利用 ClickHouse 聚合函数。下面,我们展示了如何使用 argMax
函数 来计算每年浏览量最高的问题。
BigQuery


ClickHouse
-- ClickHouse
SELECT
toYear(CreationDate) AS Year,
argMax(Title, ViewCount) AS MostViewedQuestionTitle,
max(ViewCount) AS MaxViewCount
FROM stackoverflow.posts
WHERE PostTypeId = 'Question'
GROUP BY Year
ORDER BY Year ASC
FORMAT Vertical
Row 1:
──────
Year: 2008
MostViewedQuestionTitle: How to find the index for a given item in a list?
MaxViewCount: 6316987
Row 2:
──────
Year: 2009
MostViewedQuestionTitle: How do I undo the most recent local commits in Git?
MaxViewCount: 13962748
…
Row 16:
───────
Year: 2023
MostViewedQuestionTitle: How do I solve "error: externally-managed-environment" every time I use pip 3?
MaxViewCount: 506822
Row 17:
───────
Year: 2024
MostViewedQuestionTitle: Warning "Third-party cookie will be blocked. Learn more in the Issues tab"
MaxViewCount: 66975
17 rows in set. Elapsed: 0.225 sec. Processed 24.35 million rows, 1.86 GB (107.99 million rows/s., 8.26 GB/s.)
Peak memory usage: 377.26 MiB.
条件和数组
条件和数组函数使查询显着简化。以下查询计算了从 2022 年到 2023 年增长百分比最大的标签(出现次数超过 10000 次)。请注意,由于条件、数组函数以及在 HAVING
和 SELECT
子句中重用别名的能力,以下 ClickHouse 查询是多么简洁。
BigQuery

ClickHouse
SELECT
arrayJoin(arrayFilter(t -> (t != ''), splitByChar('|', Tags))) AS tag,
countIf(toYear(CreationDate) = 2023) AS count_2023,
countIf(toYear(CreationDate) = 2022) AS count_2022,
((count_2023 - count_2022) / count_2022) * 100 AS percent_change
FROM stackoverflow.posts
WHERE toYear(CreationDate) IN (2022, 2023)
GROUP BY tag
HAVING (count_2022 > 10000) AND (count_2023 > 10000)
ORDER BY percent_change DESC
LIMIT 5
┌─tag─────────┬─count_2023─┬─count_2022─┬──────percent_change─┐
│ next.js │ 13788 │ 10520 │ 31.06463878326996 │
│ spring-boot │ 16573 │ 17721 │ -6.478189718413183 │
│ .net │ 11458 │ 12968 │ -11.644046884639112 │
│ azure │ 11996 │ 14049 │ -14.613139725247349 │
│ docker │ 13885 │ 16877 │ -17.72826924216389 │
└─────────────┴────────────┴────────────┴─────────────────────┘
5 rows in set. Elapsed: 0.096 sec. Processed 5.08 million rows, 155.73 MB (53.10 million rows/s., 1.63 GB/s.)
Peak memory usage: 410.37 MiB.
本文总结了我们为从 BigQuery 迁移到 ClickHouse 的用户提供的基本指南。我们建议从 BigQuery 迁移的用户阅读 ClickHouse 数据建模指南,以了解有关高级 ClickHouse 功能的更多信息。