未记录事实核验 · 状态:维护中

记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。

warning 适用边界与维护说明

本文适用于已有SQL、索引和MySQL基础的读者学习执行计划解读、候选查询改写及性能诊断流程,不是经实例运行验证的性能结论或完整监控项目。三行users数据及未填充的关联表仅用于演示写法,不能保证复现注释中的访问类型、排序或分组计划;JSON片段是带注释的字段示意,不是完整实例输出。应结合目标实例的计划、数据分布、结果语义和实际耗时判断优化效果,不能按访问类型固定排名,也不能将全表扫描、临时表或额外排序直接视为错误。预处理统计中的重新准备次数与执行次数之比不是计划缓存命中率;随机排序、大小写比较和分页示例须遵守正文注明的语义边界。执行计划变化对比仍需自行保存并比较EXPLAIN结果,所建视图仅提供语句摘要统计。EXPLAIN ANALYZE需MySQL 8.0.18及以上且会实际执行查询,监控配置变更需要相应权限,生产使用应评估执行负载与监控开销。本次仅作结构与内容一致性审阅,未进行实例运行验证或外部事实核验。

🔍 MySQL查询执行计划详解 - EXPLAIN深度分析与优化

abstract 📋 本节概览

🎯 EXPLAIN基础语法

EXPLAIN输出格式

-- 创建测试表
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100),
    age INT,
    city VARCHAR(50),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_username (username),
    INDEX idx_age_city (age, city),
    INDEX idx_created (created_at)
) ENGINE=InnoDB;

-- 插入测试数据
INSERT INTO users (username, email, age, city) VALUES
('alice', 'alice@email.com', 25, 'Beijing'),
('bob', 'bob@email.com', 30, 'Shanghai'),
('charlie', 'charlie@email.com', 35, 'Guangzhou');

-- 基本EXPLAIN
EXPLAIN SELECT * FROM users WHERE username = 'alice';

-- 详细格式的EXPLAIN
EXPLAIN FORMAT=JSON SELECT * FROM users WHERE age > 25;

-- 查看实际执行统计(MySQL 8.0.18及以上;会实际执行查询)
EXPLAIN ANALYZE SELECT * FROM users WHERE age > 25 ORDER BY created_at;

EXPLAIN输出列详解

note 💡 如何读懂EXPLAIN结果?

把EXPLAIN想象成MySQL的"执行计划书",告诉你MySQL打算如何执行你的查询:

列名 通俗解释 关键值解读
id SELECT查询块标识 🔢 标识SELECT查询块;不能仅凭id大小确定实际执行顺序,应结合完整计划及优化器改写分析
select_type 查询类型 📝 SIMPLE(简单查询)、SUBQUERY(子查询)、UNION(联合查询)
table 当前访问的表 📊 显示表名或别名
type 访问方式(★重要) 🚦 常见访问方式包括const、ref、range和ALL;实际性能还取决于扫描行数、回表、数据分布及查询需求,不能据此作固定排名
possible_keys 可选的索引 🗝️ MySQL考虑使用的索引清单
key 实际使用的索引 ✅ 真正用到的索引,NULL表示没用索引
key_len 索引使用长度 📏 访问时使用的索引键长度,可辅助判断使用了哪些索引列;不能据其长短直接判断效率
rows 预估扫描行数 📈 MySQL估计要检查多少行数据,越少越好
Extra 额外执行信息 💡 重要提示信息,如"Using index"表示覆盖索引,"Using filesort"表示额外排序;是否需要优化应结合成本和查询需求判断

📊 关键字段深度解析

1. type字段 - 访问类型

以下访问类型及后续Extra、排序和分组注释用于说明候选计划特征,不是实际输出记录。小样本、空表、已有索引和优化器选择都可能使结果不同;勾叉标记不表示固定的性能优劣,应以目标实例的EXPLAIN和执行统计为准。

-- system表示表仅有一行的特殊访问情形;以下系统表查询不保证出现system
EXPLAIN SELECT * FROM mysql.proxies_priv WHERE host = '';

-- const: 主键或唯一索引的等值查询
EXPLAIN SELECT * FROM users WHERE id = 1;

-- eq_ref: 唯一索引扫描(通常出现在连接查询)
CREATE TABLE orders (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,
    amount DECIMAL(10,2),
    FOREIGN KEY (user_id) REFERENCES users(id)
);

EXPLAIN SELECT u.username, o.amount 
FROM users u 
JOIN orders o ON u.id = o.user_id;

-- ref: 非唯一索引的等值查询
EXPLAIN SELECT * FROM users WHERE username = 'alice';

-- range: 索引范围扫描
EXPLAIN SELECT * FROM users WHERE age BETWEEN 25 AND 35;
EXPLAIN SELECT * FROM users WHERE id IN (1, 2, 3);

-- index: 索引全扫描
EXPLAIN SELECT username FROM users;

-- ALL: 全表扫描;是否需要优化取决于表大小、过滤比例及访问成本
EXPLAIN SELECT * FROM users WHERE email LIKE '%gmail%';

2. Extra字段 - 执行信息

-- Using index: 覆盖索引
EXPLAIN SELECT username FROM users WHERE username = 'alice';

-- Using where: 使用WHERE过滤
EXPLAIN SELECT * FROM users WHERE age > 25;

-- Using index condition: 索引条件下推
EXPLAIN SELECT * FROM users WHERE age > 25 AND city = 'Beijing';

-- Using temporary: 使用临时表
EXPLAIN SELECT DISTINCT age FROM users;

-- Using filesort: 文件排序
EXPLAIN SELECT * FROM users ORDER BY email;

-- Using sort_union/Using union/Using intersect: 索引合并
CREATE INDEX idx_email ON users (email);
EXPLAIN SELECT * FROM users WHERE username = 'alice' OR email = 'alice@email.com';

-- Impossible WHERE: 条件永远为假
EXPLAIN SELECT * FROM users WHERE 1 = 0;

-- No matching min/max row: 没有符合条件的行
EXPLAIN SELECT MIN(id) FROM users WHERE age > 100;

🔧 查询优化实战

子查询优化

-- 相关子查询 vs JOIN
-- 相关EXISTS子查询示例:性能需结合优化器改写、索引和数据分布判断
EXPLAIN SELECT * FROM users u 
WHERE EXISTS (
    SELECT 1 FROM orders o 
    WHERE o.user_id = u.id AND o.amount > 100
);

-- 等价的JOIN与DISTINCT候选写法:是否更快需比较实际执行计划与耗时
EXPLAIN SELECT DISTINCT u.* FROM users u 
JOIN orders o ON u.id = o.user_id 
WHERE o.amount > 100;

-- 子查询类型分析
-- DEPENDENT SUBQUERY: 依赖外层查询的子查询;性能取决于执行次数、访问方式及数据分布
EXPLAIN SELECT * FROM users 
WHERE age > (SELECT AVG(age) FROM users u2 WHERE u2.city = users.city);

-- SUBQUERY: 独立子查询
EXPLAIN SELECT * FROM users 
WHERE age > (SELECT AVG(age) FROM users);

-- DERIVED: 派生表
EXPLAIN SELECT * FROM (
    SELECT city, AVG(age) as avg_age 
    FROM users 
    GROUP BY city
) t WHERE avg_age > 30;

连接查询优化

note 💡 JOIN优化要点

JOIN查询的性能关键在于连接顺序和索引使用,详细的JOIN语法可参考 SQL

-- 创建更多测试表
CREATE TABLE profiles (
    user_id INT PRIMARY KEY,
    bio TEXT,
    avatar_url VARCHAR(200)
);

CREATE TABLE user_stats (
    user_id INT PRIMARY KEY,
    login_count INT DEFAULT 0,
    last_login TIMESTAMP
);

-- 多表连接分析
EXPLAIN SELECT 
    u.username,
    p.bio,
    s.login_count
FROM users u
LEFT JOIN profiles p ON u.id = p.user_id
LEFT JOIN user_stats s ON u.id = s.user_id
WHERE u.age > 25;

-- 连接顺序优化
-- MySQL按成本估算选择连接顺序,但不保证实际最优;可在支持该提示的版本中尝试控制顺序并比较效果
EXPLAIN SELECT /*+ JOIN_ORDER(u, p, s) */
    u.username,
    p.bio,
    s.login_count
FROM users u
JOIN profiles p ON u.id = p.user_id
JOIN user_stats s ON u.id = s.user_id;

-- 驱动表选择原则
-- 1. 小表驱动大表
-- 2. 有索引的表作为被驱动表
-- 3. WHERE条件过滤性好的表作为驱动表

排序和分组优化

-- 利用索引排序 vs 文件排序
-- ✅ 使用索引排序(无Using filesort)
EXPLAIN SELECT * FROM users ORDER BY id;
EXPLAIN SELECT * FROM users WHERE age > 25 ORDER BY age, city;

-- ❌ 文件排序(有Using filesort)
EXPLAIN SELECT * FROM users ORDER BY email;
EXPLAIN SELECT * FROM users ORDER BY age DESC, city ASC;  -- 排序方向不一致

-- GROUP BY优化
-- ✅ 利用索引分组
EXPLAIN SELECT age, COUNT(*) FROM users GROUP BY age;

-- ❌ 临时表分组
EXPLAIN SELECT city, COUNT(*) FROM users GROUP BY city;

-- 创建适合的索引
CREATE INDEX idx_city_age ON users (city, age);
EXPLAIN SELECT city, AVG(age) FROM users GROUP BY city;

📈 高级分析技巧

JSON格式执行计划

-- JSON格式提供更详细的信息
EXPLAIN FORMAT=JSON 
SELECT u.username, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.age > 25
GROUP BY u.id, u.username
HAVING COUNT(o.id) > 0
ORDER BY order_count DESC
LIMIT 10;

-- JSON格式关键字段:
/*
{
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "2.40"  -- 查询成本
    },
    "nested_loop": [
      {
        "table": {
          "table_name": "u",
          "access_type": "range",
          "possible_keys": ["idx_age_city"],
          "key": "idx_age_city",
          "key_length": "5",
          "rows_examined_per_scan": 2,
          "rows_produced_per_join": 2,
          "filtered": "100.00",
          "cost_info": {
            "read_cost": "1.20",
            "eval_cost": "0.20",
            "prefix_cost": "1.40",
            "data_read_per_join": "368"
          }
        }
      }
    ]
  }
}
*/

预处理语句执行与重新准备统计

-- 查看SQL级PREPARE与EXECUTE的调用计数;这些计数不代表计划缓存状态
SHOW STATUS LIKE 'Com_prepare';
SHOW STATUS LIKE 'Com_execute';

-- 将EXPLAIN查询本身准备为参数化语句
PREPARE stmt FROM 'EXPLAIN SELECT * FROM users WHERE age > ?';
SET @age = 25;
EXECUTE stmt USING @age;
DEALLOCATE PREPARE stmt;

-- 单独准备并执行待观察的查询;在释放语句前读取统计
-- 需要Performance Schema已启用相应预处理语句监控,否则可能无记录
PREPARE stats_stmt FROM 'SELECT * FROM users WHERE age > ?';
EXECUTE stats_stmt USING @age;

-- 查看本连接中该语句的执行与重新准备次数;以下比值不是计划缓存命中率
SELECT
    COUNT_EXECUTE,
    COUNT_REPREPARE,
    COUNT_REPREPARE / NULLIF(COUNT_EXECUTE, 0) as reprepares_per_execution
FROM performance_schema.prepared_statements_instances
WHERE STATEMENT_NAME = 'stats_stmt'
  AND OWNER_THREAD_ID = (
      SELECT THREAD_ID
      FROM performance_schema.threads
      WHERE PROCESSLIST_ID = CONNECTION_ID()
  );

DEALLOCATE PREPARE stats_stmt;

🚀 性能监控与诊断

实时查询分析

-- 查看当前运行的查询
SELECT 
    ID,
    USER,
    HOST,
    DB,
    COMMAND,
    TIME,
    STATE,
    LEFT(INFO, 100) as QUERY_START
FROM INFORMATION_SCHEMA.PROCESSLIST
WHERE COMMAND = 'Query' AND TIME > 1
ORDER BY TIME DESC;

-- 使用Performance Schema分析
-- 启用语句监控
UPDATE performance_schema.setup_consumers 
SET ENABLED = 'YES' 
WHERE NAME LIKE 'events_statements%';

-- 查看最耗时的语句
SELECT 
    DIGEST_TEXT,
    COUNT_STAR as exec_count,
    ROUND(AVG_TIMER_WAIT/1000000000000, 3) as avg_time_sec,
    ROUND(MAX_TIMER_WAIT/1000000000000, 3) as max_time_sec,
    ROUND(SUM_TIMER_WAIT/1000000000000, 3) as total_time_sec
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME = DATABASE()
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;

执行计划变化监控

-- 创建监控视图
CREATE VIEW v_query_plans AS
SELECT 
    DIGEST,
    DIGEST_TEXT,
    COUNT_STAR,
    ROUND(AVG_TIMER_WAIT/1000000000000, 3) as avg_time,
    FIRST_SEEN,
    LAST_SEEN
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME = DATABASE();

-- 定期检查执行计划变化
-- 可以通过脚本定期保存EXPLAIN结果,对比变化

-- 查看索引使用统计
SELECT 
    OBJECT_NAME as table_name,
    INDEX_NAME,
    COUNT_FETCH,
    COUNT_INSERT,
    COUNT_UPDATE,
    COUNT_DELETE,
    ROUND(COUNT_FETCH / (COUNT_FETCH + COUNT_INSERT + COUNT_UPDATE + COUNT_DELETE) * 100, 2) as read_ratio
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE OBJECT_SCHEMA = DATABASE()
    AND COUNT_FETCH + COUNT_INSERT + COUNT_UPDATE + COUNT_DELETE > 0
ORDER BY COUNT_FETCH DESC;

🎓 优化最佳实践

执行计划分析流程

%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TD
    A[获取执行计划]
    B[分析type字段]
    A --> B
    B --> C{type是否为ALL?}
    C -->|是| D[检查是否缺少索引]
    C -->|否| E[分析key字段]
    E --> F{是否使用了合适的索引?}
    F -->|否| G[优化索引设计]
    F -->|是| H[分析rows字段]
    H --> I{扫描行数是否过多?}
    I -->|是| J[优化WHERE条件]
    I -->|否| K[分析Extra字段]
    K --> L{是否有Using filesort?}
    L -->|是| M[优化ORDER BY]
    L -->|否| N[检查其他优化点]
    D --> O[比较候选改写与索引方案的成本]
    G --> O
    J --> O
    M --> O
    N --> P[验证结果语义与实际性能]
    O --> P
    classDef decision fill:#F6EAD8,stroke:#C08A1A,color:#2D2A26,font-weight:bold
    class C,F,I,L decision

常见优化技巧

-- 1. 避免全表扫描
-- ❌ 没有合适索引
SELECT * FROM users WHERE UPPER(username) = 'ALICE';

-- 候选改写:仅在排序规则和业务要求下与原大小写比较等价时使用;此SQL未创建函数索引
SELECT * FROM users WHERE username = 'alice';

-- 2. 优化JOIN查询
-- 连接查询示例:前文users.id已有主键索引,orders.user_id有外键所需索引,不能据此认定缺少连接索引
SELECT * FROM users u, orders o WHERE u.id = o.user_id;

-- ✅ 确保连接字段有索引
-- 已有 users.id(PRIMARY) 和 orders.user_id(INDEX)

-- 3. 避免不必要的排序
-- ❌ 无法利用索引排序
SELECT * FROM users ORDER BY RAND();

-- 若业务要求按id排序,可利用主键顺序;这不是随机排序的等价替代
SELECT * FROM users ORDER BY id;

-- 4. 优化LIMIT查询
-- ❌ 大偏移量分页
SELECT * FROM users ORDER BY created_at LIMIT 10000, 20;

-- 按id游标分页的独立示例:假设上一页末尾id为10000;不等价于前文按created_at的偏移分页
SELECT * FROM users WHERE id > 10000 ORDER BY id LIMIT 20;

tip 💡 执行计划分析要点

关键指标

  • type为ALL时检查扫描成本;小表或高返回比例查询可能适合全表扫描
  • rows是估算值,应结合过滤比例、实际扫描量与总耗时判断
  • Extra中的Using index可提示覆盖索引,但仍需评估整体成本
  • Using temporary和Using filesort是诊断线索,不是必须消除的错误

优化策略

  • 为WHERE条件创建合适索引
  • 使用覆盖索引减少回表
  • 优化JOIN顺序和条件
  • 避免在索引列上使用函数

warning ⚠️ 常见分析误区

  • 仅关注单个查询,忽视整体性能
  • 过度优化低频查询
  • 忽视统计信息的准确性
  • 不考虑数据量变化对计划的影响

🔗 相关工具推荐

可视化工具

  • MySQL Workbench: 图形化执行计划展示
  • phpMyAdmin: Web界面的EXPLAIN工具
  • Navicat: 商业数据库管理工具

命令行工具

  • pt-query-digest: Percona Toolkit查询分析
  • mysqldumpslow: MySQL自带慢查询分析
  • mysqlslap: 负载测试工具

🔗 下一步学习

掌握执行计划分析后,建议继续学习:


最后更新:2024-06-29