“游标具象化” 是数据库分页优化中的核心概念,但其本质常被误解为“指针”或“位置标记”。实际上,游标(Cursor)在工程实践中是“可比较的排序字段值”,通过 记录上一页最后一条数据的排序键,实现高效、无偏移的分页。


一、核心原理:游标 ≠ 指针,而是“排序锚点”

▶ 1. 传统 OFFSET 的缺陷
-- 跳过 100 万行 → 扫描 1,000,010 行
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;
  • 问题
    • 必须扫描 offset + size
    • 成本随 offset 线性增长
▶ 2. 游标分页的本质
-- 记录上一页最后 id=1000000
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 10;
  • 关键
    • 游标 = 排序字段的值(如 id=1000000
    • 不是物理位置,而是逻辑排序锚点

💡 核心认知
游标是“上次看到的最大值”,而非“跳过的行数”


二、工程实现:四类游标场景

▶ 场景 1:单字段主键(最简单)
  • 表结构
    CREATE TABLE orders (
        id BIGINT AUTO_INCREMENT PRIMARY KEY,
        user_id INT,
        amount DECIMAL(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 分页
  • 表结构
    CREATE TABLE logs (
        id BIGINT,
        user_id INT,
        created_at DATETIME,
        INDEX idx_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-breaker
    SELECT * FROM logs 
    WHERE (created_at, id) > ('2023-01-01 10:00:00', 1000)
    ORDER BY created_at, id 
    LIMIT 10;
    
▶ 场景 4:反向分页(上一页)
  • 逻辑
    -- 上一页:小于当前最小值
    SELECT * FROM orders 
    WHERE id < ? 
    ORDER BY id DESC 
    LIMIT 10;
    

三、避坑指南:游标的五大陷阱

陷阱 破局方案
忽略排序字段唯一性 复合排序时,末尾加主键确保唯一性
错误处理 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, 1012.3 秒
  • 游标分页:0.008 秒

五、终极心法

**“游标不是魔法,
而是排序的锚点——

  • 当你 记录最大值
    你在跳过扫描;
  • 当你 复合排序
    你在确保连续;
  • 当你 接受最终一致
    你在拥抱现实。

真正的分页优化,
始于对排序的敬畏,
成于对细节的精控。”


结语

从今天起:

  1. 深度分页必用游标方案
  2. 复合排序末尾加主键
  3. EXPLAIN 验证执行计划(type=range)

因为最好的分页,
不是跳过百万行,
而是精准定位下一程。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐