SQL窗口函数实战:5个高频场景完整代码,数据分析必掌握
90%的数据分析师,SQL只用到了不到30%的功力。
剩下那70%,就是 窗口函数(Window Function)。
遇到排名、同比、累计、移动平均这类问题,大多数人的第一反应是写子查询、用Excel辅助处理,或者导出后用Python转一遍。折腾2小时,写了100行代码,最后的结果——一个窗口函数,5行SQL就能搞定。
这篇文章,用 5个真实业务场景 + 完整可运行代码,带你把窗口函数彻底搞懂。文末附建表语句,可以直接复制到MySQL/PostgreSQL跑起来。
一、窗口函数是什么?(一句话搞懂)
普通聚合函数(SUM、AVG)会把多行压成一行;窗口函数在保留原来每一行的同时,给每行附加一个基于"窗口范围"计算的结果。
基本语法:
函数名() OVER (
PARTITION BY 分组字段 -- 按哪个维度分组(不压缩行数)
ORDER BY 排序字段 -- 窗口内排序规则
ROWS BETWEEN 范围起点 AND 范围终点 -- 滑动窗口范围(可省略)
)
三个关键子句:
- PARTITION BY:类似GROUP BY,但不合并行,只是分区
- ORDER BY:控制窗口内行的顺序,影响排名、移动计算等
- ROWS BETWEEN:定义"窗口滑动范围",默认从分区起点到当前行
常用范围关键词:
UNBOUNDED PRECEDING:分区的第一行CURRENT ROW:当前行UNBOUNDED FOLLOWING:分区的最后一行N PRECEDING / N FOLLOWING:当前行往前/往后N行
二、建表语句(所有示例可直接运行)
-- 门店销售表
CREATE TABLE store_sales (
id INT AUTO_INCREMENT PRIMARY KEY,
city VARCHAR(50),
store_name VARCHAR(100),
sales DECIMAL(12,2),
sale_date DATE
);
INSERT INTO store_sales (city, store_name, sales, sale_date) VALUES
('北京', '中关村店', 152000, '2026-03-01'),
('北京', '望京店', 138000, '2026-03-01'),
('北京', '朝阳店', 175000, '2026-03-01'),
('北京', '西城店', 96000, '2026-03-01'),
('上海', '徐汇店', 210000, '2026-03-01'),
('上海', '浦东店', 189000, '2026-03-01'),
('上海', '静安店', 165000, '2026-03-01'),
('上海', '虹桥店', 142000, '2026-03-01');
-- 月度销售汇总表
CREATE TABLE monthly_sales (
month VARCHAR(7),
sales DECIMAL(12,2)
);
INSERT INTO monthly_sales VALUES
('2025-09', 820000), ('2025-10', 910000), ('2025-11', 870000),
('2025-12', 1050000), ('2026-01', 980000), ('2026-02', 1100000),
('2026-03', 1230000);
-- 用户订单表
CREATE TABLE orders (
order_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
order_date DATE,
amount DECIMAL(10,2)
);
INSERT INTO orders (user_id, order_date, amount) VALUES
(1001, '2026-01-05', 299), (1001, '2026-01-18', 150), (1001, '2026-02-03', 520),
(1001, '2026-03-12', 88), (1002, '2026-01-10', 450), (1002, '2026-02-20', 320),
(1002, '2026-03-05', 780);
-- 用户总消费表
CREATE TABLE user_spend (
user_id INT PRIMARY KEY,
total_spend DECIMAL(12,2)
);
INSERT INTO user_spend VALUES
(1001,1057),(1002,1550),(1003,320),(1004,4800),(1005,750),
(1006,2100),(1007,190),(1008,3300),(1009,880),(1010,650);
-- 产品销售表
CREATE TABLE products (
category VARCHAR(50),
product_name VARCHAR(100),
sales DECIMAL(12,2)
);
INSERT INTO products VALUES
('手机','型号A',52000),('手机','型号B',38000),('手机','型号C',71000),
('电脑','笔记本X',89000),('电脑','台式机Y',45000),('电脑','笔记本Z',63000),
('配件','耳机M',21000),('配件','键盘N',18000),('配件','鼠标O',15000);
三、函数1:ROW_NUMBER() — 排名去重,再也不怕重复值
业务场景:每个城市销售额最高的TOP3门店
传统写法:子查询 + JOIN,至少20行。
窗口函数写法:
-- 方法:先用子查询(或CTE)计算排名,再外层过滤
-- 注意:窗口函数不能直接在WHERE里用
WITH ranked AS (
SELECT
city,
store_name,
sales,
ROW_NUMBER() OVER (
PARTITION BY city
ORDER BY sales DESC
) AS rn
FROM store_sales
)
SELECT city, store_name, sales
FROM ranked
WHERE rn <= 3
ORDER BY city, rn;
运行结果示例:
+--------+----------+--------+
| city | store_name | sales |
+--------+----------+--------+
| 北京 | 朝阳店 | 175000 |
| 北京 | 中关村店 | 152000 |
| 北京 | 望京店 | 138000 |
| 上海 | 徐汇店 | 210000 |
| 上海 | 浦东店 | 189000 |
| 上海 | 静安店 | 165000 |
+--------+----------+--------+
ROW_NUMBER / RANK / DENSE_RANK 三者区别:
-- 假设销售额有并列时,三种函数返回不同结果
SELECT
store_name, sales,
ROW_NUMBER() OVER (ORDER BY sales DESC) AS row_num, -- 唯一序号,并列也不同
RANK() OVER (ORDER BY sales DESC) AS rnk, -- 并列相同名次,下一名跳号
DENSE_RANK() OVER (ORDER BY sales DESC) AS dense_rnk -- 并列相同名次,下一名连续
FROM store_sales;
选哪个:
- 要唯一序号(如分页、去重取第一条)→
ROW_NUMBER() - 要带跳号的名次(如体育比赛排名)→
RANK() - 要连续名次(如等级评定)→
DENSE_RANK()
四、函数2:LAG() / LEAD() — 同比环比,一行搞定
业务场景:计算每月销售额环比增长率
LAG(字段, n) 取"往前第n行的值";LEAD(字段, n) 取"往后第n行的值"。
-- 环比增长率
SELECT
month,
sales,
LAG(sales, 1) OVER (ORDER BY month) AS last_month_sales,
ROUND(
(sales - LAG(sales, 1) OVER (ORDER BY month))
/ LAG(sales, 1) OVER (ORDER BY month) * 100,
2
) AS mom_growth_pct
FROM monthly_sales
ORDER BY month;
运行结果示例:
+---------+---------+------------------+----------------+
| month | sales | last_month_sales | mom_growth_pct |
+---------+---------+------------------+----------------+
| 2025-09 | 820000 | NULL | NULL |
| 2025-10 | 910000 | 820000 | 10.98 |
| 2025-11 | 870000 | 910000 | -4.40 |
| 2025-12 | 1050000 | 870000 | 20.69 |
| 2026-01 | 980000 | 1050000 | -6.67 |
| 2026-02 | 1100000 | 980000 | 12.24 |
| 2026-03 | 1230000 | 1100000 | 11.82 |
+---------+---------+------------------+----------------+
做同比(year-over-year):如果数据是月粒度,往前取12个月:
LAG(sales, 12) OVER (ORDER BY month) AS same_month_last_year
⚠️ LAG/LEAD 第一行(或最后一行)会返回 NULL,记得用 COALESCE(LAG(...), 0) 或在业务层处理。
五、函数3:SUM() OVER — 累计求和 + 移动平均
业务场景1:每个用户的消费累计金额
SELECT
user_id,
order_date,
amount,
SUM(amount) OVER (
PARTITION BY user_id
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_amount
FROM orders
ORDER BY user_id, order_date;
运行结果示例:
+---------+------------+--------+------------------+
| user_id | order_date | amount | cumulative_amount |
+---------+------------+--------+------------------+
| 1001 | 2026-01-05 | 299.00 | 299.00 |
| 1001 | 2026-01-18 | 150.00 | 449.00 |
| 1001 | 2026-02-03 | 520.00 | 969.00 |
| 1001 | 2026-03-12 | 88.00 | 1057.00 |
| 1002 | 2026-01-10 | 450.00 | 450.00 |
| 1002 | 2026-02-20 | 320.00 | 770.00 |
| 1002 | 2026-03-05 | 780.00 | 1550.00 |
+---------+------------+--------+------------------+
业务场景2:7日移动平均(平滑噪声,看趋势)
SELECT
sale_date,
SUM(sales) AS daily_sales,
ROUND(
AVG(SUM(sales)) OVER (
ORDER BY sale_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
), 2
) AS moving_avg_7d
FROM store_sales
GROUP BY sale_date
ORDER BY sale_date;
往前取6行 + 当前行,共7行,算平均——7日均线的SQL实现就是这样。
六、函数4:NTILE() — 用户分层,4行代码搞定RFM
业务场景:把用户按消费金额分成4层(高/中高/中低/低价值用户)
SELECT
user_id,
total_spend,
NTILE(4) OVER (ORDER BY total_spend DESC) AS value_tier,
CASE NTILE(4) OVER (ORDER BY total_spend DESC)
WHEN 1 THEN '高价值'
WHEN 2 THEN '中高价值'
WHEN 3 THEN '中低价值'
WHEN 4 THEN '低价值'
END AS tier_label
FROM user_spend
ORDER BY total_spend DESC;
运行结果示例:
+---------+-------------+------------+-----------+
| user_id | total_spend | value_tier | tier_label |
+---------+-------------+------------+-----------+
| 1004 | 4800.00 | 1 | 高价值 |
| 1008 | 3300.00 | 1 | 高价值 |
| 1006 | 2100.00 | 2 | 中高价值 |
| 1002 | 1550.00 | 2 | 中高价值 |
| 1001 | 1057.00 | 3 | 中低价值 |
| 1009 | 880.00 | 3 | 中低价值 |
| 1005 | 750.00 | 4 | 低价值 |
| 1010 | 650.00 | 4 | 低价值 |
...
进阶:NTILE + RFM 三维度分层
-- 分别对R/F/M三个维度打分,再组合
WITH rfm_raw AS (
SELECT
user_id,
DATEDIFF(CURDATE(), MAX(order_date)) AS recency, -- 最近购买距今天数
COUNT(*) AS frequency, -- 购买频次
SUM(amount) AS monetary -- 总消费金额
FROM orders
GROUP BY user_id
),
rfm_scored AS (
SELECT
user_id,
NTILE(5) OVER (ORDER BY recency ASC) AS r_score, -- 越近分越高(ASC)
NTILE(5) OVER (ORDER BY frequency DESC) AS f_score,
NTILE(5) OVER (ORDER BY monetary DESC) AS m_score
FROM rfm_raw
)
SELECT
user_id,
r_score, f_score, m_score,
CONCAT(r_score, f_score, m_score) AS rfm_label
FROM rfm_scored;
七、函数5:FIRST_VALUE() / LAST_VALUE() — 取组内极值
业务场景:每个产品类别中,每行都显示该类别的最高销售额产品名称
SELECT
category,
product_name,
sales,
FIRST_VALUE(product_name) OVER (
PARTITION BY category
ORDER BY sales DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS top_product,
LAST_VALUE(product_name) OVER (
PARTITION BY category
ORDER BY sales DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS bottom_product
FROM products
ORDER BY category, sales DESC;
运行结果示例:
+--------+--------------+-------+-------------+----------------+
| category | product_name | sales | top_product | bottom_product |
+--------+--------------+-------+-------------+----------------+
| 手机 | 型号C | 71000 | 型号C | 型号B |
| 手机 | 型号A | 52000 | 型号C | 型号B |
| 手机 | 型号B | 38000 | 型号C | 型号B |
| 电脑 | 笔记本X | 89000 | 笔记本X | 台式机Y |
...
八、三个高频踩坑点
❌ 坑1:WHERE 里直接过滤窗口函数结果
-- ❌ 错误写法(会报语法错误)
SELECT *, ROW_NUMBER() OVER (PARTITION BY city ORDER BY sales DESC) AS rn
FROM store_sales
WHERE rn <= 3; -- 不能在 WHERE 里用窗口函数别名
-- ✅ 正确写法:用 CTE 或子查询包一层
WITH ranked AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY city ORDER BY sales DESC) AS rn
FROM store_sales
)
SELECT * FROM ranked WHERE rn <= 3;
❌ 坑2:LAST_VALUE() 默认窗口不到分区末尾
-- ❌ 错误写法:LAST_VALUE 默认窗口到 CURRENT ROW,拿到的是当前行自身
LAST_VALUE(product_name) OVER (PARTITION BY category ORDER BY sales DESC)
-- ✅ 正确写法:必须显式指定到 UNBOUNDED FOLLOWING
LAST_VALUE(product_name) OVER (
PARTITION BY category
ORDER BY sales DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
)
❌ 坑3:PARTITION BY 和 GROUP BY 同时用时维度冲突
-- ❌ 错误(GROUP BY 之后行已经聚合,窗口分区维度失效)
SELECT city, SUM(sales),
RANK() OVER (PARTITION BY store_name ORDER BY SUM(sales) DESC) AS rnk
FROM store_sales
GROUP BY city; -- 已GROUP BY city,store_name维度已消失
-- ✅ 正确:先确定数据粒度,再决定窗口分区
SELECT city, store_name, sales,
RANK() OVER (PARTITION BY city ORDER BY sales DESC) AS rnk
FROM store_sales;
九、性能注意事项
- ORDER BY 建立索引:窗口函数中的 ORDER BY 字段建议加索引,避免全表排序
- PARTITION BY 字段选择性:分区字段选择性太低(如只有2-3个值)时,每个分区数据量大,考虑是否需要加二级排序减少扫描行数
- 多个窗口函数合并 OVER 子句:如果多个窗口函数的 OVER 子句完全相同,数据库可以复用同一个排序扫描,性能更好
- 避免嵌套窗口函数:SQL不允许在一个窗口函数内再调用另一个窗口函数,需要用CTE分层
总结:5个函数速查表
| 函数 | 核心用途 | 替代的传统写法 | 代码量对比 |
|---|---|---|---|
| ROW_NUMBER / RANK / DENSE_RANK | 组内排名、取TOP N | 子查询 + LIMIT + JOIN | 20行 → 5行 |
| LAG / LEAD | 环比、同比、前后行对比 | 自JOIN + 时间偏移 | 15行 → 3行 |
| SUM/AVG OVER | 累计求和、移动平均 | Python shift + cumsum | Python脚本 → 纯SQL |
| NTILE | 分层分箱(RFM/分位数) | 手动写CASE分档 | 10行 → 1行 |
| FIRST_VALUE / LAST_VALUE | 组内极值(取第一/最后名的字段值) | 双层子查询 + JOIN | 30行 → 8行 |
MySQL 8.0+、PostgreSQL 9.1+、SQL Server 2012+、Oracle 11g+ 均支持完整窗口函数,Hive/Spark SQL 也全部支持。
窗口函数是数据分析 SQL 从"能用"到"精通"之间最关键的一道门槛。掌握了这5个函数,你在数据分析的 SQL 能力上就已经超过了大多数同岗位的人。
如果这篇文章对你有帮助,点个赞👍 收藏一下,下次用到直接来查。
评论区告诉我:这5个函数里,你目前最常用哪一个?或者你遇到过哪些窗口函数相关的坑?
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)