MySql深页查询实现方案

 更新时间:2025年11月03日 11:37:23   作者:伤惢无泪  
本文给大家介绍了MySql深页查询实现方案,结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧

此文章使用的是“延迟关联”查询方案进行 测试于分析

其他方案:可采用 SELECT * FROM user_info WHERE id > last_id ORDER BY id LIMIT 10;
last_id(上一个查询的最后id)

方案1:
select user_id from user_info where venture = 'TH' limit 824000,4000
方案2:
select user_id
from user_info a join (select id 
from user_info where venture = 'TH' limit 824000,4000) b on a.id = b.id

🏆预期结果(venture无索引的情况下)

结论:方案1更快

方案1执行流程:

执行步骤:

  1. 全表扫描 - 从第一行开始逐行检查 venture 字段
  2. 过滤匹配 - 找到 venture='TH' 的记录
  3. 跳过前824000条 - 继续扫描直到跳过824000条匹配记录
  4. 返回4000条 - 取接下来的4000条记录的 user_id

方案2执行流程:

子查询执行步骤:

  1. 全表扫描 - 从第一行开始逐行检查 venture 字段
  2. 过滤匹配 - 找到 venture='TH' 的记录
  3. 跳过前824000条 - 继续扫描直到跳过824000条匹配记录
  4. 返回4000个ID - 取接下来的4000条记录的 id

外层查询执行步骤:

  1. 主键查找 - 通过主键索引直接定位4000个ID对应的记录
  2. 获取user_id - 返回对应的 user_id 字段

📊性能对比分析

扫描成本对比:

操作

方案1

方案2

全表扫描次数

1次

1次(子查询)

扫描的数据量

需要扫描到第824000+4000条匹配记录

需要扫描到第824000+4000条匹配记录

额外操作

4000次主键查找

🏆预期结果(venture有索引的情况下)

分页深度

方案1性能

方案2性能

推荐方案

浅分页 (0-1万)

⭐⭐⭐⭐⭐

⭐⭐⭐

方案1

中等分页 (1-10万)

⭐⭐⭐

⭐⭐⭐⭐

方案2

深分页 (10万+)

⭐⭐⭐⭐

方案2

方案1执行流程:

详细执行步骤:

  1. 使用索引定位 - 通过 idx_venture 索引快速找到所有 venture='TH' 的记录位置
  2. 按索引顺序遍历 - 沿着索引链表/B+树遍历匹配的记录
  3. 跳过前824000条 - 这是最耗时的步骤!需要:
    • 遍历824000个索引条目
    • 对每个索引条目进行回表操作(获取完整记录)
  1. 获取目标数据 - 继续遍历4000条记录,回表获取 user_id
  2. 返回结果 - 返回4000个 user_id 值

关键问题: 虽然有索引,但仍需要跳过824000条记录,每条(824000+4000)都要回表!

方案2执行流程:

子查询执行:

select id from user_info
where venture = 'TH' limit 824000, 4000

执行步骤:

  1. 使用索引定位 - 通过 idx_venture 索引找到所有 venture='TH' 的记录
  2. 按索引顺序遍历 - 沿着索引遍历匹配的记录
  3. 跳过前824000条 - 遍历824000个索引条目
  4. 获取ID值 - 继续遍历4000条,但只需要获取主键ID(不需要回表!)
  5. 返回ID列表 - 返回4000个ID值:[1000001, 1000002, ..., 1005000]

外层查询执行:

select user_id from user_info a 
join (...) b on a.id = b.id

执行步骤:

  1. 主键查找 - 对4000个ID进行主键索引查找(非常快!)
  2. 获取字段值 - 直接从主键索引或数据页获取 user_id
  3. 返回结果 - 返回4000个 user_id 值

📊性能差异对比

操作类型

方案1

方案2

索引扫描

824000 + 4000 条

824000 + 4000 条

回表操作

824000 + 4000 次

0 次(子查询)+ 4000 次(外层)

主键查找

0 次

4000 次

总回表次数

828000 次

4000 次

疑问???

方案1为什么需要回表前面的824000次,它不是有个计数器,从824001开始算有效数据,只回表有效数据吗?

理想执行流程:

  1. 使用索引找到 venture='TH' 的记录
  2. 用计数器跳过前824000条(只计数,不回表)
  3. 从第824001条开始回表获取 user_id
  4. 只回表4000次

MySQL的实际执行流程:

实际执行流程:

  1. 使用 idx_venture 索引找到第一条 venture='TH' 的记录
  2. 回表获取完整记录(包括 user_id)
  3. 计数器 +1,判断是否达到824000
  4. 如果未达到,继续下一条记录,重复步骤2-3
  5. 达到824000后,继续处理4000条记录并返回

🤔 为什么MySQL不能用"计数器跳过"?

1. 索引结构限制

✨ Apply

idx_venture 索引结构:
venture='TH' -> [record_ptr_1, record_ptr_2, record_ptr_3, ...]
  • 索引只能顺序遍历,不能直接跳到第824001个位置
  • 每个索引条目都需要逐个检查才能确定是否匹配条件

2. WHERE条件的复杂性

即使是简单的 where venture = 'TH',MySQL也需要:

  • 读取索引条目
  • 验证条件匹配
  • 如果需要其他字段(如 user_id),必须回表

3. LIMIT的语义要求

LIMIT 824000, 4000 的语义是:

  • "给我第824001到828000条符合条件的记录"
  • MySQL必须确保前824000条确实符合WHERE条件
  • 这就要求逐条验证和计数

方案2拿到4000个主键id后的jion操作,是一条条的拿id去查询,还是批量的去查询?

📊 MySQL JOIN的执行策略

MySQL会根据数据量、索引情况等因素选择不同的JOIN算法:

1. Nested Loop Join(嵌套循环连接)

执行方式:一条条查询

for each row in subquery_result (4000 rows):
    lookup row in user_info where id = subquery_row.id

特点:

  • 对子查询的每一行,都去主表中查找匹配的记录
  • 4000次独立的主键查找
  • 适用于小结果集的情况

2. Hash Join(哈希连接)

执行方式:批量处理

1. 将子查询结果(4000个ID)构建成哈希表
2. 扫描主表相关记录,与哈希表匹配

特点:

  • MySQL 8.0.18+ 支持
  • 更适合大数据量的JOIN
  • 批量处理,效率更高

3. 实际上最可能的执行方式

对于此场景(4000个主键ID),MySQL最可能采用:

优化后的主键批量查找:

SQL-- MySQL内部可能优化为类似这样的查询
select user_id from user_info
where id IN (1000001, 1000002, 1000003, ..., 1005000)

🏆测试

demo有200w数据

--方案1:
select c2 from demo where c1 = 'VN' limit 824000,4000
--方案2:
select c2
from demo a join (select id 
from demo where c1 = 'VN' limit 824000,4000) b on a.id = b.id

方案1的执行记录:

第一行是无索引的情况,第二行是有索引的情况

无索引下查询耗时:900ms左右

有索引下查询耗时:2200ms左右

可以看出加了索引,耗时更久了,原因是:需要回表828000次

方案2的执行记录:

第一行是无索引的情况,第二行是有索引的情况

无索引下查询耗时:918ms左右

有索引下查询耗时:245ms左右

可以看出加了索引,耗时快了好几倍,原因是:需要只需要回表4000次

测试结果疑问??

问题1:为什么方案1使用"索引查询"方式更慢的情况下,而MySQL并没有选择使用时间更短的"全表扫描"方式去查询?它不是有优化器吗??

查看优化器估算成本信息

1、查看"索引"情况下的优化器估算成本信息

-- 查看优化器的成本估算
EXPLAIN FORMAT=JSON 
SELECT c2 FROM demo WHERE c1 = 'VN' LIMIT 824000,4000;

结果如下:

{
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "123426.40"
    },
    "table": {
      "table_name": "demo",
      "access_type": "ref",
      "possible_keys": [
        "idx_c1"
      ],
      "key": "idx_c1",
      "used_key_parts": [
        "c1"
      ],
      "key_length": "138",
      "ref": [
        "const"
      ],
      "rows_examined_per_scan": 992739,
      "rows_produced_per_join": 992739,
      "filtered": "100.00",
      "cost_info": {
        "read_cost": "24152.50",
        "eval_cost": "99273.90",
        "prefix_cost": "123426.40",
        "data_read_per_join": "840M"
      },
      "used_columns": [
        "c1",
        "c2"
      ]
    }
  }
}

2、查看"全表扫描"情况下的优化器估算成本信息

EXPLAIN FORMAT=JSON 
SELECT c2 FROM demo IGNORE INDEX (idx_c1) 
WHERE c1 = 'VN' LIMIT 824000,4000;
IGNORE INDEX (idx_c1) 表示:强制不走索引查询

结果如下:

{
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "206070.21"
    },
    "table": {
      "table_name": "demo",
      "access_type": "ALL",
      "rows_examined_per_scan": 1985479,
      "rows_produced_per_join": 198547,
      "filtered": "10.00",
      "cost_info": {
        "read_cost": "186215.42",
        "eval_cost": "19854.79",
        "prefix_cost": "206070.21",
        "data_read_per_join": "168M"
      },
      "used_columns": [
        "c1",
        "c2"
      ],
      "attached_condition": "(`demo`.`demo`.`c1` = 'VN')"
    }
  }
}

成本对比分析

使用索引 vs 强制全表扫描

执行方式

总成本

读取成本

评估成本

预估扫描行数

数据传输量

使用索引

123,426.40

24,152.50

99,273.90

992,739

840M

全表扫描

206,070.21

186,215.42

19,854.79

1,985,479

168M

关键发现

1. 优化器的成本估算矛盾

  • 优化器认为索引更优:成本 123,426 < 206,070
  • 实际性能却相反:索引 2000-2300ms > 全表扫描 823-966ms
  • 这说明优化器的成本模型存在系统性偏差

2. 成本构成的巨大差异

索引方式

  • 读取成本低(24,152),但评估成本极高(99,273)
  • 数据传输量大(840M vs 168M)

全表扫描

  • 读取成本高(186,215),但评估成本很低(19,854)
  • 数据传输量小得多

3. 为什么优化器判断错误?

优化器没有正确评估的因素

  1. LIMIT大偏移量的真实成本
    • 索引需要遍历99万行才能跳过82.4万行
    • 全表扫描虽然扫描198万行,但是顺序读取
  1. 回表操作的隐藏成本
    • 索引查询需要99万次回表操作
    • 每次回表都是随机I/O,成本被严重低估
  1. 数据访问模式差异
    • 全表扫描:顺序I/O,对磁盘友好
    • 索引+回表:随机I/O,磁盘性能差

深层原因分析

为什么数据传输量差这么多?

  • 索引方式 840M:包含了大量的索引遍历和回表开销
  • 全表扫描 168M:只传输最终需要的数据,过滤效率高

评估成本的巨大差异

  • 索引方式:99,273(高CPU成本,大量条件判断和回表)
  • 全表扫描:19,854(简单的WHERE条件过滤)

结论

这个对比完美解释了MySQL优化器的局限性:

  1. 成本模型过于简化:没有准确反映大偏移量LIMIT的真实开销
  2. I/O模式评估不准确:低估了随机I/O vs 顺序I/O的性能差异
  3. 回表成本计算有误:大量回表操作的真实成本被严重低估

实际建议

  • 在这种场景下,应该删除或忽略这个索引
  • 或者使用覆盖索引 (c1, c2) 避免回表
  • 继续使用子查询优化方案,这是最佳选择

问题2:通过问题1发现“索引需要遍历99万行才能跳过82.4万行”这句话,跟我们前面理解的“扫描824000+4000行”,条数相差有点大,多扫描了10w+的条数

1、先统计VN的全量数据

SELECT COUNT(1) FROM demo WHERE c1 = 'VN';
只有873557条

数据分析

实际数据

  • c1 = 'VN' 的总记录数:873,557
  • 执行计划显示的扫描行数:992,739

为什么扫描行数比实际记录数多?

这个差异(992,739 - 873,557 = 119,182)说明了几个重要问题:

1. 优化器估算不准确(数据量大或者复杂sql场景下,优化器的局限性有限)

  • 优化器高估了匹配记录数
  • 实际只有 87万条,但估算了 99万条
  • 这进一步证明了统计信息可能不够准确

2. 索引扫描的额外开销

可能的原因包括:

  • 索引页的预读:MySQL 可能读取了额外的索引页
  • 索引碎片:索引不够紧凑,需要扫描更多页面
  • 缓冲区管理:为了找到所有匹配记录,可能扫描了额外的索引条目

3. LIMIT 大偏移量的影响

现在我们知道:

  • 总共有 873,557c1='VN' 的记录
  • 需要跳过前 824,000
  • 只返回 4,000

这意味着:

  • 需要处理 95% 的匹配数据才能到达目标位置
  • 几乎要遍历所有的匹配记录
  • 这就是为什么性能这么差的根本原因

到此这篇关于MySql深页查询实现方案的文章就介绍到这了,更多相关mysql深页查询内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 解决mysql的int型主键自增问题

    解决mysql的int型主键自增问题

    这篇文章主要介绍了解决mysql的int型主键自增问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-07-07
  • CentOS6.7 mysql5.6.33修改数据文件位置的方法

    CentOS6.7 mysql5.6.33修改数据文件位置的方法

    mysql存放的数据文件,分区容量较小,目前已经满,导致mysql连接不上,怎么解决呢?下面小编给大家分享CentOS6.7 mysql5.6.33修改数据文件位置的方法,一起看看吧
    2017-06-06
  • MySQL联合查询详细示例代码

    MySQL联合查询详细示例代码

    MySQL联合查询是数据库操作中十分重要的技能之一,它允许用户从多个表中提取并组合数据,下面这篇文章主要介绍了MySQL联合查询的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2025-10-10
  • mysql sharding(碎片)介绍

    mysql sharding(碎片)介绍

    这篇文章主要介绍了mysql sharding(碎片)介绍,本文讲解了Sharding的应用场景一般都哪些、Sharding与数据库分区(Partition)的区别等内容,需要的朋友可以参考下
    2015-03-03
  • 解析MySQL binlog

    解析MySQL binlog

    我们都知道,binlog可以说是MySQL中比较重要的日志了,在日常学习及运维过程中,也经常会遇到。不清楚你对binlog了解多少呢?本篇文章将从binlog作用、binlog相关参数、解析binlog内容三个方面带你了解binlog
    2021-06-06
  • MySQL查看和修改字符编码的实现方法

    MySQL查看和修改字符编码的实现方法

    下面小编就为大家带来一篇MySQL查看和修改字符编码的实现方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-11-11
  • 一文详解如何在MySQL中创建函数

    一文详解如何在MySQL中创建函数

    这篇文章主要为大家介绍了一文详解如何在MySQL中创建函数,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-05-05
  • MySQL设置global变量和session变量的两种方法详解

    MySQL设置global变量和session变量的两种方法详解

    这篇文章主要介绍了MySQL设置global变量和session变量的两种方法,每种方法给大家介绍的非常详细 ,需要的朋友可以参考下
    2018-10-10
  • MySQL8下忘记密码后重置密码的办法(MySQL老方法不灵了)

    MySQL8下忘记密码后重置密码的办法(MySQL老方法不灵了)

    这篇文章主要介绍了MySQL8下忘记密码后重置密码的办法,MySQL的密码是存放在user表里面的,修改密码其实就是修改表中记录,重置的思路是是想办法不用密码进入系统,然后用数据库命令修改表user中的密码记录
    2018-08-08
  • MySQL不用like+%实现模糊查询

    MySQL不用like+%实现模糊查询

    本文主要介绍了MySQL不用like+%实现模糊查询,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-01-01

最新评论