最常见原因是排序不唯一。如仅按created_at desc分页,多条记录时间相同时数据库每次返回行序可能不同,导致offset跳过或重复数据;应改用created_at desc, id desc组合排序并建联合索引。

JOIN后ORDER BY字段不稳定导致OFFSET跳行
漏数据最常见原因是排序不唯一。比如只按 created_at DESC 分页,但多条记录时间相同,数据库每次执行时返回的行序可能不同——OFFSET 1000 实际跳过的“第1000行”不是固定记录,翻页时就可能重复或跳过。
实操建议:
- 强制补全排序依据:用
ORDER BY created_at DESC, id DESC(id主键天然唯一) - 确保该组合字段有联合索引,例如
CREATE INDEX idx_orders_created_id ON orders(created_at, id) - 子查询和外层查询的
ORDER BY必须完全一致,否则外层重排序会破坏偏移逻辑 - 避免在排序中使用
RAND()、函数或非确定性表达式,它们会让索引失效
LEFT JOIN后WHERE条件意外过滤NULL行
写 WHERE right_table.status = 'active' 看似合理,但 LEFT JOIN 中右表无匹配时字段为 NULL,这条 WHERE 实际把所有 NULL 行都干掉了——等效于转成 INNER JOIN,主表本该保留的记录就“漏”了。
实操建议:
- 检查 WHERE 中是否引用了右表字段;若必须过滤,改用
ON条件:LEFT JOIN users u ON o.user_id = u.id AND u.status = 'active' - 真要保留主表全部记录,WHERE 中对右表字段的判断得允许 NULL:
WHERE u.status = 'active' OR u.status IS NULL - 用
EXPLAIN看执行计划里有没有Using where挤在 JOIN 后面,那是危险信号
分页逻辑没下推到主表,中间集膨胀后截断
直接在 JOIN ... ORDER BY ... LIMIT OFFSET 上分页,数据库先完成全量 JOIN(比如 1 万订单 × 平均 3 条明细 = 3 万行),再丢弃前 1 万行——但你只想查 20 条订单,却让数据库反复搬运 3 万行中间结果。
实操建议:
- 改用“先分页主表再 JOIN”:子查询只查主表
id,带索引排序和LIMIT/OFFSET,外层用该id关联从表 - 子查询严禁出现从表字段、JOIN、
GROUP BY或函数,否则优化器放弃索引下推 - 验证是否生效:用
EXPLAIN看子查询的rows是否接近OFFSET + LIMIT值(比如OFFSET 40000 LIMIT 20对应 ~40020),而不是全表扫描行数
游标分页没处理并发写入或排序字段重复
游标分页看似稳定,但若上一页最后一条的 id = 12345,期间新插入一条 created_at 相同、id = 12344 的记录,下一页查 WHERE created_at 就会漏掉它——因为 <code>id 排除了刚插入的更小 ID。
实操建议:
- 游标字段必须是单调递增且不可变的(首选主键
id,慎用时间戳) - 若必须用时间字段,补上唯一字段:游标存
(created_at, id)二元组,查询条件写WHERE (created_at, id) - 高并发场景下,游标值不能仅依赖前端传入,服务端应从上一页最后一条结果中提取并校验











