[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

相关推荐
倒流时光三十年几秒前
PostgreSQL Semi-Join(半连接)通俗讲解
数据库·postgresql
Web Security Loop28 分钟前
MAC卸载JDK环境
java·macos·jdk
NiceCloud喜云1 小时前
腾讯云国际版云数据库选型:MySQL、Redis 怎么按业务架构来判断
数据库·mysql·腾讯云
ew452181 小时前
MySQL JOIN 关联语法、差异、标准用法、经典坑点汇总
数据库·mysql
Rain的Java大神之路2 小时前
线上接口负载满了如何解决
java·数据库·redis·后端·缓存·面试·架构
Nturmoils2 小时前
给运营导个 CSV,在 ksql 里用 \copy 就够了
数据库
ciqingloveless2 小时前
Oracle打开数据库加密
数据库·oracle
counting money2 小时前
Tomcat 专题:从基础架构到性能调优与 WebSocket 实战
java·websocket·tomcat
m0_462605222 小时前
大模型week02
数据库
DBA_G3 小时前
GBase 8a数据库集群多维度资源管控策略矩阵解析
数据库·oracle