flowchart LR
A[业务明细表<br/>长表 Long Format] -->|行转列 Pivot| B[报表宽表<br/>Wide Format]
B -->|列转行 Unpivot| C[标准指标明细<br/>Long Format]
A -->|窗口函数| D[排名/累计/环比/移动计算]
C -->|窗口函数| D
D -->|条件聚合或 PIVOT| E[最终报表/接口/看板]
flowchart TD
A[读取销售明细] --> B{product 是什么}
B -->|手机| C[写入手机金额表达式]
B -->|电脑| D[写入电脑金额表达式]
B -->|配件| E[写入配件金额表达式]
C --> F[按 region 聚合]
D --> F
E --> F
F --> G[输出一行多列]
2. 最通用的写法:条件聚合
条件聚合是跨数据库兼容性最好的行转列方式。
1 2 3 4 5 6 7 8 9
SELECT region, SUM(CASEWHEN product ='手机'THEN amount ELSE0END) AS phone_amount, SUM(CASEWHEN product ='电脑'THEN amount ELSE0END) AS computer_amount, SUM(CASEWHEN product ='配件'THEN amount ELSE0END) AS accessory_amount, SUM(amount) AS total_amount FROM sales_detail GROUPBY region ORDERBY region;
SELECT region, SUM( CASE WHEN product ='手机'AND channel ='线上' THEN amount ELSE0 END ) AS phone_online_amount, SUM( CASE WHEN product ='手机'AND channel ='线下' THEN amount ELSE0 END ) AS phone_offline_amount, SUM( CASE WHEN product ='电脑'AND channel ='线上' THEN amount ELSE0 END ) AS computer_online_amount, SUM( CASE WHEN product ='电脑'AND channel ='线下' THEN amount ELSE0 END ) AS computer_offline_amount FROM sales_detail GROUPBY region;
这种写法简单直接,但维度组合较多时,列数会迅速膨胀。
假设有:
20 个商品分类;
5 个销售渠道;
4 个指标。
最终可能产生:
1
20 × 5 × 4 = 400 列
这通常意味着结果已经不适合直接由 SQL 固定展开,应考虑:
返回长表给前端或 BI;
使用动态 SQL;
使用指标表或数据立方体;
在数据仓库中构建专用宽表;
将展示层转换与底层查询解耦。
5. PostgreSQL 的 FILTER 写法
PostgreSQL 支持在聚合函数上使用 FILTER:
1 2 3 4 5 6 7
SELECT region, SUM(amount) FILTER (WHERE product ='手机') AS phone_amount, SUM(amount) FILTER (WHERE product ='电脑') AS computer_amount, SUM(amount) FILTER (WHERE product ='配件') AS accessory_amount FROM sales_detail GROUPBY region;
它和条件聚合表达的是同一件事,但通常更容易阅读。
为了避免没有匹配记录时返回 NULL,可以使用 COALESCE:
1 2 3 4 5 6 7 8
SELECT region, COALESCE( SUM(amount) FILTER (WHERE product ='手机'), 0 ) AS phone_amount FROM sales_detail GROUPBY region;
6. SQL Server 的 PIVOT
SQL Server 提供原生 PIVOT:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
SELECT region, COALESCE([手机], 0) AS phone_amount, COALESCE([电脑], 0) AS computer_amount, COALESCE([配件], 0) AS accessory_amount FROM ( SELECT region, product, amount FROM sales_detail ) AS source_data PIVOT ( SUM(amount) FOR product IN ([手机], [电脑], [配件]) ) AS pivot_result;
语义可以拆解为:
1 2 3
SUM(amount) 对什么指标聚合 FOR product 哪一个字段的值需要转成列 IN (...) 最终生成哪些固定列
flowchart LR
A[phone_amount] --> D[product=手机<br/>amount=字段值]
B[computer_amount] --> E[product=电脑<br/>amount=字段值]
C[accessory_amount] --> F[product=配件<br/>amount=字段值]
D --> G[统一长表]
E --> G
F --> G
SELECT region, '手机'AS product, phone_amount AS amount FROM region_sales_wide
UNIONALL
SELECT region, '电脑'AS product, computer_amount AS amount FROM region_sales_wide
UNIONALL
SELECT region, '配件'AS product, accessory_amount AS amount FROM region_sales_wide;
为什么通常使用 UNION ALL,而不是 UNION?
UNION ALL 直接合并结果;
UNION 还会进行去重;
去重通常需要排序或哈希;
列转行本来就需要保留每个指标;
使用 UNION 可能产生不必要的性能损耗,甚至误删业务上合法的重复记录。
3. PostgreSQL:LATERAL + VALUES
PostgreSQL 可以使用 LATERAL 和 VALUES:
1 2 3 4 5 6 7 8 9 10 11
SELECT t.region, v.product, v.amount FROM region_sales_wide AS t CROSSJOINLATERAL ( VALUES ('手机', t.phone_amount), ('电脑', t.computer_amount), ('配件', t.accessory_amount) ) AS v(product, amount);
这种方式只需要扫描一次主表,代码也比多个 UNION ALL 更紧凑。
4. SQL Server:CROSS APPLY + VALUES
SQL Server 可以使用 CROSS APPLY:
1 2 3 4 5 6 7 8 9 10 11
SELECT t.region, v.product, v.amount FROM region_sales_wide AS t CROSS APPLY ( VALUES ('手机', t.phone_amount), ('电脑', t.computer_amount), ('配件', t.accessory_amount) ) AS v(product, amount);
5. SQL Server 的 UNPIVOT
1 2 3 4 5 6 7 8 9 10 11 12 13
SELECT region, product_column, amount FROM region_sales_wide UNPIVOT ( amount FOR product_column IN ( phone_amount, computer_amount, accessory_amount ) ) AS unpivot_result;
此时 product_column 的值是原始字段名:
1 2 3
phone_amount computer_amount accessory_amount
如果需要中文业务名称,可以再使用 CASE 映射:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
SELECT region, CASE product_column WHEN'phone_amount'THEN'手机' WHEN'computer_amount'THEN'电脑' WHEN'accessory_amount'THEN'配件' ENDAS product, amount FROM region_sales_wide UNPIVOT ( amount FOR product_column IN ( phone_amount, computer_amount, accessory_amount ) ) AS unpivot_result;
6. Oracle 的 UNPIVOT
1 2 3 4 5 6 7 8 9 10 11 12 13
SELECT region, product, amount FROM region_sales_wide UNPIVOT ( amount FOR product IN ( phone_amount AS'手机', computer_amount AS'电脑', accessory_amount AS'配件' ) );
7. 列转行时如何处理 NULL
假设:
region
phone_amount
computer_amount
华东
50000
NULL
列转行后是否保留:
1
华东,电脑,NULL
取决于业务语义。
场景一:NULL 表示没有发生
可以过滤:
1 2 3 4 5
SELECT* FROM ( -- 列转行 SQL ) AS unpivot_data WHERE amount ISNOT NULL;
场景二:NULL 表示金额为零
可以转换:
1
COALESCE(computer_amount, 0)
但要注意:
“没有数据”和“数据值为零”不是同一个概念。
在财务、库存、统计分析中,随意将 NULL 转为 0 可能会改变业务含义。
五、行转列与列转行的业务场景
1. 财务报表
原始凭证或指标明细:
organization
period
subject
amount
A 公司
2026-07
主营业务收入
100000
A 公司
2026-07
主营业务成本
60000
A 公司
2026-07
销售费用
10000
行转列后:
organization
period
revenue
cost
selling_expense
A 公司
2026-07
100000
60000
10000
2. 预算、实际、预测统一分析
业务表可能设计为:
department
budget_amount
actual_amount
forecast_amount
分析平台更适合转换成:
department
metric_type
amount
研发部
BUDGET
100000
研发部
ACTUAL
95000
研发部
FORECAST
102000
列转行后,可以统一计算:
差异率;
完成率;
排名;
同比;
环比;
异常阈值。
3. 问卷和动态属性
错误或过度宽表化的设计:
1 2 3 4 5
question_1 question_2 question_3 ... question_200
更合理的长表:
respondent_id
question_id
answer
1001
Q1
A
1001
Q2
B
固定模板导出时再进行行转列。
4. 商品规格
宽表:
1
color、size、weight、material
长表:
sku_id
attribute_name
attribute_value
10001
color
black
10001
size
XL
当属性高度动态时,长表更有扩展性;当属性固定且高频查询时,适度宽表化可能更高效。
六、窗口函数:不减少明细行的组内计算
1. 窗口函数解决什么问题
普通聚合:
1 2 3 4 5
SELECT region, SUM(amount) FROM sales_detail GROUPBY region;
会把多条明细压缩成一条汇总记录。
窗口函数:
1 2 3 4 5 6 7 8 9
SELECT id, region, product, amount, SUM(amount) OVER ( PARTITIONBY region ) AS region_total_amount FROM sales_detail;
不会减少原始行数,而是在每条明细旁边附加组内汇总值。
结果类似:
id
region
product
amount
region_total_amount
1
华东
手机
50000
105000
2
华东
电脑
40000
105000
3
华东
配件
15000
105000
这是窗口函数与 GROUP BY 最重要的区别:
flowchart TD
A[多条明细记录] --> B{使用什么计算}
B -->|GROUP BY| C[多行压缩为一行]
B -->|窗口函数| D[保留每条明细]
C --> E[只得到分组汇总]
D --> F[明细 + 排名/累计/组内指标]
2. 基本语法
1 2 3 4 5
窗口函数 OVER ( PARTITIONBY 分区字段 ORDERBY 排序字段 ROWS|RANGE|GROUPS 窗口范围 )
完整示例:
1 2 3 4 5
SUM(amount) OVER ( PARTITIONBY region ORDERBY biz_date, id ROWSBETWEEN UNBOUNDED PRECEDING ANDCURRENTROW )
各部分含义:
子句
作用
PARTITION BY
将结果划分为多个独立计算分区
ORDER BY
定义分区内部的计算顺序
ROWS/RANGE/GROUPS
定义当前行能够看到的窗口范围
OVER
表明函数按窗口规则执行,而不是普通聚合
3. 窗口函数的逻辑执行位置
窗口函数通常在 WHERE、GROUP BY、HAVING 之后计算,在最终 ORDER BY 之前可被结果引用。
flowchart TD
A[FROM / JOIN] --> B[WHERE]
B --> C[GROUP BY]
C --> D[HAVING]
D --> E[窗口函数]
E --> F[SELECT]
F --> G[DISTINCT]
G --> H[ORDER BY]
H --> I[LIMIT / FETCH]
因此,不能直接在同一层 WHERE 中使用窗口函数结果:
1 2 3 4 5 6 7 8 9 10 11
-- 错误或不被大多数数据库支持 SELECT region, product, amount, ROW_NUMBER() OVER ( PARTITIONBY region ORDERBY amount DESC ) AS rn FROM sales_detail WHERE rn <=3;
正确做法是使用子查询或 CTE:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
WITH ranked_sales AS ( SELECT region, product, amount, ROW_NUMBER() OVER ( PARTITIONBY region ORDERBY amount DESC, id ) AS rn FROM sales_detail ) SELECT region, product, amount FROM ranked_sales WHERE rn <=3;
部分分析型数据库支持 QUALIFY,可以直接过滤窗口结果:
1 2 3 4 5 6 7 8 9
SELECT region, product, amount FROM sales_detail QUALIFY ROW_NUMBER() OVER ( PARTITIONBY region ORDERBY amount DESC, id ) <=3;
但 MySQL、PostgreSQL、SQL Server 等常见事务型数据库通常仍需要 CTE 或子查询。
七、常用窗口函数详解
1. ROW_NUMBER:组内唯一序号
1 2 3 4 5 6 7 8 9 10
SELECT id, region, product, amount, ROW_NUMBER() OVER ( PARTITIONBY region ORDERBY amount DESC, id ) AS row_num FROM sales_detail;
特点:
每一行都有唯一序号;
即使金额相同,序号也不同;
适合组内 Top N;
适合去重保留最新一条;
排序字段最好包含稳定的唯一键。
如果只写:
1
ORDERBY amount DESC
而多条记录的金额相同,数据库可以按任意顺序分配序号,结果可能不稳定。
更安全:
1
ORDERBY amount DESC, id ASC
2. RANK:并列排名并跳号
假设金额为:
1
100、100、90、80
RANK() 结果:
1
1、1、3、4
1 2 3 4
RANK() OVER ( PARTITIONBY region ORDERBY amount DESC )
3. DENSE_RANK:并列排名不跳号
相同数据的 DENSE_RANK() 结果:
1
1、1、2、3
1 2 3 4
DENSE_RANK() OVER ( PARTITIONBY region ORDERBY amount DESC )
三者对比:
金额
ROW_NUMBER
RANK
DENSE_RANK
100
1
1
1
100
2
1
1
90
3
3
2
80
4
4
3
选择原则:
只需要唯一行号:ROW_NUMBER
并列后需要保留名次空缺:RANK
并列后不希望跳号:DENSE_RANK
4. NTILE:分桶
将数据按金额分成 4 组:
1 2 3 4 5 6 7 8
SELECT id, region, amount, NTILE(4) OVER ( ORDERBY amount DESC ) AS amount_quartile FROM sales_detail;
典型用途:
用户价值分层;
销售额四分位;
成绩分档;
风险等级分桶。
需要注意,NTILE(4) 是尽量按行数均分,而不是按数值区间平均切割。
5. SUM OVER:分组总计
1 2 3 4 5 6 7 8 9
SELECT id, region, product, amount, SUM(amount) OVER ( PARTITIONBY region ) AS region_total_amount FROM sales_detail;
6. 累计求和
1 2 3 4 5 6 7 8 9 10 11
SELECT id, biz_date, region, amount, SUM(amount) OVER ( PARTITIONBY region ORDERBY biz_date, id ROWSBETWEEN UNBOUNDED PRECEDING ANDCURRENTROW ) AS running_amount FROM sales_detail;
窗口范围:
1
从分区第一行,到当前行
flowchart LR
A[第1行] --> B[第2行]
B --> C[第3行]
C --> D[当前行]
D -.当前累计窗口.-> A
也可以简写为:
1
ROWS UNBOUNDED PRECEDING
但显式写完整范围更容易阅读和维护。
7. 移动平均
先按天聚合销售额,再计算最近 7 个数据日的移动平均:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
WITH daily_sales AS ( SELECT biz_date, SUM(amount) AS daily_amount FROM sales_detail GROUPBY biz_date ) SELECT biz_date, daily_amount, AVG(daily_amount) OVER ( ORDERBY biz_date ROWSBETWEEN6 PRECEDING ANDCURRENTROW ) AS moving_avg_7_rows FROM daily_sales ORDERBY biz_date;
注意:
1
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
表示“当前行及前 6 行”,不一定严格等于自然时间上的 7 天。
如果日期中间存在缺失:
1
7 月 1 日、7 月 2 日、7 月 10 日
这里仍然只是 3 行数据。
如果业务要求自然日连续窗口,可以:
先用日历表补齐日期;
再使用 ROWS;
或使用数据库支持的时间范围窗口;
明确区分“7 个记录日”和“最近 7 个自然日”。
8. LAG:获取上一行
1 2 3 4 5 6 7 8 9
SELECT biz_date, region, amount, LAG(amount, 1) OVER ( PARTITIONBY region ORDERBY biz_date, id ) AS previous_amount FROM sales_detail;
LAG(amount, 1) 表示取当前行之前第 1 行的 amount。
可指定默认值:
1
LAG(amount, 1, 0)
9. LEAD:获取下一行
1 2 3 4
LEAD(amount, 1) OVER ( PARTITIONBY region ORDERBY biz_date, id )
WITH monthly_sales AS ( SELECT region, EXTRACT(YEARFROM biz_date) AS sales_year, EXTRACT(MONTHFROM biz_date) AS sales_month, SUM(amount) AS monthly_amount FROM sales_detail GROUPBY region, EXTRACT(YEARFROM biz_date), EXTRACT(MONTHFROM biz_date) ), sales_with_previous AS ( SELECT region, sales_year, sales_month, monthly_amount, LAG(monthly_amount) OVER ( PARTITIONBY region ORDERBY sales_year, sales_month ) AS previous_month_amount FROM monthly_sales ) SELECT region, sales_year, sales_month, monthly_amount, previous_month_amount, CASE WHEN previous_month_amount ISNULL OR previous_month_amount =0 THENNULL ELSE (monthly_amount - previous_month_amount) / previous_month_amount *100 ENDAS month_over_month_rate FROM sales_with_previous;
计算公式:
1
环比增长率 =(本期金额 - 上期金额)/ 上期金额 × 100%
必须处理:
上期不存在;
上期金额为 0;
金额数据类型;
整数除法;
跨年月份排序;
缺失月份。
在 MySQL 中,月份提取可以使用:
1 2
YEAR(biz_date) MONTH(biz_date)
或者:
1
DATE_FORMAT(biz_date, '%Y-%m')
但排序时最好保留真正的年份和月份字段,避免字符串格式带来的隐患。
11. FIRST_VALUE 与 LAST_VALUE
获取分区第一笔金额:
1 2 3 4
FIRST_VALUE(amount) OVER ( PARTITIONBY region ORDERBY biz_date, id )
获取分区最后一笔金额时,要特别小心:
1 2 3 4
LAST_VALUE(amount) OVER ( PARTITIONBY region ORDERBY biz_date, id )
这段 SQL 的结果可能只是“当前窗口中的最后一行”,很多情况下就是当前行自己。
如果目标是获取整个分区的最后一个值,应显式指定完整窗口:
1 2 3 4 5 6
LAST_VALUE(amount) OVER ( PARTITIONBY region ORDERBY biz_date, id ROWSBETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING )
这是窗口函数中非常常见的坑。
12. COUNT OVER:总行数与组内行数
1 2 3 4 5 6 7 8 9
SELECT id, region, product, COUNT(*) OVER () AS all_row_count, COUNT(*) OVER ( PARTITIONBY region ) AS region_row_count FROM sales_detail;
适合在分页查询中同时返回总记录数:
1 2 3 4 5 6 7 8 9 10
SELECT id, region, product, amount, COUNT(*) OVER () AS total_count FROM sales_detail WHERE biz_date >='2026-07-01' ORDERBY id LIMIT 20OFFSET0;
SELECT id, region, product, amount, SUM(amount) OVER ( PARTITIONBY region ) AS region_total_amount, amount /NULLIF( SUM(amount) OVER ( PARTITIONBY region ), 0 ) *100AS region_amount_rate FROM sales_detail;
NULLIF(value, 0) 可以避免除零错误:
1
NULLIF(region_total_amount, 0)
当总金额为 0 时返回 NULL。
八、ROWS、RANGE 与 GROUPS 的区别
窗口范围是窗口函数最容易被忽视、也最容易出错的部分。
1. ROWS
ROWS 按物理行数计算。
1
ROWSBETWEEN2 PRECEDING ANDCURRENTROW
表示:
1
当前行 + 前两行
无论排序值是否相同,都按实际行数截取。
2. RANGE
RANGE 按排序值范围或同值组计算。
当多行的 ORDER BY 值相同时,这些行可能被视为同一组同位值。
例如:
amount
ROWS 累计
RANGE 累计
100
100
200
100
200
200
80
280
280
具体语法能力和行为会因数据库版本、排序字段类型而存在差异。
3. GROUPS
GROUPS 按排序同值组计算,而不是按单行或连续值区间计算。
它适合表达:
1
当前同值组以及前两个同值组
并非所有数据库版本都支持 GROUPS,使用前需要确认兼容性。
4. 为什么建议显式写 ROWS
下面的写法:
1 2 3 4
SUM(amount) OVER ( PARTITIONBY region ORDERBY biz_date )
在许多数据库中会使用包含同位值语义的默认窗口范围。
如果同一天存在多条记录,累计结果可能一次性包含同一天所有记录,而不是逐行递增。
为了得到稳定的逐行累计结果,建议:
1 2 3 4 5
SUM(amount) OVER ( PARTITIONBY region ORDERBY biz_date, id ROWSBETWEEN UNBOUNDED PRECEDING ANDCURRENTROW )
WITH ranked_sales AS ( SELECT id, region, product, amount, ROW_NUMBER() OVER ( PARTITIONBY region ORDERBY amount DESC, id ) AS rn FROM sales_detail ) SELECT id, region, product, amount FROM ranked_sales WHERE rn <=3 ORDERBY region, rn;
2. 每个商品保留最新一条记录
假设商品价格历史表:
1 2 3 4 5 6
CREATE TABLE product_price_history ( id BIGINTPRIMARY KEY, product_id BIGINTNOT NULL, price DECIMAL(18, 2) NOT NULL, effective_at TIMESTAMPNOT NULL );
查询每个商品最新价格:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
WITH latest_price AS ( SELECT id, product_id, price, effective_at, ROW_NUMBER() OVER ( PARTITIONBY product_id ORDERBY effective_at DESC, id DESC ) AS rn FROM product_price_history ) SELECT id, product_id, price, effective_at FROM latest_price WHERE rn =1;
WITH login_ordered AS ( SELECT user_id, login_date, ROW_NUMBER() OVER ( PARTITIONBY user_id ORDERBY login_date ) AS rn FROM user_login ), login_grouped AS ( SELECT user_id, login_date, login_date - rn *INTERVAL'1 day'AS group_key FROM login_ordered ) SELECT user_id, MIN(login_date) AS start_date, MAX(login_date) AS end_date, COUNT(*) AS consecutive_days FROM login_grouped GROUPBY user_id, group_key;
WITH status_compare AS ( SELECT order_id, change_time, status, LAG(status) OVER ( PARTITIONBY order_id ORDERBY change_time ) AS previous_status FROM order_status_log ), status_flag AS ( SELECT order_id, change_time, status, CASE WHEN previous_status = status THEN0 ELSE1 ENDAS is_new_group FROM status_compare ), status_grouped AS ( SELECT order_id, change_time, status, SUM(is_new_group) OVER ( PARTITIONBY order_id ORDERBY change_time ROWS UNBOUNDED PRECEDING ) AS group_no FROM status_flag ) SELECT order_id, status, MIN(change_time) AS start_time, MAX(change_time) AS end_time FROM status_grouped GROUPBY order_id, group_no, status;
这是窗口函数中非常典型的“先比较,再累计分组”模式。
十、行转列与窗口函数组合
在真实报表中,经常需要先使用窗口函数计算排名,再将排名结果转成列。
例如,每个地区销售额最高的 3 个商品,输出为:
region
top1_product
top1_amount
top2_product
top2_amount
top3_product
top3_amount
1. 先按地区、商品聚合
1 2 3 4 5 6 7 8
WITH product_sales AS ( SELECT region, product, SUM(amount) AS product_amount FROM sales_detail GROUPBY region, product )
2. 使用窗口函数排名
1 2 3 4 5 6 7 8 9 10 11
, ranked_product AS ( SELECT region, product, product_amount, ROW_NUMBER() OVER ( PARTITIONBY region ORDERBY product_amount DESC, product ) AS rn FROM product_sales )
WITH metric_data AS ( SELECT department, 'BUDGET'AS metric_type, budget_amount AS amount FROM department_finance
UNIONALL
SELECT department, 'ACTUAL'AS metric_type, actual_amount AS amount FROM department_finance
UNIONALL
SELECT department, 'FORECAST'AS metric_type, forecast_amount AS amount FROM department_finance ) SELECT department, metric_type, amount, DENSE_RANK() OVER ( PARTITIONBY metric_type ORDERBY amount DESC ) AS metric_rank FROM metric_data;
转换后,所有指标都具有统一结构,因此可以复用同一套分析逻辑。
flowchart TD
A[预算/实际/预测宽表] --> B[列转行]
B --> C[统一 metric_type + amount]
C --> D[窗口函数分指标排名]
D --> E[差异分析/排名/占比/异常识别]
十二、GROUP BY 与窗口函数如何选择
对比项
GROUP BY
窗口函数
是否减少行数
会
不会
是否保留明细
否
是
分组汇总
擅长
支持
组内排名
不直接支持
擅长
累计计算
写法复杂
擅长
前后行比较
不擅长
LAG/LEAD
Top N
需要关联或子查询
ROW_NUMBER/RANK
报表总计
擅长
可附加到明细
使用位置
聚合阶段
聚合之后的分析阶段
常见组合方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
WITH aggregated AS ( SELECT region, product, SUM(amount) AS product_amount FROM sales_detail GROUPBY region, product ) SELECT region, product, product_amount, RANK() OVER ( PARTITIONBY region ORDERBY product_amount DESC ) AS product_rank FROM aggregated;
先 GROUP BY 形成正确粒度,再用窗口函数做组内分析,是企业 SQL 中非常常见的模式。
十三、常见错误与陷阱
1. 在 WHERE 中直接使用窗口别名
错误:
1 2 3 4 5 6 7 8
SELECT *, ROW_NUMBER() OVER ( PARTITIONBY region ORDERBY amount DESC ) AS rn FROM sales_detail WHERE rn =1;
原因:WHERE 执行时,窗口函数结果还没有生成。
解决:使用 CTE 或子查询。
2. 行转列时把 AVG 的 ELSE 写成 0
错误:
1
AVG(CASEWHEN product ='手机'THEN amount ELSE0END)
这会把无关行当作零值参与平均。
正确:
1
AVG(CASEWHEN product ='手机'THEN amount END)
3. 排名没有稳定排序字段
1 2 3 4
ROW_NUMBER() OVER ( PARTITIONBY region ORDERBY amount DESC )
当金额相同时,结果不确定。
建议:
1
ORDERBY amount DESC, id
4. LAST_VALUE 没有指定完整窗口
错误:
1 2 3 4
LAST_VALUE(amount) OVER ( PARTITIONBY region ORDERBY biz_date )
它可能返回当前行,而不是分区最后一行。
正确:
1 2 3 4 5 6
LAST_VALUE(amount) OVER ( PARTITIONBY region ORDERBY biz_date, id ROWSBETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING )
WITH item_summary AS ( SELECT order_id, SUM(item_amount) AS item_amount FROM order_item GROUPBY order_id ), payment_summary AS ( SELECT order_id, SUM(payment_amount) AS payment_amount FROM order_payment GROUPBY order_id ) SELECT o.id, i.item_amount, p.payment_amount FROM orders AS o LEFTJOIN item_summary AS i ON i.order_id = o.id LEFTJOIN payment_summary AS p ON p.order_id = o.id;
行转列和窗口函数都不能自动修复错误的数据粒度。
十四、性能优化
1. 先过滤,再转换
不要对全表先行转列,再过滤日期:
1 2 3 4 5 6
-- 不推荐的思路 SELECT* FROM ( -- 对全量数据行转列 ) AS pivot_data WHERE ...
应该尽量在源数据阶段缩小范围:
1 2 3 4 5 6 7
SELECT region, SUM(CASEWHEN product ='手机'THEN amount ELSE0END) AS phone_amount FROM sales_detail WHERE biz_date >='2026-07-01' AND biz_date <'2026-08-01' GROUPBY region;
CREATE INDEX idx_sales_region_date_id ON sales_detail(region, biz_date, id);
但索引是否能消除排序,取决于:
查询过滤条件;
JOIN 顺序;
分区字段;
排序方向;
数据库优化器;
是否还需要读取其他字段;
是否发生聚合;
数据量和选择性。
不能看到 PARTITION BY + ORDER BY 就机械创建索引,必须查看执行计划。
5. 关注排序和临时空间
窗口查询的高成本操作常见于:
Sort;
Window Aggregate;
Temporary Table;
Disk Spill;
Filesort;
大分区内存占用。
应重点观察:
排序数据量;
单个分区最大行数;
是否落盘;
临时表大小;
执行内存;
并行度;
是否重复排序。
6. 复用相同窗口定义
如果多个窗口函数使用相同分区和排序规则,可以使用命名窗口。
MySQL、PostgreSQL 等数据库支持类似写法:
1 2 3 4 5 6 7 8 9 10 11 12 13
SELECT id, region, biz_date, amount, SUM(amount) OVER sales_window AS running_amount, AVG(amount) OVER sales_window AS running_avg FROM sales_detail WINDOW sales_window AS ( PARTITIONBY region ORDERBY biz_date, id ROWSBETWEEN UNBOUNDED PRECEDING ANDCURRENTROW );
这样可以减少重复代码,也便于数据库识别可复用的排序规则。
7. 避免无意义的大窗口
1
SUM(amount) OVER ()
意味着所有结果行属于一个分区。
当数据量非常大时:
所有数据都要参与计算;
分页也未必能提前停止;
可能需要扫描完整结果;
延迟和内存压力可能显著增加。
8. 用 EXPLAIN 验证,而不是凭感觉优化
需要关注:
1 2
EXPLAIN SELECT ...;
或数据库支持的实际执行分析命令。
重点检查:
是否扫描了过多数据;
过滤是否尽早生效;
是否发生重复扫描;
是否出现超大排序;
是否使用临时表;
是否发生磁盘溢写;
估算行数是否明显失真;
统计信息是否过期。
十五、数据库实现选择建议
MySQL
优先使用:
行转列:SUM(CASE WHEN ... THEN ... END)
列转行:UNION ALL
窗口函数:MySQL 8.0 的 OVER
动态行转列:应用层拼接或存储过程动态 SQL
MySQL 没有类似 SQL Server 的原生 PIVOT/UNPIVOT 语法,条件聚合通常最实用。
flowchart TD
A{结果列是否固定}
A -->|是| B{数据量是否较大}
A -->|否| C{是否用于 BI 或动态表格}
B -->|是| D[数据库条件聚合或 PIVOT]
B -->|否| E[数据库或应用层均可]
C -->|是| F[优先返回长表]
C -->|否| G[应用层生成动态列]
D --> H[固定报表/导出]
E --> H
F --> I[BI/前端完成透视]
G --> I
适合在数据库做
列固定;
数据量大;
需要减少网络传输;
转换逻辑可以被 SQL 清晰表达;
需要数据库直接输出固定报表;
需要利用数据库聚合能力。
适合在应用层做
列高度动态;
不同用户选择不同指标;
结果量较小;
前端组件天然支持动态列;
需要复杂格式、合并单元格或多层表头;
数据库 SQL 因动态列变得难以维护。
适合在数据仓库或 BI 做
超大规模离线分析;
多维钻取;
大量指标复用;
需要预聚合;
需要统一指标口径;
需要面向分析师自由透视。
十七、实战设计:财务经营分析报表
假设底层有统一指标明细表:
1 2 3 4 5 6 7
CREATE TABLE finance_metric_detail ( id BIGINTPRIMARY KEY, accounting_period VARCHAR(16) NOT NULL, organization_id BIGINTNOT NULL, metric_code VARCHAR(64) NOT NULL, metric_value DECIMAL(18, 2) NOT NULL );
WITH metric_summary AS ( SELECT accounting_period, organization_id, SUM( CASE WHEN metric_code ='REVENUE' THEN metric_value ELSE0 END ) AS revenue, SUM( CASE WHEN metric_code ='COST' THEN metric_value ELSE0 END ) AS cost FROM finance_metric_detail GROUPBY accounting_period, organization_id )
2. 计算派生指标和窗口指标
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
, metric_analysis AS ( SELECT accounting_period, organization_id, revenue, cost, revenue - cost AS profit, LAG(revenue) OVER ( PARTITIONBY organization_id ORDERBY accounting_period ) AS previous_revenue, DENSE_RANK() OVER ( PARTITIONBY accounting_period ORDERBY revenue DESC ) AS revenue_rank FROM metric_summary )
WITH metric_summary AS ( SELECT accounting_period, organization_id, SUM( CASE WHEN metric_code ='REVENUE' THEN metric_value ELSE0 END ) AS revenue, SUM( CASE WHEN metric_code ='COST' THEN metric_value ELSE0 END ) AS cost FROM finance_metric_detail GROUPBY accounting_period, organization_id ), metric_analysis AS ( SELECT accounting_period, organization_id, revenue, cost, revenue - cost AS profit, LAG(revenue) OVER ( PARTITIONBY organization_id ORDERBY accounting_period ) AS previous_revenue, DENSE_RANK() OVER ( PARTITIONBY accounting_period ORDERBY revenue DESC ) AS revenue_rank FROM metric_summary ) SELECT accounting_period, organization_id, revenue, cost, profit, previous_revenue, CASE WHEN previous_revenue ISNULL OR previous_revenue =0 THENNULL ELSE (revenue - previous_revenue) / previous_revenue *100 ENDAS revenue_mom_rate, revenue_rank FROM metric_analysis ORDERBY accounting_period, revenue_rank;
数据处理链路:
flowchart LR
A[财务指标长表] --> B[条件聚合行转列]
B --> C[收入/成本固定宽表]
C --> D[计算利润]
D --> E[LAG 计算上期收入]
E --> F[计算环比]
F --> G[DENSE_RANK 组织排名]
G --> H[经营分析报表]
这种设计的优势是:
底层指标结构统一;
新增指标不一定需要修改表结构;
固定报表可以通过行转列生成;
窗口函数负责跨期间和组内分析;
数据粒度清晰;
业务口径更容易复用。
十八、选型速查表
需求
推荐方案
固定几种状态转成列
条件聚合
SQL Server 固定列透视
PIVOT
Oracle 固定列透视
PIVOT
MySQL 行转列
SUM(CASE WHEN ...)
固定宽表还原成长表
UNION ALL
PostgreSQL 列转行
LATERAL + VALUES
SQL Server 列转行
CROSS APPLY 或 UNPIVOT
每组 Top N
ROW_NUMBER / RANK
组内并列排名
RANK / DENSE_RANK
累计金额
SUM() OVER
前后记录差异
LAG / LEAD
移动平均
AVG() OVER + 窗口范围
最新一条去重
ROW_NUMBER() ... WHERE rn = 1
动态列数量较少
动态 SQL
动态列数量很多
返回长表,由 BI 或应用层透视
财务宽表统一分析
先列转行,再使用窗口函数
排名结果生成固定报表
先窗口排名,再条件聚合
十九、总结
行转列、列转行和窗口函数分别解决三个层次的问题。
行转列
负责将维度值展开成字段,适合:
固定报表;
Excel 导出;
多指标横向对比;
状态统计;
财务科目展示。
最通用的写法是:
1
SUM(CASEWHENconditionTHENvalueELSE0END)
列转行
负责将多个字段还原为统一的“指标名称 + 指标值”,适合:
统一指标分析;
预算、实际、预测对比;
数据清洗;
宽表规范化;
动态分析。
最通用的写法是:
1
UNIONALL
窗口函数
负责在不丢失明细的前提下进行组内分析,适合:
排名;
累计;
移动平均;
同比环比;
前后行比较;
Top N;
去重;
连续区间识别。
三者组合后,可以形成完整的数据处理链路:
flowchart LR
A[明细长表] --> B[聚合到正确粒度]
B --> C[窗口函数分析]
C --> D[排名/累计/环比/占比]
D --> E[条件聚合行转列]
E --> F[固定格式报表]
F -->|需要再次统一分析| G[列转行]
G --> C