后台商品列表一页 20 条,运营翻到第 300 页查一个老 SKU,转圈转了四秒。开发第一反应是加索引,其实问题常常不在索引,而在 offset。

20 条一页看着不多,可第 300 页意味着数据库要跳过 5980 行,再取 20 行。翻得越深,跳过的行越多。这也解释了为什么前几页很快,越往后越慢。

深翻页慢在哪

看这条 SQL:

SELECT id, name FROM goods ORDER BY id LIMIT 20 OFFSET 5980;

数据库确实用上了主键索引,但它仍然要沿着索引走 5980 次,把行读出来再丢掉。行越大、字段越多,代价越高。改成游标方式,把上一页最后一条的 id 带过来:

SELECT id, name FROM goods WHERE id > 5980 ORDER BY id LIMIT 20;

这样每次只扫 20 行,翻到第一万页也是这个速度。代价是不能直接跳到第 300 页,得顺序翻,或者前端维护一个页码到 id 的映射。

20 条这个默认值该不该改

20 条是不少后台框架的默认值,多数场景够用。但做数据核对、批量勾选的时候,20 条一页要点十几次下一页,效率很低,这种页面给 50 或 100 条更合适。

反过来,带图片的列表,20 条已经要加载 20 张缩略图,再加就卡。所以别全局改,按页面配。好一点的后台会在右下角给个每页条数下拉,20、50、100 三档,选择记到本地。

导出别复用分页接口

最常见的坑是导出也走 20 条一页的接口,前端循环请求几百次。导出应该单独一个接口,服务端流式写 CSV。分页是给人看的,导出是给机器看的,两件事别混在一起。

如果列表已经卡到三秒以上,先看慢查询日志里是不是深 offset,再看 count 语句。很多时候一条 SELECT COUNT 比正文查询还慢,而前端只要知道有没有下一页。