两个改变你编写查询方式的 ClickHouse 内部机制

发布: (2026年2月17日 GMT+8 14:27)
8 分钟阅读
原文: Dev.to

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 会奖励那些了解其内部机制的工程师。

一旦你不再以数值思考,而是以状态思考——并且使用引擎所构建的聚合函数——查询设计就会变得更简单、更可预测。

性能也会随之自然提升。

0 浏览
Back to Blog

相关文章

阅读更多 »

当 “MySQL” 并非真正的 MySQL

封面图片:当 “MySQL” 实际上并非 MySQL https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev...