先说几个经常被问到的问题:SQL里对非数值字段做自定义规则聚合,到底怎么实现?怎么才能顺畅地分组、去重、合并、甚至直接输出结构化对象?今天把几种最实用的写法都理一理。
很多时候我们面对的字段不是数字,而是字符串、状态码、配置键值对这类东西。直接GROUP BY后对字符串做COUNT或MAX,不仅没意义,还会把业务意图弄丢。比如用户状态字段里有'pending'、'reviewing'、'approved'、'rejected',你想按"处理中"和"已终审"两类来统计,这才是需要的能力。

用 CASE WHEN + 聚合函数实现字符串字段的规则计数
怎么解决?路子其实不复杂:先用CASE WHEN构造一个逻辑分组列,再在外面套一层聚合函数。注意SELECT和GROUP BY里要同步写同样的CASE表达式,保证数据库知道你在对同一个派生列进行分组和计数。
SELECT
CASE
WHEN status IN ('pending', 'reviewing') THEN 'processing'
WHEN status IN ('approved', 'rejected') THEN 'completed'
ELSE 'other'
END AS status_group,
COUNT(*) AS cnt
FROM orders
GROUP BY
CASE
WHEN status IN ('pending', 'reviewing') THEN 'processing'
WHEN status IN ('approved', 'rejected') THEN 'completed'
ELSE 'other'
END;
关键点在于各数据库的兼容性:MySQL和PostgreSQL允许在GROUP BY里直接写表达式;SQL Server要求这个表达式必须出现在SELECT列表中,或者用别名引用;SQLite虽然支持,但要确保表达式是确定性的,免得结果不稳定。
这一招几乎是最强通用方案了——不依赖特定数据库扩展,业务逻辑一目了然。
GROUP_CONCAT / STRING_AGG 里的重复与顺序,别指望默认值
需要把同一组的多个字符串拼成一个字段时,GROUP_CONCAT(MySQL)和STRING_AGG(PostgreSQL、SQL Server)出场率很高。但默认行为会给你挖不少坑:
GROUP_CONCAT(tag)不去重,相同tag会反复出现- 不指定
ORDER BY时,拼接顺序完全不可控 - MySQL的默认长度限制是1024字符,超长直接截断,数据会丢失
实操建议很直接:
• 去重拼接(MySQL):GROUP_CONCAT(DISTINCT tag ORDER BY tag SEPARATOR ',')
• 控制长度(MySQL):SET SESSION group_concat_max_len = 10000;
• PostgreSQL保证排序:STRING_AGG(tag, ',' ORDER BY tag)
• SQL Server注意空值处理:用STRING_AGG(ISNULL(tag, ''), ','),不然某个值为NULL时整个结果就变NULL了
一个小技巧:拼接前先想好是否要去重、是否要排序、是否要指定分隔符。把这些当成默认项写在SQL里,别靠数据库的“默认行为”。
用 JSON_OBJECT_AGG 汇总键值对,比字符串拼接更稳
有些非数值字段天生就是“键-值”结构,比如配置项、多语言文案。你如果要按主键把这些键值对聚合成一个结构化对象,用原生JSON聚合函数比手工拼字符串要安全准确得多。
直接看例子:把config_key和config_value按user_id合并为一个JSON对象。
-- PostgreSQL SELECT user_id, JSONB_OBJECT_AGG(config_key, config_value) FROM user_configs GROUP BY user_id;
各数据库的差异也要注意:
• MySQL 8.0+用的是JSON_OBJECTAGG(config_key, config_value)
• 键重复时,PostgreSQL的JSONB_OBJECT_AGG会保留最后一个值,而MySQL的JSON_OBJECTAGG直接报错——所以得提前去重
• 如果config_value本身是数字或布尔,JSON函数能自动保持类型;字符串拼接则会一律变成文本,丢掉了原始语义
这种方法对读取配置表、生成动态属性非常有用,尤其在需要直接返回前端JSON结构时,少一层序列化。
WHERE 过滤聚合结果?别想了,用 HA VING 或子查询
有时候你想筛选出“至少有两个不同标签的用户”,你不能写WHERE COUNT(DISTINCT tag) > 1——这会在聚合还没发生时就被执行,直接报错。
正确的选择只有两个:
• 简单场景用HA VING:HA VING COUNT(DISTINCT tag) > 1
• 复杂场景(比如既要聚合结果又要原始字段)用子查询或CTE:
WITH user_tag_stats AS (
SELECT user_id, COUNT(DISTINCT tag) AS distinct_tag_cnt
FROM user_tags
GROUP BY user_id
)
SELECT u.*, s.distinct_tag_cnt
FROM users u
JOIN user_tag_stats s ON u.id = s.user_id
WHERE s.distinct_tag_cnt > 1;
别想着在WHERE里用CASE WHEN去绕聚合结果——它根本还没算出来。这个限制和字段是否数值无关,是SQL的执行顺序决定的。
但真正麻烦的地方不只是语法。自定义规则聚合最大的痛点是:规则一旦变化,你得同步修改多个地方——CASE表达式、GROUP BY、HA VING、甚至展示逻辑。规则复杂到嵌套状态机时,尽早考虑把规则放到应用层,或者用数据库函数把逻辑封装起来,否则一个改动漏掉一处,结果就是不对的。
