本指南假定用户已经熟悉增量materialized view的概念,以及使用 s3 和 gcs 等 table function 进行数据加载。我们也建议用户阅读我们的优化从对象存储进行插入性能指南,其中的建议同样适用于本指南中的各类插入操作。
示例数据集
pip 等工具下载 Python 软件包的记录。
例如,这个子集仅涵盖一天的数据——2024-12-17,并可在 https://datasets-documentation.s3.eu-west-3.amazonaws.com/pypi/2024-12-17/ 公开获取。你可以使用以下方式查询:
完整的 PyPI 数据集包含超过 1 万亿行,可在我们的公开演示环境 clickpy.clickhouse.com 中查看。有关该数据集的更多信息,包括此演示如何利用 materialized view 提升性能,以及数据如何按日填充,请参见此处。
回填场景
- 在现有数据摄取的情况下回填数据 - 新数据正在持续加载,同时需要回填历史数据。这些历史数据已经明确。
- 向现有表添加 materialized view - 需要在一套历史数据已完成填充且数据已在持续stream的系统中添加新的 materialized view。
使用副本表与视图
pypi 表和 materialized view,用于计算每个 Python 项目的下载次数:
{101..200}。虽然可以直接向 pypi 插入数据,但我们也可以通过创建副本表来隔离执行此次回填操作。
如果回填失败,主表不会受到任何影响,只需对副本表执行 truncate 操作并重新执行即可。
要创建这些视图的新副本,可以使用带有后缀 _v2 的 CREATE TABLE AS 子句:
pypi_v2 和 pypi_downloads_v2,然后重新加载数据即可。
数据加载完成后,我们可以使用 ALTER TABLE MOVE PARTITION 子句,将数据从副本表移动到主表。
分区名称上面的
MOVE PARTITION 调用使用的分区名称是 ()。这表示该表只有一个分区 (即未分区) 。对于已分区的表,你需要多次调用 MOVE PARTITION——每个分区调用一次。当前分区的名称可以从 system.parts 表中查询,例如 SELECT DISTINCT partition FROM system.parts WHERE (table = 'pypi_v2')。pypi 和 pypi_downloads 已包含完整数据。pypi_downloads_v2 和 pypi_v2 可以安全地删除。
MOVE PARTITION 操作既轻量 (利用硬链接) ,又具备原子性;也就是说,它要么失败,要么成功,不会出现中间状态。
在下面的数据回填场景中,我们会大量利用这一过程。
请注意,这一过程要求用户自行确定每次插入操作的规模。
插入越大,也就是行数越多,所需的 MOVE PARTITION 操作就越少。不过,这需要与插入失败时 (例如因网络中断) 所带来的恢复成本进行权衡。你还可以通过将文件分批处理来降低风险。这既可以通过范围查询实现,例如 WHERE timestamp BETWEEN 2024-12-17 09:00:00 AND 2024-12-17 10:00:00,也可以通过 glob 模式实现。例如,
ClickPipes 在从对象存储加载数据时会采用这种方法,自动为目标表及其 materialized view 创建副本,从而无需用户执行上述步骤。再配合多个工作线程,每个工作线程分别处理不同的子集 (通过 glob 模式划分) ,并使用各自的副本表,即可快速加载数据,同时保证精确一次语义。感兴趣的读者可在这篇博客中了解更多细节。
场景 1:在现有数据持续摄取时回填数据
- 确定检查点——即需要恢复历史数据的时间戳或列值。
- 为主表和 materialized view 的目标表创建副本。
- 为所有指向步骤 (2) 中所创建目标表的 materialized view 创建副本。
- 向我们在步骤 (2) 中创建的主表副本插入数据。
- 将所有分区从副本表移动到原始表。删除副本表。
2024-12-17 09:00:00 之前的数据。按照前面介绍的流程,我们创建副本表和视图,并通过基于 timestamp 的过滤器加载这部分子集数据。
对 Parquet 中的 timestamp 列进行过滤通常非常高效。ClickHouse 只需读取 timestamp 列即可确定需要加载的完整数据范围,从而尽可能减少网络流量。ClickHouse 查询引擎还可以利用 Parquet 索引,例如最小值-最大值索引。
直接使用 ClickHouse Cloud 中的 ClickPipes如果你使用的是 ClickHouse Cloud,并且数据可以单独隔离在自己的存储桶中 (即不需要过滤器) ,那么应使用 ClickPipes 来恢复历史备份。除了通过多个工作线程并行加载来缩短加载时间外,ClickPipes 还会自动完成上述流程,并为主表和 materialized view 创建副本表。
场景 2:向现有表添加 materialized views
避免使用 POPULATE除了在已暂停摄取的小型数据集上为 materialized views 执行回填之外,我们不建议使用
POPULATE 命令。该操作可能会遗漏插入其源表的行,因为 materialized view 是在 POPULATE 完成后才创建的。此外,POPULATE 会对全部数据运行,在大型数据集上很容易受到中断或内存限制的影响。时间戳或单调递增列可用
- 创建 materialized view,并添加一个过滤条件,使其只处理大于最近未来某个任意时间点的行。
- 运行
INSERT INTO SELECT查询,将数据插入 materialized view 的目标表,同时按该视图的聚合查询从源表读取数据。
SELECT 子句,加入一个过滤条件,只考虑时间晚于不久后某个任意时刻的行——这里我们假设 2024-12-17 09:00:00 是几分钟之后的时间。
INSERT INTO SELECT 将结果插入该视图的目标表。例如,对于上述视图:
在上述示例中,我们的目标表是 SummingMergeTree。在这种情况下,我们可以直接使用原始的聚合查询。对于使用 AggregatingMergeTree 的更复杂用例,你需要对聚合使用
-State 函数。相关示例可参见这篇集成指南。pypi_downloads_per_day_v2,向其中插入数据,然后将其生成的分区附加到 pypi_downloads_per_day。
materialized view 的查询通常会更复杂 (这并不少见,否则用户也不会使用视图!) ,而且会消耗资源。在极少数情况下,查询所需的资源甚至会超出服务器的承载能力。这也凸显了 ClickHouse materialized view 的一个优势——它们是增量式的,不会一次处理整个数据集!
在这种情况下,用户有以下几种选择:
- 修改查询以回填各个时间范围,例如
WHERE timestamp BETWEEN 2024-12-17 08:00:00 AND 2024-12-17 09:00:00、WHERE timestamp BETWEEN 2024-12-17 07:00:00 AND 2024-12-17 08:00:00等。 - 使用 Null 表引擎 来填充 materialized view。这样可以模拟 materialized view 典型的增量填充过程,在数据块 (大小可配置) 上执行其查询。
使用 Null 表引擎填充 materialized view
/dev/null) 。虽然这看起来有些矛盾,但 materialized view 仍会基于插入到该表引擎中的数据执行。这使我们能够在不持久化原始数据的情况下构建 materialized view,从而避免 I/O 以及相关的存储开销。
需要注意的是,任何关联到该表引擎的 materialized views 在数据插入时仍会按数据块执行,并将结果发送到目标表。这些块的大小可以配置。更大的块虽然可能更高效 (处理速度也更快) ,但也会消耗更多资源 (主要是内存) 。使用这种表引擎意味着我们可以以增量方式构建 materialized view,即一次处理一个块,从而避免将整个聚合保存在内存中。
请看下面的示例:
pypi_v2,,用于接收将被用来构建 materialized view 的行。请注意,我们将 schema 限制为仅包含所需的列。我们的 materialized view 会对插入到该表中的行执行聚合 (一次处理一个块) ,并将结果发送到目标表 pypi_downloads_per_day。
这里我们使用
pypi_downloads_per_day 作为目标表。为了提高弹性,用户可以创建一个副本表 pypi_downloads_per_day_v2,并像前面的示例所示那样将其用作该视图的目标表。插入完成后,pypi_downloads_per_day_v2 中的分区随后可以移动到 pypi_downloads_per_day。这样一来,如果插入因内存问题或服务器中断而失败,我们就能够进行恢复;也就是说,只需 TRUNCATE pypi_downloads_per_day_v2、调整 settings,然后重试即可。pypi 中用于回填的相关数据插入到 pypi_v2 中。
639.47 MiB。
性能与资源调优
- 读取并行度 - 用于读取操作的线程数。通过
max_threads控制。在 ClickHouse Cloud 中,该值由实例规格决定,默认等于 vCPU 数量。提高该值可能会提升读取性能,但代价是更高的内存使用量。 - 插入并行度 - 用于执行插入操作的线程数。通过
max_insert_threads控制。注意:该值受max_threads限制,因此实际插入并行度为min(max_insert_threads, max_threads)。在 ClickHouse Cloud 中,该值由实例大小决定 (介于 2 到 4 之间) ,而在 OSS 中固定为 1。增大该值可能会提升性能,但代价是更高的内存使用量。 - 插入块大小 - 数据会在一个循环中处理:先被拉取、解析,再根据分区键组成为内存中的插入块。这些块随后会被排序、优化、压缩,并作为新的数据分区片段写入存储。插入块的大小由设置
min_insert_block_size_rows和min_insert_block_size_bytes(未压缩) 控制,会影响内存占用和磁盘 I/O。较大的块会占用更多内存,但会生成更少的 parts,从而减少 I/O 和后台合并。这些设置表示最小阈值 (哪个先达到,就会先触发一次刷盘) 。 - materialized view 的块大小 - 除了上述适用于主插入的机制外,在插入到 materialized view 之前,块也会先合并,以提升处理效率。这些块的大小由设置
min_insert_block_size_bytes_for_materialized_views和min_insert_block_size_rows_for_materialized_views决定。块越大,处理效率通常越高,但代价是内存占用也会增加。默认情况下,这些设置会分别回退到源表设置min_insert_block_size_rows和min_insert_block_size_bytes的值。
简单 INSERT SELECT 查询提示:对于不含复杂转换的简单
INSERT INTO t1 SELECT * FROM t2 查询,可考虑启用 optimize_trivial_insert_select=1。该设置 (自 24.7 版本起默认禁用) 会自动调整 SELECT 的并行度,使其与 max_insert_threads 一致,从而减少资源占用以及创建的 parts 数量。这对于在表之间进行大批量数据迁移尤其实用。min_insert_block_size_bytes_for_materialized_views 和 min_insert_block_size_rows_for_materialized_views 来提升性能。如需修改这些参数,请遵循针对 min_insert_block_size_rows 和 min_insert_block_size_bytes 所讨论的最佳实践。
若要降低内存占用,可以尝试调整这些设置,但这不可避免地会影响性能。以下使用前面的查询展示相关示例。
将 max_insert_threads 降低至 1 可减少内存开销。
max_threads 设置调低至 1,以进一步减少内存占用。
min_insert_block_size_rows 设为 0 (不再将其作为决定块大小的因素) ,并将 min_insert_block_size_bytes 设为 10485760 (10MiB) ,以进一步降低内存占用。
没有时间戳或单调递增列
- 暂停向主表插入数据。
- 使用
CREATE AS语法为主目标表创建一个副本表。 - 使用
ALTER TABLE ATTACH将原始目标表中的分区附加到副本表。注意: 此处的 attach 操作不同于前文使用的 move。虽然它依赖硬链接,但原始表中的数据会被保留。 - 创建新的 materialized view。
- 恢复插入。注意: 插入操作只会更新目标表,不会更新副本表;副本表只会引用原始数据。
- 对 materialized view 进行回填,沿用上文针对带时间戳数据的相同步骤,并将副本表作为来源表。
pypi_downloads_per_day (假设我们无法使用时间戳) :
INSERT INTO SELECT 方法,对 pypi_downloads_per_day 执行回填。也可以进一步采用上文所述的 Null 表方法,并可选择使用副本表来增强稳健性。
虽然此操作确实需要暂停插入,但中间步骤通常都能很快完成,从而将数据中断降至最低。