开发者

MySql深页查询实现方案

目录
  • 预期结果(venture无索引的情况下)
    • 方案1执行流程:
    • 方案2执行流程:
    • 性能对比分析
      • 扫描成本对比:
  • 预期结果(venture有索引的情况下)
    • 方案1执行流程:
      • 方案2执行流程:
        • 子查询执行:
        • 外层查询执行:
      • 性能差异对比
        • 疑问???
          • 方案1为什么需要回表前面的824000次,它不是有个计数器,从824001开始算有效数据,只回表有效数据吗?
          • 方案2拿到4000个主键id后的jion操作,是一条条的拿id去查询,还是批量的去查询?
      • 测试
        • 方案1的执行记录:
          • 方案2的执行记录:
            • 测试结果疑问??
              • 问题1:为什么方案1使用"索引查询"方式更慢的情况下,而mysql并没有选择使用时间更短的"全表扫描"方式去查询?它不是有优化器吗??
              • 问题2:通过问题1发现“索引需要遍历99万行才能跳过82.4万行”这句话,跟我们前面理解的“扫描824000+4000行”,条数相差有点大,多扫描了10w+的条数

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

          其他方案:可采用 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的执行记录:

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

          MySql深页查询实现方案

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

          MySql深页查询实现方案

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

          MySql深页查询实现方案

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

          方案2的执行记录:

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

          MySql深页查询实现方案

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

          MySql深页查询实现方案

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

          MySql深页查询实现方案

          可以看出加了索引,耗时快了好几倍,原因是:需要只需要回表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",

            &nLXirVbsp;     "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' 的记录
          • 需要跳过前 824javascript,000
          • 只返回 4,000

          这意味着:

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

          到此这篇关于MySql深页查询实现方案的文章就介绍到这了,更多相关mysql深页查询内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

          0

          上一篇:

          下一篇:

          精彩评论

          暂无评论...
          验证码 换一张
          取 消

          最新数据库

          数据库排行榜