两个改变你编写查询方式的 ClickHouse 内部机制
I’m happy to translate the article for you, but I’ll need the full text of the post (the content you’d like translated). Could you please paste the article’s body here? Once I have that, I’ll provide a Simplified‑Chinese translation while preserving the source line, markdown formatting, code blocks, URLs, and technical terms exactly as requested.
大多数 ClickHouse 错误并非来自 SQL 语法。
它们源于使用了错误的思维模型。
ClickHouse 看起来像是熟悉的分析型 SQL 数据库,但其内部行为与传统的行式系统截然不同。如果你带着 PostgreSQL 或 MySQL 的直觉去使用它,最终会遇到令人困惑的行为:结果错误、奇怪的重复、意外的合并,或查询无法按预期扩展。
有两个内部机制特别改变了我设计表和编写查询的方式:
AggregatingMergeTree存储的是聚合 状态,而不是最终值argMax以 ClickHouse 期望的方式解决分组最大值问题
这些不是技巧,而是引擎工作原理的核心。
Source: …
1. AggregatingMergeTree 关注的是状态,而不是数值
ClickHouse 中最常被误解的引擎之一是 AggregatingMergeTree。
乍一看,它好像是用来存储预聚合结果的方式。因此人们很容易这样做:
CREATE TABLE daily_metrics
(
day Date,
avg_delay Float32
)
ENGINE = AggregatingMergeTree()
ORDER BY day;
然后插入数据:
SELECT
toDate(event_time) AS day,
avg(arrival_delay) AS avg_delay
FROM flights
GROUP BY day;
随后查询:
SELECT day, avg_delay FROM daily_metrics;
看起来很合理。但从概念上讲,这是错误的。
为什么会失败
AggregatingMergeTree 不存储最终的聚合数值。它存储的是 聚合状态——可合并的中间计算表示。
ClickHouse 在后台合并数据块。随着新数据的到来:
- 行被追加到新块
- 后台合并异步地合并块
- 在这些块合并时,聚合状态会被合并
如果你存储的是普通的 avg() 或 sum() 值,ClickHouse 就没有可合并的对象。实际上你关闭了引擎的预期行为。
正确的思维模型
要在 状态 而不是数值上思考。
- 不要存储
avg(),而是存储avgState()。 - 不要存储
sum(),而是存储sumState()。 - 不要存储
uniq(),而是存储uniqState()。
可以这样定义表:
CREATE TABLE daily_metrics
(
day Date,
avg_delay AggregateFunction(avg, Float32)
)
ENGINE = AggregatingMergeTree()
ORDER BY day;
插入数据时:
SELECT
toDate(event_time) AS day,
avgState(arrival_delay) AS avg_delay
FROM flights
GROUP BY day;
现在你存储的是可合并的状态。
查询时,需要 完成(finalize)它们:
SELECT
day,
avgMerge(avg_delay) AS avg_delay
FROM daily_metrics
GROUP BY day;
为什么仍然需要 GROUP BY
另一个常见的困惑是:
“如果我在插入前已经聚合了,查询时为什么还需要
GROUP BY?”
因为合并是异步进行的。可能会有多个块包含同一天的状态。最终的聚合只有在你显式使用 avgMerge() 等函数合并这些状态时才会发生。GROUP BY 确保在查询时把同一键的所有状态正确合并。
一旦你理解了 AggregatingMergeTree 是为 增量、可合并的聚合 设计的,一切就变得可预测:
- 物化视图变得有意义
- 回填(backfill)行为正常
- 后台合并不再神秘
引擎本身没有问题,问题出在思维模型上。
2. 正确使用 argMax 解决分组最大值问题
另一个常见的分析需求表面上看起来很简单:
“对每个分组,返回具有最大值的那一行。”
例如:“对每个航空公司,返回到达延误最严重的航班。”
一种天真的做法可能是这样:
SELECT
airline,
flight_number,
max(arrival_delay)
FROM flights
GROUP BY airline, flight_number;
这会计算每个 (airline, flight_number) 组合的最大延误——而不是每个航空公司最差的航班。它还会为每个航空公司产生多行记录,导致后续需要额外的过滤或子查询。
ClickHouse 原生解决方案
SELECT
airline,
argMax(flight_number, arrival_delay) AS flight_number,
max(arrival_delay) AS max_delay
FROM flights
GROUP BY airline;
argMax(value, weight) 的含义是:“返回与最大权重关联的值。”
在本例中:
- 跟踪每个航空公司的最大
arrival_delay - 返回对应的
flight_number - 每个航空公司恰好产生一行记录
无需子查询、无需连接、也不会产生行膨胀。
为什么这很重要
argMax 与 ClickHouse 在内部执行聚合的方式保持一致。它将值的选择和聚合合并为一次遍历。与其在关系代数层面思考重写查询,不如直接使用表达意图的聚合函数。这既更快,也更直观。
一旦开始使用 argMax,许多复杂的“每组最佳”查询就可以简化为单条 SELECT 语句。
更大的模式
这两个例子都指向同一个教训:
ClickHouse 不仅仅是“更快的 Postgres”。
它的设计围绕着:
- 不可变的部件
- 背景合并
- 可合并的聚合状态
- 列式执行
- 专用聚合函数
如果你把它当作传统的行式数据库来使用,你将一直与之抗争。若你将思考方式与它的数据存储和合并方式对齐,整类错误和低效将不复存在。
思考
将 AggregatingMergeTree 视为基于状态的引擎,并使用诸如 argMax 之类的函数进行分组逻辑,会改变你在 ClickHouse 中建模数据的方式。
这些并非边缘案例。它们在真实的生产系统中会出现:
- 预聚合物化视图
- 流式摄取管道
- 分析仪表盘
- 按租户划分的指标
- 最佳/最差报告
ClickHouse 会奖励那些了解其内部机制的工程师。
一旦你不再以数值思考,而是以状态思考——并且使用引擎所构建的聚合函数——查询设计就会变得更简单、更可预测。
性能也会随之自然提升。