目录

ClickHouse 分组取每组第一条记录,为什么别用 any?

刚上手 ClickHouse 那阵,我被分组查询坑得不轻。

写惯了 MySQL 的 GROUP_CONCATGROUP 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 指的是 AtomicOrdinaryReplicated 这类库引擎,不是建表用的 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 到底抓了哪一行。

官方给的建议是:如果只是想要一个任意但稳定的值,用 minmax 替代 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_valuePARTITION 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/