多个COUNT(DISTINCT)不能共用同一GROUP BY,因语义冲突导致结果不可信;正确做法是拆为独立子查询并LEFT JOIN对齐,或依场景选用CASE WHEN、UNION ALL、窗口函数等替代方案。

多个COUNT(DISTINCT)不能共用同一个GROUP BY
直接写 COUNT(DISTINCT user_id) 和 COUNT(DISTINCT order_id) 在同一 GROUP BY province 里,看似语法通过,实际结果不可信。原因不是语法错,而是语义冲突:两个去重逻辑本应互不干扰,但数据库不会自动隔离上下文。
常见错误现象包括:
- 某省有用户但无订单,
COUNT(DISTINCT order_id)返回NULL或0(取决于引擎),导致 JOIN 后整行丢失 - JOIN 时因 NULL 匹配失败,省份维度对不齐,最终指标被低估
- 旧版 Hive 或 Presto 对
COUNT(DISTINCT NULL)处理不一致,有的报错,有的返回 0
正确做法是拆成独立子查询,再用 LEFT JOIN 对齐:
SELECT t1.province, COALESCE(t1.user_cnt, 0) AS user_cnt, COALESCE(t2.order_cnt, 0) AS order_cnt, COALESCE(t3.sku_cnt, 0) AS sku_cnt FROM ( SELECT province, COUNT(DISTINCT user_id) AS user_cnt FROM orders WHERE status = 'paid' GROUP BY province ) t1 LEFT JOIN ( SELECT province, COUNT(DISTINCT order_id) AS order_cnt FROM orders WHERE status = 'paid' GROUP BY province ) t2 ON t1.province = t2.province LEFT JOIN ( SELECT province, COUNT(DISTINCT sku_id) AS sku_cnt FROM order_items GROUP BY province ) t3 ON t1.province = t3.province;
用CASE WHEN在聚合内做条件统计
当指标之间存在逻辑关联(比如“成功订单数”“失败订单数”“总金额”),没必要拆子查询,用 CASE WHEN 嵌套在聚合函数里更高效、更安全。
典型场景:
- 同一张表里按状态分流统计
- 避免多次扫描大表,减少 I/O 开销
- WHERE 提前过滤后,
CASE内部不再需要重复判断
示例(统计每个客户的成功/失败订单数及总金额):
SELECT customer_id, COUNT(*) AS total_orders, SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) AS success_cnt, SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed_cnt, SUM(CASE WHEN status = 'success' THEN amount ELSE 0 END) AS success_amount FROM orders WHERE order_time >= '2026-08-01' GROUP BY customer_id;
注意:SUM(CASE ...) 比 COUNT(CASE ...) 更稳妥——后者遇到全为 ELSE NULL 会返回 0(因 COUNT(NULL) 不计数),而前者明确置 0,语义更可控。
跨表无关统计合并到单结果集
订单总数和分销记录总数来自不同表、无主外键关系,也不能 JOIN,硬连会笛卡尔积爆炸。这时候别强行 JOIN,用 UNION ALL + 外层聚合 是最轻量解法。
关键点:
- 每个子查询必须输出**相同字段名和类型**,缺失项补
0或NULL -
UNION ALL不去重、不排序,性能优于UNION - 外层
SUM()能天然把各子查询的“占位 0”抵消掉,只留下真实值
示例:
SELECT SUM(total_count) AS total_count, SUM(record_count) AS record_count FROM ( SELECT COUNT(*) AS total_count, 0 AS record_count FROM `order` WHERE DATE(order_create_time) = '2026-08-04' UNION ALL SELECT 0 AS total_count, COUNT(*) AS record_count FROM record WHERE DATE(create_time) = '2026-08-04' ) t;
别漏掉反引号——order 是 MySQL 保留字,不加会报错 ERROR 1064。
窗口函数替代部分子查询场景
如果指标需要“保留明细行 + 附加汇总值”,比如“每个订单显示所属客户总下单数”,就别用子查询 JOIN,改用 COUNT() OVER(PARTITION BY ...)。
优势明显:
- 一次扫描完成,避免多次读表
- 无需 GROUP BY,原始行结构完整保留
- 支持动态帧范围(如“近7天累计订单数”)
示例:
SELECT order_id, customer_id, amount, COUNT(*) OVER(PARTITION BY customer_id) AS customer_order_cnt, SUM(amount) OVER(PARTITION BY customer_id) AS customer_total_amount FROM orders WHERE order_time >= '2026-08-01';
注意:MySQL 8.0+、PostgreSQL、Oracle 都支持,但 MariaDB 10.2–10.5 不支持 OVER(),上线前务必确认版本。
真正难的不是写出能跑的 SQL,而是判断该用子查询、CASE 还是窗口函数——这取决于指标间是否共享维度、是否需保留明细、以及目标数据库的能力边界。容易被忽略的是 NULL 的隐式传播和保留字引发的语法陷阱,它们往往在数据量上来后才暴露。

















