“游标具象化”是数据库分页优化中的核心概念,但其本质常被误解为“指针”或“位置标记”。实际上,游标(Cursor)在工程实践中是“可比较的排序字段值”,通过记录上一页最后一条数据的排序键,实现高效、无偏移的分页。
一、核心原理:游标 ≠ 指针,而是“排序锚点”
▶ 1.传统 OFFSET 的缺陷
-- 跳过 100 万行 → 扫描 1,000,010 行SELECT*FROMordersORDERBYidLIMIT1000000,10;- 问题:
- 必须扫描
offset + size行 - 成本随
offset线性增长
- 必须扫描
▶ 2.游标分页的本质
-- 记录上一页最后 id=1000000SELECT*FROMordersWHEREid>1000000ORDERBYidLIMIT10;- 关键:
- 游标 = 排序字段的值(如
id=1000000) - 不是物理位置,而是逻辑排序锚点
- 游标 = 排序字段的值(如
💡核心认知:
游标是“上次看到的最大值”,而非“跳过的行数”
二、工程实现:四类游标场景
▶ 场景 1:单字段主键(最简单)
- 表结构:
CREATETABLEorders(idBIGINTAUTO_INCREMENTPRIMARYKEY,user_idINT,amountDECIMAL(10,2)); - 分页逻辑:
// 第一页$lastId=0;$rows=DB::select("SELECT * FROM orders WHERE id > ? ORDER BY id LIMIT 10",[$lastId]);// 下一页(取最后一条的 id)$lastId=end($rows)->id;
▶ 场景 2:多字段排序(复合游标)
- 需求:按
user_id ASC, created_at DESC分页 - 表结构:
CREATETABLElogs(idBIGINT,user_idINT,created_atDATETIME,INDEXidx_user_time(user_id,created_at)); - 分页逻辑:
// 上一页最后一条:user_id=123, created_at='2023-01-01 10:00:00'$rows=DB::select(" SELECT * FROM logs WHERE (user_id > ?) OR (user_id = ? AND created_at < ?) ORDER BY user_id ASC, created_at DESC LIMIT 10 ",[123,123,'2023-01-01 10:00:00']);
▶ 场景 3:非唯一排序字段(需主键兜底)
- 问题:
created_at可能重复 → 游标失效
- 解决方案:
-- 添加主键作为 tie-breakerSELECT*FROMlogsWHERE(created_at,id)>('2023-01-01 10:00:00',1000)ORDERBYcreated_at,idLIMIT10;
▶ 场景 4:反向分页(上一页)
- 逻辑:
-- 上一页:小于当前最小值SELECT*FROMordersWHEREid<?ORDERBYidDESCLIMIT10;
三、避坑指南:游标的五大陷阱
| 陷阱 | 破局方案 |
|---|---|
| 忽略排序字段唯一性 | 复合排序时,末尾加主键确保唯一性 |
| 错误处理 NULL 值 | WHERE col > ?会跳过 NULL → 改用WHERE (col > ? OR col IS NULL) |
| 并发插入导致漏数据 | 游标分页无法保证强一致性 → 接受最终一致性 |
| 未使用覆盖索引 | 确保WHERE+ORDER BY字段有联合索引 |
| 前端传递游标被篡改 | 对游标值签名(如 JWT)或仅允许顺序翻页 |
四、性能对比:游标 vs OFFSET
| 指标 | OFFSET (1M, 10) | 游标分页 |
|---|---|---|
| 扫描行数 | 1,000,010 | 10 |
| 磁盘 I/O | 高(全表扫描) | 低(索引 range) |
| 响应时间 | 秒级 | 毫秒级 |
| 扩展性 | O(n) | O(1) |
📊实测数据(1 亿行表):
OFFSET 1000000, 10:12.3 秒- 游标分页:0.008 秒
五、终极心法
**“游标不是魔法,
而是排序的锚点——
- 当你记录最大值,
你在跳过扫描;- 当你复合排序,
你在确保连续;- 当你接受最终一致,
你在拥抱现实。真正的分页优化,
始于对排序的敬畏,
成于对细节的精控。”
结语
从今天起:
- 深度分页必用游标方案
- 复合排序末尾加主键
- 用
EXPLAIN验证执行计划(type=range)
因为最好的分页,
不是跳过百万行,
而是精准定位下一程。