MySQL数据库进阶(八)———查询优化与执行计划深度解析

前言

在掌握了索引原理后,我们将深入MySQL最核心的性能优化领域——查询优化与执行计划分析。本文将从优化器工作原理到实战调优技巧,全方位提升你的SQL性能优化能力。


一、查询优化的本质:从分钟级到毫秒级的蜕变

查询优化是数据库系统的核心能力,MySQL通过优化器将SQL转换为高效执行计划。优化前后的性能差异可能达到千倍级:

-- 优化前(执行时间12.8秒)
SELECT * FROM orders o
JOIN users u ON o.user_id = u.id
WHERE u.reg_time > '2023-01-01'
ORDER BY o.amount DESC
LIMIT 100;

-- 优化后(执行时间23毫秒)
-- 具体优化策略见下文...

查询优化的核心目标:

  1. 减少数据扫描量(磁盘I/O最小化)

  2. 避免临时表与文件排序

  3. 充分利用索引优势

  4. 减少锁竞争与上下文切换

二、MySQL优化器工作原理揭秘

1. 查询处理全流程

graph LR
    A[SQL语句] --> B[解析器]
    B --> C[预处理器]
    C --> D[优化器]
    D --> E[执行计划]
    E --> F[执行引擎]
    F --> G[结果返回]

MySQL数据库进阶(八)———查询优化与执行计划深度解析_第1张图片

2. 优化器决策过程

优化器基于成本模型(Cost-Based Optimizer)决策

# 伪代码:优化器决策逻辑
def choose_plan(query):
    possible_plans = generate_plans(query)  # 生成候选执行计划
    
    for plan in possible_plans:
        plan.cost = calculate_cost(plan)    # 计算IO/CPU/内存成本
        
    return min(possible_plans, key=lambda p: p.cost)  # 选择成本最低计划

成本计算因素

  • 单表扫描行数

  • 索引过滤效率

  • 连接操作复杂度

  • 内存排序开销

  • 临时表创建成本

3. 优化器局限性

场景 问题 解决方案
多表JOIN顺序选择 n!级组合爆炸 STRAIGHT_JOIN提示
统计信息过期 成本计算偏差 ANALYZE TABLE更新统计
复杂子查询 嵌套执行效率低 改写为JOIN
函数表达式 索引失效 计算列+索引

三、执行计划(EXPLAIN)深度解析 

1. EXPLAIN核心字段解读

EXPLAIN FORMAT=JSON
SELECT p.name, SUM(oi.quantity) AS total
FROM products p
JOIN order_items oi ON p.id = oi.product_id
WHERE p.category = 'Electronics'
GROUP BY p.id
HAVING total > 100;

JSON关键路径分析

{
  "query_block": {
    "cost_info": {"query_cost": "126.75"},
    "ordering_operation": {
      "using_temporary_table": true,  // 使用临时表
      "using_filesort": true,         // 文件排序
      "grouping_operation": {
        "table": {
          "table_name": "p",
          "access_type": "ref",       // 索引访问类型
          "key": "idx_category",     // 使用索引
          "rows_examined_per_scan": 342,
          "attached_condition": "p.category = 'Electronics'"
        },
        "block-nl-join": {            // 嵌套循环连接
          "table": {
            "table_name": "oi",
            "access_type": "ref",
            "key": "idx_product",
            "rows_examined_per_scan": 15
          }
        }
      }
    }
  }
}

2. 执行计划类型详解

访问类型(type) 扫描方式 性能等级 触发场景
system 系统表单行访问 ⭐⭐⭐⭐⭐ 内存表或const join
const 主键/唯一索引等值 ⭐⭐⭐⭐⭐ WHERE id=1
eq_ref 关联查询主键连接 ⭐⭐⭐⭐ JOIN ON t1.id=t2.primary
ref 非唯一索引等值 ⭐⭐⭐ WHERE index_col=value
range 索引范围扫描 ⭐⭐ WHERE id>100
index 全索引扫描 SELECT 索引列
ALL 全表扫描 无可用索引

3. 关键性能诊断点

  • Extra字段解析

    • Using where:存储引擎返回后过滤

    • Using index:覆盖索引(最优)

    • Using temporary:需创建临时表

    • Using filesort:额外排序操作

    • Select tables optimized away:优化器已优化

  • Rows列:预估扫描行数(与实际差异大需ANALYZE TABLE)

  • Filtered列:条件过滤百分比(<10%需优化)

四、十大实战查询优化技巧

1. JOIN优化:驱动表选择原则

-- 优化前(大表驱动小表)
SELECT * FROM large_table l 
JOIN small_table s ON l.key = s.key;

-- 优化后(小表驱动大表)
SELECT /*+ STRAIGHT_JOIN(s,l) */ *
FROM small_table s 
JOIN large_table l ON s.key = l.key;

2. 子查询优化:转为JOIN

-- 优化前(DEPENDENT SUBQUERY)
SELECT * FROM users u 
WHERE EXISTS (
  SELECT 1 FROM orders o 
  WHERE o.user_id = u.id AND o.amount > 1000
);

-- 优化后(性能提升50倍)
SELECT u.* 
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.amount > 1000
GROUP BY u.id;  -- 去重

3. LIMIT分页优化

-- 优化前(扫描100000行)
SELECT * FROM orders 
ORDER BY id DESC 
LIMIT 100000, 20;

-- 优化后(扫描20行)
SELECT * FROM orders 
WHERE id < (SELECT id FROM orders ORDER BY id DESC LIMIT 100000, 1)
ORDER BY id DESC 
LIMIT 20;

4. 文件排序优化

-- 优化前(Using filesort)
SELECT * FROM products 
WHERE category='Books' 
ORDER BY price DESC;

-- 优化后(利用索引排序)
ALTER TABLE products 
ADD INDEX idx_category_price (category, price DESC);

SELECT * FROM products 
WHERE category='Books' 
ORDER BY price DESC; -- Using index

5. 函数索引优化

-- 优化前(全表扫描)
SELECT * FROM users 
WHERE DATE_FORMAT(reg_time, '%Y-%m') = '2023-07';

-- 优化后(函数索引)
ALTER TABLE users 
ADD INDEX idx_reg_month ((DATE_FORMAT(reg_time, '%Y-%m')));

SELECT * FROM users 
WHERE DATE_FORMAT(reg_time, '%Y-%m') = '2023-07'; -- 索引生效

6. UNION优化

-- 优化前(两次全表扫描)
SELECT * FROM logs_2023 WHERE type='ERROR'
UNION ALL
SELECT * FROM logs_2022 WHERE type='ERROR';

-- 优化后(分区表查询)
CREATE TABLE all_logs (
    id BIGINT,
    log_time DATETIME,
    type VARCHAR(20)
) PARTITION BY RANGE (YEAR(log_time)) (
    PARTITION p2022 VALUES LESS THAN (2023),
    PARTITION p2023 VALUES LESS THAN (2024)
);

SELECT * FROM all_logs 
WHERE type='ERROR'; -- 分区裁剪

7. 数据类型优化

-- 优化前(隐式转换)
SELECT * FROM users 
WHERE phone = 13800138000; -- phone是varchar

-- 优化后(消除类型转换)
SELECT * FROM users 
WHERE phone = '13800138000';

-- 优化前(大文本检索)
SELECT * FROM articles 
WHERE content LIKE '%database%';

-- 优化后(全文索引)
ALTER TABLE articles 
ADD FULLTEXT INDEX ft_content (content);

8. 批量写入优化

-- 优化前(单条插入)
INSERT INTO sales (product_id, amount) VALUES (101, 20);
INSERT INTO sales (product_id, amount) VALUES (102, 15);

-- 优化后(批量提交)
START TRANSACTION;
INSERT INTO sales (product_id, amount) VALUES 
(101,20), (102,15), ... , (199,30);
COMMIT;

-- 禁用索引加速
ALTER TABLE sales DISABLE KEYS;
-- 批量插入...
ALTER TABLE sales ENABLE KEYS;

9. 统计信息管理

-- 手动更新统计信息
ANALYZE TABLE orders, order_items;

-- 查看统计信息
SELECT 
  table_name, 
  index_name,
  stat_value AS pages,
  stat_description 
FROM mysql.innodb_index_stats
WHERE table_name = 'orders';

-- 设置采样页数(提高准确性)
SET GLOBAL innodb_stats_persistent_sample_pages = 200;

10. 参数级优化

# my.cnf 关键参数
[mysqld]
# 排序缓冲区
sort_buffer_size = 4M

# 连接缓冲区
join_buffer_size = 8M 

# 临时表大小
tmp_table_size = 64M
max_heap_table_size = 64M

# 查询缓存(MySQL 8.0已移除)
# query_cache_type = 0

五、慢查询日志深度分析

1. 慢日志配置

-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';

-- 设置阈值(单位:秒)
SET GLOBAL long_query_time = 0.5;

-- 记录未用索引的查询
SET GLOBAL log_queries_not_using_indexes = ON;

-- 日志路径
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

2. 慢日志分析工具

# mysqldumpslow 基础分析
mysqldumpslow -s t /var/log/mysql/slow.log

# pt-query-digest 专业分析
pt-query-digest /var/log/mysql/slow.log > slow_report.txt

分析报告关键部分

# Profile
# Rank Query ID           Response time Calls R/Call 
# ==== ================== ============= ===== =======
#    1 0xABCDEF123456789  112.4638 78.2%   120  0.9372
#    2 0xBCDEF123456789A   28.1195 19.5%    85  0.3308

# Query 1: 0.94 QPS, ID 0xABCDEF123456789
SELECT * FROM orders WHERE user_id=? AND status=?

3. 慢查询优化四步法

  1. 定位问题SQL:pt-query-digest分析

  2. EXPLAIN诊断:分析执行计划瓶颈

  3. 索引优化:添加缺失索引或调整索引

  4. 重写SQL:调整查询结构与逻辑

六、高级优化策略

1. 物化视图(MySQL 8.0+)

-- 创建物化视图
CREATE MATERIALIZED VIEW sales_summary
REFRESH FAST ON COMMIT
AS
SELECT product_id, SUM(amount) total 
FROM sales 
GROUP BY product_id;

-- 查询优化
SELECT * FROM sales_summary WHERE total > 1000; -- 避免全表聚合

2. 并行查询(MySQL 8.0+)

-- 启用并行扫描
ALTER TABLE orders PARALLEL 8;

-- 并行查询示例
SELECT /*+ PARALLEL(4) */ 
    user_id, COUNT(*) 
FROM big_table
GROUP BY user_id;

3. 查询重写插件

-- 安装Rewriter插件
INSTALL PLUGIN rewriter SONAME 'rewriter.so';

-- 定义重写规则
INSERT INTO query_rewrite.rewrite_rules (pattern, replacement)
VALUES(
  'SELECT * FROM products WHERE price < ?',
  'SELECT * FROM products WHERE price < ? AND is_active=1'
);

-- 刷新规则
CALL query_rewrite.flush_rewrite_rules();

七、优化器提示(Hints)实战

提示类型 语法示例 适用场景
索引提示 USE INDEX(idx_name) 强制使用特定索引
JOIN顺序提示 STRAIGHT_JOIN 固定JOIN顺序
子查询策略提示 /*+ SEMIJOIN(MATERIALIZATION) */ 控制子查询执行策略
资源控制提示 /*+ MAX_EXECUTION_TIME(1000) */ 设置查询超时时间
并行查询提示 /*+ PARALLEL(4) */ 指定并行度

-- 综合使用示例
SELECT /*+ 
    STRAIGHT_JOIN(o,u), 
    USE INDEX(o.idx_user_status), 
    MAX_EXECUTION_TIME(500) 
*/
u.name, o.order_no
FROM orders o FORCE INDEX (idx_user_status)
JOIN users u ON o.user_id = u.id
WHERE o.status = 'paid'
AND u.reg_time > '2023-01-01';

八、总结与最佳实践

查询优化黄金法则:

  1. 先测量后优化:EXPLAIN+慢日志定位瓶颈

  2. 索引优先原则:80%性能问题通过索引解决

  3. 避免全表扫描:尤其大数据量表

  4. 警惕隐式转换:统一字符集与数据类型

  5. 分而治之:复杂查询拆分为简单操作

不同规模数据优化策略:

数据量 优化重点 工具链
<10万 基础索引优化 EXPLAIN
10-500万 执行计划调优+参数优化 pt-query-digest
500万+ 架构优化+高级特性 分区表/并行查询/物化视图

你可能感兴趣的:(数据库,数据库,mysql)