
本文字数:15807;估计阅读时间:40 分钟
作者:Tom Schreiber
本文在公众号【ClickHouselnc】首发

试想一下,你本来准备出门旅行,但到了机场才得知行程取消,于是连行李都不用收拾。ClickHouse 现在处理数据的方式,正是如此——只有在真正需要的时候才“动手”。
作为当前性能最强的分析型数据库之一,ClickHouse 的速度优势很大程度上源于它能避免无谓的计算。它处理的数据越少,查询速度就越快。如今,ClickHouse 引入了一项全新的优化技术——惰性物化。这项技术会在真正需要某列数据之前,暂时不去读取它,从而进一步节省资源。
这种“懒加载”策略在实际场景中表现出色,特别适用于对大数据集进行排序并使用 LIMIT 子句的 Top N 查询,这种模式在可观测性(observability)和通用数据分析中非常常见。在这些场景中,惰性物化可以将性能提升数十倍乃至百倍。
先给你揭个底:我们将演示一个 ClickHouse 查询如何从 219 秒骤降到 139 毫秒,性能提升高达 1,576 倍——无需更改任何 SQL 语句。查询、表结构、服务器都保持不变,唯一的变化,只是 ClickHouse 何时读取数据。
本文将带你深入了解惰性物化的原理,并讲解它如何融入 ClickHouse 的整体 I/O 优化体系。为了完整呈现这项优化的价值,我们也会简单介绍 ClickHouse 在 I/O 层面的其他关键组件,展示惰性物化的独特之处,以及它与已有优化机制之间的协同效果。
我们将从 ClickHouse 已具备的核心 I/O 节省技术讲起,然后逐步演示一个真实查询是如何一层层利用这些优化,直到惰性物化介入并带来决定性的性能飞跃。
ClickHouse 提升 I/O 效率的关键机制
为了最大限度减少 I/O,ClickHouse 多年来持续引入了一系列分层优化技术。这些机制构成了其卓越性能的核心基础:
-
列式存储可以跳过查询中不涉及的整列数据,并通过对相似值进行分组,实现高效压缩,从而在加载数据时大幅减少 I/O 开销。
-
稀疏主索引、数据跳过索引以及投影功能可以筛选出哪些行块(granule)可能匹配查询条件,从而跳过无关数据的读取。这些方法可以单独使用,也可以结合使用,灵活应用于不同场景。
-
PREWHERE 语句还支持对非索引列的过滤条件进行预判断,从而提前排除后续不需要的数据。这不仅可以独立工作,也可以与索引机制配合,进一步优化 granule 的选取过程。
-
查询条件缓存(query condition cache)则用于加速重复查询。它记录上次查询中哪些 granule 满足全部过滤条件,从而在后续查询中跳过不必要的读取和判断。由于它只是对索引和 PREWHERE 的缓存,不影响主流程,因此本文将不再深入讨论。为避免影响测试结果,我们在所有示例中都关闭了该功能。
上述所有优化手段,包括接下来要介绍的惰性物化,都聚焦于在查询执行过程中减少数据读取。而另一种方向是通过物化视图提前计算和压缩结果数据,从源头上减少表体积和计算工作量,但这不是本文的重点。
惰性物化补全 I/O 优化体系
前面提到的优化手段虽已大幅减少读取数据,但仍默认:只要一行数据通过了 WHERE 条件,其所有列都会立即加载,以供排序、聚合或 LIMIT 等操作使用。但如果有些列其实直到后面才需要,或者根本不会用到呢?
这正是惰性物化的作用所在。它为整个 I/O 优化体系补上了关键一环:
-
通过索引和 PREWHERE,ClickHouse 会先过滤出满足查询条件的行。
-
接下来,惰性物化只在执行计划真正需要某列数据时,才去读取该列。例如执行排序操作时,只加载参与排序的列。其他列会延迟读取,很多情况下,在 LIMIT 的限制下,仅读取一部分数据就足够生成最终结果。这种策略在 Top N 查询中尤其高效,因为最终往往只需要某些列的少量行。
这种列级的精细延迟加载能力,得益于 ClickHouse 的列式存储架构。而传统的行式数据库必须同时读取整行数据,根本无法实现这种按需加载的效率。
为了展示惰性物化的效果,我们将通过一个实际示例,逐步演示各层优化机制的作用。
测试环境:数据集与硬件配置
我们选用的是 Amazon 用户评论数据集,包含约 1.5 亿条商品评论,时间跨度从 1995 年至 2015 年。
测试在一台配置如下的 AWS EC2 实例(m6i.8xlarge)上进行,部署了 ClickHouse 25.4:
• 32 核 vCPU
• 128 GiB 内存
• 1 TiB gp3 SSD(默认设置:3000 IOPS,最大吞吐 125 MiB/s)
• Ubuntu Linux 24.04
我们首先在测试机器上创建了 Amazon 用户评论数据表:
CREATE TABLE amazon.amazon_reviews
(
`review_date` Date CODEC(ZSTD(1)),
`marketplace` LowCardinality(String) CODEC(ZSTD(1)),
`customer_id` UInt64 CODEC(ZSTD(1)),
`review_id` String CODEC(ZSTD(1)),
`product_id` String CODEC(ZSTD(1)),
`product_parent` UInt64 CODEC(ZSTD(1)),
`product_title` String CODEC(ZSTD(1)),
`product_category` LowCardinality(String) CODEC(ZSTD(1)),
`star_rating` UInt8 CODEC(ZSTD(1)),
`helpful_votes` UInt32 CODEC(ZSTD(1)),
`total_votes` UInt32 CODEC(ZSTD(1)),
`vine` Bool CODEC(ZSTD(1)),
`verified_purchase` Bool CODEC(ZSTD(1)),
`review_headline` String CODEC(ZSTD(1)),
`review_body` String CODEC(ZSTD(1))
)
ENGINE = MergeTree
ORDER BY (review_date, product_category);
燃后,我们创建了存储评论数据的表结构,然后从托管于我们公开 S3 示例库中的 Parquet 文件中加载数据。
INSERT INTO amazon.amazon_reviews
SELECT * FROM s3Cluster('default', 'https://datasets-documentation.s3.eu-west-3.amazonaws.com/amazon_reviews/amazon_reviews_*.snappy.parquet');
加载完成后,我们检查表的整体大小,结果如下:
SELECT
formatReadableQuantity(sum(rows)) AS rows,
formatReadableSize(sum(data_uncompressed_bytes)) AS data_size,
formatReadableSize(sum(data_compressed_bytes)) AS compressed_size
FROM system.parts
WHERE active AND database = 'amazon' AND table = 'amazon_reviews';
┌─rows───────────┬─data_size─┬─compressed_size─┐
│ 150.96 million │ 70.47 GiB │ 30.05 GiB │
└────────────────┴───────────┴─────────────────┘
-
原始数据大小约为 7

最低0.47元/天 解锁文章

被折叠的 条评论
为什么被折叠?



