从 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 存储桶中以 Parquet 格式提供了这些表格的数据,以及用于在 BigQuery 中创建和加载表格的 DDL 命令,可在 此处 获取。
迁移数据
在 BigQuery 和 ClickHouse Cloud 之间迁移数据可分为两种主要的工作负载类型
- 初始批量加载并定期更新 - 必须迁移初始数据集,以及定期更新,例如每天更新。这里的更新通过重新发送已更改的行来处理 - 通过可用于比较的列(例如日期)或 XMIN 值来标识。删除通过对数据集进行完整的定期重新加载来处理。
- 实时复制或 CDC - 必须迁移初始数据集。对该数据集的更改必须在 ClickHouse 中以接近实时的速度反映出来,仅接受几秒钟的延迟。这实际上是一个 更改数据捕获 (CDC) 过程,其中 BigQuery 中的表格必须与 ClickHouse 中的表格同步,即 BigQuery 表格中的插入、更新和删除必须应用于 ClickHouse 中的等效表格。
通过 Google Cloud Storage (GCS) 进行批量加载
BigQuery 支持将数据导出到 Google 的对象存储 (GCS)。对于我们的示例数据集
将 7 个表格导出到 GCS。该命令可在 此处 获取。
将数据导入 ClickHouse Cloud。为此,我们可以使用 gcs 表函数。DDL 和导入查询可在 此处 获取。请注意,由于 ClickHouse Cloud 实例由多个计算节点组成,因此我们使用 s3Cluster 表函数 而不是
gcs
表函数。此函数也适用于 gcs 存储桶,并且 利用 ClickHouse Cloud 服务的所有节点 以并行方式加载数据。
这种方法有很多优势
- BigQuery 导出功能支持用于导出数据子集的过滤器。
- BigQuery 支持导出到 Parquet、Avro、JSON 和 CSV 格式以及几种 压缩类型 - 所有这些都受 ClickHouse 支持。
- GCS 支持 对象生命周期管理,允许在指定时间段后删除已导出并导入 ClickHouse 的数据。
- Google 允许每天免费导出高达 50 TB 的数据到 GCS。用户仅需为 GCS 存储付费。
- 导出会自动生成多个文件,每个文件最多包含 1 GB 的表格数据。这对 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');
我们在新的架构中不保留任何空值。上面的插入会将其隐式转换为其各自类型的默认值 - 整数为 0,字符串为空值。ClickHouse 也会自动将任何数值转换为其目标精度。
ClickHouse 主键有何不同?
如 此处 所述,与 BigQuery 一样,ClickHouse 不会强制执行表格主键列值的唯一性。
与 BigQuery 中的集群类似,ClickHouse 表格的数据存储在磁盘上,按主键列排序。查询优化器利用这种排序顺序来防止重新排序,最大限度地减少连接的内存使用,并启用对 limit 子句的短路。与 BigQuery 不同,ClickHouse 会根据主键列值自动创建 (稀疏)主键索引。此索引用于加速包含主键列过滤器的所有查询。具体来说
- 内存和磁盘效率是 ClickHouse 通常使用的规模的关键。数据以称为部分的块写入 ClickHouse 表格,并应用规则在后台合并部分。在 ClickHouse 中,每个部分都有自己的主键索引。当部分合并时,合并后的部分的主键索引也会合并。请注意,这些索引并非针对每一行构建。相反,部分的主键索引每个行组有一个索引条目 - 这种技术称为稀疏索引。
- 稀疏索引之所以可能,是因为 ClickHouse 将部分的行存储在磁盘上,并按指定键排序。稀疏主索引不会直接定位单个行(如基于 B 树的索引),而是允许它快速(通过对索引条目进行二进制搜索)识别可能匹配查询的行组。然后,以并行方式将位于的可能匹配的行组流入 ClickHouse 引擎,以找到匹配项。这种索引设计允许主索引很小(完全适合主内存),同时仍然显着提高查询执行时间,尤其是对于数据分析用例中常见的范围查询。有关更多详细信息,我们建议您参阅此深入指南。
在 ClickHouse 中选择的主键不仅会确定索引,还会确定数据写入磁盘的顺序。因此,它会极大地影响压缩级别,进而影响查询性能。导致大多数列的值以连续顺序写入的排序键将允许选定的压缩算法(和编解码器)更有效地压缩数据。
表中的所有列都将根据指定排序键的值进行排序,无论它们是否包含在键本身中。例如,如果使用
CreationDate
作为键,则所有其他列中值的顺序将对应于CreationDate
列中值的顺序。可以指定多个排序键 - 这将以与SELECT
查询中ORDER BY
子句相同的语义进行排序。
选择排序键
有关选择排序键的注意事项和步骤,以 posts 表为例,请参阅此处。
数据建模技术
我们建议从 BigQuery 迁移的用户阅读在 ClickHouse 中建模数据的指南。本指南使用相同的 Stack Overflow 数据集,并探讨了使用 ClickHouse 功能的多种方法。
分区
BigQuery 用户将熟悉表分区的概念,该概念通过将表划分为称为分区的更小、更易于管理的块来提高大型数据库的性能和可管理性。这种分区可以通过指定列(例如日期)上的范围、定义的列表或通过键上的散列来实现。这允许管理员根据特定标准(如日期范围或地理位置)组织数据。
分区有助于通过启用通过分区修剪更快的访问数据和更有效的索引来提高查询性能。它还有助于维护任务,如备份和数据清除,方法是允许对单个分区而不是整个表进行操作。此外,分区可以通过将负载分布到多个分区来显着提高 BigQuery 数据库的可扩展性。
在 ClickHouse 中,分区是在初始定义表时通过PARTITION BY
子句指定的。该子句可以包含任何列/s 上的 SQL 表达式,其结果将定义将行发送到哪个分区。
数据部分在磁盘上的每个分区上逻辑关联,可以独立查询。对于下面的示例,我们使用表达式toYear(CreationDate)
按年份对 posts 表进行分区。当行插入到 ClickHouse 时,此表达式将针对每个行进行评估 - 然后将行路由到结果分区,形成属于该分区的新的数据部分。
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 年的帖子
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.
- 查询优化 - 虽然分区可以帮助提高查询性能,但这在很大程度上取决于访问模式。如果查询仅针对少数分区(理想情况下为一个分区),则性能可能会提高。这通常仅在分区键不在主键中且您按它进行过滤时才有用。但是,需要涵盖多个分区的查询可能会比不使用分区的情况更糟糕(因为可能存在更多部分,这是分区的结果)。如果分区键已经是主键中的早期条目,则针对单个分区的优势将不那么明显,甚至不存在。如果每个分区中的值是唯一的,分区也可以用来优化
GROUP BY
查询。但是,一般来说,用户应该确保主键已优化,并且仅在访问模式访问特定可预测的一天子集(例如,按天分区,大多数查询在最后一天)的特殊情况下才将分区视为查询优化技术。
建议
用户应该将分区视为数据管理技术。当需要在操作时间序列数据时(例如,可以简单地删除最旧的分区)从集群中过期数据时,它是理想的选择。
重要:确保您的分区键表达式不会导致高基数集,即应避免创建超过 100 个分区。例如,不要按高基数列(如客户标识符或姓名)对数据进行分区。相反,使客户标识符或姓名成为ORDER BY
表达式中的第一列。
在内部,ClickHouse为插入的数据创建部分。随着更多数据的插入,部分的数量会增加。为了防止部分数量过高(这会降低查询性能(因为需要读取更多文件)),部分将在后台异步进程中合并在一起。如果部分数量超过预先配置的限制,则 ClickHouse 会在插入时抛出异常,作为"部分过多"错误。在正常操作下这不会发生,并且仅在 ClickHouse 配置错误或使用不当(例如许多小型插入)时才会发生。由于部分是针对每个分区独立创建的,因此增加分区数量会导致部分数量增加,即它是分区数量的倍数。因此,高基数分区键会导致此错误,应避免。
物化视图与投影
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.
此查询需要扫描所有 9000 万行(诚然很快),因为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 次)。请注意,以下 ClickHouse 查询由于条件语句、数组函数以及在 HAVING
和 SELECT
子句中重用别名的功能而非常简洁。
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 功能。