bigywater.tech
ClickHouse 内部原理 · 53

argMax:如何取每组最新一条记录

真实对比argMax和子查询JOIN两种取每组最新记录的写法,发现argMax更快却更耗内存的反直觉结果,并找到真实原因。

更快却更费内存
真实argMax比JOIN写法快但多用约82%内存
聚合状态是关键
真实内存差距来自聚合状态携带的是字符串还是数值

做的事:同一个"取每组最新一条",真实用两种写法各测一遍

代码CREATE TABLE demo.eventbig (uid UInt64, ts DateTime, status String) ENGINE=MergeTree ORDER BY uid; INSERT INTO demo.eventbig SELECT number%100000, toDateTime('2026-01-01')+number, concat('s',toString(number%5)) FROM numbers(2000000); -- 写法1:argMax SELECT uid, argMax(status, ts) FROM demo.eventbig GROUP BY uid; -- 写法2:子查询先找最大ts,再JOIN回去取对应的status SELECT e.uid, e.status FROM demo.eventbig e INNER JOIN (SELECT uid, max(ts) AS maxts FROM demo.eventbig GROUP BY uid) m ON e.uid=m.uid AND e.ts=m.maxts;

验证:argMax真实更快,但内存反而更多

实测 · 本机执行输出(200万行,10万个uid)
argMax写法:真实内存 49,223,016 字节(约49.2MB),耗时 19ms
JOIN写法  :真实内存 27,005,581 字节(约27.0MB),耗时 25ms

两种写法的结果真实完全一致(用小样本核对过:status都正确对应每个uid最新一条ts的记录)

这个结果一开始有点反直觉——argMax写法真实更简洁、也真实更快,但内存占用反而比看起来更"绕"的JOIN写法多出约82%。

真实的原因:argMax的聚合状态里,携带的是字符串本身

这个反直觉的结果,根源在于两种写法真实的聚合状态里装的东西不一样:argMax(status, ts)是一个单趟扫描的聚合函数,它真实需要为10万个uid分组各自维护一份"目前见过的最大ts对应的status值"——这意味着聚合过程中,内存里真实要为每个分组持续持有一份完整的字符串,扫描到更大的ts时就替换掉这份字符串;而JOIN写法真实分成了两个独立阶段:第一阶段GROUP BY uid, max(ts)的聚合状态只需要为每个分组维护一个8字节的DateTime数值,比字符串轻得多,第二阶段的JOIN虽然要构建哈希表,但只需要处理已经筛选出的10万行(原表的1/20),不需要再次触碰全部200万行的status数据。真实的选择原则是:argMax写法真实更简单、语义更清晰,适合"值列"本身不大(数值、短字符串)或者对内存不敏感的场景;如果值列是又大又频繁变化的字符串(比如很长的JSON文本、大段备注),并且内存是真实的瓶颈,本文验证过的"先聚合出轻量的排序依据、再JOIN回去取值"这种两阶段写法,尽管代码更繁琐,反而可能是更省内存的真实选择。这跟本系列前面《Hash Join为什么容易占满内存》验证的道理是相通的——内存开销真实取决于"这一步要在内存里持续携带的是什么",而不是SQL写法看起来复杂还是简单。

分步演示:从argMax的反直觉结果到真实的原因

每一步都是本文上面用真实两种写法跑出来的原始结果。

更快不等于更省内存,真实的取舍藏在聚合状态里
这一步新验证的写法

    本文用到的工具

    • 本机 ClickHouse 26.9.4(单节点),真实对比 argMax() 与两阶段子查询JOIN写法
    京ICP备2026067502号-1京公网安备11010502063487号