设想一个场景:

用户想看到最近的100条数据,但是数据库没有“分页”的功能,所以任务给到后端,前端向后端/数据库发来一条查询请求,要数据库中对应表中的所有数据(假设没有鉴权),千万/上亿条数据库每次查询,会把服务打崩,在数据库层面的“精细选择”功能:分页,应运而生。

分页的本质

每次只取一部分数据(通常和库里所有数据比起来比例悬殊)返回,需要的时候再取。 解决的矛盾:数据量巨大,但是用户注意力和资源是很有限的。

分页方式

offset分页

-- 第 1 页
SELECT * FROM experiments ORDER BY created_at DESC LIMIT 20 OFFSET 0;
-- 第 2 页
SELECT * FROM experiments ORDER BY created_at DESC LIMIT 20 OFFSET 20;
-- 第 1000 页
SELECT * FROM experiments ORDER BY created_at DESC LIMIT 20 OFFSET 19980;

缺陷: 当offset为19980时,数据库仍然需要扫描前19980行数据并丢弃,查询速度和offset大小成反比,这就是深分页问题。

cursor(游标)分页

-- 第一次请求
SELECT * FROM experiments 
ORDER BY created_at DESC, id DESC 
LIMIT 20;
 
-- 后续请求:带上「上一页最后一条的 created_at 和 id」作为游标
SELECT * FROM experiments 
WHERE (created_at, id) < ('2026-04-28 10:00:00', 12345)
ORDER BY created_at DESC, id DESC 
LIMIT 20;

只要(created_at, id) 上有索引,无论翻到第几页,数据库都是直接定位 + 顺序读 20 行,性能恒定。

分页中的问题

位置问题

offset描述的在数据行中的相对位置,如果有新的数据插入或者数据删除,offset就会出现重复/缺少的问题。 cursor是锚定了某个属性比如:id,位置是绝对的,在有新数据插入的情况下也能保持稳定/一致。

必须使用offset

典型的场景:电商网站,商品搜索栏最下方,通常有直接跳到XXX页的功能,这时候就必须使用offset。

分页缓存

结合在之前位置问题中的内容,为了减少缓存失效,最好对cursor的结果缓存。