Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > mysql深页查询

MySql深页查询实现方案

作者:伤惢无泪

本文给大家介绍了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条 - 这是最耗时的步骤!需要:
  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, ...]

2. WHERE条件的复杂性

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

3. LIMIT的语义要求

LIMIT 824000, 4000 的语义是:

方案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

特点:

2. Hash Join(哈希连接)

执行方式:批量处理

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

特点:

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. 优化器的成本估算矛盾

2. 成本构成的巨大差异

索引方式

全表扫描

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

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

  1. LIMIT大偏移量的真实成本
  1. 回表操作的隐藏成本
  1. 数据访问模式差异

深层原因分析

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

评估成本的巨大差异

结论

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

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

实际建议

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

1、先统计VN的全量数据

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

数据分析

实际数据

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

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

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

2. 索引扫描的额外开销

可能的原因包括:

3. LIMIT 大偏移量的影响

现在我们知道:

这意味着:

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

您可能感兴趣的文章:
阅读全文