外观
MariaDB 执行计划分析
执行计划概述
什么是执行计划
执行计划是 MariaDB 查询优化器生成的查询执行方案,包含了查询如何执行的详细信息,如使用的索引、表连接顺序、数据访问方式等。通过分析执行计划,可以了解查询的执行过程,找出性能瓶颈,从而进行针对性优化。
执行计划的重要性
- 帮助理解查询的执行过程
- 识别性能瓶颈
- 验证索引是否被正确使用
- 比较不同查询写法的性能差异
- 评估优化效果
执行计划生成过程
- 词法分析:将 SQL 语句分解为标记
- 语法分析:检查 SQL 语句的语法正确性
- 语义分析:验证表、列和函数的存在性
- 查询优化:查询优化器生成多个执行计划,选择成本最低的一个
- 执行计划生成:生成最终的执行计划
查看执行计划
EXPLAIN 命令
EXPLAIN 命令是查看执行计划的主要工具,它可以显示 MariaDB 如何执行 SELECT 语句。
sql
-- 基本用法
EXPLAIN SELECT * FROM mytable WHERE created_at < '2025-01-01' ORDER BY id DESC LIMIT 100;
-- 显示扩展执行计划
EXPLAIN EXTENDED SELECT * FROM mytable WHERE created_at < '2025-01-01' ORDER BY id DESC LIMIT 100;
-- 显示分区信息
EXPLAIN PARTITIONS SELECT * FROM mytable WHERE created_at < '2025-01-01' ORDER BY id DESC LIMIT 100;
-- 显示格式化的 JSON 执行计划
EXPLAIN FORMAT=JSON SELECT * FROM mytable WHERE created_at < '2025-01-01' ORDER BY id DESC LIMIT 100;
-- 显示优化后的 SQL 语句
EXPLAIN EXTENDED SELECT * FROM mytable WHERE created_at < '2025-01-01' ORDER BY id DESC LIMIT 100;
SHOW WARNINGS;EXPLAIN ANALYZE
MariaDB 10.1.2 及以上版本支持 EXPLAIN ANALYZE 命令,它可以实际执行查询并显示详细的执行统计信息。
sql
-- 显示实际执行计划
EXPLAIN ANALYZE SELECT * FROM mytable WHERE created_at < '2025-01-01' ORDER BY id DESC LIMIT 100;SHOW WARNINGS
SHOW WARNINGS 命令可以显示优化器对查询的改写和优化信息,与 EXPLAIN EXTENDED 配合使用。
sql
EXPLAIN EXTENDED SELECT * FROM mytable WHERE created_at < '2025-01-01' ORDER BY id DESC LIMIT 100;
SHOW WARNINGS;执行计划字段解读
基本字段
| 字段名称 | 描述 |
|---|---|
| id | 查询块 ID,用于标识查询中的不同部分 |
| select_type | 查询类型,如 SIMPLE、PRIMARY、UNION、SUBQUERY 等 |
| table | 访问的表名 |
| partitions | 访问的分区,适用于分区表 |
| type | 访问类型,如 ALL、index、range、ref、eq_ref、const、system 等 |
| possible_keys | 可能使用的索引列表 |
| key | 实际使用的索引 |
| key_len | 使用的索引长度,用于判断索引使用的列数 |
| ref | 与索引比较的列或常量 |
| rows | 估计需要扫描的行数 |
| filtered | 过滤后的行数百分比 |
| Extra | 额外信息,如 Using index、Using where、Using filesort 等 |
select_type 详解
- SIMPLE:简单查询,不包含子查询或 UNION
- PRIMARY:主查询,包含子查询或 UNION 时的外层查询
- SUBQUERY:子查询中的第一个 SELECT
- DERIVED:派生表,FROM 子句中的子查询
- UNION:UNION 中的第二个或后续 SELECT
- UNION RESULT:UNION 的结果集
- DEPENDENT SUBQUERY:依赖外部查询结果的子查询
- DEPENDENT UNION:依赖外部查询结果的 UNION
- MATERIALIZED:物化子查询
type 详解
从最优到最差的访问类型:
- system:表只有一行记录(系统表)
- const:通过主键或唯一索引访问,只返回一行记录
- eq_ref:表连接时,被连接表使用主键或唯一索引访问
- ref:使用非唯一索引访问,可能返回多行
- range:使用索引范围扫描,如 BETWEEN、IN、>、< 等
- index:全索引扫描,比 ALL 好但比 range 差
- ALL:全表扫描,性能最差
- NULL:不访问表,直接返回结果
Extra 详解
- Using index:使用索引覆盖查询,不需要回表访问数据
- Using where:使用 WHERE 条件过滤,但不使用索引
- Using index condition:使用索引条件下推,先在存储引擎层过滤数据
- Using filesort:需要额外的排序操作,性能较差
- Using temporary:需要使用临时表,性能较差
- Using join buffer:使用连接缓冲区优化连接操作
- Using sort_union:使用排序合并算法处理 OR 条件
- Using union:使用合并算法处理 OR 条件
- Using intersect:使用交集算法处理 AND 条件
- Impossible where:WHERE 条件永远为假,不返回任何行
- Select tables optimized away:优化器直接计算出结果,不访问表
- Zero limit:LIMIT 0,不执行查询
执行计划分析方法
单表查询分析
- 检查访问类型:优先选择 const、eq_ref、ref 等高效访问类型,避免 ALL 和 index
- 检查索引使用:确保使用了合适的索引,避免全表扫描
- 检查扫描行数:估计扫描行数应尽量少
- 检查过滤比例:filtered 字段应尽量高
- 检查 Extra 信息:避免 Using filesort 和 Using temporary
多表连接分析
- 检查连接顺序:小表驱动大表,减少连接次数
- 检查连接类型:确保连接列有索引,使用 eq_ref 或 ref 连接类型
- 检查索引使用:确保连接列使用了合适的索引
- 检查 Extra 信息:避免 Using join buffer,说明连接列没有索引
子查询分析
- 检查 select_type:避免 DEPENDENT SUBQUERY,它会为外部查询的每一行执行一次
- 考虑改写为 JOIN:某些子查询可以改写为 JOIN,提高性能
- 使用物化子查询:MariaDB 10.6+ 支持物化子查询,提高子查询性能
排序和分组分析
- 检查 Using filesort:尽量避免,考虑使用索引排序
- 检查 Using temporary:尽量避免,考虑使用索引分组
- 确保排序和分组使用相同的索引:遵循最左前缀原则
执行计划优化技巧
优化访问类型
- 添加合适的索引:为 WHERE 条件、JOIN 条件、ORDER BY 和 GROUP BY 中的列添加索引
- 优化索引结构:使用联合索引,遵循最左前缀原则
- 避免索引失效:
- 避免在查询条件中使用函数或表达式
- 避免使用 !=、<>、NOT IN 等操作符
- 避免使用 OR 连接不同的索引列
- 确保查询条件与索引列类型一致
优化连接操作
- 小表驱动大表:将行数少的表作为驱动表
- 确保连接列有索引:提高连接效率
- 使用合适的连接类型:根据业务需求选择 INNER JOIN、LEFT JOIN、RIGHT JOIN 等
- 避免笛卡尔积:确保连接条件正确
优化子查询
- 改写为 JOIN:某些子查询可以改写为 JOIN,提高性能
- 使用 EXISTS 代替 IN:对于大数据集,EXISTS 通常比 IN 更高效
- 使用物化子查询:MariaDB 10.6+ 支持物化子查询,提高子查询性能
- 避免相关子查询:相关子查询会为外部查询的每一行执行一次
优化排序和分组
- 使用索引排序:为 ORDER BY 列添加索引
- 使用索引分组:为 GROUP BY 列添加索引
- 确保排序和分组使用相同的索引:遵循最左前缀原则
- 避免 SELECT DISTINCT:DISTINCT 通常需要排序,影响性能
执行计划分析案例
案例 1:全表扫描优化
原始查询:
sql
SELECT * FROM users WHERE age > 30;执行计划:
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| 1 | SIMPLE | users | NULL | ALL | NULL | NULL | NULL | NULL | 100000 | 33.33 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+-------------+分析:
- type 为 ALL,全表扫描
- possible_keys 为 NULL,没有合适的索引
- rows 为 100000,需要扫描大量行
优化方案: 为 age 列添加索引
sql
CREATE INDEX idx_age ON users(age);优化后的执行计划:
+----+-------------+-------+------------+-------+---------------+--------+---------+------+-------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+--------+---------+------+-------+----------+-----------------------+
| 1 | SIMPLE | users | NULL | range | idx_age | idx_age| 4 | NULL | 33333 | 100.00 | Using index condition |
+----+-------------+-------+------------+-------+---------------+--------+---------+------+-------+----------+-----------------------+优化效果:
- type 变为 range,使用索引范围扫描
- key 为 idx_age,使用了创建的索引
- rows 变为 33333,扫描行数减少
- Extra 为 Using index condition,使用了索引条件下推
案例 2:排序优化
原始查询:
sql
SELECT * FROM orders WHERE user_id = 1 ORDER BY created_at DESC;执行计划:
+----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-----------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-----------------------------+
| 1 | SIMPLE | orders | NULL | ref | idx_user_id | idx_user_id | 4 | const | 10 | 100.00 | Using index condition; Using filesort |
+----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-----------------------------+分析:
- type 为 ref,使用了 user_id 索引
- Extra 包含 Using filesort,需要额外排序
优化方案: 创建联合索引,包含 user_id 和 created_at
sql
CREATE INDEX idx_user_id_created_at ON orders(user_id, created_at DESC);优化后的执行计划:
+----+-------------+--------+------------+------+-------------------------+-------------------------+---------+-------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+-------------------------+-------------------------+---------+-------+------+----------+-----------------------+
| 1 | SIMPLE | orders | NULL | ref | idx_user_id,idx_user_id_created_at | idx_user_id_created_at | 8 | const | 10 | 100.00 | Using index condition |
+----+-------------+--------+------------+------+-------------------------+-------------------------+---------+-------+------+----------+-----------------------+优化效果:
- Extra 中的 Using filesort 消失,使用索引排序
- 提高了查询性能,避免了额外的排序操作
执行计划分析工具
MariaDB 内置工具
- EXPLAIN:基本执行计划查看
- EXPLAIN EXTENDED:扩展执行计划,显示优化后的 SQL
- EXPLAIN PARTITIONS:显示分区信息
- EXPLAIN FORMAT=JSON:JSON 格式的执行计划,包含更详细的信息
- EXPLAIN ANALYZE:显示实际执行统计信息
第三方工具
Percona Monitoring and Management (PMM):
- 提供可视化的执行计划分析
- 支持比较不同查询的执行计划
- 提供优化建议
MySQL Workbench:
- 可视化执行计划查看
- 支持执行计划比较
- 提供索引建议
phpMyAdmin:
- 基本执行计划查看
- 适合简单查询分析
执行计划分析最佳实践
定期分析慢查询的执行计划:
- 结合慢查询日志,分析执行计划
- 找出性能瓶颈,进行针对性优化
比较不同查询写法的执行计划:
- 尝试不同的查询写法
- 比较执行计划,选择最优方案
验证索引使用情况:
- 确保创建的索引被正确使用
- 删除未使用的索引,减少维护成本
分析复杂查询的执行计划:
- 分解复杂查询为多个简单查询
- 分析每个部分的执行计划
- 优化每个部分,然后组合
使用 EXPLAIN ANALYZE 验证实际执行情况:
- EXPLAIN 显示的是估计值
- EXPLAIN ANALYZE 显示实际执行统计信息
- 结合使用,更准确地分析性能
常见问题(FAQ)
Q: 为什么执行计划显示使用了索引,但查询仍然很慢?
A: 可能的原因:
- 索引选择性差,返回了大量行
- 索引覆盖不全,需要回表访问数据
- 服务器负载高,资源不足
- 锁等待或死锁
- 查询涉及多个表连接,连接效率低
Q: 如何理解执行计划中的 rows 字段?
A: rows 字段是查询优化器估计的需要扫描的行数,不是实际行数。它基于索引统计信息计算得出,可能与实际行数有差异。可以使用 EXPLAIN ANALYZE 查看实际扫描行数。
Q: 为什么相同的查询在不同时间的执行计划不同?
A: 可能的原因:
- 表统计信息更新,导致优化器选择不同的执行计划
- 数据分布发生变化
- 服务器负载变化
- 优化器版本变化
- 配置参数变化
Q: 如何强制使用特定索引?
A: 使用 FORCE INDEX 提示:
sql
SELECT * FROM mytable FORCE INDEX (idx_name) WHERE name = 'test';注意:应谨慎使用,优化器通常会选择最优索引。
Q: 如何查看优化器对查询的改写?
A: 使用 EXPLAIN EXTENDED 配合 SHOW WARNINGS:
sql
EXPLAIN EXTENDED SELECT * FROM mytable WHERE created_at < '2025-01-01';
SHOW WARNINGS;可以查看优化器对查询的改写和优化信息。
总结
执行计划分析是 MariaDB 性能优化的重要手段,通过分析执行计划,可以了解查询的执行过程,找出性能瓶颈,从而进行针对性优化。执行计划的主要字段包括 id、select_type、table、type、possible_keys、key、key_len、ref、rows、filtered 和 Extra。
执行计划分析的主要步骤包括:
- 查看执行计划
- 解读执行计划字段
- 识别性能瓶颈
- 提出优化方案
- 验证优化效果
建议 DBA 定期分析慢查询的执行计划,结合其他监控数据,持续优化数据库性能。同时,使用 EXPLAIN ANALYZE 可以查看实际执行统计信息,更准确地分析性能问题。
