一、MySQL查询优化器是个什么玩意儿

咱们先打个比方,如果把MySQL比作一个外卖小哥,那么查询优化器就是他的导航系统。当你要执行一条SQL语句时,优化器会帮你选择最快的"送餐路线"。不过这个导航有点特别,它不仅要考虑路况(数据分布),还要考虑交通工具的性能(索引情况)。

举个实际例子,假设我们有个订单表:

-- 创建订单表(技术栈:MySQL 8.0)
CREATE TABLE orders (
    id INT PRIMARY KEY,
    user_id INT,
    order_date DATETIME,
    amount DECIMAL(10,2),
    status VARCHAR(20),
    INDEX idx_user (user_id),
    INDEX idx_date_status (order_date, status)
);

当执行这个查询时:

EXPLAIN SELECT * FROM orders 
WHERE user_id = 100 AND status = 'completed';

优化器会像个精明的会计,先算算哪种方案更划算:

  1. 用idx_user索引找到user_id=100的记录,再过滤status
  2. 用idx_date_status索引找到status='completed'的记录,再过滤user_id
  3. 干脆全表扫描

二、优化器的决策三板斧

1. 成本估算的艺术

优化器主要看三个成本:

  • IO成本:从磁盘读数据的开销
  • CPU成本:处理数据的开销
  • 内存成本:临时存储的开销

比如这个查询:

-- 查询最近一个月已完成的大额订单
EXPLAIN SELECT * FROM orders
WHERE order_date > '2023-06-01' 
  AND status = 'completed'
  AND amount > 1000;

优化器会这样盘算:

  1. idx_date_status能快速定位时间范围
  2. 然后在这个范围内筛选status和amount
  3. 如果符合条件的记录很少,就赚到了

2. 统计信息的玄学

MySQL会收集表的统计信息,就像人口普查:

-- 查看表的统计信息
SHOW TABLE STATUS LIKE 'orders';

这些数据决定了优化器的判断。有时候你会看到"ANALYZE TABLE"这个命令,就是让MySQL重新做人口普查。

3. 连接查询的排列组合

遇到多表查询时,优化器要考虑各种连接顺序:

-- 多表连接示例
EXPLAIN SELECT o.*, u.name 
FROM orders o JOIN users u ON o.user_id = u.id
WHERE o.status = 'completed' AND u.vip = 1;

可能的执行计划包括:

  1. 先过滤vip用户,再找他们的订单
  2. 先找出已完成订单,再关联用户表
  3. 其他各种排列组合

三、执行计划里的摩斯密码

EXPLAIN的输出就像天书,我们来破译下:

EXPLAIN FORMAT=JSON SELECT * FROM orders WHERE user_id = 100;

关键字段解读:

  • type:ALL(全表扫描)、index(索引扫描)、range(范围扫描)等
  • key:实际使用的索引
  • rows:预估检查的行数
  • Extra:额外信息,比如"Using where"表示要过滤条件

举个实际案例:

-- 复合索引的使用
EXPLAIN SELECT * FROM orders
WHERE order_date BETWEEN '2023-01-01' AND '2023-06-30'
  AND status = 'completed'
ORDER BY amount DESC LIMIT 10;

理想情况下应该:

  1. 使用idx_date_status索引
  2. 避免filesort排序
  3. 只读取必要的行

四、实战中的优化技巧

1. 索引选择的陷阱

有时候你以为用了索引,其实并没有:

-- 索引失效的典型案例
EXPLAIN SELECT * FROM orders WHERE YEAR(order_date) = 2023;

这种对字段使用函数会导致索引失效,应该改成:

EXPLAIN SELECT * FROM orders 
WHERE order_date BETWEEN '2023-01-01' AND '2023-12-31';

2. 分页查询的优化

常见的大坑:

-- 低效的分页查询
SELECT * FROM orders ORDER BY id LIMIT 100000, 20;

可以优化为:

-- 高效的分页(假设id是连续的)
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 20;

3. 子查询的优化

有些子查询可以改写成JOIN:

-- 原始子查询
EXPLAIN SELECT * FROM users 
WHERE id IN (SELECT DISTINCT user_id FROM orders WHERE amount > 1000);

-- 优化为JOIN
EXPLAIN SELECT DISTINCT u.* FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.amount > 1000;

五、高级优化策略

1. 索引合并的妙用

MySQL可以把多个索引合并使用:

-- 索引合并示例
EXPLAIN SELECT * FROM orders
WHERE user_id = 100 OR status = 'completed';

注意看Extra字段会出现"Using union(idx_user,idx_status)"

2. 直方图统计

MySQL 8.0的新功能:

-- 创建直方图统计
ANALYZE TABLE orders UPDATE HISTOGRAM ON amount WITH 100 BUCKETS;

-- 查看直方图
SELECT * FROM INFORMATION_SCHEMA.COLUMN_STATISTICS;

这能帮助优化器了解数据分布,对不均匀的数据特别有用。

3. 优化器提示

有时候你可以强行指导优化器:

-- 使用FORCE INDEX
EXPLAIN SELECT * FROM orders FORCE INDEX(idx_date_status)
WHERE user_id = 100 AND order_date > '2023-01-01';

但这是个双刃剑,数据库升级后可能会失效。

六、常见误区与注意事项

  1. 不是所有查询都需要索引,小表全表扫描可能更快
  2. 不要过度优化,先确保SQL写法正确
  3. 注意隐式类型转换会导致索引失效
  4. 定期更新统计信息(ANALYZE TABLE)
  5. 多使用EXPLAIN验证你的猜想

七、总结与最佳实践

经过这么多案例,我们可以总结出几个要点:

  1. 理解业务场景比盲目加索引更重要
  2. 复合索引要注意最左前缀原则
  3. 多表连接时,小表驱动大表
  4. 必要时使用覆盖索引避免回表
  5. 监控慢查询日志,持续优化

最后记住,优化器不是万能的,它只是基于统计信息做出最佳猜测。作为开发者,我们需要:

  • 了解表结构和数据特征
  • 掌握EXPLAIN工具
  • 定期检查执行计划
  • 在业务变化时重新评估