LIMIT offset, length 是最常用写法,但 offset 偏移量大时性能骤降,且排序字段不唯一会导致漏行或重复;应优先考虑游标分页,并严格校验分页参数。

MySQL 的 LIMIT 分页语法怎么写才不跳数据
直接说结论:LIMIT offset, length 是最常用写法,但 offset 偏移量变大时性能会明显下降,且在有重复排序字段(比如时间相同)时容易漏行或重复。不是所有分页都适合用它。
典型错误是这样写:SELECT * FROM user ORDER BY id LIMIT 10000, 20 —— 当 offset 超过几万,MySQL 要先扫描前 10020 行再丢弃前 10000 行,IO 和 CPU 开销陡增。
-
offset为 0 时最快,越往后越慢 - 如果
ORDER BY字段不唯一(如多个记录created_at相同),LIMIT无法保证稳定顺序,翻页可能看到重复或丢失数据 - PHP 中拼接 SQL 时,务必对
$page和$pageSize做整型校验,否则易被注入或报错
PHP 中计算 offset 的安全公式
分页本质是算出从第几条开始取,公式就是:offset = ($page - 1) * $pageSize。注意:页码必须从 1 开始,不能从 0;$page 必须是正整数,$pageSize 通常限制在 1–100 之间。
常见疏漏:
立即学习“PHP免费学习笔记(深入)”;
- 没做
max(1, (int)$page)校验,传入负数或字符串导致offset为 0 或负值,MySQL 报错Invalid argument - 没限制
$pageSize上限,攻击者传?limit=1000000可能拖垮数据库 - 没统一处理空参,默认值设在 SQL 层(如
IFNULL)不如在 PHP 层早拦住
php $page = max(1, (int)($_GET['page'] ?? 1)); $pageSize = max(1, min(100, (int)($_GET['limit'] ?? 20))); $offset = ($page - 1) * $pageSize;$stmt = $pdo->prepare("SELECT id, name FROM user ORDER BY id ASC LIMIT :offset, :length"); $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); $stmt->bindValue(':length', $pageSize, PDO::PARAM_INT); $stmt->execute();
什么时候该换 Cursor 分页代替 LIMIT
当列表需按时间倒序、且数据高频写入(如消息流、日志),用 LIMIT offset, length 会越来越卡,还可能因新插入数据导致“上一页末尾”和“下一页开头”之间出现断层或重复。这时应改用基于游标的分页(Cursor-based Pagination)。
核心思路:不依赖行号,而用上一页最后一条的排序字段值作为下一页起点。
- 要求排序字段(如
id或created_at)必须有索引,且尽量唯一 - 首次请求不带
cursor,后续请求传上一页最后一条的id(比如?cursor=12345) - SQL 改为:
WHERE id ,避免OFFSET - PHP 中需确保
cursor是合法整型,且大于 0,否则拒绝查询
php
$cursor = (int)($_GET['cursor'] ?? 0);
if ($cursor > 0) {
$stmt = $pdo->prepare("SELECT id, title FROM article WHERE id < ? ORDER BY id DESC LIMIT 20");
$stmt->execute([$cursor]);
} else {
$stmt = $pdo->prepare("SELECT id, title FROM article ORDER BY id DESC LIMIT 20");
$stmt->execute();
}
count(*) 分页总数要不要查
查总数(SELECT COUNT(*))看起来直观,但对大表是性能杀手。尤其当只展示「下一页」按钮、不显示总页数时,完全没必要查。
更务实的做法:
- 只查一页数据(比如 20 条),然后判断是否查到了满额(20 条)。如果不到 20 条,说明已是最后一页
- 需要总数时,用近似值:
SHOW TABLE STATUS LIKE 'user'查Rows字段(MyISAM 准确,InnoDB 是估算) - 或者加缓存:总数变化不频繁时,用 Redis 缓存
user:count,定时或写操作后更新 - 绝对避免在分页接口里每次执行
SELECT COUNT(*) FROM ... WHERE ... ORDER BY ...,WHERE 条件复杂时可能比主查询还慢
游标分页天然不依赖总数,所以也绕开了这个问题。











