[mysql] 深分页优化

深分页优化

业务逻辑

现有50万条订单数据,复合索引:(product_id, amount DESC) 。 按product_idamount排序后,取第 9991-10000 条数据。

sql 复制代码
CREATE TABLE `orders` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int NOT NULL,
  `product_id` int NOT NULL,
  `amount` decimal(10,2) NOT NULL,
  `order_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `address` text NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_orders_product_amount` (`product_id`,`amount` DESC)
) ENGINE=InnoDB AUTO_INCREMENT=500001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

原始查询

sql 复制代码
select * from orders order by product_id,amount desc limit 9990,10;
优点:
  • 写法最简单直观 :直接表达 "按product_idamount排序后,取第 9991-10000 条数据" 的业务逻辑,易于理解和维护。
  • 无额外逻辑开销:没有子查询或关联操作,语法层面最简洁。
缺点:
  • 性能极差(深分页场景)
    虽然表中存在idx_orders_product_amount索引(与排序条件product_id, amount DESC完全匹配),但SELECT *需要回表获取所有字段(索引仅包含product_idamount和主键id)。
    更关键的是,LIMIT 9990,10需要扫描前9990+10=10000条数据(通过索引定位后回表),再丢弃前 9990 条,仅保留 10 条。当偏移量9990很大时,扫描和回表的成本极高(IO 密集型操作)。
  • 无法避免全量扫描风险:若索引失效(如统计信息不准确导致优化器放弃索引),会触发全表扫描 + 文件排序,性能灾难级下降。

隐式连接子查询

sql 复制代码
select t1.* FROM orders t1, (select id from orders order by product_id,amount desc limit 9990, 10) t2 where t1.id = t2.id;
优点:
  • 减少回表开销
    子查询select id ...仅需获取id,而idx_orders_product_amount索引的叶子节点包含主键id(InnoDB 二级索引特性),因此子查询可直接通过索引完成(覆盖索引扫描),无需回表。
    主查询通过id(主键索引)关联t1,回表效率极高(主键查询是 O (1) 操作)。
    相比原始查询,避免了 "扫描 10000 条数据时每条都回表取所有字段" 的开销,性能提升显著。
缺点:
  • 仍依赖offset扫描 :子查询LIMIT 9990,10仍需扫描前 10000 条索引记录(虽然是索引扫描,比全表扫描快,但偏移量越大,扫描量越多)。
  • 隐式连接可读性差 :使用逗号分隔表的隐式连接(t1, t2)容易混淆关联条件,尤其在多表连接时可能引发逻辑错误(如遗漏关联条件导致笛卡尔积)。
  • 排序依赖外部保证 :主查询未显式排序,虽然结果顺序通常与子查询一致(因id是按排序取的),但数据库不保证,极端情况下可能出现顺序错乱。

⭐显式内连接子查询

sql 复制代码
SELECT o.*
FROM orders o
INNER JOIN (
  -- 子查询:利用索引快速定位需要的记录ID
  SELECT id
  FROM orders
  ORDER BY product_id, amount DESC
  LIMIT 9990, 10
) AS sub ON o.id = sub.id
ORDER BY o.product_id, o.amount DESC;
优点:
  • 继承第二个查询的性能优势 :子查询同样使用覆盖索引获取id,主查询通过主键回表,性能与第二个查询基本一致。
  • 可读性和规范性更好 :使用INNER JOIN显式声明关联关系,关联条件清晰,符合现代 SQL 编码规范,降低维护成本。
  • 排序明确 :主查询显式添加ORDER BY,确保结果顺序与业务预期一致(避免数据库优化导致的顺序错乱)。
缺点:
  • 仍未解决offset扫描问题 :与第二个查询相同,子查询LIMIT 9990,10仍需扫描前 10000 条索引记录,偏移量极大时(如offset=10万)性能仍会下降。

范围查询优化

sql 复制代码
SELECT *
FROM orders
WHERE 
  -- 处理product_id大于基准值的情况
  (product_id > (SELECT product_id FROM orders ORDER BY product_id, amount DESC LIMIT 9990, 1))
  OR 
  -- 处理product_id等于基准值但amount小于基准值的情况
  (product_id = (SELECT product_id FROM orders ORDER BY product_id, amount DESC LIMIT 9990, 1)
   AND amount <= (SELECT amount FROM orders ORDER BY product_id, amount DESC LIMIT 9990, 1))
ORDER BY product_id, amount DESC
LIMIT 10;
优点:
  • 性能最优(深分页场景)
    核心是将 "偏移量扫描" 转化为 "范围查询":通过子查询获取第 9990 条记录的product_idamount,再用(product_id > ...) OR (product_id = ... AND amount <= ...)直接定位起始位置,只需扫描 10 条数据即可。
    无论偏移量多大(如offset=100万),扫描量始终是固定的(子查询扫描 1 条 + 主查询扫描 10 条),性能几乎不受偏移量影响。
  • 索引利用率最大化WHERE条件与ORDER BY均匹配idx_orders_product_amount索引,可通过索引快速定位范围,避免全表扫描。
缺点:
  • 写法复杂,易出错
    需要处理product_id相同的边界情况(product_id = ... AND amount <= ...),逻辑繁琐;若amount存在重复且未结合id(唯一键),可能出现漏数据或重复数据(例如两条记录product_idamount完全相同,范围查询可能误判)。
  • 子查询冗余 :三次子查询LIMIT 9990,1(实际执行时数据库可能优化为一次,但逻辑上冗余),增加理解成本。
  • 不支持跳页灵活性 :若业务需要 "随机跳页"(如直接从第 1 页跳到第 100 页),需要先查询目标页的起始product_idamount,额外增加一次查询开销。

总结对比

维度 原始查询 隐式连接子查询 ⭐显式内连接子查询 范围查询优化
性能(深分页) 最差(全量扫描 + 回表) 较好(索引扫描 + 主键回表) 较好(同左,更稳定) 最优(范围查询,固定扫描量)
可读性 最好(直观) 较差(隐式连接歧义) 好(显式连接,清晰) 最差(逻辑复杂)
维护成本 中(易混淆关联条件) 低(规范写法) 高(边界条件多)
适用场景 偏移量极小(如前 10 页) 需跳页,偏移量中等 需跳页,偏移量中等(推荐) 偏移量极大(如 1000 页 +),顺序翻页

补充:游标优化

游标(Cursor)是维护一个 "查询位置指针",每次查询都从 "上一次的结束位置" 开始,而非从开头计算偏移量。本质是 "流式分页",不支持跳页,但适合 "加载更多"(如滚动到底部加载下一页)的场景,如APP首页瀑布流。

orders表为例,用主键id作为游标:

第一次查询(第一页):

sql 复制代码
-- 首次查询:获取前10条,记录最后一条的id作为游标
SELECT id, order_no, create_time 
FROM orders 
ORDER BY create_time DESC, id DESC 
LIMIT 10;

-- 假设返回的最后一条id是10000,记录游标值:last_id = 10000

第二次查询(第二页):

sql 复制代码
-- 基于上一页的游标查询下一页
SELECT id, order_no, create_time 
FROM orders 
WHERE create_time <= (SELECT create_time FROM orders WHERE id = 10000) 
  AND id < 10000 -- 用id避免create_time重复导致的漏数据
ORDER BY create_time DESC, id DESC 
LIMIT 10;

-- 更新游标:last_id = 本次返回的最后一条id

以此类推,每次查询都依赖上一次的last_id,无需计算offset

相关推荐
大数据狂人28 分钟前
从 Hive 数仓出发,全面剖析 StarRocks、MySQL、HBase 的使用场景与区别
hive·mysql·hbase
失因34 分钟前
Linux 权限管理与 ACL 访问控制
linux·运维·服务器·数据库·centos
小醉你真好1 小时前
Spring Boot + ShardingSphere 分库分表实战
java·spring boot·后端·mysql
Vdeilae1 小时前
IIS 让asp.net core 项目一直运行
java·服务器·asp.net
cookqq1 小时前
mongodb源代码分析创建db流程分析
数据库·sql·mongodb·nosql
yh云想2 小时前
存储函数与触发器:数据库自动化与业务逻辑封装的核心技术
数据库·sql
ZZH1120KQ2 小时前
ORACLE复杂查询
数据库·oracle
YY_TJJ2 小时前
8.4 Java Web(Maven P50-P57)
java·开发语言·maven
山茶花开时。2 小时前
[Oracle] TO_DATE()函数
数据库·oracle
cc蒲公英2 小时前
uniapp x swiper/image组件mode=“aspectFit“ 图片有的闪现后黑屏
java·前端·uni-app