据坊间传 ClickHouse 越“懒”越快:惰性物化(Lazy Materialization)正式登场

图片

本文字数: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       │
└────────────────┴───────────┴─────────────────┘
  • 原始数据大小约为 70 GiB

  • 使用 ZSTD(1) 压缩后,磁盘占用约 30 GiB

  • 总行数约 1.5 亿

ClickHouse 很快,但磁盘未必跟得上

1.5 亿行的数据量对 ClickHouse 来说不算什么。例如,下面这个查询对 helpful_votes 列进行排序(该列并非排序键),返回前 3 项,即便在操作系统缓存未命中的情况下,也只用了 70 毫秒,处理速度高达每秒 21.5 亿行。

SELECT helpful_votes
FROM amazon.amazon_reviews
ORDER BY helpful_votes DESC
LIMIT 3;
┌─helpful_votes─┐
│         47524 │
│         41393 │
│         41278 │
└───────────────┘

3 rows in set. Elapsed: 0.070 sec. Processed 150.96 million rows, 603.83 MB (2.15 billion rows/s., 8.61 GB/s.)
Peak memory usage: 3.59 MiB.

虽然该查询未使用任何索引、PREWHERE 或其他 I/O 优化技术(因为没有过滤条件),但由于列式存储的优势,ClickHouse 只读取了所需的 helpful_votes 列,跳过了其余列的数据。

再来看另一个查询:它只选取 review_body 这一列,在冷缓存条件下执行。结果令人惊讶——耗时接近 3 分钟,尽管只读取了一个列。

SELECT review_body
FROM amazon.amazon_reviews
FORMAT Null;
Query id: b9566386-047d-427c-a5ec-e90bee027b02

0 rows in set. Elapsed: 176.640 sec. Processed 150.96 million rows, 56.02 GB (854.61 thousand rows/s., 317.13 MB/s.)
Peak memory usage: 733.14 MiB.

仅读取一个列,查询却耗时将近 3 分钟!

这次性能瓶颈并不在 ClickHouse,而是磁盘吞吐能力。review_body 是一个大列,总量高达 56 GB,而上一个示例仅处理了 600 MB。虽然 ClickHouse 使用了 32 条并发数据流读取数据,但由于磁盘速度相对较慢,查询日志显示大部分时间都花在等待读取操作上。

SELECT 
  round(ProfileEvents['DiskReadElapsedMicroseconds'] / 1e6) AS disk_read_seconds,
  ProfileEvents['ConcurrencyControlSlotsAcquired'] AS parallel_streams,
  formatReadableTimeDelta(round(disk_read_seconds / parallel_streams), 'seconds') AS time_per_stream
FROM system.query_log
WHERE query_id = 'b9566386-047d-427c-a5ec-e90bee027b02' 
  AND type = 'QueryFinish';
┌─disk_read_seconds─┬─parallel_streams─┬─time_per_stream─┐
│              5512 │               32 │ 172 seconds     │
└───────────────────┴──────────────────┴─────────────────┘

很显然,面对冷启动状态时的大规模全表扫描,即使是 ClickHouse 也难以发挥全部优势。接下来,我们让它真正施展一下优化能力。

更贴近实际场景的查询 —— 优化效果真正发挥的时刻

虽然旅程一波三折,我还是打算安心去海边度假,因此希望在电子书阅读器中装上真正有价值的书评。于是我请 ClickHouse 帮我筛选出从 2010 年以来,所有关于电子书购买的五星认证评论中最有帮助的那些,并显示点赞数、书名、评论标题以及评论内容。

SELECT
    helpful_votes,
    product_title,
    review_headline,
    review_body
FROM amazon.amazon_reviews
WHERE review_date >= '2010-01-01'
AND product_category = 'Digital_Ebook_Purchase'
AND verified_purchase
AND star_rating > 4
ORDER BY helpful_votes DESC
LIMIT 3
FORMAT Vertical;
Row 1:
──────
helpful_votes:   6376
product_title:   Wheat Belly: Lose the Wheat, Lose the Weight, and Find Your Path Back to Health
review_headline: Overweight? Diabetic? Got High Blood Pressure, Arthritis? Get this Book!
review_body:     I've been following Dr. Davis' heart scan blog for the past ...

Row 2:
──────
helpful_votes:   4149
product_title:   The Life-Changing Magic of Tidying Up: The Japanese Art of Decluttering and Organizing
review_headline: Truly life changing
review_body:     I rarely write reviews, but this book truly sparked somethin...

Row 3:
──────
helpful_votes:   2623
product_title:   The Fast Metabolism Diet: Eat More Food and Lose More Weight
review_headline: Fantastic Results **UPDATED 1/23/2015**
review_body:     I have been on this program for 7 days so far.  I know it ma...

这个查询会提取四个字段,其中包括三列大字段(product_title、review_headline、review_body),都是表中占用空间最大的字段之一。

SELECT
    name as column,
    formatReadableSize(sum(data_uncompressed_bytes)) AS data_size,
    formatReadableSize(sum(data_compressed_bytes)) AS compressed_size
FROM system.columns
WHERE database = 'amazon' AND table = 'amazon_reviews'
GROUP BY name
ORDER BY sum(data_uncompressed_bytes) DESC;
┌─column────────────┬─data_size──┬─compressed_size─┐
│ review_body       │ 51.13 GiB  │ 21.60 GiB       │
│ product_title     │ 8.12 GiB   │ 3.53 GiB        │
│ review_headline   │ 3.38 GiB   │ 1.58 GiB        │
│ review_id         │ 2.07 GiB   │ 1.35 GiB        │
│ product_id        │ 1.55 GiB   │ 720.97 MiB      │
│ customer_id       │ 1.12 GiB   │ 524.35 MiB      │
│ product_parent    │ 1.12 GiB   │ 571.63 MiB      │
│ helpful_votes     │ 575.86 MiB │ 72.11 MiB       │
│ total_votes       │ 575.86 MiB │ 83.50 MiB       │
│ review_date       │ 287.93 MiB │ 239.43 KiB      │
│ marketplace       │ 144.51 MiB │ 414.92 KiB      │
│ product_category  │ 144.25 MiB │ 838.96 KiB      │
│ star_rating       │ 143.96 MiB │ 41.99 MiB       │
│ verified_purchase │ 143.96 MiB │ 20.50 MiB       │
│ vine              │ 1.75 MiB   │ 844.89 KiB      │
└───────────────────┴────────────┴─────────────────┘

这个查询访问的数据总量超过 60 GiB(未压缩)。正如我们前文提到的,即使使用 32 条并发读取流,若从相对较慢的磁盘读取这些数据,在操作系统缓存未命中的情况下,也需要超过 3 分钟。

不过,这个查询包含多个字段的过滤条件(review_date、product_category、verified_purchase 和 star_rating),并在按 helpful_votes 排序之后加了 LIMIT 子句。这样的查询场景,正是 ClickHouse 分层 I/O 优化机制最能发挥作用的典型例子:

  • 索引机制可以快速排除不符合主键或排序键(如 review_date 和 product_category)条件的数据;

  • PREWHERE 会深入过滤,进一步排除不满足所有条件的行;

  • 惰性物化(lazy materialization)则让那些体积巨大的 SELECT 字段(product_title、review_headline、review_body)延迟到真正需要时才加载——通常是在排序和 LIMIT 之后。理想情况下,这些大字段的大部分内容根本不需要加载。

这三种机制层层递进,能有效减少读取的数据量,降低内存消耗,并显著加快查询执行速度。接下来,我们将一层层展开,看看它们分别带来怎样的实际提升。

在冷启动环境下的测试说明

在以下的测试环节中,我们会在每次执行查询前,使用 Linux 命令行清空操作系统的文件系统(页)缓存。

echo 3 | sudo tee /proc/sys/vm/drop_caches >/dev/null.

这样可以模拟磁盘冷启动的最不利场景,确保查询表现反映的是实际的磁盘读取,而非缓存命中带来的“假快”

无优化:全表扫描下的基准表现

在展示优化效果之前,我们先看一下 ClickHouse 在不使用任何优化手段时的查询表现:不启用索引、不使用 PREWHERE,也不启用惰性物化。

为此,我们首先创建一个不包含主键或排序键的基准版本表。这意味着 ClickHouse 无法利用任何基于索引的优化。下面是用于创建该基准表的命令:

CREATE TABLE amazon.amazon_reviews_no_pk
Engine = MergeTree 
ORDER BY ()
AS SELECT * FROM amazon.amazon_reviews;

接着,我们在这个表上执行示例查询,同时显式禁用 PREWHERE 和惰性物化功能。

SELECT
    helpful_votes,
    product_title,
    review_headline,
    review_body
FROM amazon.amazon_reviews_no_pk
WHERE review_date >= '2010-01-01'
AND product_category = 'Digital_Ebook_Purchase'
AND verified_purchase
AND star_rating > 4
ORDER BY helpful_votes DESC
LIMIT 3
FORMAT Null
SETTINGS
    optimize_move_to_prewhere = false,
    query_plan_optimize_lazy_materialization = false;
3 rows in set. Elapsed: 219.508 sec. Processed 150.96 million rows, 72.13 GB (687.71 thousand rows/s., 328.60 MB/s.)
Peak memory usage: 953.25 MiB.

在这次查询中,ClickHouse 从磁盘中逐块读取了表中全部约 1.5 亿行数据。数据以 granule(ClickHouse 的最小处理单元,默认每个 granule 含 8192 行)的形式组织,共涉及 8 个字段,总数据量为 72 GB。整个处理过程耗时 220 秒,内存使用峰值为 953 MiB。

图片

ClickHouse 使用流式处理方式处理表数据:它不会将全部数据一次性加载至内存,而是逐个 granule 读取并处理。因此,即使处理的数据量高达 72 GB,内存占用仍控制在 1 GiB 以内。

基准测试完成,接下来我们看看开启第一层优化后,查询性能会有怎样的提升。

① 启用主索引优化

显然,扫描整个数据集并不是理想的方案。我们开始逐步应用 ClickHouse 的优化策略,第一步是启用主索引。此次查询仍禁用了 PREWHERE 和惰性物化,但我们使用了包含主键的原始表结构,该表以(review_date, product_category)作为复合排序键。

SELECT
    helpful_votes,
    product_title,
    review_headline,
    review_body
FROM amazon.amazon_reviews
WHERE review_date >= '2010-01-01'
AND product_category = 'Digital_Ebook_Purchase'
AND verified_purchase
AND star_rating > 4
ORDER BY helpful_votes DESC
LIMIT 3
FORMAT Null
SETTINGS
   optimize_move_to_prewhere = false,
   query_plan_optimize_lazy_materialization = false;
0 rows in set. Elapsed: 95.865 sec. Processed 53.01 million rows, 27.67 GB (552.98 thousand rows/s., 288.68 MB/s.)
Peak memory usage: 629.00 MiB.

由于查询中包含对这两个主键字段的过滤条件,ClickHouse 会先加载稀疏主索引,对这些主键列进行分析,选出可能包含匹配行的 granule。随后,这些 granule 以及与之位置对齐的其他列 granule 会一同被流式加载到内存中,剩余的过滤条件会在这一阶段之后应用。

图片

这样,ClickHouse 实际只需处理 5300 万行,而非全表的 1.5 亿行,读取数据量从 72 GB 降至 28 GB,查询耗时也从 220 秒减少至 96 秒,几乎减半。

主索引的作用,是根据主键字段进行 granule 剪枝。

但仍有一个问题:所有与这些 granule 对齐的其他列也会被加载,即便这些列中某些行随后会因为不满足非主键字段过滤条件而被排除。也就是说,仍存在冗余数据读取。

为进一步优化,我们引入 PREWHERE。

② 启用 PREWHERE 优化

在这一步中,我们启用了 PREWHERE(仍未启用惰性物化),再次运行相同查询。PREWHERE 在主索引基础上增加了一层预过滤逻辑,可在磁盘读取非过滤列数据前排除无效行,从而减少 I/O。

SELECT
    helpful_votes,
    product_title,
    review_headline,
    review_body
FROM amazon.amazon_reviews
WHERE review_date >= '2010-01-01'
AND product_category = 'Digital_Ebook_Purchase'
AND verified_purchase
AND star_rating > 4
ORDER BY helpful_votes DESC
LIMIT 3
FORMAT Null
SETTINGS
    optimize_move_to_prewhere = true,
    query_plan_optimize_lazy_materialization = false;
0 rows in set. Elapsed: 61.148 sec. Processed 53.01 million rows, 16.28 GB (866.94 thousand rows/s., 266.24 MB/s.)
Peak memory usage: 583.30 MiB.

这次查询仍处理 5300 万行数据,但实际读取的数据量下降至 16.28 GB(前一次为 27.67 GB),执行时间进一步缩短为 61 秒,相比之前减少了约 36%。同时,内存峰值也有所下降。

PREWHERE 的工作原理是逐步过滤数据,而不是一次性加载所有目标列的 granule。它首先读取主索引选出的 granule,验证其是否包含匹配数据。本例中,这些 granule 全部匹配,于是下一步加载 verified_purchase 列的对应 granule,用于进一步过滤。

图片

接下来,ClickHouse 读取 verified_purchase 列中预选的 granule,用于评估对应的过滤条件 verified_purchase(该条件等价于 verified_purchase == True)。

在本例中,4 个 granule 中有 3 个包含满足条件的行,因此仅加载了这 3 个 granule 所对应的、在 star_rating 列中位置对齐的 granule,继续进行下一步过滤:

图片

verified_purchase 列中有 3 个 granule 包含匹配数据,ClickHouse 只加载与这三者对齐的 star_rating 列 granule,并应用下一层过滤条件 star_rating > 4。

最后,在剩下的 2 个匹配 granule 上,ClickHouse 才会读取剩余的四个目标字段(helpful_votes、product_title、review_headline、review_body)的对应 granule。

图片

至此,PREWHERE 的处理流程完成。

整个过程体现了 PREWHERE 的设计初衷:通过按列、逐步、按需加载数据,在执行主操作(如排序、聚合、LIMIT)前最大限度地减少无效读取。ClickHouse 采用基于代价的策略,优先读取开销最小的列,逐步筛选出最终需要的数据。

需要强调的是,即便没有索引,PREWHERE 依然适用。它可以在非索引字段的过滤条件上独立工作,通过早期排除无效数据同样能够显著减少 I/O。

PREWHERE 后的执行流程

在 PREWHERE 完成过滤后,ClickHouse 继续执行以下操作:加载选中数据、进行排序,并应用 LIMIT 子句。

图片

我们迄今为止应用的每一层优化都在持续压缩查询耗时,通过跳过不必要的数据来减少 I/O 读写,同时简化整体查询执行流程。

从最初耗时 220 秒的全表扫描,到启用主索引和 PREWHERE 后的 61 秒,性能已提升明显。但这还不是终点。最后一层优化,将带来最大的加速效果。

③ 启用惰性物化

现在我们启用所有 I/O 优化功能,并引入惰性物化机制,最后一次执行这个查询。

SELECT
    helpful_votes,
    product_title,
    review_headline,
    review_body
FROM amazon.amazon_reviews
WHERE review_date >= '2010-01-01'
AND product_category = 'Digital_Ebook_Purchase'
AND verified_purchase
AND star_rating > 4
ORDER BY helpful_votes DESC
LIMIT 3
FORMAT Null
SETTINGS
    optimize_move_to_prewhere = true,
    query_plan_optimize_lazy_materialization = true;
0 rows in set. Elapsed: 0.181 sec. Processed 53.01 million rows, 807.55 MB (292.95 million rows/s., 4.46 GB/s.)
Peak memory usage: 3.88 MiB.

令人惊讶的是:查询时间从 61 秒骤降至 181 毫秒,整整提升了 338 倍。

ClickHouse 处理的数据行数仍为 5300 万,但读取的列数据减少了 20 倍,内存使用下降了 150 倍,查询几乎在瞬间完成。

这一变化的原理其实非常直观:

在完成 PREWHERE 过滤后,ClickHouse 不会立即加载所有剩余的列,而是仅加载执行计划中下一步所需的数据。

本查询接下来的步骤是对 helpful_votes 列排序并应用 LIMIT,因此 ClickHouse 会先读取该列中经过 PREWHERE 过滤后的 granule,完成排序和限制处理之后,才去加载其对应行中 product_title、review_headline 和 review_body 这些大型字段的数据。

图片

就这样,这最后一层优化“精准落地”,将原本 220 秒的查询压缩至仅 181 毫秒。查询结构未变,数据表未变,执行环境也没变 —— 就连磁盘依然很慢。唯一的变化,是 ClickHouse 更聪明地决定了何时读取哪些数据,最终带来了高达 1,215 倍的速度提升。

在这个案例中,惰性物化的作用尤为显著,因为查询中涉及体积庞大的文本列,而最终实际只需提取其中的 3 行数据。但在其他场景下,索引优化或 PREWHERE 可能对性能影响更大。ClickHouse 的这些优化机制彼此协同、层层递进,从多个维度减少不必要的 I/O 操作。

补充说明:惰性物化会自动应用于带 LIMIT N 的查询,但仅在 N 不超过指定阈值的情况下生效。这个阈值由参数 query_plan_max_limit_for_lazy_materialization 控制(默认值为 10)。如果将其设置为 0,则表示对所有 LIMIT 查询启用惰性物化,无上限限制。

无谓词优化场景下的惰性物化表现

要想利用索引和 PREWHERE 机制,查询中必须包含过滤条件 —— 索引依赖主键字段的过滤,PREWHERE 则适用于任意字段。然而,惰性物化的特别之处在于:即使在完全没有过滤条件的情况下,它依然能够带来显著的性能提升。

为验证这一点,我们将示例查询中的所有过滤条件移除,目标是找出获得点赞数最多的评论 —— 不限制时间、产品类别、评分或是否为认证购买 —— 并返回其书名、评论标题和正文内容。

我们首先在禁用惰性物化的情况下运行该查询,同时清除操作系统文件系统缓存,以模拟真实冷启动场景:

SELECT
    helpful_votes,
    product_title,
    review_headline,
    review_body
FROM amazon.amazon_reviews
ORDER BY helpful_votes DESC
LIMIT 3
FORMAT Vertical
SETTINGS
    query_plan_optimize_lazy_materialization = false;
Row 1:
──────
helpful_votes:   47524
product_title:   Kindle: Amazon's Original Wireless Reading Device (1st generation)
review_headline: Why and how the Kindle changes everything
review_body:     This is less a \"pros and cons\" review than a hopefully use...

Row 2:
──────
helpful_votes:   41393
product_title:   BIC Cristal For Her Ball Pen, 1.0mm, Black, 16ct (MSLP16-Blk)
review_headline: FINALLY!
review_body:     Someone has answered my gentle prayers and FINALLY designed ...

Row 3:
──────
helpful_votes:   41278
product_title:   The Mountain Kids 100% Cotton Three Wolf Moon T-Shirt
review_headline: Dual Function Design
review_body:     This item has wolves on it which makes it intrinsically swee...


0 rows in set. Elapsed: 219.071 sec. Processed 150.96 million rows, 71.38 GB (689.08 thousand rows/s., 325.81 MB/s.)
Peak memory usage: 1.11 GiB.

随后,我们启用惰性物化,重新执行同样的查询(仍在冷缓存下):

SELECT
    helpful_votes,
    product_title,
    review_headline,
    review_body
FROM amazon.amazon_reviews
ORDER BY helpful_votes DESC
LIMIT 3
FORMAT Vertical
SETTINGS
    query_plan_optimize_lazy_materialization = true;
Row 1:
──────
helpful_votes:   47524
product_title:   Kindle: Amazon's Original Wireless Reading Device (1st generation)
review_headline: Why and how the Kindle changes everything
review_body:     This is less a \"pros and cons\" review than a hopefully use...

Row 2:
──────
helpful_votes:   41393
product_title:   BIC Cristal For Her Ball Pen, 1.0mm, Black, 16ct (MSLP16-Blk)
review_headline: FINALLY!
review_body:     Someone has answered my gentle prayers and FINALLY designed ...

Row 3:
──────
helpful_votes:   41278
product_title:   The Mountain Kids 100% Cotton Three Wolf Moon T-Shirt
review_headline: Dual Function Design
review_body:     This item has wolves on it which makes it intrinsically swee...


0 rows in set. Elapsed: 0.139 sec. Processed 150.96 million rows, 1.81 GB (1.09 billion rows/s., 13.06 GB/s.)
Peak memory usage: 3.80 MiB.

结果令人惊艳:查询时间从 219 秒骤降至 139 毫秒,性能提升高达 1,576 倍。读取数据量减少了 40 倍,内存使用下降了 300 倍。

这一示例充分展现了惰性物化在 ClickHouse I/O 优化体系中的独特优势。

与索引或 PREWHERE 依赖列过滤条件以跳过数据不同,惰性物化通过延迟加载、按需访问的机制,仅在真正需要时才读取数据列,从而大幅降低 I/O 和内存占用。这使得它在无谓词查询中依然具备强劲的性能提升能力。

从查询执行计划验证惰性物化

我们可以通过使用 EXPLAIN 子句,查看逻辑执行计划,确认前文示例查询中是否启用了惰性物化。

EXPLAIN actions = 1
SELECT
    helpful_votes,
    product_title,
    review_headline,
    review_body
FROM amazon.amazon_reviews
ORDER BY helpful_votes DESC
LIMIT 3
SETTINGS
    query_plan_optimize_lazy_materialization = true;
...
Lazily read columns: review_headline, review_body, product_title 
  Limit                    
    Sorting                             
      ReadFromMergeTree

从执行计划自下而上的操作符顺序中,我们可以清楚看到:ClickHouse 将对三个大型字符串列的读取操作延后到了排序和 LIMIT 应用之后。

分层优化:越优化,越“懒惰”

这次优化之旅始于一次典型的全表扫描:220 秒的耗时、读取了 72 GB 数据、内存占用达 1 GiB。在 ClickHouse 的 I/O 分层优化机制帮助下,我们一步步压缩了查询时间:

  • ① 首先,主索引筛除了不满足过滤条件(review_date、product_category)的 granule,有效减少了数据量;

  • ② 接着,PREWHERE 提前排除了通过索引但不满足非索引字段(verified_purchase、star_rating)过滤条件的 granule,进一步减少了不必要的读取操作;

  • ③ 最后,惰性物化则将对大字段(product_title、review_headline、review_body)的加载延迟至排序和 LIMIT 之后,避免了对大部分无用数据的加载。

图片

每一层优化都发挥了作用,但在本次测试的数据规模与查询形态下,惰性物化起到了决定性的性能突破。

最终结果令人瞩目:

  • 从 220 秒 → 0.18 秒:在带过滤条件的查询中,性能提升超过 1200 倍;

  • 从 219 秒 → 0.139 秒:在无过滤的 Top N 查询中,性能提升超过 1500 倍。

数据表未变,机器配置未变,SQL 查询也完全相同。唯一的变化在于:ClickHouse 更智能地控制了数据的读取时机和方式。

惰性物化不仅让 ClickHouse 变得更快,也为其 I/O 优化体系画上了关键的一笔。更值得一提的是:它(连同 PREWHERE)默认即已启用。你无需额外配置,即可自动获得这一优化带来的性能收益。

征稿启示

面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:Tracy.Wang@clickhouse.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值