SQL 窗口函数(ROW_NUMBER / RANK / DENSE_RANK / NTILE)的业务场景模板

12次阅读

该用 row_number 而不是 rank 时:需严格顺序唯一编号(如分页、取最新一条),因 row_number 不跳号不并列,而 rank 并列后跳号会导致漏数据。

SQL 窗口函数(ROW_NUMBER / RANK / DENSE_RANK / NTILE)的业务场景模板

什么时候该用 ROW_NUMBER 而不是 RANK

当你要严格按顺序给每行唯一编号(比如分页取第 11–20 条、取每个用户最新一条订单),必须用 ROW_NUMBER。它不跳号,不并列,纯粹按 ORDER BY 排序后硬生生从 1 开始标号。

常见错误是拿 RANK 做分页:如果两个用户下单时间相同,RANK 可能同时返回 1,下一行就直接跳到 3,导致 LIMIT 10 OFFSET 10 漏掉数据。

  • ROW_NUMBER():适合“取 Top N”“分页”“去重选最新”
  • RANK():适合“成绩排名”,允许并列且要体现名次断层(如两个第 1 名,下一个就是第 3 名)
  • DENSE_RANK():适合“梯队划分”,并列后不跳号(两个第 1 名,下一个是第 2 名)

NTILE(4) 划分四分位时的边界陷阱

NTILE 看似简单,但它的分组是“尽力平均”,不是按值切分。比如 10 行数据执行 NTILE(4),结果不是每组 2.5 行——SQL 会拆成 3、3、2、2 这样的分布,具体哪几行进哪组取决于排序顺序,不可预测。

业务上想按销售额把客户分成高 / 中高 / 中低 / 低四档,不能只靠 NTILE,得先算出 PERCENTILE_CONT(0.25) 等分位点,再用 CASE WHEN 手动判断。否则销售数据稍有倾斜,NTILE(4) 就会让“高”组塞进 30% 的人。

  • 永远检查 COUNT(*) 分组后各桶行数是否符合预期
  • 如果需要等宽区间(如 0–25%、25–50%),改用 PERCENT_RANK() 或聚合函数预计算阈值
  • NTILENULL 值参与排序时行为因数据库而异(PostgreSQL 忽略,MySQL 可能报错)

窗口函数里 PARTITION BYORDER BY 的依赖关系

ORDER BY 在窗口函数中不是可选的——对 ROW_NUMBERRANKDENSE_RANK 来说,没写 ORDER BY 语法直接报错;对 NTILE 也是强制要求。但很多人以为加了 PARTITION BY 就可以省略 ORDER BY,其实不行。

更隐蔽的问题是:同一个 PARTITION BY 下,如果 ORDER BY 字段有重复值,ROW_NUMBER 仍会强行编号(顺序由底层存储或随机决定),而 RANKDENSE_RANK 会并列。这意味着,仅靠 user_id 分组 + created_at 排序,若存在毫秒级相同时间,不同数据库可能给出不同编号结果。

  • 关键排序字段尽量带唯一性兜底,例如 ORDER BY created_at, id
  • PARTITION BY 定义范围,ORDER BY 定义顺序,二者缺一不可
  • 在 PostgreSQL 中,ORDER BY 允许用表达式(如 ORDER BY EXTRACT(YEAR FROM order_date)),但 MySQL 8.0+ 要求必须是列或别名

MySQL 8.0 vs PostgreSQL 对 DENSE_RANK 的 NULL 处理差异

如果你在 MySQL 里写 DENSE_RANK() OVER (ORDER BY score DESC),遇到 score IS NULL 的行,它们会被排在最末尾(即最低名次);但在 PostgreSQL 中,默认 NULLS LAST,行为一致。但一旦你显式写 ORDER BY score DESC NULLS FIRST,MySQL 直接报错不支持 NULLS FIRST/LAST 语法,PostgreSQL 则正常执行。

这意味着跨数据库迁移含窗口函数的报表 SQL 时,NULL 排序逻辑最容易出问题——表面跑通,结果错位。

  • MySQL 8.0 不支持 NULLS FIRST/LAST,所有 NULL 统一归到排序末端
  • PostgreSQL 和 SQL Server 支持,且默认策略可能不同(需查文档)
  • 稳妥做法:提前用 CASE WHEN score IS NULL THEN -999999 ELSE score END 替换,消除歧义
实际用哪一种,取决于你心里那个“排名”到底要不要跳号、允不允许并列、分组是不是真要平均、以及 NULL 算不算有效数据——这些不是语法问题,是业务定义问题。

text=ZqhQzanResources