分页这事儿,翻来覆去就那么几招。但偏偏有些数据库环境不支持 LIMIT,或者出于某些历史原因必须绕开它。其实,不用 LIMIT 照样能实现分页,关键是要换一套思路:借助行号变量、TOP 嵌套,或者干脆上窗口函数。直接删掉 LIMIT 还硬套原逻辑,十有八九会查出数据乱序、重复甚至错漏。
下面就分三种常见场景,说说具体怎么避开 LIMIT 完成分页。
MySQL 用变量模拟行号分页(仅限低并发、小数据量)
思路其实很朴素:手动给结果集编个号,再按编号范围过滤。子查询里用 @rownum 累加,外层 WHERE 一截就行。这样确实不依赖 LIMIT,但坑也不少:
- 必须配
ORDER BY,否则行号顺序完全不可控,分页也就没意义了。 @rownum的初始化必须在同一条 SQL 里完成。如果分开写(比如先SET @rownum=0再SELECT),大概率会失效,甚至出现诡异行为。- 高并发场景下,多个连接可能覆盖同一个变量值,导致分页结果一片混乱。
- 如果你的 MySQL 版本已经到 8.0+,建议直接上
ROW_NUMBER()窗口函数,更干净也更可靠。
示例看看逻辑就行(每页 10 条,取第 3 页):
SELECT * FROM (
SELECT @rownum := @rownum + 1 AS rn, t.*
FROM orders t,
(SELECT @rownum := 0) r
ORDER BY created_at DESC
) tmp
WHERE rn BETWEEN 21 AND 30;
SQL Server / Access 用 TOP + 子查询实现分页
这是 LIMIT 缺失时最主流的替代方案,在 SQL Server 2012 之前以及 Access 里几乎是必杀技。核心逻辑是:先取前 N 条记录的主键或排序字段值,然后用这个值反向筛选出后面的数据。
- 第一页最简单:直接
SELECT TOP 10 * ... ORDER BY。 - 后续页需要子查询找出上一页的最后一条记录的排序字段值(比如
id),再用WHERE id > 那个值做范围扫描。 - 排序字段必须有索引,否则子查询里做
TOP N会触发全表扫描,性能惨不忍睹。 - Access 不支持
OFFSET,而且TOP必须与ORDER BY出现在同一个层级。写成SELECT TOP 10 * FROM (SELECT * FROM t ORDER BY id)这种嵌套会直接报错。正确的姿势是SELECT TOP 10 * FROM t ORDER BY id。
来看个 SQL Server 第二页的典型写法(每页 10 条):
SELECT TOP 10 *
FROM orders
WHERE id > (
SELECT MAX(id)
FROM (
SELECT TOP 10 id
FROM orders
ORDER BY id
) t
)
ORDER BY id;
注意这里内层的 TOP 10 取的正是第一页的 id,用 MAX 拿到最大值,外层的 WHERE id > 那个值 就跳过了第一页。
所有数据库通用:窗口函数 + 拆层绕过 LIMIT 限制
MySQL 8.0+、PostgreSQL、SQL Server 都支持 ROW_NUMBER(),但有个细节经常被忽略:ROW_NUMBER() 不能和 LIMIT 出现在同一个查询层级。比如下面这个写法,在 MySQL 里会直接报错:
-- 错误写法 SELECT *, ROW_NUMBER() OVER (ORDER BY id) rn FROM t LIMIT 10;
正确的做法是把窗口函数放在内层子查询,外层再来做 LIMIT 或者 WHERE rn BETWEEN。如果目标是彻底不用 LIMIT,就用 WHERE rn BETWEEN x AND y 截取范围。
还要特别注意:业务过滤条件(比如 WHERE status = 'shipped')必须写在窗口查询的最内层。否则行号是对全表计算的,分页结果就会偏——本该属于该页的数据可能被挤到别的页里去了。
一个无 LIMIT 的干净示例(每页 10 条,取第三页):
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (ORDER BY created_at DESC) AS rn FROM orders WHERE status = 'shipped' ) t WHERE t.rn BETWEEN 21 AND 30;
说到底,不用 LIMIT 实现分页本身不复杂,真正的难点在于:保证排序字段稳定、索引能命中、内外过滤条件完全一致。少写一个 WHERE 或者多套一层无意义的子查询,都可能让分页结果漏数据或出现重复行。理解底层逻辑之后,这些“变通写法”反而能让你更清楚数据库到底是怎么处理“行号”和“范围”的。
