GROUP BY 本身不能识别“从未变更”的状态,需用 COUNT(DISTINCT status) = 1 或 MIN(status) = MAX(status) 配合 HAVING 筛选;若含 NULL 需 COALESCE 处理,复杂时应使用窗口函数结合子查询。

GROUP BY 本身不能直接识别“从未变更”的状态
“从未变更”是跨行比较的逻辑,GROUP BY 只负责分组聚合,不提供行间状态对比能力。你真正需要的是:先按用户分组拿到状态变化次数,再筛选出变化次数为 0 或 1(且所有记录状态一致)的用户。关键在如何定义“变更”——通常指同一 user_id 下存在至少两个不同 status 值。
用 COUNT(DISTINCT status) 配合 HAVING 筛选不变用户
这是最直接、兼容性最好的方法,适用于 MySQL、PostgreSQL、SQL Server 等主流数据库:
SELECT user_id FROM user_status_log GROUP BY user_id HAVING COUNT(DISTINCT status) = 1;
注意点:
-
COUNT(DISTINCT status)对空值(NULL)默认忽略;如果业务中status允许为NULL且需将其视为一种有效状态,得先用COALESCE(status, 'NULL_VAL')统一处理 - 该写法假设每条记录代表一次状态快照(非增量日志)。如果是带时间戳的变更日志,且允许重复插入相同状态,仍可用此法——只要最终
DISTINCT结果为 1,就说明没变过 - 性能上,若表很大,建议在
(user_id, status)上建联合索引
当需要排除“只有一条记录”的用户时,加 MIN() = MAX() 校验
有些场景下,“从未变更”隐含“至少有两条记录且状态相同”,比如系统要求用户必须经历两次确认才生效。此时仅靠 COUNT(DISTINCT) = 1 会把单条记录用户也纳入。稳妥做法是同时校验记录数和极值:
SELECT user_id FROM user_status_log GROUP BY user_id HAVING COUNT(*) > 1 AND MIN(status) = MAX(status);
这样既避免了 DISTINCT 在部分旧版 MySQL 中的潜在优化问题,又明确排除了单记录用户。注意:
-
MIN(status) = MAX(status)要求status是可比类型(如字符串、数字),对 JSON 或数组类型不适用 - 若
status含NULL,MIN/MAX会跳过它,导致NULL和非NULL混存时也可能满足等式——此时必须显式处理NULL,例如:MIN(COALESCE(status, '##NULL##')) = MAX(COALESCE(status, '##NULL##'))
用窗口函数标记首次/末次状态(适合复杂判定)
如果“从未变更”还需结合时间字段(如 created_at),比如要求“首条和末条记录状态相同且中间无其他值”,就得用窗口函数:
SELECT user_id
FROM (
SELECT user_id,
FIRST_VALUE(status) OVER (PARTITION BY user_id ORDER BY created_at) AS first_status,
LAST_VALUE(status) OVER (PARTITION BY user_id ORDER BY created_at ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_status,
COUNT(*) OVER (PARTITION BY user_id) AS cnt
FROM user_status_log
) t
WHERE first_status = last_status AND cnt > 1;
这里容易踩的坑:
-
LAST_VALUE默认窗口是ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,不加ROWS BETWEEN ...修饰会出错或返回意外值 - 不同数据库对
FIRST_VALUE/LAST_VALUE的NULL处理略有差异,PostgreSQL 支持IGNORE NULLS,MySQL 8.0+ 不支持,需提前COALESCE - 窗口函数无法在
HAVING中使用,必须嵌套子查询
真正难的不是写出 GROUP BY,而是想清楚“从未变更”在你数据模型里究竟对应哪几种物理表现——单记录?多记录但值全同?还是时间轴上首尾一致?这些语义差异会直接决定该用聚合函数还是窗口函数,以及是否要额外处理 NULL 和边界情况。

















