如何用 ROW_NUMBER() + 子查询实现跨页去重分页

12次阅读

直接用ROW_NUMBER()分页会导致重复,因相同排序值的行顺序随机;须先按业务逻辑去重(如GROUP BY或PARTITION BY+rn=1),再编号分页,且跨页应采用键集分页避免OFFSET缺陷。

如何用 ROW_NUMBER() + 子查询实现跨页去重分页

为什么直接用 ROW_NUMBER() 会重复分页

当你对含重复数据的表(比如多条记录共享同一 user_id)直接套 ROW_NUMBER() OVER (ORDER BY ...) 分页时,相同排序值的行会被随机打乱顺序,导致第 1 页出现的某条 user_id = 100 记录,在第 2 页又因窗口函数重排而再次出现——本质是去重逻辑没落在分页之前。

子查询里先 DISTINCT 再编号,不行

DISTINCTROW_NUMBER() 不能共存于同一层 select(语法报错),强行在子查询中 SELECT DISTINCT ... FROM t 后再套 ROW_NUMBER(),会丢失原始行信息(比如你本想取每用户最新一条订单,但 DISTINCT user_id 不知道哪条是最新)。

  • 错误写法:
    SELECT *, ROW_NUMBER() OVER (ORDER BY user_id) rn FROM (SELECT DISTINCT user_id FROM orders) t
  • 问题:丢掉了 order_timeamount 等关键字段,无法支撑“每个用户取最新订单”这类真实需求

正确做法:用 GROUP BY 或窗口内去重 + ROW_NUMBER()

核心是把“去重逻辑”显式表达为聚合或优先级选择,再编号。常见两种路径:

  • 按业务主键 GROUP BY,用 MAX(order_time)MAX(id) 拿最新行,再对结果集编号:
    SELECT *, ROW_NUMBER() OVER (ORDER BY latest_time DESC) rn FROM (  SELECT user_id, MAX(order_time) AS latest_time, MAX(amount) AS amount  FROM orders  GROUP BY user_id) t
  • ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_time DESC) 先标出每组第一条,再外层筛选 rn = 1,最后重新编号分页:
    SELECT *, ROW_NUMBER() OVER (ORDER BY user_id) rn FROM (  SELECT * FROM (    SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_time DESC) rn_inner    FROM orders  ) t WHERE rn_inner = 1) t2
  • 注意:第二层 ROW_NUMBER()ORDER BY 必须和分页意图一致(比如按创建时间倒序),否则页与页之间顺序不稳

跨页分页时 OFFSET 的坑

OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY 看似简洁,但底层仍要扫描前 30 行;如果去重后总行数少(比如 25 条),第 3 页就为空——而用户可能以为数据丢了。更稳的方式是用键集分页(Keyset Pagination),即记住上一页最后一条的 user_idlatest_time,下一页查:

WHERE (latest_time, user_id) < (‘2024-05-01’, ‘u999’) ORDER BY latest_time DESC, user_id DESC LIMIT 10

真正难的不是写对语法,而是把“去重策略”和“分页稳定性”绑在一起设计;一旦 PARTITION BY 字段和 ORDER BY 字段没对齐,或者没处理好 NULL 值排序优先级,跨页就会漏数或重复。

text=ZqhQzanResources