Mysql小表驱动大表优化原理

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

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

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

想象一下MySQL如何执行一个关联查询(比如使用JOIN或IN/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查询或子查询中,被驱动表的关联字段上建立了索引。这是让"小表驱动大表"原则生效的前提。
    • 在编写IN或EXISTS子查询时,有意识地思考哪个表更小,从而选择更合适的写法。
    • 多使用EXPLAIN分析你的查询,观察驱动表的选择是否符合你的预期。

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

相关推荐
AIgorithmGEEK7 小时前
MySQL 索引全解(下):建、查、删实战与知识速查表
数据库·mysql·b+树·索引
怕浪猫8 小时前
2028编程圈很火的10个Skills
算法·面试·github
行百里er9 小时前
Redis 完结篇——Redis 热点问题在 Spring Boot 项目中实战
redis·后端
唠点键盘之外的10 小时前
26 向量数据库实战:从 0 搭一条能用的检索链路(pgvector + Spring AI)
数据库·人工智能·spring
云和恩墨10 小时前
数据库监控简史:监控不止于告警——从图表大爆发到健康管理闭环
数据库
ly768910 小时前
MongoDB 事务开销实测:快照隔离下的写冲突、重试与分片事务边界
数据库·mongodb·事务·分片集群·快照隔离·写冲突
豆角焖肉10 小时前
Spring AOP 自定义注解实现系统操作日志
数据库·自定义注解·springaop·操作日志·aop 切面
高频因子挖掘机10 小时前
只取最近 60 个交易日,却每次拉全历史:量化脚本变慢的原因与改法
后端·github·api
孟健11 小时前
从5000亿Token到83%字节匹配:AI逆向工程与重构的工程闭环实践
后端·架构·ai编程
金伟API102411 小时前
不会写复杂SQL怎么办?
数据库·sql