很多“把多个指标塞进一条 SQL” 的需求,表面看只是少写几行代码,实际考验的是你有没有把指标之间的维度关系想清楚。下面按几种最常见的业务场景拆开说明:哪些统计必须拆开做,哪些可以合并,怎样在减少扫描次数的同时,尽量避免语义错位、NULL 传播和版本兼容问题。
多个去重指标为什么不能硬塞进同一个 GROUP BY
最容易出错的一类写法,是在同一个 GROUP BY province 里同时放多个 COUNT(DISTINCT ...),比如既统计 COUNT(DISTINCT user_id),又统计 COUNT(DISTINCT order_id)。这类 SQL 往往能通过语法检查,但结果未必可信。

问题不在于数据库“不会算”,而在于多个去重逻辑本身就不是同一个统计上下文。用户去重、订单去重、商品去重,背后的明细粒度可能完全不同;如果直接放进同一层聚合,数据库不会自动帮你隔离这些语义。
常见的异常现象包括:
- 某个省份有用户但没有订单,
COUNT(DISTINCT order_id)可能得到NULL或0,具体表现取决于引擎实现 - 后续再做 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;
这类写法的关键,不是“拆得多优雅”,而是保证每个指标单独成立,再用 COALESCE(..., 0) 把缺失值补齐,避免维度对齐后出现空洞。对于省份、渠道、日期这类汇总口径比较严格的报表,这一步通常不能省。
同表关联指标,优先考虑 CASE WHEN 条件聚合
如果多个指标来自同一张表,而且只是按不同条件分流统计,那么没必要把每个指标都拆成子查询。像“成功订单数”“失败订单数”“成功订单金额”这类指标,天然共享同一批明细,直接在聚合函数中嵌套 CASE WHEN 往往更合适。
这种方式适合几类典型场景:
- 同一张事实表按状态、类型、来源做条件拆分
- 希望减少对大表的重复扫描,降低 I/O 开销
- 已经在
WHERE中做过时间或范围过滤,不想在多个子查询里重复写筛选条件
示例里按客户统计总订单数、成功单数、失败单数以及成功金额,全部可以在一次聚合中完成:
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 ...) 更稳妥。原因是 COUNT(NULL) 天生不计数,如果某个分支全部落到 ELSE NULL,返回结果虽然常常也是 0,但语义其实依赖数据库对 NULL 的处理方式;而 SUM(CASE WHEN ... THEN 1 ELSE 0 END) 是显式累加,结果更可控,也更方便与金额类字段写法保持一致。
换句话说,只要几个指标共享同一明细粒度,而且不涉及彼此独立的去重逻辑,条件聚合通常就是优先选项。它代码更短,也更容易让后续维护者看懂这几个指标之间的关系。
跨表且无关联关系时,用 UNION ALL 合并结果
还有一类需求,看起来也像“多指标同查”,但本质上各指标来自不同表,而且彼此没有主外键关系。例如统计某天的订单总数,同时统计某天的分销记录总数。这里如果强行 JOIN,只会得到无意义的笛卡尔积,数据量一大还会直接拖垮查询。
这种场景更轻量的办法,是让每个子查询只产出自己那一列结果,再用 UNION ALL 拼成一个临时结果集,最后由外层做一次汇总。
使用时要注意三件事:
- 每个子查询输出的字段名和字段类型要一致,不存在的指标用
0或NULL占位 UNION ALL不去重、不排序,通常比UNION更省资源- 外层
SUM()会把占位列自然抵消,最后只留下真实指标值
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;
这段 SQL 还有一个很实用的提醒:order 是 MySQL 保留字,表名必须写成反引号包裹的 `order`,否则会直接报 ERROR 1064。这类问题和统计思路无关,却很容易在临时拼 SQL 时被忽略。
因此,面对“指标来自不同表,但又只想返回一行汇总结果”的需求,先判断表之间是否真的存在可用于对齐的业务键。如果没有,UNION ALL + 外层聚合 往往比 JOIN 更直接,也更不容易引入伪关联。
既要保留明细又要补充汇总值,可改用窗口函数
前面几种写法,核心都是为了输出汇总结果。但在实际分析中,常常还有另一种需求:既要保留订单级、记录级明细,又要在每一行旁边补上该客户的总订单数、总金额这类汇总指标。遇到这种情况,再用子查询做回表 JOIN 就显得笨重了。
更合适的工具是窗口函数,也就是 COUNT() OVER(PARTITION BY ...)、SUM() 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 支持这类 OVER() 写法,但 MariaDB 10.2–10.5 不支持,上线前最好先确认目标数据库版本,别等到发布时才发现语法根本过不去。
如果你的目标是“明细结果集 + 每行附带汇总列”,窗口函数通常比“先聚合、再 JOIN 回明细”更清晰。它不是为了炫技,而是因为它更贴合这类查询本身的结构。
最后怎么选:先看维度关系,再看数据库能力
真正难的部分,从来不是把 SQL 写到“能跑”,而是判断当前这个指标组合到底该用哪一种方法。一个比较实用的判断顺序是:

- 如果是多个彼此独立的去重统计,优先拆成独立子查询,再按维度
LEFT JOIN - 如果指标来自同表同粒度,只是按条件拆分,优先用
CASE WHEN条件聚合 - 如果指标来自不同表且无关联键,不要硬 JOIN,直接考虑
UNION ALL + SUM() - 如果既要明细又要分组汇总值,优先评估窗口函数是否可用
除此之外,还有两个很容易在后期放大问题的细节不能忽略。第一是 NULL 的隐式传播,它可能在 JOIN、COUNT、聚合补零这些环节里悄悄改变结果;第二是数据库方言差异,例如保留字、函数支持范围、窗口函数版本限制等,这些往往小数据时不明显,到了线上才集中暴露。
所以,与其追求“一条 SQL 写完所有指标”,不如先确认三个问题:这些指标是否共享维度、是否必须保留明细、当前数据库是否支持目标语法。把这三个判断做对,SQL 的写法通常就不会偏到太远。









