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个函数里,你目前最常用哪一个?或者你遇到过哪些窗口函数相关的坑?

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐