SQL内功笔记 · 第6篇:窗口函数的使用ROW_NUMBER等

窗口函数

窗口函数的本质 = 在"分组后的有序数据集"上做计算,但不丢行(不像 GROUP BY)

基础结构:

mysql 复制代码
函数() OVER (
  PARTITION BY 分组
  ORDER BY 排序
)

-- PARTITION BY → 分组(类似 GROUP BY,但不聚合)
-- ORDER BY → 分组内排序
-- 不写 PARTITION → 全表一个窗口

ROW_NUMBER()

去重取一条(去重保留最优记录)

mysql 复制代码
SELECT * FROM (
  SELECT t.*, ROW_NUMBER() OVER ( PARTITION BY 分组键 ORDER BY 优先级/时间/质量 DESC) AS rn
  FROM t
) s WHERE rn = 1;
-- PARTITION BY = 你要"去重"的维度(如 code、user_id)
-- ORDER BY = 你定义的"谁更优"(时间最新、优先级最小/最大、评分最高等)
-- ROW_NUMBER 保证唯一(不会并列)

建模思想

Step 1:列出"候选来源",统一为一张"候选表"

Step 2:设计 sorting(命中精度、业务优先级、时效性、质量/权重)最终转成一个可排序的字段

Step 3:统一排序规则,单字段(ORDER BY sorting ASC),多字段(ORDER BY match_level ASC, update_time DESC

Step 4:row_number 取最优


RANK()

排名(允许并列,但会跳过后续排名)

mysql 复制代码
SELECT
  t.*,
  RANK() OVER (PARTITION BY 分组键 ORDER BY 排序字段 DESC) AS rk
FROM t;

-- 与 ROW_NUMBER 的区别:
-- 排序值相同时 → 并列排名(相同名次)
-- 并列后 → 跳过下一个名次(如 1,1,3,4)
-- 适用于:排行榜、竞赛排名、需要"并列但留空位"的场景

典型场景:取每组前 N 名(允许并列)

mysql 复制代码
SELECT * FROM (
  SELECT
    t.*,
    RANK() OVER (PARTITION BY 部门 ORDER BY 销售额 DESC) AS rk
  FROM 销售表 t
) s WHERE rk <= 3;
-- 如果第3名有并列,会返回多个第3名
-- 但第4名会被跳过(因为并列占用了名次)

DENSE_RANK()

密集排名(允许并列,但不跳过排名)

mysql 复制代码
SELECT
  t.*,
  DENSE_RANK() OVER (PARTITION BY 分组键 ORDER BY 排序字段 DESC) AS drk
FROM t;

-- 与 RANK 的区别:
-- 排序值相同时 → 并列排名(相同名次)
-- 并列后 → 不跳过下一个名次(如 1,1,2,3)
-- 适用于:需要连续排名、不关心空位的场景

典型场景:取每组前 N 名(密集版)

mysql 复制代码
SELECT * FROM (
  SELECT
    t.*,
    DENSE_RANK() OVER (PARTITION BY 班级 ORDER BY 总分 DESC) AS drk
  FROM 成绩表 t
) s WHERE drk <= 3;
-- 如果第1名有并列,第2名仍然存在(不会被跳过)
-- 适合"我要前3档"而不是"前3个人"

三种排名函数对比

函数 并列时 排名是否连续 典型用途
ROW_NUMBER 不并列(随机/按其他字段) 连续 去重取一条
RANK 并列 不连续(跳过) 排行榜、竞赛
DENSE_RANK 并列 连续 分档、等级划分
  • ROW_NUMBER() :生成连续唯一的序号,即使数据相同也强制排序。核心用途:数据去重,从一组重复记录中按规则(如时间最新、优先级最高)选取唯一的一条。
  • RANK() :允许并列排名,并列后会跳过后续名次(如 1,1,3)。核心用途:传统排行榜、竞赛排名,需要体现"并列但名次稀缺"的场景。
  • DENSE_RANK() :允许并列排名,但名次连续不跳过(如 1,1,2)。核心用途:等级划分、分档评级,关注"档位"而非具体名次。

选择指南 :需要唯一序号选 ROW_NUMBER;需要传统排名(允许跳名次)选 RANK;需要连续档位选 DENSE_RANK


SUM()

累计值

mysql 复制代码
SELECT
  t.*,
  SUM(变动量) OVER (
    PARTITION BY 账户/物料
    ORDER BY 时间
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS balance
FROM t;

-- ORDER BY 时间 决定累计顺序
-- 窗口框架 UNBOUNDED PRECEDING → CURRENT ROW = 从开头累计到当前
-- 分组后各自独立累计
  • SUM() OVER(...) :最典型的累计计算。通过 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 框架,实现从分区开头到当前行的累计,常用于计算余额、累计销售额、累计用户数等。关键ORDER BY 决定了累计的顺序。

LAG()

对比前一条(环比/差值/增长率)

mysql 复制代码
SELECT
  t.*,
  LAG(指标, 1) OVER (PARTITION BY 分组键 ORDER BY 时间) AS prev_val,
  指标 - LAG(指标, 1) OVER (PARTITION BY 分组键 ORDER BY 时间) AS diff,
  CASE
    WHEN LAG(指标,1) OVER (PARTITION BY 分组键 ORDER BY 时间) = 0 THEN NULL
    ELSE (指标 - LAG(指标,1) OVER (PARTITION BY 分组键 ORDER BY 时间))
         / LAG(指标,1) OVER (PARTITION BY 分组键 ORDER BY 时间)
  END AS growth_rate
FROM t;
-- LAG(col, 1) 取上一行
-- 用于:环比、涨跌幅、相邻差值
-- 注意除零保护

LEAD()

对比后一条(下期值/未来值/趋势判断)

mysql 复制代码
SELECT
  t.*,
  LEAD(指标, 1) OVER (PARTITION BY 分组键 ORDER BY 时间) AS next_val,
  LEAD(指标, 1) OVER (PARTITION BY 分组键 ORDER BY 时间) - 指标 AS next_diff,
  CASE
    WHEN 指标 = 0 THEN NULL
    ELSE (LEAD(指标,1) OVER (PARTITION BY 分组键 ORDER BY 时间) - 指标) / 指标
  END AS next_growth_rate
FROM t;

-- LEAD(col, 1) 取下一行(与 LAG 相反)
-- 用于:下期预测、趋势判断、环比下期
-- 注意最后一行 LEAD 返回 NULL

典型场景:判断趋势(涨/跌/平)

mysql 复制代码
SELECT
  t.*,
  CASE
    WHEN LEAD(价格, 1) OVER (PARTITION BY 商品 ORDER BY 日期) > 价格 THEN '上涨'
    WHEN LEAD(价格, 1) OVER (PARTITION BY 商品 ORDER BY 日期) < 价格 THEN '下跌'
    WHEN LEAD(价格, 1) OVER (PARTITION BY 商品 ORDER BY 日期) = 价格 THEN '持平'
    ELSE '最后一天'
  END AS 趋势
FROM 价格表 t;

LAG 与 LEAD 对比

函数 方向 用途
LAG 取上一行(过去) 环比、同比、差值
LEAD 取下一行(未来) 趋势预测、下期对比
  • LAG(列, N) :访问当前行之前 第 N 行的数据。核心用途:计算环比(本期 vs 上期)、计算差值、计算增长率。务必注意处理边界值(NULL)和除零问题。
  • LEAD(列, N) :访问当前行之后 第 N 行的数据。核心用途:进行趋势预测(与下期对比)、计算未来差值、判断价格或指标的涨跌趋势。

LAG vs LEAD:一个回头看(分析历史),一个向前看(预测未来),是时间序列和趋势分析的基础。


ROWS vs RANGE

本文示例主要使用了 ROWS 物理行框架。另一个重要概念是 RANGE 逻辑值框架:

  • ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING:取前后各一行。
  • RANGE BETWEEN INTERVAL 7 DAY PRECEDING AND CURRENT ROW:取当前行值之前7天内的所有行(按值范围)。
    RANGE 在处理时间范围累计时更为直观。

性能与最佳实践

  • 索引 :在 PARTITION BYORDER BY 的列上建立索引可以大幅提升窗口函数性能。
  • 避免嵌套:尽量避免在子查询中多层嵌套窗口函数,考虑使用 CTE(公用表表达式)来分步计算,提升可读性和可调试性。
  • 理解执行顺序 :窗口函数在 SELECT 列表计算,但在 WHERE, GROUP BY, HAVING 之后。不能直接在 WHERE 中引用窗口函数列,需要嵌套子查询或使用 CTE。

总结

窗口函数是 SQL 中用于在"分组后的有序数据集"上进行计算而不聚合(不丢行)的强大工具。其核心在于 OVER() 子句,它定义了数据窗口的划分(PARTITION BY)和排序(ORDER BY)。

掌握窗口函数,能让你用声明式的 SQL 优雅地解决复杂的数据切片、对比、累计和排名问题,将许多原本需要应用程序循环处理的逻辑移回数据库层,极大地提升开发效率和执行性能。

相关推荐
Chase_______1 小时前
【Java基础核心知识点全解·09】Java 内存布局与垃圾回收详解:栈、堆、栈帧、GC Roots 与对象回收
java·开发语言
锋行天下1 小时前
让nginx网关扛下所有攻击
前端·后端·nginx
武子康1 小时前
Java-11 深入浅出 MyBatis 一级缓存详解:从原理到失效场景 Executor
java·后端
川石课堂软件测试1 小时前
使用mock进行接口测试教程
数据库·python·功能测试·测试工具·华为·单元测试·appium
寻道码路1 小时前
LangChain4j Java AI 应用开发实战(十):Embedding 模型与文本分类 - 语义向量化
java·人工智能·ai·embedding
折哥的程序人生 · 物流技术专研1 小时前
Java 23 种设计模式:从踩坑到精通 | 抽象工厂 —— 支付/收款如何成套创建?跨平台 UI 如何一键换肤?
java·开发语言·后端·设计模式
方也_arkling1 小时前
【Java-Day11】抽象类和抽象方法
java·开发语言
Solis程序员2 小时前
MongoDB 超全入门到实战:从原理、CRUD到高可用架构
数据库·mongodb·架构
XS0301062 小时前
并发编程 七
java