用条件聚合做行转列
把行变成列的标准手法是条件聚合:为每个目标列写一个 SUM(CASE WHEN 维度 = 值 THEN 度量 ELSE 0 END),一次扫描就能同时算出多列结果。MySQL 8 与 PostgreSQL 都支持这种写法;MySQL 还可以用 IF() 简写,PostgreSQL 只能用 CASE 或 FILTER 子句。相比把多个指标拆成多条查询再由应用层拼装,条件聚合只扫一遍数据,与 GROUP BY 天然配合,是报表与看板最常用的透视实现方式。
基本信息
| 适用平台 | MySQL 8.0 / PostgreSQL / SQL Server / Oracle |
|---|---|
| 通用写法 | SUM(CASE WHEN 条件 THEN 度量 ELSE 0 END) |
| MySQL 简写 | SUM(IF(条件, 度量, 0)),PostgreSQL 没有 IF() |
| NULL 处理 | SUM 忽略 NULL;无匹配行时 SUM 返回 NULL,可用 COALESCE 兜底 |
| 列数限制 | SQL 的列必须在写语句时确定,维度值动态变化时要靠应用层拼 SQL 或改用 JSON 聚合 |
典型使用场景
- 订单状态透视:一行同时展示已支付、已退款、待发货等各状态数量
- 月份宽表:把 1 月到 12 月的金额铺成 12 列,便于导出与图表
- 渠道拆解:同一行内比较各渠道的销售额与订单量
- 看板一次查询返回全部指标,减少往返次数
- 与总量列配合算占比:既看绝对值也看结构
MySQL 写法
MySQL 写法
SELECT DATE(created_at) AS d,
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) AS paid_cnt,
SUM(CASE WHEN status = 'refunded' THEN 1 ELSE 0 END) AS refund_cnt,
SUM(amount) AS total_amount,
SUM(IF(channel = 'app', amount, 0)) AS app_amount # MySQL 可用 IF 简写
FROM orders
WHERE created_at >= '2026-09-01'
GROUP BY DATE(created_at)
ORDER BY d;PostgreSQL 写法
PostgreSQL 写法
-- PostgreSQL 没有 IF(),可以用 CASE,也可以用 SQL 标准的 FILTER
SELECT date_trunc('month', created_at)::date AS m,
COUNT(*) FILTER (WHERE status = 'paid') AS paid_cnt,
COUNT(*) FILTER (WHERE status = 'refunded') AS refund_cnt,
COALESCE(SUM(amount) FILTER (WHERE status = 'paid'), 0) AS paid_amount
FROM orders
GROUP BY 1
ORDER BY 1;几种写法对照
| 写法 | 支持的库 | 备注 |
|---|---|---|
| SUM(CASE WHEN … THEN 1 ELSE 0 END) | 通用 | 跨库一致,最稳的选择 |
| SUM(IF(cond, val, 0)) | MySQL | 更短,PostgreSQL 不支持 |
| COUNT(*) FILTER (WHERE …) | PostgreSQL 等 | 可读性好;COUNT 无匹配时返回 0 |
| SUM(…) FILTER (WHERE …) | PostgreSQL 等 | 无匹配行时返回 NULL,需要 COALESCE |
| 原生 PIVOT 语法 | Oracle / SQL Server | MySQL 与 PostgreSQL 未提供 |
条件聚合的列数是写死的:如果维度值来自数据(渠道列表经常变),要么由应用层根据维度凑 SQL,要么先聚合成 JSON 交给前端做透视。
透视的筛选条件必须放在 CASE 分支里:WHERE 在聚合前就执行,把状态条件写进 WHERE 会让其他状态整列消失,只剩一个非零分支。
各分支列相加应等于总量列:这个等式是最实用的口径自检,一旦对不上就说明 CASE 条件与业务口径不一致,或存在未覆盖的状态取值。
别把 COUNT(*) 与 COUNT(CASE WHEN … THEN 1 END) 混用:前者统计所有行,后者只统计满足条件的行;写成 COUNT(*) 做条件计数会得到全量行数,指标静默失真。