order by 和 limit 必须配合使用才安全,单独 limit 不保证结果顺序;分页必须加 order by 且字段需有索引,避免 offset 大偏移导致性能骤降。

ORDER BY 和 LIMIT 必须一起用才安全
单独写 LIMIT 10 不保证返回哪 10 条,MySQL 可能按任意物理顺序返回。只要涉及分页,ORDER BY 就不是可选项,而是强制前提——否则第 2 页可能重复或漏掉数据。
常见错误是先查总数再分页,但没加 ORDER BY,结果两次查询排序不一致,COUNT(*) 和实际列表对不上。
-
ORDER BY字段必须有索引,否则LIMIT越大越慢(MySQL 要先排完整张表) - 避免用
ORDER BY RAND()分页,它会强制全表扫描 - 如果按时间排序,优先用
created_at DESC而非id DESC,除非id真的严格递增且无跳号
用 OFFSET 实现简单分页的代价很高
LIMIT 1000, 20 表示跳过前 1000 行再取 20 行,MySQL 仍需扫描并丢弃这 1000 行。当页码很大(比如第 5000 页),性能急剧下降。
这不是语法问题,是执行逻辑决定的:没有“直接定位到第 N 页”的机制,只能从头数。
- 10 万行表,
LIMIT 99980, 20可能比LIMIT 20慢上百倍 - 加
WHERE created_at 这类条件能缩小扫描范围,但前提是该字段有索引且选择性高 - 业务上真要支持深分页?考虑改用游标分页(cursor-based pagination),用上一页最后一条的
id或created_at做下一页起点
游标分页怎么写才不翻车
游标分页本质是“基于上次结果继续往后拿”,绕开 OFFSET 的扫描缺陷。核心是把排序字段和主键组合成唯一锚点。
假设按 created_at DESC, id DESC 排序,第一页取:SELECT * FROM posts ORDER BY created_at DESC, id DESC LIMIT 20
拿到第 20 条的 created_at 和 id(比如 '2024-02-10 14:30:00' 和 12345),第二页就写:SELECT * FROM posts WHERE (created_at, id)
- 必须用元组比较(
(a,b) ),不能拆成两个 <code>AND条件,否则可能漏数据 - 复合排序字段顺序必须和
ORDER BY完全一致,且所有字段都参与比较 - 如果排序字段允许 NULL,
NULL在 MySQL 中被当作最小值,容易导致边界错乱,建议提前过滤或设默认值
count(*) 分页总数要不要查
用户界面上显示“共 123456 条”看起来友好,但 SELECT COUNT(*) FROM table 在大表上很重,尤其带复杂 WHERE 条件时。更糟的是,这个总数可能在你查完立刻就变了。
多数场景下,用户并不需要精确总数——他们只关心“有没有下一页”。用 LIMIT 21 查 21 条,显示前 20 条,如果第 21 条存在,就说明还有下一页。
- 想估算总数?看
EXPLAIN的rows值,误差可能达 ±40%,但够做分页按钮显隐判断 - 真要总数且表变动少?加个定时任务把
COUNT(*)结果缓存到 Redis,更新频率按业务容忍度定 - 千万级表还坚持显示总数?前端改文案,比如“已加载 20 条,更多内容正在加载…”
游标分页的正确性高度依赖排序字段的稳定性和唯一性,一旦业务允许修改排序字段(比如编辑了 created_at),旧游标就可能失效或重复。这种细节往往上线后才暴露。










