应使用 COUNT(DISTINCT category) 统计每个用户购买的不同商品类别数,配合 GROUP BY user_id 和 HAVING 筛选;NULL 值默认被忽略,需用 COALESCE 显式处理;务必为 (user_id, category) 建联合索引以提升性能。

GROUP BY 后怎么统计每个用户买了几类商品
关键不是数「买了多少件」,而是数「买了多少个不同的商品类别」。得用 COUNT(DISTINCT category),而不是 COUNT(*) 或 COUNT(category)——后者会把 NULL 或重复类别也算进去,结果不准。
假设表叫 orders,字段有 user_id、product_id、category(或通过 JOIN products 补全),先按用户分组,再算每人涉及的去重类别数:
SELECT user_id, COUNT(DISTINCT category) AS category_count FROM orders GROUP BY user_id;
HAVING 筛选 category_count = 1 的用户
HAVING 是唯一能在分组后过滤聚合结果的地方;用 WHERE 会报错,因为它在分组前执行,根本看不到 COUNT(DISTINCT category) 这个值。
常见错误写法:WHERE COUNT(DISTINCT category) = 1 → 语法错误;HAVING COUNT(DISTINCT category) > 1 写反了条件 → 拿到的是「买多类」的人。
正确写法:
SELECT user_id FROM orders GROUP BY user_id HAVING COUNT(DISTINCT category) = 1;
- 如果
category可能为NULL,COUNT(DISTINCT category)会忽略它,导致本该算作「未分类」的记录被漏计——此时要确认业务是否把 NULL 视为一类,必要时用COUNT(DISTINCT COALESCE(category, 'UNCATEGORIZED')) - MySQL 5.7 默认开启
ONLY_FULL_GROUP_BY,若 SELECT 列里有非分组、非聚合字段(比如写了SELECT user_id, category),会报错;只查user_id最安全
想同时看到类别名?得加子查询或窗口函数
单纯 GROUP BY user_id HAVING COUNT(DISTINCT category) = 1 只能返回 user_id,拿不到具体是哪一类。因为分组后每组只保留一个聚合结果,原始 category 值已丢失。
两种稳妥做法:
- 用子查询关联原表:先找出单类用户,再
JOIN回去取任意一条的category(因只有一类,任取即可) - 用窗口函数(PostgreSQL / MySQL 8.0+ / SQL Server):
SELECT DISTINCT user_id, FIRST_VALUE(category) OVER (PARTITION BY user_id ORDER BY category) AS category FROM orders WHERE user_id IN (SELECT user_id FROM orders GROUP BY user_id HAVING COUNT(DISTINCT category) = 1)
别用 SELECT user_id, category FROM orders GROUP BY user_id HAVING COUNT(DISTINCT category) = 1 ——多数数据库会报错,即使 MySQL 允许,category 值也是不确定的(非确定性行为)。
性能和 NULL 处理是实际跑不通的主因
线上表动辄百万行,COUNT(DISTINCT ...) 在无索引时很慢;而 category 字段若没建索引,GROUP BY user_id + COUNT(DISTINCT category) 组合会让优化器放弃使用 user_id 索引,转为临时表 + 文件排序。
建议操作:
- 给
(user_id, category)建联合索引,覆盖查询所需字段 - 检查
category是否允许 NULL;若业务上 NULL 代表「未归类」且需单独视为一类,必须显式处理,不能依赖默认聚合逻辑 - 在数据量大时,先用
EXPLAIN看执行计划,重点关注type是否为index或range,以及是否出现Using temporary; Using filesort
真正卡住的往往不是语法,而是 NULL 语义没对齐,或者索引没覆盖到 DISTINCT 所需字段。

















