如何通过分页查询避免数据重复,有效提高查询速度?
- 内容介绍
- 文章标签
- 相关推荐
遇到的痛点:
-
查询慢: 当使用
LIMIt offset。limit时offset 越大,MySQL 必须扫描越多行,导致响应时间成倍增长。 - 数据重复或遗漏: 分页查询会出现同一条记录被多次返回或根本无法看到。
- 性能瓶颈: 全表扫描、缺失索引还有无序排序都直接拖累整体吞吐量。话说回来,
- 实现复杂度升高: 需要在业务端维护游标或状态信息。增加程序维护成本,
什么是分页查询?
Paging 是把海量数据拆分成若干页,每页只返回固定数量的数据。常见做法是使用 MySQL 的 LIMIt offset。limit,但它对大量偏移量极为低效。
为什么分页会变慢?说起来,
- LIMIt offset: MySQL 必须先扫描 offset 行。接下来再返回 limit 行;offset 越大,扫描成本越高。怎么说呢,
- No index on order column: 没有覆盖索引时会触发全表扫描。怎么说呢,
- SORT instability: 若排序字段值相同且没有唯一标识符。结果可能不确定,从而产生重复/漏掉的数据。
常见问题的观点是,数据重复与遗漏
-
#1 排序字段不唯一: 时间戳、状态等字段往往有大量相同值;如果仅按这些字段排序,就无法保证顺序稳定。从解决办法来看,在 ORDER BY 后面追加唯一主键。如
ID DESC 或 PRIMARY_KEY ASC. - #2 新增/删除导致位置变化: 当新行插入到已查询过的位置后下一个 page 就会出现重叠或缺失。说到解决办法,采用 Cursor / Keyset 分页。不再依赖 OFFSET,而是基于上一页最终一条记录的关键字段进行过滤。
- #3 大 OFFSET 导致全表扫描: OFFSET>10k 时几乎等价于 “SELECT * FROM table WHERE …LIMIT ,”,性能直线下降。至于解决办法,切换到游标分页或者预取主键列表 + 再回表检索。话说回来,
方案一这方面。加唯一键排序
- 实现方式: sql SELECT * FROM orders WHERE order_date>= :last_date AND id> :last_id ORDER BY order_date ASC,id ASC LIMIT :page_size;- 每次只扫描比上一次更大的 ID 范围;- 不需要 OFFSET;按理说,- 对写入频繁且需避免重复/漏查的问题尤为适用。**优点** • 单行定位,无需全表遍历; • 与 INSERT/DELETE 同步性好,可通过组合条件锁定窗口。**缺点** • 必须在业务层保存 `last_date` 与 `last_id`;• 对于完全无顺序需求的场景略显复杂。
方案二这方面,游标分页——最强防止重叠与漏查策略!
- 主要思路:"下一次从上一次返回最终一条记录开始" 而不是用 OFFSET。可以利用数据库自身生成的游标或者自定义 token 来保持状态。
- 典型实现: sql -- 第一次请求 SELECT * FROM products WHERE created_at <= NOW ORDER BY created_at DESC, id DESC LIMIT :page_size; -- 第二次请求传递前一次结果中的最后一条 created_at 与 id: SELECT * FROM products WHERE <= ORDER BY created_at DESC, id DESC LIMIT :page_size;
- 优点: - 完全避免因新增/删除导致的数据重叠和遗漏;
- 缺点:- 服务端需要维护游标状态或客户端持久化 token;
-
;- 在极高并发下需要谨慎处理事务隔离级别以确保一致性。css
/* 简单演示可视化效果 */
.cursor-page { margin-bottom:12px;padding:8px,border:1px solid #ddd;}
请根据业务需求选择合适方式。
使用索引提高速度
-
在
ORDER BY和WHERE子句中使用覆盖索引,例如:sql CREATE INDEX idx_orders_created ON orders; - 避免 SELECT *。只取需要列,减少 I/O。
子查询 + 主键回表
1️⃣ 提取主键列表:
sql SELECT id FROM orders WHERE ... ORDER BY created_at ASC,id ASC LIMIT :page_size OFFSET :offset;② 再根据主键回表获取完整行:sql SELECT * FROM orders WHERE id IN;- 主键已建 B‑Tree 索引,所以回表速度快。临时表策略
-
对于极大数据集,可先把关键列存入临时表。再做分页:
sql CREATE TEMPORARY TABLE tmp AS SELECT id FROM orders WHERE ... ORDER BY created_at,id LIMIT…,SELECT o.* FROM orders o JOIN tmp t ON o.id=t.id; - 临时表占内存或磁盘,可根据资源调整。说起来,
实践案例分析
场景 原始实现 调整后 高频写入 + 分页读取 LIMIT offset+ 全部列Keyset 分页 + 覆盖索引 数据量>10M 多次全表扫描 临时主键列表 + 回表 前端无限滚动 每次请求大偏移量 游标分页 + token 结论
- Keyset / 游标分页 能彻底消除 “重复 & 漏掉” 痛点。并显著降低 CPU 和磁盘 I/O。话说回来,
- 覆盖索引 与 子查询回表 配合使用。可让每页读取仅触碰数十行,而非数百万行。
1️⃣ 查询慢 → 减少 OFFSET、改用 Keyset;
2️⃣ 数据重复 → 加唯一主键排序、使用 Cursor;
3️⃣ 性能瓶颈 → 建覆盖索引、拆分子查询;
4️⃣ 程序复杂度 ↑ → 用统一 token 或服务端缓存维持状态。
遵循上述方法,你可以在保留业务灵活性的同时让分页查询跑得更快、更稳!不过,
-
在
遇到的痛点:
-
查询慢: 当使用
LIMIt offset。limit时offset 越大,MySQL 必须扫描越多行,导致响应时间成倍增长。 - 数据重复或遗漏: 分页查询会出现同一条记录被多次返回或根本无法看到。
- 性能瓶颈: 全表扫描、缺失索引还有无序排序都直接拖累整体吞吐量。话说回来,
- 实现复杂度升高: 需要在业务端维护游标或状态信息。增加程序维护成本,
什么是分页查询?
Paging 是把海量数据拆分成若干页,每页只返回固定数量的数据。常见做法是使用 MySQL 的 LIMIt offset。limit,但它对大量偏移量极为低效。
为什么分页会变慢?说起来,
- LIMIt offset: MySQL 必须先扫描 offset 行。接下来再返回 limit 行;offset 越大,扫描成本越高。怎么说呢,
- No index on order column: 没有覆盖索引时会触发全表扫描。怎么说呢,
- SORT instability: 若排序字段值相同且没有唯一标识符。结果可能不确定,从而产生重复/漏掉的数据。
常见问题的观点是,数据重复与遗漏
-
#1 排序字段不唯一: 时间戳、状态等字段往往有大量相同值;如果仅按这些字段排序,就无法保证顺序稳定。从解决办法来看,在 ORDER BY 后面追加唯一主键。如
ID DESC 或 PRIMARY_KEY ASC. - #2 新增/删除导致位置变化: 当新行插入到已查询过的位置后下一个 page 就会出现重叠或缺失。说到解决办法,采用 Cursor / Keyset 分页。不再依赖 OFFSET,而是基于上一页最终一条记录的关键字段进行过滤。
- #3 大 OFFSET 导致全表扫描: OFFSET>10k 时几乎等价于 “SELECT * FROM table WHERE …LIMIT ,”,性能直线下降。至于解决办法,切换到游标分页或者预取主键列表 + 再回表检索。话说回来,
方案一这方面。加唯一键排序
- 实现方式: sql SELECT * FROM orders WHERE order_date>= :last_date AND id> :last_id ORDER BY order_date ASC,id ASC LIMIT :page_size;- 每次只扫描比上一次更大的 ID 范围;- 不需要 OFFSET;按理说,- 对写入频繁且需避免重复/漏查的问题尤为适用。**优点** • 单行定位,无需全表遍历; • 与 INSERT/DELETE 同步性好,可通过组合条件锁定窗口。**缺点** • 必须在业务层保存 `last_date` 与 `last_id`;• 对于完全无顺序需求的场景略显复杂。
方案二这方面,游标分页——最强防止重叠与漏查策略!
- 主要思路:"下一次从上一次返回最终一条记录开始" 而不是用 OFFSET。可以利用数据库自身生成的游标或者自定义 token 来保持状态。
- 典型实现: sql -- 第一次请求 SELECT * FROM products WHERE created_at <= NOW ORDER BY created_at DESC, id DESC LIMIT :page_size; -- 第二次请求传递前一次结果中的最后一条 created_at 与 id: SELECT * FROM products WHERE <= ORDER BY created_at DESC, id DESC LIMIT :page_size;
- 优点: - 完全避免因新增/删除导致的数据重叠和遗漏;
- 缺点:- 服务端需要维护游标状态或客户端持久化 token;
-
;- 在极高并发下需要谨慎处理事务隔离级别以确保一致性。css
/* 简单演示可视化效果 */
.cursor-page { margin-bottom:12px;padding:8px,border:1px solid #ddd;}
请根据业务需求选择合适方式。
使用索引提高速度
-
在
ORDER BY和WHERE子句中使用覆盖索引,例如:sql CREATE INDEX idx_orders_created ON orders; - 避免 SELECT *。只取需要列,减少 I/O。
子查询 + 主键回表
1️⃣ 提取主键列表:
sql SELECT id FROM orders WHERE ... ORDER BY created_at ASC,id ASC LIMIT :page_size OFFSET :offset;② 再根据主键回表获取完整行:sql SELECT * FROM orders WHERE id IN;- 主键已建 B‑Tree 索引,所以回表速度快。临时表策略
-
对于极大数据集,可先把关键列存入临时表。再做分页:
sql CREATE TEMPORARY TABLE tmp AS SELECT id FROM orders WHERE ... ORDER BY created_at,id LIMIT…,SELECT o.* FROM orders o JOIN tmp t ON o.id=t.id; - 临时表占内存或磁盘,可根据资源调整。说起来,
实践案例分析
场景 原始实现 调整后 高频写入 + 分页读取 LIMIT offset+ 全部列Keyset 分页 + 覆盖索引 数据量>10M 多次全表扫描 临时主键列表 + 回表 前端无限滚动 每次请求大偏移量 游标分页 + token 结论
- Keyset / 游标分页 能彻底消除 “重复 & 漏掉” 痛点。并显著降低 CPU 和磁盘 I/O。话说回来,
- 覆盖索引 与 子查询回表 配合使用。可让每页读取仅触碰数十行,而非数百万行。
1️⃣ 查询慢 → 减少 OFFSET、改用 Keyset;
2️⃣ 数据重复 → 加唯一主键排序、使用 Cursor;
3️⃣ 性能瓶颈 → 建覆盖索引、拆分子查询;
4️⃣ 程序复杂度 ↑ → 用统一 token 或服务端缓存维持状态。
遵循上述方法,你可以在保留业务灵活性的同时让分页查询跑得更快、更稳!不过,
-
在

