做后台系统或者数据展示页面时,分页查询几乎是家常便饭。可你有没有遇到过这种情况:点到第10页还能接受,翻到第100页,页面转圈圈半天出不来结果?这时候别急着怀疑服务器,问题很可能出在数据库的分页查询和索引设计上。
为什么OFFSET越往后越慢?
常见的分页写法是用 LIMIT 和 OFFSET,比如:
SELECT * FROM orders ORDER BY created_time DESC LIMIT 20 OFFSET 2000;
这句的意思是从第2000条开始取20条数据。看起来没问题,但数据库执行时得先扫描前2020条记录,再扔掉前2000条,只返回最后20条。随着OFFSET增大,扫描的数据量成倍增长,尤其是表里有几十万甚至上百万条数据时,性能直线下降。
传统索引对OFFSET帮助有限
有人会说:“我加了索引啊,created_time 上有B树索引。”确实,有序字段加索引能加快排序过程,但OFFSET机制决定了它仍需跳过大量已匹配的行。索引能帮你快速找到每一条记录的位置,但跳过2000条这个动作本身还是得一步步来,没法“瞬移”。
改用“游标分页”避开深翻页陷阱
更高效的做法是放弃OFFSET,改用基于游标的分页(也叫“键集分页”)。核心思路是:不问“第几页”,而是问“从上次结束的地方接着查”。
假设你要按创建时间倒序展示订单,最后一次查到的记录时间是 '2024-03-15 10:23:10',下一页就可以这样写:
SELECT * FROM orders WHERE created_time < '2024-03-15 10:23:10' ORDER BY created_time DESC LIMIT 20;
这样数据库直接利用索引定位到小于该时间的记录,无需跳过前面几千条。只要 created_time 字段上有索引,查询几乎都是毫秒级。
复合索引提升多条件分页效率
如果分页还带筛选条件,比如查某个用户的订单,只建单字段索引就不够用了。假设你经常执行:
SELECT * FROM orders WHERE user_id = 12345 ORDER BY created_time DESC LIMIT 20 OFFSET 500;
这时候应该建立一个 (user_id, created_time) 的复合索引。这样数据库能直接在 user_id = 12345 的数据范围内,按时间倒序取出最新20条,避免回表和额外排序。
注意复合索引的顺序很重要。把等值查询的字段放前面,范围排序的放后面,才能充分发挥作用。
别让SELECT * 拖累性能
很多人习惯写 SELECT *,但在大表分页中这是个隐患。如果表里有TEXT字段或者大字段,即使只显示几条,数据库也要读完整行数据。建议明确指定需要的字段:
SELECT id, user_id, amount, created_time FROM orders WHERE created_time < '2024-03-15 10:23:10' ORDER BY created_time DESC LIMIT 20;
这样不仅减少I/O,还能利用覆盖索引,进一步提速。
真实场景:后台订单列表优化
我们公司之前有个运营后台,查订单翻到50页就卡。后来改成游标分页,前端传上次最后一条的时间戳,后端拼进WHERE条件,再配合 (status, created_time) 复合索引,现在翻到几千条都感觉不到延迟。用户反馈“终于不用等了”。
当然,这种方案对“随机跳页”支持不好,比如用户想直接跳第100页。但大多数业务场景其实是连续翻页,比如刷动态、看日志、查流水,游标方式完全够用,甚至体验更好。