它触及了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中的体现
-
IN
vsEXISTS
-
IN
: 适合子查询(内表)是小表的情况。sqlSELECT * FROM A WHERE id IN (SELECT id FROM B)
IN
会先执行(SELECT id FROM B)
,得到一个小结果集(内表),然后用A表的id去遍历这个小结果集。这里B
表是小表,作为驱动表是高效的。 -
EXISTS
: 适合主查询(外表)是小表的情况。sqlSELECT * FROM A WHERE EXISTS (SELECT 1 FROM B WHERE B.id = A.id)
EXISTS
会先遍历A
表(外层循环),对于A表的每一行,去执行子查询判断是否存在。如果A
表是小表,B
表是大表且B.id
有索引,这种写法就非常高效。
-
-
JOIN
MySQL的优化器会尝试自动选择最好的驱动表。你可以通过EXPLAIN
命令查看执行计划。在EXPLAIN
的输出中,排在第一行的表就是驱动表。- 优化器会根据表的大小 (行数)、索引 、过滤条件等因素来选择驱动表。
- 你通常不需要手动指定,但要理解优化器为什么会这么选。例如,如果你给被驱动表的关联字段加上了索引,就相当于为优化器选择"小表驱动大表"的策略铺平了道路。
总结
为什么是小表驱动大表?
- 核心目标:减少磁盘I/O,这是数据库性能的瓶颈。
- 实现方式: 通过减少外层循环的次数,并将内层循环的全表扫描转换为高效的索引查找。
- 实践指导:
- 确保你的
JOIN
查询或子查询中,被驱动表的关联字段上建立了索引。这是让"小表驱动大表"原则生效的前提。 - 在编写
IN
或EXISTS
子查询时,有意识地思考哪个表更小,从而选择更合适的写法。 - 多使用
EXPLAIN
分析你的查询,观察驱动表的选择是否符合你的预期。
- 确保你的
记住这个口诀:小表驱动大表,索引建在被驱动表上。