ClickHouse 分组取每组第一条记录,为什么别用 any?
刚上手 ClickHouse 那阵,我被分组查询坑得不轻。
写惯了 MySQL 的 GROUP_CONCAT 和 GROUP BY 后随手取列,到了 ClickHouse 全报错。
这篇把我趟过的几个坑整理一下,重点说清楚「分组后怎么正确取每组第一条记录」。
创建数据库
先从最基础的建库说起。
CREATE DATABASE [IF NOT EXISTS] db_name
[ON CLUSTER cluster_name] -- 可选,指定在某个集群上创建
[ENGINE = engine_name] -- 可选,库级引擎,默认 Atomic
[COMMENT 'comment'] -- 可选,注释一个最常见的写法:
CREATE DATABASE IF NOT EXISTS my_database;
CREATE DATABASE IF NOT EXISTS my_database ON CLUSTER my_cluster;这里要纠正一个常见误区。
数据库级别的 ENGINE 指的是 Atomic、Ordinary、Replicated 这类库引擎,不是建表用的 MergeTree。
MergeTree 是表引擎,写在 CREATE TABLE 上,别写到 CREATE DATABASE 里。
groupArray:相当于 MySQL 的 GROUP_CONCAT
groupArray(x) 把分组内某列的所有值收集成一个数组。
效果类似 MySQL 的 GROUP_CONCAT,区别是它返回数组而不是拼接字符串。
SELECT sex, groupArray(id) AS ids
FROM user
GROUP BY sex;注意 ClickHouse 的语法是 GROUP BY sex,不能简写成 group sex。
我一开始照着某篇博客抄了 group sex,结果一直报语法错误,找了半天才发现是关键字漏了。
还有一点官方文档写得很明确:groupArray 收集元素的顺序是不确定的。
想拼成字符串的话,外面套一层 arrayStringConcat(groupArray(id), ',') 就行。
any 函数:能取,但结果不保证
要取 GROUP BY 之外的非聚合列,很多教程会让你用 any()。
SELECT sex, any(name) AS one_name
FROM user
GROUP BY sex;any(x) 返回分组内「遇到的第一个值」。
但 ClickHouse 官方文档明确写了:由于查询执行顺序是任意的,any 的结果是非确定性的。
网上不少文章说「加个 ORDER BY 就能让 any 变确定」,这个说法并不靠谱。
原因是 ORDER BY 作用在聚合之后,它只排最终结果的行序,管不着每个分组内部 any 到底抓了哪一行。
官方给的建议是:如果只是想要一个任意但稳定的值,用 min 或 max 替代 any。
如果想按某列取对应那一行的值,往下看 argMin / argMax。
argMin / argMax:取每组第一条记录的正解
这才是「分组取每组第一条记录」最该用的函数。
argMin(arg, val) 返回 val 最小那一行对应的 arg 值;argMax 则取 val 最大那一行。
比如按时间取每个用户最早的一条记录:
SELECT
user_id,
argMin(name, created_at) AS first_name, -- created_at 最小那行的 name
argMin(amount, created_at) AS first_amount, -- 同一行的 amount
min(created_at) AS first_time
FROM orders
GROUP BY user_id;它确定、好懂,而且同一个排序列下取出来的多个字段来自同一行,不会出现张冠李戴。
要取最新一条,把 argMin 换成 argMax 即可。
有个小坑提醒一下:argMin / argMax 会跳过排序列为 NULL 的行。
如果存在并列(多行 val 相等),返回哪一行同样不保证,必要时把排序列做成唯一值。
groupArray + 数组函数:灵活但啰嗦
如果你确实想保留组内全部数据再挑,可以靠数组函数组合。
先收集,再排序,最后取第一个元素。
SELECT
user_id,
arrayElement(arraySort(x -> x.1, groupArray((created_at, amount))), 1).2 AS first_amount
FROM orders
GROUP BY user_id;思路是:groupArray 把 (时间, 金额) 收成数组,arraySort 按时间排,arrayElement(..., 1) 取第一个。
能用,但可读性比 argMin 差远了,数据量大时也更费内存。
日常我基本只在需要「组内全量明细」时才走这条路,单纯取第一条还是 argMin 香。
first_value:窗口函数写法
如果你不想压缩成一行,而是要保留每一行并在旁边带上「组内第一个值」,用窗口函数。
SELECT
id,
created_at,
first_value(amount) OVER (PARTITION BY user_id ORDER BY created_at) AS first_amount
FROM orders;first_value 按 PARTITION BY 分区、ORDER BY 排序,返回窗口内第一行的值。
它和 argMin 的区别在于:argMin 把每组压成一行,first_value 不减少行数。
ClickHouse 从较早版本起就内置了窗口函数,新版本无需再开实验开关,直接用就行。
关键数据一览
| 需求 | 推荐函数 | 结果是否确定 |
|---|---|---|
| 收集组内所有值 | groupArray(x) |
元素顺序不确定 |
| 取任意但稳定的值 | min(x) / max(x) |
确定 |
| 按某列取对应行的值 | argMin / argMax |
并列时不确定 |
| 每行旁带组内首值 | first_value() OVER(...) |
确定 |
| 任意值(不推荐) | any(x) |
非确定 |
常见问题
ClickHouse 里 any 和 argMin 有什么区别?
any 取分组内随便一个值,结果不保证稳定。
argMin(arg, val) 是按 val 排序后取最小那一行的 arg,结果明确可控。
要取「每组第一条」,优先用 argMin / argMax。
groupArray 出来的顺序为什么和我插入的不一样?
官方文档说明 groupArray 收集元素的顺序是不确定的。
需要有序就外层套 arraySort,或者改用 argMin 直接拿目标行。
加了 ORDER BY 能让 any 变确定吗?
不能。
ORDER BY 排的是聚合后的最终结果行序,不影响每个分组内部 any 抓取的是哪一行。
想要确定结果就别用 any,换成 min / max / argMin。
first_value 和 argMin 该选哪个?
要把每组压成一行汇总,用 argMin。
要保留全部明细、只是顺带显示组内第一个值,用窗口函数 first_value。
总结
ClickHouse 的分组逻辑和 MySQL 差别不小,最容易踩的就是拿 any 当万能取值器。
我的习惯是:取全量用 groupArray,取每组某行用 argMin / argMax,要带明细就上 first_value。
把 any 留给「真的不在乎取哪一行」的场景,能省掉很多排查时间。
如果大家在 ClickHouse 分组查询上还踩过别的坑,或者有更优雅的写法,欢迎在评论区交流~~~
版权声明
未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!
本文原文链接: https://fiveyoboy.com/articles/clickhouse-group-by-first-row/
备用原文链接: https://blog.fiveyoboy.com/articles/clickhouse-group-by-first-row/