📊 SQL 入门 Day 13:窗口函数进阶

场景引入

某电商平台运营需要分析每位用户的消费习惯:不仅要看总消费排名,还要看每位用户每次消费相比于前一次是增长还是下降、相比于全站平均线处于什么位置。上节课的 ROW_NUMBER()/RANK() 只解决了排名问题,今天我们用更强大的窗口函数搞定"同比环比"、"移动平均"、"相对位置分析"。

一、LAG 与 LEAD --- 访问前后行

最常用的"同比/环比"函数,能在不改变结果行数的情况下,拿到当前行之前或之后某行的值。

sql 复制代码
SELECT
    user_id,
    order_date,
    amount,
    LAG(amount, 1) OVER (
        PARTITION BY user_id
        ORDER BY order_date
    ) AS prev_amount,
    amount - LAG(amount, 1) OVER (
        PARTITION BY user_id
        ORDER BY order_date
    ) AS growth_amount
FROM orders;

执行结果:

user_id order_date amount prev_amount growth_amount
1 2026-01-01 100 NULL NULL
1 2026-02-01 150 100 50
1 2026-03-01 120 150 -30
  • LAG(column, offset, default) --- 向上取第 offset 行
  • LEAD(column, offset, default) --- 向下取第 offset 行
  • 第一行的 prev_amount 为 NULL,可用第三个参数设默认值:LAG(amount, 1, 0)

场景:计算环比增长率

sql 复制代码
SELECT
    user_id,
    order_date,
    amount,
    ROUND(
        (amount - LAG(amount, 1) OVER (PARTITION BY user_id ORDER BY order_date))
        / LAG(amount, 1) OVER (PARTITION BY user_id ORDER BY order_date) * 100,
        2
    ) || '%' AS growth_rate
FROM orders;

二、FIRST_VALUE 与 LAST_VALUE --- 窗口内首尾值

用于获取分区内第一个或最后一个值。注意 :LAST_VALUE 默认的窗口帧是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,结果通常不是你想要的,需要显式指定帧范围。

sql 复制代码
SELECT
    user_id,
    order_date,
    amount,
    FIRST_VALUE(amount) OVER (
        PARTITION BY user_id
        ORDER BY order_date
    ) AS first_amount,
    LAST_VALUE(amount) OVER (
        PARTITION BY user_id
        ORDER BY order_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
    ) AS last_amount
FROM orders;

执行结果:

user_id order_date amount first_amount last_amount
1 2026-01-01 100 100 120
1 2026-02-01 150 100 120
1 2026-03-01 120 100 120

三、窗口帧(Window Frame)深入

窗口帧决定了窗口函数作用的行范围,语法:

sql 复制代码
{ROWS | RANGE | GROUPS} BETWEEN frame_start AND frame_end

帧边界选项:

边界 含义
UNBOUNDED PRECEDING 分区第一行
n PRECEDING 当前行前 n 行
CURRENT ROW 当前行
n FOLLOWING 当前行后 n 行
UNBOUNDED FOLLOWING 分区最后一行

移动平均(3 期)

sql 复制代码
SELECT
    sales_date,
    amount,
    ROUND(AVG(amount) OVER (
        ORDER BY sales_date
        ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
    ), 2) AS moving_avg_3
FROM daily_sales;

执行结果:

sales_date amount moving_avg_3
2026-01-01 100 100.00
2026-01-02 200 150.00
2026-01-03 150 150.00
2026-01-04 300 216.67
2026-01-05 250 233.33

ROWS vs RANGE vs GROUPS

  • ROWS:严格按行数计算,不理会对等值
  • RANGE:按 ORDER BY 列的值计算,相同值的行视为一组
  • GROUPS:按 ORDER BY 列的分组计算(SQL 标准较新特性)
sql 复制代码
-- RANGE: 相同日期的行会一起被包含
-- 如果 1 月 2 号有 3 条记录,RANGE 1 PRECEDING 会包含全部 3 条
SELECT
    order_date,
    amount,
    SUM(amount) OVER (
        ORDER BY order_date
        RANGE BETWEEN INTERVAL 1 DAY PRECEDING AND CURRENT ROW
    ) AS range_sum
FROM orders;

小贴士: RANGE 在 MySQL 中只支持数值和日期,不支持字符串。大多数场景用 ROWS 更直观可控。

四、NTILE --- 分桶统计

将分区内的数据均匀分成 N 组,常用于"Top N% 分析"。

sql 复制代码
SELECT
    employee_id,
    salary,
    NTILE(4) OVER (ORDER BY salary DESC) AS quartile
FROM employees;

执行结果:

employee_id salary quartile
5 50000 1
3 45000 1
1 40000 2
4 35000 2
2 30000 3
6 28000 3
7 25000 4

应用: 四分位分析、A/B 测试分桶、成绩等级划分。

五、NTH_VALUE --- 第 N 个值

取窗口内第 N 行的值(比 FIRST_VALUE 更通用)。

sql 复制代码
SELECT
    department_id,
    salary,
    NTH_VALUE(salary, 2) OVER (
        PARTITION BY department_id
        ORDER BY salary DESC
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
    ) AS second_highest
FROM employees;

六、综合实战:用户 RFM 基础分析

sql 复制代码
SELECT
    user_id,
    COUNT(*) OVER w AS freq,
    MAX(order_date) OVER w AS last_order,
    SUM(amount) OVER w AS monetary,
    ROW_NUMBER() OVER (ORDER BY SUM(amount) OVER w DESC) AS rank
FROM orders
WINDOW w AS (PARTITION BY user_id)
GROUP BY user_id, order_date;

说明: MySQL 8.0+ 支持 WINDOW 子句,可将重复的窗口定义抽离出来,让 SQL 更简洁。

⚠️ 注意事项

  1. LAG/LEAD 返回 NULL:第一行/最后一行没有前驱/后继,记得用 COALESCE 或第三个参数处理
  2. LAST_VALUE 默认行为 :不加帧范围时,只返回当前行之前的最后一个值,需要用 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING 获取分区内的真正末行
  3. 窗口函数与 GROUP BY:窗口函数在 GROUP BY 之后执行,所以 GROUP BY 后的非聚合列不能在窗口中直接引用
  4. 性能注意:窗口函数虽然方便,但会扫描全分区数据,大表上 ORDER BY 后的分区数过多可能导致性能瓶颈
  5. ORDER BY 必须带:部分窗口函数(如 RANK、LAG)必须指定 ORDER BY,否则报错

总结

  • LAG/LEAD → 环比/同比、前后行对比
  • FIRST_VALUE/LAST_VALUE/NTH_VALUE → 窗口边界分析
  • 窗口帧 (ROWS/RANGE) → 移动平均、累计统计
  • NTILE → 分桶、四分位
  • WINDOW 子句 → 复用窗口定义,SQL 更清爽

进阶窗口函数让我们能优雅地解决"行间计算",这是传统 GROUP BY 做不到的。掌握这些技巧后,90% 的报表分析 SQL 都能信手拈来!🚀

相关推荐
子非鱼a几秒前
【SQL注入】 ShadowCart
sql
薛定谔的算法1 分钟前
从 NestJS 到 Spring Boot:一次完整的 Node.js → Java 后端迁移实战
后端
Shinomiya2 分钟前
我最近在学 Linux 进程控制:fork、退出码与进程等待
后端
企业数字化笔记3 分钟前
固定资产Excel批量导入怎么防重复?资产编码、重复检查和错误回执
java·后端·excel
SamDeepThinking14 分钟前
Java 17内存屏障:为什么需要,怎么用
java·后端·面试
azhou的代码园1 小时前
景区游船预约服务系统
人工智能·spring boot·后端
小灰灰搞电子1 小时前
Rust Attribute(属性标记)完整整理
开发语言·后端·rust
程序员阿鹏1 小时前
如何实现MySQL分库分表?
数据结构·数据库·sql·mysql·缓存
tedcloud1232 小时前
God‘s Eye View 怎么搭建?在云服务器上部署一个实时 3D 地球可视化平台
linux·服务器·开发语言·后端·rust
深入云栈2 小时前
Netty 4.2.x 源码深度解析 (十三):Epoll 传输 —— Linux 高性能 IO 的 Netty 实现
java·后端