Mysql小表驱动大表优化原理

它触及了MySQL查询性能优化的一个核心原则。简单来说,"小表驱动大表" 的核心目的是为了减少磁盘I/O操作和比较次数,从而提升查询效率。

下面我们通过几个层面来详细解释为什么。

1. 从嵌套循环的概念理解

想象一下MySQL如何执行一个关联查询(比如使用JOININ/EXISTS子查询)。在大多数情况下,尤其是在没有高效索引时,它的底层操作类似于一个"嵌套循环"。

  • 驱动表(外层循环): 首先被访问的表,遍历它的每一行。
  • 被驱动表(内层循环): 针对驱动表的每一行,去这个表里查找匹配的数据。

场景对比:

假设我们有两张表:

  • 小表: 有100条记录。
  • 大表: 有10万条记录。

情况一:大表驱动小表(错误的做法)

sql 复制代码
-- 假设MySQL选择大表作为驱动表(通常不会,这里是为了对比)
for (每条记录 in 大表) { // 循环10万次
    for (每条记录 in 小表) { // 每次循环100次
        if (条件匹配) {
            输出结果;
        }
    }
}

总比较次数: 10万 * 100 = 1000万次。

情况二:小表驱动大表(正确的做法)

sql 复制代码
for (每条记录 in 小表) { // 循环100次
    for (每条记录 in 大表) { // 每次循环10万次
        if (条件匹配) {
            输出结果;
        }
    }
}

总比较次数: 100 * 10万 = 1000万次。

咦? 从上面的简单计算来看,总比较次数是一样的啊?为什么说小表驱动大表更快?

2. 关键因素:磁盘I/O和索引

上面的例子假设的是全表扫描,并且没有考虑最重要的因素:磁盘I/O。数据库的数据是存储在磁盘上的,而操作是在内存中进行的。将数据从磁盘读入内存是数据库操作中最耗时的部分。

现在,我们引入索引,特别是被驱动表上的索引。

情况二(优化版):小表驱动大表,且大表(被驱动表)的关联字段有索引

sql 复制代码
for (每条记录 in 小表) { // 循环100次,将小表数据读入内存
    // 根据小表当前记录的关联字段值,去大表的索引(B+Tree)中进行查找
    // 索引查找非常快,近似于O(log n)的复杂度,假设3次磁盘I/O就能找到
    if (在大表中通过索引找到匹配记录) { // 每次循环约3次I/O
        输出结果;
    }
}

总磁盘I/O次数估算: 100(次小表循环) * 3(次索引查找I/O) ≈ 300次磁盘I/O。

情况一(优化版):大表驱动小表,且小表(被驱动表)的关联字段有索引

sql 复制代码
for (每条记录 in 大表) { // 循环10万次,需要分批将大表数据读入内存,I/O量巨大
    // 根据大表当前记录的关联字段值,去小表的索引中查找
    if (在小表中通过索引找到匹配记录) { // 每次循环约3次I/O
        输出结果;
    }
}

总磁盘I/O次数估算: 10万(次大表循环) * 3(次索引查找I/O) ≈ 30万次磁盘I/O。

结论对比:

驱动表 被驱动表索引 总比较次数(理论) 总磁盘I/O次数(核心影响)
大表 1000万 极高(全表扫描10万*100次)
小表 1000万 极高(全表扫描100*10万次)
小表 100 * log(10万) 极低(约300次)
大表 10万 * log(100) 较高(约30万次)

可以看到,当被驱动表的关联字段上有索引时,"小表驱动大表"的策略能将内层循环的全表扫描转换为高效的索引查找,从而极大地减少了磁盘I/O次数。 即使被驱动表没有索引,用小表驱动也能减少外层循环的次数,虽然内层循环仍然是全表扫描,但总体开销通常也更小。

3. 这个原则在SQL中的体现

  1. IN vs EXISTS

    • IN 适合子查询(内表)是小表的情况。

      sql 复制代码
      SELECT * FROM A WHERE id IN (SELECT id FROM B)

      IN会先执行(SELECT id FROM B),得到一个小结果集(内表),然后用A表的id去遍历这个小结果集。这里B表是小表,作为驱动表是高效的。

    • EXISTS 适合主查询(外表)是小表的情况。

      sql 复制代码
      SELECT * FROM A WHERE EXISTS (SELECT 1 FROM B WHERE B.id = A.id)

      EXISTS会先遍历A表(外层循环),对于A表的每一行,去执行子查询判断是否存在。如果A表是小表,B表是大表且B.id有索引,这种写法就非常高效。

  2. JOIN MySQL的优化器会尝试自动选择最好的驱动表。你可以通过EXPLAIN命令查看执行计划。在EXPLAIN的输出中,排在第一行的表就是驱动表。

    • 优化器会根据表的大小 (行数)、索引过滤条件等因素来选择驱动表。
    • 你通常不需要手动指定,但要理解优化器为什么会这么选。例如,如果你给被驱动表的关联字段加上了索引,就相当于为优化器选择"小表驱动大表"的策略铺平了道路。

总结

为什么是小表驱动大表?

  1. 核心目标:减少磁盘I/O,这是数据库性能的瓶颈。
  2. 实现方式: 通过减少外层循环的次数,并将内层循环的全表扫描转换为高效的索引查找
  3. 实践指导:
    • 确保你的JOIN查询或子查询中,被驱动表的关联字段上建立了索引。这是让"小表驱动大表"原则生效的前提。
    • 在编写INEXISTS子查询时,有意识地思考哪个表更小,从而选择更合适的写法。
    • 多使用EXPLAIN分析你的查询,观察驱动表的选择是否符合你的预期。

记住这个口诀:小表驱动大表,索引建在被驱动表上。

相关推荐
小时前端3 小时前
🚀 面试必问的8道JavaScript异步难题:搞懂这些秒杀90%的候选人
javascript·面试
Takklin4 小时前
JavaScript 面试笔记:作用域、变量提升、暂时性死区与 const 的可变性
javascript·面试
程序员三明治4 小时前
【MyBatis从入门到入土】告别JDBC原始时代:零基础MyBatis极速上手指南
数据库·mysql·mybatis·jdbc·数据持久化·数据
ShooterJ4 小时前
MySQL基因分片设计方案详解
后端
PetterHillWater4 小时前
ANOVA在软件工程中的应用
后端
cxyxiaokui0014 小时前
还在用 @Autowired 字段注入?你可能正在写出“脆弱”的 Java 代码
java·后端·spring
回家路上绕了弯4 小时前
深入 Zookeeper 数据模型:树形 ZNode 结构的设计与实践
后端·zookeeper
cookqq4 小时前
MongoDB源码delete分析oplog:从删除链路到核心函数实现
数据结构·数据库·sql·mongodb·nosql
GeekAGI4 小时前
Redis 不同架构下的故障发现和自动切换机制
后端