直接用 LIMIT + OFFSET 在大偏移量时变慢,因 PostgreSQL 需扫描跳过前 OFFSET 行;必须配合 ORDER BY 和匹配索引,否则结果不可重现、性能陡降,高并发下应改用游标分页。

为什么直接用 LIMIT + OFFSET 在大偏移量时会变慢
因为 PostgreSQL 必须扫描并跳过前 OFFSET 行,哪怕你只要 10 条。比如 OFFSET 100000,它就得定位到第 100001 行——全表或索引扫描开销陡增,尤其当排序字段没索引、或数据分布不均时。
常见现象:EXPLAIN ANALYZE 显示 Buffers 高、Execution Time 突增,且越往后页越慢。
- 确保
ORDER BY字段有索引(如CREATE INDEX ON users (created_at DESC, id DESC);) - 避免
OFFSET超过几万行;超过就该换游标分页 - 不要在未加
ORDER BY的查询里用LIMIT分页——结果无序、不可重现
基础分页的正确写法:必须带 ORDER BY 和索引支持
分页不是“取第 N 页”,而是“取排序后从某位置开始的 N 条”。所以 ORDER BY 不是可选项,是前提。
假设按时间倒序查用户:
SELECT id, name, created_at FROM users ORDER BY created_at DESC, id DESC LIMIT 20 OFFSET 40;
这里 OFFSET 40 表示跳过前 40 条,取接下来 20 条。但注意:
-
created_at可能重复,所以补上id DESC保证排序唯一性 - 对应索引必须匹配排序方向:
INDEX ON users (created_at DESC, id DESC) - 如果
WHERE条件涉及非索引字段,性能瓶颈会立刻转移到过滤阶段,LIMIT帮不上忙
LIMIT 的参数行为:只接受整数,不能用变量名直接拼接
PostgreSQL 不允许在 LIMIT 或 OFFSET 子句里直接写参数占位符(如 $1),除非通过 PREPARE 或函数封装。SQL 标准要求它们是常量表达式。
错误写法(会报错):
SELECT * FROM logs ORDER BY ts LIMIT $1 OFFSET $2;
正确做法:
- 应用层拼成整数再传入(如 Python 的
cursor.execute("... LIMIT %s OFFSET %s", (20, 40))) - 用
PREPARE预编译(适合固定几个分页尺寸):PREPARE paginated_query AS SELECT ... LIMIT $1 OFFSET $2; - 封装成 SQL 函数,用
INT参数接收:CREATE FUNCTION get_logs(p_limit INT, p_offset INT) RETURNS TABLE(...) AS $$ SELECT ... LIMIT $1 OFFSET $2; $$ LANGUAGE sql;
容易被忽略的边界问题:空结果、重复数据、并发写入
分页不是原子快照。如果有人在你查第 1 页后插入/删除了排序靠前的记录,第 2 页可能漏掉或重复某条。
- 空结果不等于“到底了”——可能是中间删了数据,导致
OFFSET落空;建议用SELECT COUNT(*)单独估算总条数(但别在大表上频繁查) - 用
OFFSET分页时,如果排序字段值大量相同(比如 10 万条status = 'pending'),PostgreSQL 可能按不同顺序返回这些行,造成翻页跳变 - 生产环境高并发写入场景下,基础
LIMIT/OFFSET分页只适合低频管理后台;实时列表请切到基于游标的分页(WHERE created_at )

















