游标分页(Cursor-based Pagination)

_

游标分页是一种基于游标(通常是唯一标识或时间戳)的分页技术,相比传统的LIMIT offset, size分页有显著的性能优势。

传统分页的问题

-- 传统分页:性能随offset增大而下降
SELECT * FROM orders
ORDER BY created_time DESC, id DESC
LIMIT 100000, 20;  -- 需要扫描100020行

游标分页的原理

基本思想

使用唯一的、有序的游标来标记分页位置,而不是使用页码。

实现方式

1. 基于自增主键的游标分页

- 第一页
SELECT * FROM orders
ORDER BY id DESC
LIMIT 20;

-- 第二页:使用上一页最后一条的id作为游标
SELECT * FROM orders
WHERE id < 上一页最后一条的id  -- 游标条件
ORDER BY id DESC
LIMIT 20;

2. 基于时间戳的游标分页

-- 第一页
SELECT * FROM orders
ORDER BY created_time DESC, id DESC
LIMIT 20;

-- 第二页
SELECT * FROM orders
WHERE (created_time < 上一页最后时间)
   OR (created_time = 上一页最后时间 AND id < 上一页最后ID)
ORDER BY created_time DESC, id DESC
LIMIT 20;

3. 复合游标分页(推荐)

-- 使用(时间戳, ID)作为复合游标
-- 第一页请求
SELECT id, content, created_time
FROM posts
ORDER BY created_time DESC, id DESC
LIMIT 10;

-- 第二页请求(客户端记住上一页最后一条的created_time和id)
SELECT id, content, created_time
FROM posts
WHERE (created_time < '2023-01-01 12:00:00')
   OR (created_time = '2023-01-01 12:00:00' AND id < 1000)
ORDER BY created_time DESC, id DESC
LIMIT 10;

实际应用示例

API设计示例

// 第一页请求
GET /api/posts?limit=20

// 响应
{
  "data": [...],
  "next_cursor": "2023-01-01T10:00:00Z_1000",  // 最后一条的游标
  "has_more": true
}

// 第二页请求
GET /api/posts?limit=20&cursor=2023-01-01T10:00:00Z_1000

完整SQL实现

-- 创建适合游标分页的索引
CREATE INDEX idx_posts_cursor ON posts(created_time DESC, id DESC);

-- 分页查询函数
CREATE FUNCTION get_posts_by_cursor(
  p_cursor_time TIMESTAMP DEFAULT NULL,
  p_cursor_id BIGINT DEFAULT NULL,
  p_limit INT DEFAULT 20
) RETURNS TABLE AS $$
BEGIN
  IF p_cursor_time IS NULL THEN
    -- 第一页
    RETURN QUERY
    SELECT * FROM posts
    ORDER BY created_time DESC, id DESC
    LIMIT p_limit;
  ELSE
    -- 后续分页
    RETURN QUERY
    SELECT * FROM posts
    WHERE (created_time < p_cursor_time)
       OR (created_time = p_cursor_time AND id < p_cursor_id)
    ORDER BY created_time DESC, id DESC
    LIMIT p_limit;
  END IF;
END;
$$ LANGUAGE plpgsql;

游标分页的优势

性能对比

分页方式

第1页

第100页

第10000页

LIMIT OFFSET

非常慢

游标分页

具体优势

  1. 恒定性能:无论翻到第几页,性能基本一致

  2. 无数据跳过:不需要OFFSET,避免扫描和跳过大量数据

  3. 实时性更好:适合频繁更新的数据流

  4. 无重复数据:避免传统分页在数据更新时出现的重复或丢失

适用场景

推荐使用游标分页的场景

  • ✅ 社交媒体时间线(Twitter、微博)

  • ✅ 消息列表、聊天记录

  • ✅ 实时数据流(日志、监控数据)

  • ✅ 无限滚动页面

传统分页仍适用的场景

  • ✅ 数据基本不变的列表(商品分类、用户列表)

  • ✅ 需要跳转到特定页码的场景

  • ✅ 数据量不大且offset较小的情况

注意事项

1. 游标必须唯一且有序

-- 错误:只使用时间戳,可能重复
WHERE created_time < '2023-01-01'

-- 正确:使用(时间戳, ID)保证唯一性
WHERE (created_time < '2023-01-01')
   OR (created_time = '2023-01-01' AND id < 1000)

2. 客户端实现

lass CursorPager {
  constructor() {
    this.currentCursor = null;
    this.hasMore = true;
  }

  async loadNextPage() {
    if (!this.hasMore) return;

    const params = { limit: 20 };
    if (this.currentCursor) {
      params.cursor = this.currentCursor;
    }

    const response = await api.get('/items', { params });
    this.currentCursor = response.next_cursor;
    this.hasMore = response.has_more;
    return response.data;
  }
}

3. 边界情况处理

-- 处理游标不存在的情况
SELECT * FROM posts
WHERE (created_time < COALESCE(:cursor_time, NOW()))
   AND (id < COALESCE(:cursor_id, 2^62))  -- 很大的数作为默认值
ORDER BY created_time DESC, id DESC
LIMIT :limit;

总结

游标分页是现代应用中处理大量数据分页的最佳实践,特别适合:

  • 高性能要求的实时应用

  • 大数据量的分页场景

  • 需要良好用户体验的无限滚动

虽然实现比传统分页稍复杂,但带来的性能优势是巨大的。

数据库规范:innodb表行记录物理长度不超过8KB 2026-03-07

评论区