常识来了
白蓝主题五 · 清爽阅读
首页  > 网络排错

分页查询慢?可能是索引没用对

做后台开发或者维护系统的时候,经常要查数据列表,比如订单、用户记录、日志这些。数据一多,页面就得翻页看,这时候用的分页查询如果写得不好,页面加载慢得像卡碟,用户刷新一下就怀疑人生。

常见的分页写法陷阱

很多人写分页直接用 LIMIT 和 OFFSET,比如:

SELECT * FROM orders ORDER BY create_time DESC LIMIT 20 OFFSET 10000;

看着没啥问题,第500页,每页20条,OFFSET 就是 499*20=9980,取下一页就是10000。可问题是,数据库得先从第一行扫到第10000行,再取20条。数据量越大,前面的扫描越慢,特别是ORDER BY字段没有合理索引时,简直是全表扫描的节奏。

索引不是建了就万事大吉

有人说了,我create_time加了索引啊,怎么还是慢?注意,加了索引不等于就能高效跳过前一万条。B+树索引适合快速定位起始点,但OFFSET方式本质是“跳过N条”,数据库还得一条条数过去,索引的优势被浪费了。

更糟的是,如果查询还涉及多个字段排序,或者WHERE条件和排序字段不一致,索引可能完全用不上。比如你按user_id查,却按create_time排序,复合索引没设计好,就会导致回表频繁甚至全表扫描。

改用“游标分页”避开深翻页

真正高效的分页,是避免用OFFSET。换成基于上一页最后一条数据的“游标”来查下一页。比如上一页最后一条记录的create_time是'2024-03-15 10:23:45',主键id是5678,那么下一页可以这样写:

SELECT * FROM orders WHERE create_time < '2024-03-15 10:23:45' OR (create_time = '2024-03-15 10:23:45' AND id < 5678) ORDER BY create_time DESC, id DESC LIMIT 20;

只要你在(create_time, id)上建了复合索引,数据库可以直接从索引中定位到断点位置,不用从头数。这种写法在微博、朋友圈这类无限滚动列表里很常见,翻到第1000页也不卡。

索引设计要点

想让分页快,索引得配合查询逻辑。基本原则是:索引字段顺序要和ORDER BY一致,如果有WHERE条件,把等值条件放前面,范围或排序字段放后面。

比如你常按status查,再按时间倒序排,那索引应该是(status, create_time, id),而不是反过来。这样先过滤状态,再在结果内按时间排序,效率最高。

别忘了覆盖索引

如果索引里已经包含了查询需要的所有字段,数据库就不必回表查数据行,速度能提升一大截。比如你的列表只展示订单号、金额、状态、时间,那可以把这几个字段都放进索引:

ALTER TABLE orders ADD INDEX idx_status_time_cover (status, create_time, order_no, amount, id);

这样查询可以直接从索引拿数据,减少IO,特别适合高并发场景。

实际调优时可以用EXPLAIN看看执行计划,重点看type是不是range以上,key有没有命中,rows是不是太大,Extra有没有Using filesort或者Using temporary这种危险信号。

分页看着简单,真跑在生产环境,差一个索引设计,响应时间可能从20毫秒飙到2秒。别让用户的等待,毁在一条没写好的SQL上。