LIMIT offset, length 是最常用写法,但 offset 偏移量大时性能骤降,且排序字段不唯一会导致漏行或重复;应优先考虑游标分页,并严格校验分页参数。
直接说结论: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 无法保证稳定顺序,翻页可能看到重复或丢失数据$page 和 $pageSize 做整型校验,否则易被注入或报错分页本质是算出从第几条开始取,公式就是:offset = ($page - 1) * $pageSize。注意:页码必须从 1 开始,不能从 0;$page 必须是正整数,$pageSize 通常限制在 1–100 之间。
常见疏漏:
立即学习“PHP免费学习笔记(深入)”;
max(1, (int)$page) 校验,传入负数或字符串导致 offset 为 0 或负值,MySQL 报错 Invalid argument
$pageSize 上限,攻击者传 ?limit=1000000 可能拖垮数据库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();
当列表需按时间倒序、且数据高频写入(如消息流、日志),用 LIMIT offset, length 会越来越卡,还可能因新插入数据导致“上一页末尾”和“下一页开头”之间出现断层或重复。这
时应改用基于游标的分页(Cursor-based Pagination)。
核心思路:不依赖行号,而用上一页最后一条的排序字段值作为下一页起点。
id 或 created_at)必须有索引,且尽量唯一cursor,后续请求传上一页最后一条的 id(比如 ?cursor=12345)WHERE id ,避免 OFFSET
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();
}
查总数(SELECT COUNT(*))看起来直观,但对大表是性能杀手。尤其当只展示「下一页」按钮、不显示总页数时,完全没必要查。
更务实的做法:
SHOW TABLE STATUS LIKE 'user' 查 Rows 字段(MyISAM 准确,InnoDB 是估算)user:count,定时或写操作后更新SELECT COUNT(*) FROM ... WHERE ... ORDER BY ...,WHERE 条件复杂时可能比主查询还慢游标分页天然不依赖总数,所以也绕开了这个问题。