据坊间传 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       │
└────────────────┴───────────┴─────────────────┘
  • 原始数据大小约为 7

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值