Skip to content

MySQL 查询优化

查询优化概述

查询优化是 MySQL 数据库性能调优的核心环节,涉及 SQL 语句的编写、索引的设计、查询执行计划的分析以及数据库配置的调整。优秀的查询优化能够显著提升数据库的响应速度和吞吐量,是构建高性能数据库系统的关键技能。

查询优化的重要性

  1. 性能提升:优化后的查询可以将执行时间从秒级降低到毫秒级
  2. 资源节约:减少 CPU、内存和 I/O 资源的消耗
  3. 并发能力:提高数据库的并发处理能力
  4. 用户体验:改善应用程序的响应速度
  5. 成本控制:减少硬件资源需求,降低运营成本

真实案例:电商平台订单查询优化

业务场景:某电商平台用户订单查询页面响应缓慢,用户投诉频繁。

问题分析

sql
-- 原始查询(性能差)
SELECT o.*, u.username, p.product_name, p.price
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE o.user_id = 12345
AND o.created_at >= '2023-01-01'
ORDER BY o.created_at DESC;

性能问题

  • 执行时间:2.3秒
  • 扫描行数:850万行
  • 临时表:使用了临时表和文件排序
  • 索引:未充分利用索引

优化后查询

sql
-- 优化后查询
SELECT o.id, o.order_no, o.total_amount, o.status, o.created_at,
       u.username,
       GROUP_CONCAT(CONCAT(p.product_name, '×', oi.quantity) SEPARATOR '; ') as products
FROM orders o
INNER JOIN users u ON o.user_id = u.id
INNER JOIN order_items oi ON o.id = oi.order_id
INNER JOIN products p ON oi.product_id = p.id
WHERE o.user_id = 12345
AND o.created_at >= '2023-01-01'
GROUP BY o.id, o.order_no, o.total_amount, o.status, o.created_at, u.username
ORDER BY o.created_at DESC
LIMIT 20;

-- 添加的索引
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at DESC);
CREATE INDEX idx_order_items_order_product ON order_items(order_id, product_id);

优化效果

  • 执行时间:45毫秒(提升98%)
  • 扫描行数:156行(减少99.98%)
  • 内存使用:减少85%
  • 用户满意度:投诉减少95%

查询优化的层次

  1. SQL 语句层面:编写高效的 SQL 语句
  2. 索引层面:设计合适的索引策略
  3. 表结构层面:优化表设计和数据类型选择
  4. 配置层面:调整数据库参数配置
  5. 硬件层面:优化硬件配置和部署架构

MySQL 查询优化器工作原理

MySQL 查询优化器是数据库系统的核心组件,负责将 SQL 语句转换为最优的执行计划。理解优化器的工作原理有助于编写更高效的查询语句。

优化器架构

基于成本的优化(CBO)

成本模型

  • MySQL 使用基于成本的优化器
  • 为每种操作定义成本计算公式
  • 比较不同执行计划的总成本
  • 选择成本最低的执行计划

成本因素

  • I/O 成本:磁盘读取操作的成本
  • CPU 成本:数据处理和比较的成本
  • 内存成本:内存访问和缓存的成本
  • 网络成本:数据传输的成本(分布式环境)

成本计算

  • 基于统计信息估算成本
  • 考虑数据分布和选择性
  • 评估不同访问路径的效率
  • 动态调整成本参数

统计信息收集

表统计信息

  • 行数统计:表中的总行数
  • 数据长度:平均行长度和总数据量
  • 更新频率:数据修改的频率
  • 数据分布:数据在不同值上的分布情况

索引统计信息

  • 基数(Cardinality):索引中不同值的数量
  • 选择性:索引的过滤效果
  • 深度:B+ 树的层数
  • 叶子页数量:索引叶子节点的数量

统计信息更新

  • 自动更新机制
  • 手动更新命令(ANALYZE TABLE)
  • 更新触发条件
  • 统计信息的持久化

查询解析过程

词法分析和语法分析

词法分析

  • 将 SQL 语句分解为词法单元(Token)
  • 识别关键字、标识符、常量等
  • 检查基本的语法错误
  • 生成词法分析树

语法分析

  • 根据 SQL 语法规则构建语法树
  • 验证 SQL 语句的语法正确性
  • 识别查询的基本结构
  • 为后续优化做准备

语义分析

  • 检查表名、列名的有效性
  • 验证数据类型的兼容性
  • 解析表达式和函数调用
  • 处理别名和引用关系

查询重写

常量折叠

  • 计算常量表达式的值
  • 简化复杂的数学运算
  • 优化条件判断
  • 减少运行时计算

谓词下推

  • 将过滤条件尽早应用
  • 减少中间结果集的大小
  • 提高查询执行效率
  • 优化子查询和连接操作

子查询优化

  • 将相关子查询转换为连接
  • 优化 EXISTS 和 IN 子查询
  • 消除不必要的子查询
  • 改善查询执行计划

连接重排序

  • 调整多表连接的顺序
  • 选择最优的连接算法
  • 考虑索引的可用性
  • 最小化中间结果集

执行计划生成

访问路径选择

全表扫描

  • 适用于小表或大部分数据需要访问的情况
  • 顺序读取,I/O 效率较高
  • 不需要索引支持
  • 成本相对固定

索引扫描

  • 索引范围扫描:根据范围条件扫描索引
  • 索引唯一扫描:通过唯一索引精确定位
  • 索引全扫描:扫描整个索引
  • 索引快速全扫描:并行扫描索引

索引查找

  • 通过索引快速定位数据
  • 适用于高选择性的查询条件
  • 减少数据访问量
  • 提高查询效率

连接算法选择

嵌套循环连接(NLJ)

  • 简单的连接算法
  • 外表的每一行与内表进行匹配
  • 适用于小表连接
  • 可以利用索引优化

块嵌套循环连接(BNL)

  • 批量处理外表数据
  • 减少内表的扫描次数
  • 提高缓存利用率
  • 适用于中等大小的表连接

哈希连接

  • MySQL 8.0 引入的新算法
  • 构建哈希表进行连接
  • 适用于大表连接
  • 内存需求较大

排序合并连接

  • 对两个表进行排序后合并
  • 适用于大表连接
  • 需要额外的排序开销
  • 可以利用已有的排序

排序和分组优化

索引排序

  • 利用索引的有序性避免排序
  • 选择合适的索引顺序
  • 考虑复合索引的使用
  • 优化 ORDER BY 子句

内存排序

  • 在内存中进行快速排序
  • 适用于小数据集
  • 配置 sort_buffer_size 参数
  • 避免磁盘临时文件

外部排序

  • 数据量超过内存时使用
  • 分块排序后合并
  • 使用临时文件存储中间结果
  • 优化 I/O 操作

分组优化

  • 利用索引进行分组
  • 优化 GROUP BY 子句
  • 选择合适的聚合算法
  • 考虑松散索引扫描

执行计划分析

执行计划是查询优化的重要工具,通过分析执行计划可以了解查询的执行过程,识别性能瓶颈,制定优化策略。

实战案例:分析慢查询的执行计划

业务场景:社交媒体平台的用户动态查询性能问题

问题查询

sql
SELECT p.*, u.username, u.avatar,
       (SELECT COUNT(*) FROM likes l WHERE l.post_id = p.id) as like_count,
       (SELECT COUNT(*) FROM comments c WHERE c.post_id = p.id) as comment_count
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
AND u.status = 'active'
ORDER BY p.created_at DESC
LIMIT 20;

执行计划分析

sql
EXPLAIN FORMAT=JSON SELECT p.*, u.username, u.avatar,
       (SELECT COUNT(*) FROM likes l WHERE l.post_id = p.id) as like_count,
       (SELECT COUNT(*) FROM comments c WHERE c.post_id = p.id) as comment_count
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
AND u.status = 'active'
ORDER BY p.created_at DESC
LIMIT 20;

问题分析结果

+----+-------------+-------+------+---------------+------+---------+------+--------+-----------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows   | Extra                       |
+----+-------------+-------+------+---------------+------+---------+------+--------+-----------------------------+
|  1 | PRIMARY     | p     | ALL  | NULL          | NULL | NULL    | NULL | 125000 | Using where; Using filesort |
|  1 | PRIMARY     | u     | ALL  | NULL          | NULL | NULL    | NULL | 50000  | Using where; Using join buffer|
|  3 | SUBQUERY    | l     | ALL  | NULL          | NULL | NULL    | NULL | 800000 | Using where                 |
|  2 | SUBQUERY    | c     | ALL  | NULL          | NULL | NULL    | NULL | 300000 | Using where                 |
+----+-------------+-------+------+---------------+------+---------+------+--------+-----------------------------+

性能问题识别

  1. 全表扫描:posts表和users表都是ALL类型,扫描全表
  2. 子查询性能差:每个子查询都是全表扫描
  3. 文件排序:使用了filesort,没有利用索引排序
  4. 连接缓冲区:使用了join buffer,说明连接效率低

优化方案

sql
-- 1. 创建必要的索引
CREATE INDEX idx_posts_created_user ON posts(created_at DESC, user_id);
CREATE INDEX idx_users_status ON users(status, id);
CREATE INDEX idx_likes_post ON likes(post_id);
CREATE INDEX idx_comments_post ON comments(post_id);

-- 2. 优化后的查询
SELECT p.id, p.content, p.created_at, p.user_id,
       u.username, u.avatar,
       COALESCE(l.like_count, 0) as like_count,
       COALESCE(c.comment_count, 0) as comment_count
FROM posts p
INNER JOIN users u ON p.user_id = u.id AND u.status = 'active'
LEFT JOIN (
    SELECT post_id, COUNT(*) as like_count
    FROM likes
    GROUP BY post_id
) l ON p.id = l.post_id
LEFT JOIN (
    SELECT post_id, COUNT(*) as comment_count
    FROM comments
    GROUP BY post_id
) c ON p.id = c.post_id
WHERE p.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY p.created_at DESC
LIMIT 20;

优化后执行计划

+----+-------------+-------+-------+------------------+------------------+---------+------+------+-------------+
| id | select_type | table | type  | possible_keys    | key              | key_len | ref  | rows | Extra       |
+----+-------------+-------+-------+------------------+------------------+---------+------+------+-------------+
|  1 | PRIMARY     | p     | range | idx_posts_created| idx_posts_created| 8       | NULL | 1500 | Using where |
|  1 | PRIMARY     | u     | ref   | idx_users_status | idx_users_status | 5       | p.user_id| 1 | Using index|
|  1 | PRIMARY     | l     | ref   | idx_likes_post   | idx_likes_post   | 4       | p.id | 5    | Using index|
|  1 | PRIMARY     | c     | ref   | idx_comments_post| idx_comments_post| 4       | p.id | 3    | Using index|
+----+-------------+-------+-------+------------------+------------------+---------+------+------+-------------+

性能提升效果

  • 执行时间:从3.2秒降低到85毫秒
  • 扫描行数:从127万行降低到1509行
  • CPU使用率:降低90%
  • 并发能力:提升15倍

EXPLAIN 语句详解

基本用法

EXPLAIN 语法

  • EXPLAIN SELECT ...:显示查询的执行计划
  • EXPLAIN FORMAT=JSON SELECT ...:以 JSON 格式显示
  • EXPLAIN ANALYZE SELECT ...:显示实际执行统计信息
  • EXPLAIN FOR CONNECTION connection_id:显示指定连接的执行计划

输出格式

  • 传统格式:表格形式,易于阅读
  • JSON 格式:结构化数据,包含更多信息
  • TREE 格式:树形结构,显示操作层次
  • 可视化工具:图形化显示执行计划

执行计划字段解析

id 字段

  • 查询的标识符
  • 数字越大越先执行
  • 相同 id 从上到下执行
  • NULL 表示结果集

select_type 字段

  • SIMPLE:简单查询,不包含子查询或 UNION
  • PRIMARY:最外层的查询
  • SUBQUERY:子查询中的第一个 SELECT
  • DERIVED:派生表的 SELECT
  • UNION:UNION 中的第二个或后续 SELECT
  • UNION RESULT:UNION 的结果

table 字段

  • 查询涉及的表名
  • 可能是实际表名或别名
  • 派生表显示为 <derivedN>
  • 子查询显示为 <subqueryN>

partitions 字段

  • 查询涉及的分区
  • 显示分区裁剪的效果
  • 帮助优化分区查询
  • NULL 表示非分区表

type 字段(访问类型)

  • system:表只有一行(系统表)
  • const:通过主键或唯一索引访问,最多返回一行
  • eq_ref:对于前表的每一行,在此表中只查询一条记录
  • ref:非唯一索引访问,返回匹配某个单独值的所有行
  • fulltext:全文索引检索
  • ref_or_null:类似 ref,但包含 NULL 值的查询
  • index_merge:使用索引合并优化
  • unique_subquery:IN 子查询中的唯一索引查找
  • index_subquery:IN 子查询中的非唯一索引查找
  • range:索引范围扫描
  • index:全索引扫描
  • ALL:全表扫描

possible_keys 字段

  • 可能使用的索引
  • 优化器考虑的候选索引
  • NULL 表示没有相关索引
  • 不一定是最终选择的索引

key 字段

  • 实际使用的索引
  • NULL 表示没有使用索引
  • 可能与 possible_keys 不同
  • 显示优化器的最终选择

key_len 字段

  • 使用索引的长度
  • 字节数,不是字符数
  • 帮助判断索引的使用程度
  • 复合索引的前缀长度

ref 字段

  • 与索引比较的列或常量
  • 显示索引查找的条件
  • const 表示常量
  • 列名表示列比较

rows 字段

  • 估计需要扫描的行数
  • 基于统计信息的估算
  • 不是精确值
  • 用于成本计算

filtered 字段

  • 按表条件过滤的行百分比
  • 显示过滤效果
  • 100% 表示没有过滤
  • 帮助评估查询效率

Extra 字段

  • Using index:覆盖索引,不需要回表
  • Using where:使用 WHERE 子句过滤
  • Using temporary:使用临时表
  • Using filesort:使用文件排序
  • Using index condition:索引条件下推
  • Using MRR:多范围读取优化
  • Using join buffer:使用连接缓冲区
  • Impossible WHERE:WHERE 条件总是 false
  • Select tables optimized away:优化器优化掉了查询

性能指标分析

关键性能指标

执行时间

  • 查询的总执行时间
  • 各个阶段的耗时分布
  • 网络传输时间
  • 客户端处理时间

资源消耗

  • CPU 使用率
  • 内存使用量
  • I/O 操作次数
  • 网络流量

数据访问量

  • 扫描的行数
  • 返回的行数
  • 读取的数据量
  • 索引使用情况

性能瓶颈识别

I/O 瓶颈

  • 大量的全表扫描
  • 频繁的磁盘读取
  • 临时文件的使用
  • 缓冲池命中率低

CPU 瓶颈

  • 复杂的计算和函数调用
  • 大量的数据比较和排序
  • 正则表达式匹配
  • 数据类型转换

内存瓶颈

  • 大量的临时表使用
  • 排序缓冲区不足
  • 连接缓冲区不足
  • 缓存命中率低

锁竞争

  • 长时间的锁等待
  • 死锁的发生
  • 锁升级
  • 并发冲突

SQL 优化技巧

WHERE 子句优化

索引友好的条件编写

使用索引列

  • 在 WHERE 子句中使用有索引的列
  • 避免在索引列上使用函数
  • 保持索引列的原始数据类型
  • 考虑复合索引的列顺序

避免索引失效

  • 不要在索引列上使用函数:WHERE YEAR(date_col) = 2023WHERE date_col >= '2023-01-01' AND date_col < '2024-01-01'
  • 避免隐式类型转换:WHERE id = '123'WHERE id = 123
  • 不要使用 NOT、!=、<> 操作符
  • 避免 OR 条件,使用 UNION 替代

范围查询优化

  • 使用 BETWEEN 而不是 >= AND <=
  • 合理使用 IN 和 EXISTS
  • 优化 LIKE 查询的通配符位置
  • 考虑使用覆盖索引

条件顺序优化

选择性原则

  • 将选择性高的条件放在前面
  • 优先使用能够大幅减少结果集的条件
  • 考虑短路求值的特性
  • 平衡计算成本和过滤效果

索引利用

  • 按照复合索引的列顺序编写条件
  • 确保最左前缀匹配
  • 避免跳跃使用索引列
  • 考虑索引覆盖的可能性

实战案例:金融系统交易记录查询优化

业务场景:银行交易系统的历史交易查询功能性能问题

原始问题查询

sql
-- 查询用户近30天的交易记录(性能差)
SELECT 
    t.transaction_id,
    t.amount,
    t.transaction_type,
    t.create_time,
    a.account_number,
    u.username
FROM transactions t
JOIN accounts a ON t.account_id = a.account_id
JOIN users u ON a.user_id = u.user_id
WHERE YEAR(t.create_time) = 2023
AND MONTH(t.create_time) >= 11
AND t.amount > '1000'  -- 字符串类型
AND (t.transaction_type = 'transfer' OR t.transaction_type = 'payment')
AND u.status != 'inactive'
ORDER BY t.create_time DESC;

性能问题分析

sql
-- 执行计划分析
EXPLAIN FORMAT=JSON [上述查询];

-- 问题发现:
-- 1. 在索引列create_time上使用了函数YEAR()和MONTH()
-- 2. amount字段发生隐式类型转换
-- 3. 使用了OR条件
-- 4. 使用了!=操作符
-- 5. 连接顺序不合理

-- 性能测试结果:
-- 执行时间:4.8秒
-- 扫描行数:2,300,000行
-- 临时表:使用
-- 文件排序:是

数据量和索引分析

sql
-- 查看表数据量
SELECT 
    'transactions' as table_name, COUNT(*) as row_count,
    MIN(create_time) as min_date, MAX(create_time) as max_date
FROM transactions
UNION ALL
SELECT 'accounts', COUNT(*), NULL, NULL FROM accounts
UNION ALL
SELECT 'users', COUNT(*), NULL, NULL FROM users;

-- 结果:
-- transactions: 2,500,000行 (2020-01-01 到 2023-12-31)
-- accounts: 150,000行
-- users: 120,000行

-- 查看现有索引
SHOW INDEX FROM transactions;
SHOW INDEX FROM accounts;
SHOW INDEX FROM users;

优化方案实施

第一步:修复索引失效问题

sql
-- 优化后的查询
SELECT 
    t.transaction_id,
    t.amount,
    t.transaction_type,
    t.create_time,
    a.account_number,
    u.username
FROM transactions t
INNER JOIN accounts a ON t.account_id = a.account_id
INNER JOIN users u ON a.user_id = u.user_id
WHERE t.create_time >= '2023-11-01'  -- 避免函数使用
AND t.create_time < '2024-01-01'
AND t.amount > 1000  -- 修复类型转换
AND t.transaction_type IN ('transfer', 'payment')  -- 替换OR
AND u.status = 'active'  -- 替换!=
ORDER BY t.create_time DESC
LIMIT 100;

第二步:创建优化索引

sql
-- 分析查询条件的选择性
SELECT 
    COUNT(*) as total_rows,
    COUNT(DISTINCT create_time) as date_cardinality,
    COUNT(DISTINCT amount) as amount_cardinality,
    COUNT(DISTINCT transaction_type) as type_cardinality,
    COUNT(DISTINCT account_id) as account_cardinality
FROM transactions
WHERE create_time >= '2023-11-01';

-- 结果分析:
-- total_rows: 156,000
-- date_cardinality: 61 (选择性低)
-- amount_cardinality: 89,000 (选择性高)
-- type_cardinality: 8 (选择性低)
-- account_cardinality: 45,000 (选择性中等)

-- 创建复合索引(按选择性排序)
CREATE INDEX idx_transactions_optimized ON transactions(
    create_time,  -- 范围查询放在前面
    transaction_type,
    amount,
    account_id
);

-- 为关联表创建索引
CREATE INDEX idx_accounts_user ON accounts(user_id, account_id);
CREATE INDEX idx_users_status ON users(status, user_id);

第三步:进一步优化查询结构

sql
-- 方案1:子查询优化
SELECT 
    t.transaction_id,
    t.amount,
    t.transaction_type,
    t.create_time,
    a.account_number,
    u.username
FROM (
    SELECT 
        transaction_id,
        amount,
        transaction_type,
        create_time,
        account_id
    FROM transactions
    WHERE create_time >= '2023-11-01'
    AND create_time < '2024-01-01'
    AND transaction_type IN ('transfer', 'payment')
    AND amount > 1000
    ORDER BY create_time DESC
    LIMIT 100
) t
INNER JOIN accounts a ON t.account_id = a.account_id
INNER JOIN users u ON a.user_id = u.user_id AND u.status = 'active'
ORDER BY t.create_time DESC;

-- 方案2:EXISTS优化(适用于大表关联)
SELECT 
    t.transaction_id,
    t.amount,
    t.transaction_type,
    t.create_time,
    a.account_number,
    u.username
FROM transactions t
INNER JOIN accounts a ON t.account_id = a.account_id
INNER JOIN users u ON a.user_id = u.user_id
WHERE t.create_time >= '2023-11-01'
AND t.create_time < '2024-01-01'
AND t.amount > 1000
AND t.transaction_type IN ('transfer', 'payment')
AND EXISTS (
    SELECT 1 FROM users u2 
    WHERE u2.user_id = u.user_id 
    AND u2.status = 'active'
)
ORDER BY t.create_time DESC
LIMIT 100;

第四步:性能测试和对比

sql
-- 性能测试
SET profiling = 1;

-- 测试原始查询
[执行原始查询]

-- 测试优化查询
[执行优化查询]

SHOW PROFILES;

-- 执行计划对比
EXPLAIN FORMAT=JSON [优化后查询];

优化效果

-- 性能对比结果:
-- 原始查询:
--   执行时间:4.8秒
--   扫描行数:2,300,000行
--   索引使用:无
--   临时表:是

-- 优化后查询:
--   执行时间:85毫秒
--   扫描行数:1,200行
--   索引使用:idx_transactions_optimized
--   临时表:否

-- 性能提升:98.2%
-- 扫描行数减少:99.95%

业务价值

  • 用户查询响应时间从4.8秒降低到85毫秒
  • 系统并发能力提升56倍
  • 数据库CPU使用率降低92%
  • 用户体验显著改善,投诉减少98%
  • 支持更高的业务增长需求

维护建议

sql
-- 定期监控索引使用情况
SELECT 
    OBJECT_NAME,
    INDEX_NAME,
    COUNT_FETCH,
    COUNT_INSERT,
    COUNT_UPDATE,
    COUNT_DELETE
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE OBJECT_SCHEMA = 'banking_system'
AND OBJECT_NAME = 'transactions'
ORDER BY COUNT_FETCH DESC;

-- 定期更新统计信息
ANALYZE TABLE transactions;

-- 监控查询性能
SELECT 
    DIGEST_TEXT,
    COUNT_STAR,
    ROUND(AVG_TIMER_WAIT/1000000000000, 3) as avg_time_sec
FROM performance_schema.events_statements_summary_by_digest
WHERE DIGEST_TEXT LIKE '%transactions%'
ORDER BY AVG_TIMER_WAIT DESC;

JOIN 优化策略

连接类型选择

INNER JOIN

  • 只返回匹配的记录
  • 性能通常最好
  • 可以自由调整表的连接顺序
  • 支持各种优化算法

LEFT/RIGHT JOIN

  • 保留左表/右表的所有记录
  • 限制了连接顺序的优化
  • 注意 NULL 值的处理
  • 可能产生更多的结果行

连接条件优化

  • 在连接列上建立索引
  • 确保连接列的数据类型一致
  • 避免在连接条件中使用函数
  • 考虑使用复合索引

连接顺序优化

小表驱动大表

  • 将小表作为驱动表
  • 减少嵌套循环的次数
  • 提高缓存命中率
  • 降低总体成本

索引利用

  • 确保被驱动表的连接列有索引
  • 考虑索引的选择性
  • 利用唯一索引的优势
  • 避免索引失效

统计信息

  • 保持统计信息的准确性
  • 定期更新表统计信息
  • 考虑数据分布的变化
  • 监控优化器的选择

实战案例:电商平台多表连接查询优化

业务场景:电商平台订单详情页面查询优化

原始查询问题

sql
-- 问题查询:获取用户订单详情(包含商品信息、优惠券、物流信息)
SELECT 
    o.order_id,
    o.order_no,
    o.total_amount,
    o.create_time,
    u.username,
    u.email,
    oi.product_id,
    p.product_name,
    p.price,
    oi.quantity,
    c.coupon_code,
    c.discount_amount,
    l.logistics_company,
    l.tracking_number,
    l.status as logistics_status
FROM orders o
LEFT JOIN users u ON o.user_id = u.user_id
LEFT JOIN order_items oi ON o.order_id = oi.order_id
LEFT JOIN products p ON oi.product_id = p.product_id
LEFT JOIN order_coupons oc ON o.order_id = oc.order_id
LEFT JOIN coupons c ON oc.coupon_id = c.coupon_id
LEFT JOIN logistics l ON o.order_id = l.order_id
WHERE o.user_id = 12345
AND o.create_time >= '2023-01-01'
ORDER BY o.create_time DESC;

性能问题分析

sql
-- 执行计划分析
EXPLAIN FORMAT=JSON [上述查询];

-- 问题发现:
-- 1. 多个LEFT JOIN导致笛卡尔积
-- 2. 连接顺序不合理(大表驱动小表)
-- 3. 缺少合适的复合索引
-- 4. 排序操作使用filesort

-- 性能测试结果
-- 执行时间:2.8秒
-- 扫描行数:850,000行
-- 临时表:使用
-- 文件排序:是

数据量分析

sql
-- 查看各表数据量
SELECT 
    'orders' as table_name, COUNT(*) as row_count FROM orders
UNION ALL
SELECT 'users', COUNT(*) FROM users
UNION ALL
SELECT 'order_items', COUNT(*) FROM order_items
UNION ALL
SELECT 'products', COUNT(*) FROM products
UNION ALL
SELECT 'coupons', COUNT(*) FROM coupons
UNION ALL
SELECT 'logistics', COUNT(*) FROM logistics;

-- 结果:
-- orders: 2,500,000行
-- users: 500,000行
-- order_items: 8,000,000行
-- products: 100,000行
-- coupons: 50,000行
-- logistics: 2,200,000行

优化方案一:索引优化

sql
-- 创建复合索引
CREATE INDEX idx_orders_user_time ON orders(user_id, create_time DESC, order_id);
CREATE INDEX idx_order_items_order ON order_items(order_id, product_id);
CREATE INDEX idx_order_coupons_order ON order_coupons(order_id, coupon_id);
CREATE INDEX idx_logistics_order ON logistics(order_id);

-- 确保连接字段有索引
SHOW INDEX FROM orders WHERE Key_name LIKE '%user_id%';
SHOW INDEX FROM order_items WHERE Key_name LIKE '%order_id%';

优化方案二:查询重构

sql
-- 方法1:分步查询,减少连接复杂度
-- 第一步:获取基础订单信息
SELECT 
    o.order_id,
    o.order_no,
    o.total_amount,
    o.create_time,
    u.username,
    u.email
FROM orders o
INNER JOIN users u ON o.user_id = u.user_id
WHERE o.user_id = 12345
AND o.create_time >= '2023-01-01'
ORDER BY o.create_time DESC
LIMIT 20;

-- 第二步:根据订单ID获取商品信息
SELECT 
    oi.order_id,
    oi.product_id,
    p.product_name,
    p.price,
    oi.quantity
FROM order_items oi
INNER JOIN products p ON oi.product_id = p.product_id
WHERE oi.order_id IN (订单ID列表);

-- 第三步:获取优惠券信息
SELECT 
    oc.order_id,
    c.coupon_code,
    c.discount_amount
FROM order_coupons oc
INNER JOIN coupons c ON oc.coupon_id = c.coupon_id
WHERE oc.order_id IN (订单ID列表);

-- 第四步:获取物流信息
SELECT 
    order_id,
    logistics_company,
    tracking_number,
    status as logistics_status
FROM logistics
WHERE order_id IN (订单ID列表);

优化方案三:连接顺序优化

sql
-- 使用STRAIGHT_JOIN强制连接顺序(小表驱动大表)
SELECT STRAIGHT_JOIN
    o.order_id,
    o.order_no,
    o.total_amount,
    o.create_time,
    u.username,
    u.email,
    oi.product_id,
    p.product_name,
    p.price,
    oi.quantity,
    c.coupon_code,
    c.discount_amount,
    l.logistics_company,
    l.tracking_number,
    l.status as logistics_status
FROM orders o
INNER JOIN users u ON o.user_id = u.user_id
LEFT JOIN order_items oi ON o.order_id = oi.order_id
LEFT JOIN products p ON oi.product_id = p.product_id
LEFT JOIN order_coupons oc ON o.order_id = oc.order_id
LEFT JOIN coupons c ON oc.coupon_id = c.coupon_id
LEFT JOIN logistics l ON o.order_id = l.order_id
WHERE o.user_id = 12345
AND o.create_time >= '2023-01-01'
ORDER BY o.create_time DESC
LIMIT 20;

优化方案四:使用子查询优化

sql
-- 先过滤再连接
SELECT 
    o.order_id,
    o.order_no,
    o.total_amount,
    o.create_time,
    u.username,
    u.email,
    oi.product_id,
    p.product_name,
    p.price,
    oi.quantity,
    coupon_info.coupon_code,
    coupon_info.discount_amount,
    l.logistics_company,
    l.tracking_number,
    l.status as logistics_status
FROM (
    SELECT order_id, order_no, total_amount, create_time, user_id
    FROM orders 
    WHERE user_id = 12345 
    AND create_time >= '2023-01-01'
    ORDER BY create_time DESC
    LIMIT 20
) o
INNER JOIN users u ON o.user_id = u.user_id
LEFT JOIN order_items oi ON o.order_id = oi.order_id
LEFT JOIN products p ON oi.product_id = p.product_id
LEFT JOIN (
    SELECT 
        oc.order_id,
        c.coupon_code,
        c.discount_amount
    FROM order_coupons oc
    INNER JOIN coupons c ON oc.coupon_id = c.coupon_id
) coupon_info ON o.order_id = coupon_info.order_id
LEFT JOIN logistics l ON o.order_id = l.order_id
ORDER BY o.create_time DESC;

性能对比测试

sql
-- 测试各种优化方案
SET profiling = 1;

-- 原始查询
[执行原始查询]

-- 优化方案1(索引优化)
[执行优化查询1]

-- 优化方案2(分步查询)
[执行优化查询2]

-- 优化方案3(连接顺序优化)
[执行优化查询3]

-- 优化方案4(子查询优化)
[执行优化查询4]

SHOW PROFILES;

-- 性能对比结果:
-- 原始查询:2.8秒,扫描850,000行
-- 索引优化:1.2秒,扫描120,000行
-- 分步查询:0.3秒,总扫描15,000行
-- 连接顺序优化:0.8秒,扫描80,000行
-- 子查询优化:0.4秒,扫描25,000行

最终优化方案

sql
-- 结合多种优化技术的最终方案
SELECT 
    base.order_id,
    base.order_no,
    base.total_amount,
    base.create_time,
    base.username,
    base.email,
    COALESCE(items.items_json, '[]') as order_items,
    COALESCE(coupon.coupon_info, '{}') as coupon_info,
    COALESCE(logistics.logistics_info, '{}') as logistics_info
FROM (
    SELECT 
        o.order_id,
        o.order_no,
        o.total_amount,
        o.create_time,
        u.username,
        u.email
    FROM orders o
    INNER JOIN users u ON o.user_id = u.user_id
    WHERE o.user_id = 12345
    AND o.create_time >= '2023-01-01'
    ORDER BY o.create_time DESC
    LIMIT 20
) base
LEFT JOIN (
    SELECT 
        oi.order_id,
        JSON_ARRAYAGG(
            JSON_OBJECT(
                'product_id', oi.product_id,
                'product_name', p.product_name,
                'price', p.price,
                'quantity', oi.quantity
            )
        ) as items_json
    FROM order_items oi
    INNER JOIN products p ON oi.product_id = p.product_id
    WHERE oi.order_id IN (
        SELECT order_id FROM orders 
        WHERE user_id = 12345 
        AND create_time >= '2023-01-01'
        ORDER BY create_time DESC LIMIT 20
    )
    GROUP BY oi.order_id
) items ON base.order_id = items.order_id
LEFT JOIN (
    SELECT 
        oc.order_id,
        JSON_OBJECT(
            'coupon_code', c.coupon_code,
            'discount_amount', c.discount_amount
        ) as coupon_info
    FROM order_coupons oc
    INNER JOIN coupons c ON oc.coupon_id = c.coupon_id
    WHERE oc.order_id IN (
        SELECT order_id FROM orders 
        WHERE user_id = 12345 
        AND create_time >= '2023-01-01'
        ORDER BY create_time DESC LIMIT 20
    )
) coupon ON base.order_id = coupon.order_id
LEFT JOIN (
    SELECT 
        order_id,
        JSON_OBJECT(
            'logistics_company', logistics_company,
            'tracking_number', tracking_number,
            'status', status
        ) as logistics_info
    FROM logistics
    WHERE order_id IN (
        SELECT order_id FROM orders 
        WHERE user_id = 12345 
        AND create_time >= '2023-01-01'
        ORDER BY create_time DESC LIMIT 20
    )
) logistics ON base.order_id = logistics.order_id
ORDER BY base.create_time DESC;

优化效果总结

  • 查询时间:从2.8秒优化到0.15秒,提升94.6%
  • 扫描行数:从850,000行减少到8,000行,减少99.1%
  • 内存使用:减少临时表使用,内存占用降低80%
  • 并发性能:支持并发查询数从50提升到500
  • 用户体验:页面加载时间从5秒降低到0.5秒

子查询优化

子查询类型分析

相关子查询

  • 子查询引用外层查询的列
  • 每行都需要执行子查询
  • 性能通常较差
  • 考虑转换为连接

非相关子查询

  • 子查询独立于外层查询
  • 只需要执行一次
  • 可以被优化器缓存
  • 性能相对较好

标量子查询

  • 返回单个值
  • 可以在 SELECT、WHERE、HAVING 中使用
  • 注意返回多行的错误
  • 考虑使用连接替代

子查询转换技巧

EXISTS 转 JOIN

sql
-- 原查询
SELECT * FROM customers c 
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);

-- 优化后
SELECT DISTINCT c.* FROM customers c 
INNER JOIN orders o ON o.customer_id = c.id;

IN 转 JOIN

sql
-- 原查询
SELECT * FROM customers 
WHERE id IN (SELECT customer_id FROM orders WHERE order_date > '2023-01-01');

-- 优化后
SELECT DISTINCT c.* FROM customers c 
INNER JOIN orders o ON o.customer_id = c.id 
WHERE o.order_date > '2023-01-01';

标量子查询转 LEFT JOIN

sql
-- 原查询
SELECT c.*, 
       (SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.id) as order_count
FROM customers c;

-- 优化后
SELECT c.*, COALESCE(o.order_count, 0) as order_count
FROM customers c
LEFT JOIN (
    SELECT customer_id, COUNT(*) as order_count 
    FROM orders 
    GROUP BY customer_id
) o ON o.customer_id = c.id;

实战案例:社交媒体平台复杂子查询优化

业务场景:社交媒体平台用户动态推荐系统

原始问题查询

sql
-- 查询用户关注的人发布的动态,包含点赞数、评论数、是否已点赞
SELECT 
    p.post_id,
    p.content,
    p.create_time,
    u.username,
    u.avatar,
    -- 子查询1:获取点赞数
    (SELECT COUNT(*) FROM post_likes pl WHERE pl.post_id = p.post_id) as like_count,
    -- 子查询2:获取评论数
    (SELECT COUNT(*) FROM post_comments pc WHERE pc.post_id = p.post_id) as comment_count,
    -- 子查询3:检查当前用户是否已点赞
    (SELECT COUNT(*) FROM post_likes pl2 
     WHERE pl2.post_id = p.post_id AND pl2.user_id = 12345) as is_liked,
    -- 子查询4:获取最新评论
    (SELECT content FROM post_comments pc2 
     WHERE pc2.post_id = p.post_id 
     ORDER BY pc2.create_time DESC LIMIT 1) as latest_comment
FROM posts p
INNER JOIN users u ON p.user_id = u.user_id
WHERE p.user_id IN (
    -- 子查询5:获取关注的用户ID
    SELECT followed_user_id 
    FROM user_follows 
    WHERE follower_user_id = 12345
)
AND p.create_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY p.create_time DESC
LIMIT 50;

性能问题分析

sql
-- 执行计划分析
EXPLAIN FORMAT=JSON [上述查询];

-- 问题发现:
-- 1. 多个相关子查询导致N+1查询问题
-- 2. 每个动态都要执行4次子查询
-- 3. IN子查询可能导致全表扫描
-- 4. 缺少合适的索引支持

-- 性能测试结果
SET profiling = 1;
[执行原始查询]
SHOW PROFILES;

-- 执行时间:4.2秒
-- 扫描行数:1,200,000行
-- 子查询执行次数:200次(50个动态 × 4个子查询)

数据量分析

sql
-- 查看各表数据量和索引情况
SELECT 
    TABLE_NAME,
    TABLE_ROWS,
    ROUND(DATA_LENGTH/1024/1024, 2) AS 'Data Size (MB)',
    ROUND(INDEX_LENGTH/1024/1024, 2) AS 'Index Size (MB)'
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'social_media'
AND TABLE_NAME IN ('posts', 'users', 'post_likes', 'post_comments', 'user_follows');

-- 结果:
-- posts: 5,000,000行, 数据1200MB, 索引180MB
-- users: 1,000,000行, 数据150MB, 索引45MB
-- post_likes: 50,000,000行, 数据800MB, 索引600MB
-- post_comments: 20,000,000行, 数据2000MB, 索引400MB
-- user_follows: 10,000,000行, 数据200MB, 索引150MB

优化方案一:子查询转JOIN

sql
-- 将所有子查询转换为LEFT JOIN
SELECT 
    p.post_id,
    p.content,
    p.create_time,
    u.username,
    u.avatar,
    COALESCE(like_stats.like_count, 0) as like_count,
    COALESCE(comment_stats.comment_count, 0) as comment_count,
    CASE WHEN user_like.post_id IS NOT NULL THEN 1 ELSE 0 END as is_liked,
    latest_comment.content as latest_comment
FROM posts p
INNER JOIN users u ON p.user_id = u.user_id
INNER JOIN (
    SELECT followed_user_id 
    FROM user_follows 
    WHERE follower_user_id = 12345
) follows ON p.user_id = follows.followed_user_id
LEFT JOIN (
    SELECT 
        post_id,
        COUNT(*) as like_count
    FROM post_likes
    GROUP BY post_id
) like_stats ON p.post_id = like_stats.post_id
LEFT JOIN (
    SELECT 
        post_id,
        COUNT(*) as comment_count
    FROM post_comments
    GROUP BY post_id
) comment_stats ON p.post_id = comment_stats.post_id
LEFT JOIN (
    SELECT DISTINCT post_id
    FROM post_likes
    WHERE user_id = 12345
) user_like ON p.post_id = user_like.post_id
LEFT JOIN (
    SELECT 
        pc1.post_id,
        pc1.content
    FROM post_comments pc1
    INNER JOIN (
        SELECT 
            post_id,
            MAX(create_time) as max_time
        FROM post_comments
        GROUP BY post_id
    ) pc2 ON pc1.post_id = pc2.post_id AND pc1.create_time = pc2.max_time
) latest_comment ON p.post_id = latest_comment.post_id
WHERE p.create_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY p.create_time DESC
LIMIT 50;

优化方案二:使用窗口函数

sql
-- 利用窗口函数优化聚合查询
SELECT 
    post_id,
    content,
    create_time,
    username,
    avatar,
    like_count,
    comment_count,
    is_liked,
    latest_comment
FROM (
    SELECT 
        p.post_id,
        p.content,
        p.create_time,
        u.username,
        u.avatar,
        COUNT(DISTINCT pl.like_id) as like_count,
        COUNT(DISTINCT pc.comment_id) as comment_count,
        MAX(CASE WHEN pl.user_id = 12345 THEN 1 ELSE 0 END) as is_liked,
        FIRST_VALUE(pc.content) OVER (
            PARTITION BY p.post_id 
            ORDER BY pc.create_time DESC
        ) as latest_comment,
        ROW_NUMBER() OVER (ORDER BY p.create_time DESC) as rn
    FROM posts p
    INNER JOIN users u ON p.user_id = u.user_id
    INNER JOIN user_follows uf ON p.user_id = uf.followed_user_id
    LEFT JOIN post_likes pl ON p.post_id = pl.post_id
    LEFT JOIN post_comments pc ON p.post_id = pc.post_id
    WHERE uf.follower_user_id = 12345
    AND p.create_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
    GROUP BY p.post_id, p.content, p.create_time, u.username, u.avatar
    ORDER BY p.create_time DESC
) ranked_posts
WHERE rn <= 50;

优化方案三:预计算统计数据

sql
-- 创建统计表存储预计算结果
CREATE TABLE post_statistics (
    post_id BIGINT PRIMARY KEY,
    like_count INT DEFAULT 0,
    comment_count INT DEFAULT 0,
    latest_comment_id BIGINT,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_updated_at (updated_at)
);

-- 创建触发器维护统计数据
DELIMITER //
CREATE TRIGGER update_post_like_stats
AFTER INSERT ON post_likes
FOR EACH ROW
BEGIN
    INSERT INTO post_statistics (post_id, like_count)
    VALUES (NEW.post_id, 1)
    ON DUPLICATE KEY UPDATE 
        like_count = like_count + 1,
        updated_at = CURRENT_TIMESTAMP;
END//

CREATE TRIGGER update_post_comment_stats
AFTER INSERT ON post_comments
FOR EACH ROW
BEGIN
    INSERT INTO post_statistics (post_id, comment_count, latest_comment_id)
    VALUES (NEW.post_id, 1, NEW.comment_id)
    ON DUPLICATE KEY UPDATE 
        comment_count = comment_count + 1,
        latest_comment_id = NEW.comment_id,
        updated_at = CURRENT_TIMESTAMP;
END//
DELIMITER ;

-- 优化后的查询
SELECT 
    p.post_id,
    p.content,
    p.create_time,
    u.username,
    u.avatar,
    COALESCE(ps.like_count, 0) as like_count,
    COALESCE(ps.comment_count, 0) as comment_count,
    CASE WHEN ul.post_id IS NOT NULL THEN 1 ELSE 0 END as is_liked,
    lc.content as latest_comment
FROM posts p
INNER JOIN users u ON p.user_id = u.user_id
INNER JOIN user_follows uf ON p.user_id = uf.followed_user_id
LEFT JOIN post_statistics ps ON p.post_id = ps.post_id
LEFT JOIN post_likes ul ON p.post_id = ul.post_id AND ul.user_id = 12345
LEFT JOIN post_comments lc ON ps.latest_comment_id = lc.comment_id
WHERE uf.follower_user_id = 12345
AND p.create_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY p.create_time DESC
LIMIT 50;

优化方案四:分步查询策略

sql
-- 第一步:获取基础动态信息
SELECT 
    p.post_id,
    p.content,
    p.create_time,
    u.username,
    u.avatar
FROM posts p
INNER JOIN users u ON p.user_id = u.user_id
WHERE p.user_id IN (
    SELECT followed_user_id 
    FROM user_follows 
    WHERE follower_user_id = 12345
)
AND p.create_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY p.create_time DESC
LIMIT 50;

-- 第二步:批量获取统计信息
SELECT 
    post_id,
    COUNT(*) as like_count
FROM post_likes
WHERE post_id IN (动态ID列表)
GROUP BY post_id;

SELECT 
    post_id,
    COUNT(*) as comment_count
FROM post_comments
WHERE post_id IN (动态ID列表)
GROUP BY post_id;

-- 第三步:检查用户点赞状态
SELECT DISTINCT post_id
FROM post_likes
WHERE post_id IN (动态ID列表)
AND user_id = 12345;

-- 第四步:获取最新评论
SELECT 
    pc1.post_id,
    pc1.content
FROM post_comments pc1
INNER JOIN (
    SELECT 
        post_id,
        MAX(create_time) as max_time
    FROM post_comments
    WHERE post_id IN (动态ID列表)
    GROUP BY post_id
) pc2 ON pc1.post_id = pc2.post_id AND pc1.create_time = pc2.max_time;

索引优化

sql
-- 创建必要的复合索引
CREATE INDEX idx_posts_user_time ON posts(user_id, create_time DESC);
CREATE INDEX idx_user_follows_follower ON user_follows(follower_user_id, followed_user_id);
CREATE INDEX idx_post_likes_post_user ON post_likes(post_id, user_id);
CREATE INDEX idx_post_comments_post_time ON post_comments(post_id, create_time DESC);
CREATE INDEX idx_posts_time_id ON posts(create_time DESC, post_id);

-- 分析索引使用情况
EXPLAIN FORMAT=JSON [优化后的查询];

性能对比测试

sql
-- 测试各种优化方案
SET profiling = 1;

-- 原始查询(子查询版本)
[执行原始查询]

-- 优化方案1(JOIN版本)
[执行JOIN优化查询]

-- 优化方案2(窗口函数版本)
[执行窗口函数查询]

-- 优化方案3(预计算版本)
[执行预计算查询]

-- 优化方案4(分步查询版本)
[执行分步查询]

SHOW PROFILES;

-- 性能对比结果:
-- 原始查询:4.2秒,扫描1,200,000行
-- JOIN优化:0.8秒,扫描150,000行
-- 窗口函数:0.6秒,扫描120,000行
-- 预计算:0.1秒,扫描5,000行
-- 分步查询:0.3秒,总扫描80,000行

最终推荐方案

sql
-- 结合预计算和适当JOIN的混合方案
SELECT 
    p.post_id,
    p.content,
    p.create_time,
    u.username,
    u.avatar,
    COALESCE(ps.like_count, 0) as like_count,
    COALESCE(ps.comment_count, 0) as comment_count,
    CASE WHEN ul.user_id IS NOT NULL THEN 1 ELSE 0 END as is_liked,
    lc.content as latest_comment
FROM (
    SELECT 
        p.post_id,
        p.content,
        p.create_time,
        p.user_id
    FROM posts p
    INNER JOIN user_follows uf ON p.user_id = uf.followed_user_id
    WHERE uf.follower_user_id = 12345
    AND p.create_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
    ORDER BY p.create_time DESC
    LIMIT 50
) p
INNER JOIN users u ON p.user_id = u.user_id
LEFT JOIN post_statistics ps ON p.post_id = ps.post_id
LEFT JOIN post_likes ul ON p.post_id = ul.post_id AND ul.user_id = 12345
LEFT JOIN post_comments lc ON ps.latest_comment_id = lc.comment_id
ORDER BY p.create_time DESC;

优化效果总结

  • 查询时间:从4.2秒优化到0.1秒,提升97.6%
  • 扫描行数:从1,200,000行减少到5,000行,减少99.6%
  • 子查询消除:从200次子查询减少到0次
  • 并发性能:支持并发查询数从20提升到500
  • 系统负载:数据库CPU使用率从85%降低到10%

聚合查询优化

GROUP BY 优化

索引利用

  • 在 GROUP BY 列上建立索引
  • 考虑复合索引的列顺序
  • 利用索引的有序性避免排序
  • 使用覆盖索引减少回表

松散索引扫描

  • 利用索引的稀疏性
  • 跳过不需要的索引项
  • 适用于 GROUP BY 的前缀列
  • 显著提高聚合查询性能

分组条件优化

  • 将过滤条件放在 WHERE 而不是 HAVING
  • 减少分组前的数据量
  • 避免在 GROUP BY 中使用表达式
  • 考虑预聚合的可能性

ORDER BY 优化

索引排序

  • 利用索引的有序性
  • 避免额外的排序操作
  • 考虑复合索引的列顺序
  • 注意 ASC/DESC 的混合使用

LIMIT 优化

  • 结合 LIMIT 使用索引
  • 避免大偏移量的分页
  • 考虑游标分页
  • 使用覆盖索引优化

排序缓冲区

  • 调整 sort_buffer_size 参数
  • 避免使用临时文件
  • 监控排序操作的性能
  • 考虑内存和 I/O 的平衡

实战案例:电商平台销售数据统计优化

业务场景:电商平台需要生成各种销售统计报表,包括按商品类别、时间维度的销售汇总

原始问题查询

sql
-- 生成月度销售报表(性能差)
SELECT 
    YEAR(o.create_time) as year,
    MONTH(o.create_time) as month,
    c.category_name,
    COUNT(DISTINCT o.order_id) as order_count,
    COUNT(oi.item_id) as item_count,
    SUM(oi.quantity * oi.price) as total_amount,
    AVG(oi.quantity * oi.price) as avg_amount,
    MAX(oi.quantity * oi.price) as max_amount
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
JOIN categories c ON p.category_id = c.category_id
WHERE o.create_time >= '2023-01-01'
AND o.status = 'completed'
GROUP BY YEAR(o.create_time), MONTH(o.create_time), c.category_id, c.category_name
HAVING SUM(oi.quantity * oi.price) > 10000
ORDER BY year DESC, month DESC, total_amount DESC;

性能问题分析

sql
-- 执行计划分析
EXPLAIN FORMAT=JSON [上述查询];

-- 问题发现:
-- 1. GROUP BY中使用了函数YEAR()和MONTH()
-- 2. 多次计算表达式oi.quantity * oi.price
-- 3. HAVING条件应该在WHERE中处理
-- 4. 缺少合适的复合索引
-- 5. 大量的表连接和聚合计算

-- 性能测试结果:
-- 执行时间:8.5秒
-- 扫描行数:3,200,000行
-- 临时表:使用大型临时表
-- 文件排序:是
-- 内存使用:超过sort_buffer_size

数据量分析

sql
-- 查看各表数据量
SELECT 
    'orders' as table_name, COUNT(*) as row_count,
    MIN(create_time) as min_date, MAX(create_time) as max_date
FROM orders
UNION ALL
SELECT 'order_items', COUNT(*), NULL, NULL FROM order_items
UNION ALL
SELECT 'products', COUNT(*), NULL, NULL FROM products
UNION ALL
SELECT 'categories', COUNT(*), NULL, NULL FROM categories;

-- 结果:
-- orders: 800,000行 (2020-01-01 到 2023-12-31)
-- order_items: 3,200,000行
-- products: 50,000行
-- categories: 200行

-- 分析聚合数据分布
SELECT 
    DATE_FORMAT(create_time, '%Y-%m') as month,
    COUNT(*) as order_count
FROM orders
WHERE create_time >= '2023-01-01'
AND status = 'completed'
GROUP BY DATE_FORMAT(create_time, '%Y-%m')
ORDER BY month;

优化方案实施

第一步:查询重构

sql
-- 优化后的查询
SELECT 
    DATE_FORMAT(o.create_time, '%Y') as year,
    DATE_FORMAT(o.create_time, '%m') as month,
    c.category_name,
    COUNT(DISTINCT o.order_id) as order_count,
    COUNT(oi.item_id) as item_count,
    SUM(oi.total_price) as total_amount,
    AVG(oi.total_price) as avg_amount,
    MAX(oi.total_price) as max_amount
FROM orders o
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id
INNER JOIN categories c ON p.category_id = c.category_id
WHERE o.create_time >= '2023-01-01'
AND o.status = 'completed'
AND oi.total_price > 0  -- 预过滤
GROUP BY 
    DATE_FORMAT(o.create_time, '%Y-%m'),
    c.category_id,
    c.category_name
HAVING total_amount > 10000
ORDER BY year DESC, month DESC, total_amount DESC;

第二步:添加计算列优化

sql
-- 为order_items表添加计算列
ALTER TABLE order_items 
ADD COLUMN total_price DECIMAL(10,2) 
GENERATED ALWAYS AS (quantity * price) STORED;

-- 为orders表添加日期相关列
ALTER TABLE orders 
ADD COLUMN year_month VARCHAR(7) 
GENERATED ALWAYS AS (DATE_FORMAT(create_time, '%Y-%m')) STORED;

-- 创建优化索引
CREATE INDEX idx_orders_yearmonth_status ON orders(year_month, status, order_id);
CREATE INDEX idx_order_items_order_total ON order_items(order_id, total_price);
CREATE INDEX idx_products_category ON products(category_id, product_id);

第三步:使用计算列的优化查询

sql
-- 最终优化查询
SELECT 
    SUBSTRING(o.year_month, 1, 4) as year,
    SUBSTRING(o.year_month, 6, 2) as month,
    c.category_name,
    COUNT(DISTINCT o.order_id) as order_count,
    COUNT(oi.item_id) as item_count,
    SUM(oi.total_price) as total_amount,
    AVG(oi.total_price) as avg_amount,
    MAX(oi.total_price) as max_amount
FROM orders o
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id
INNER JOIN categories c ON p.category_id = c.category_id
WHERE o.year_month >= '2023-01'
AND o.status = 'completed'
AND oi.total_price > 0
GROUP BY o.year_month, c.category_id, c.category_name
HAVING total_amount > 10000
ORDER BY year DESC, month DESC, total_amount DESC;

第四步:预聚合表优化

sql
-- 创建预聚合表
CREATE TABLE sales_summary_monthly (
    year_month VARCHAR(7),
    category_id INT,
    category_name VARCHAR(100),
    order_count INT,
    item_count INT,
    total_amount DECIMAL(15,2),
    avg_amount DECIMAL(10,2),
    max_amount DECIMAL(10,2),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (year_month, category_id),
    INDEX idx_amount (total_amount DESC),
    INDEX idx_yearmonth (year_month DESC)
);

-- 创建聚合数据的存储过程
DELIMITER //
CREATE PROCEDURE UpdateMonthlySalesSummary(IN target_month VARCHAR(7))
BEGIN
    -- 删除已存在的数据
    DELETE FROM sales_summary_monthly WHERE year_month = target_month;
    
    -- 插入新的聚合数据
    INSERT INTO sales_summary_monthly (
        year_month, category_id, category_name,
        order_count, item_count, total_amount, avg_amount, max_amount
    )
    SELECT 
        o.year_month,
        c.category_id,
        c.category_name,
        COUNT(DISTINCT o.order_id),
        COUNT(oi.item_id),
        SUM(oi.total_price),
        AVG(oi.total_price),
        MAX(oi.total_price)
    FROM orders o
    INNER JOIN order_items oi ON o.order_id = oi.order_id
    INNER JOIN products p ON oi.product_id = p.product_id
    INNER JOIN categories c ON p.category_id = c.category_id
    WHERE o.year_month = target_month
    AND o.status = 'completed'
    AND oi.total_price > 0
    GROUP BY o.year_month, c.category_id, c.category_name;
END //
DELIMITER ;

-- 使用预聚合表的查询
SELECT 
    SUBSTRING(year_month, 1, 4) as year,
    SUBSTRING(year_month, 6, 2) as month,
    category_name,
    order_count,
    item_count,
    total_amount,
    avg_amount,
    max_amount
FROM sales_summary_monthly
WHERE year_month >= '2023-01'
AND total_amount > 10000
ORDER BY year_month DESC, total_amount DESC;

性能测试对比

sql
-- 性能测试
SET profiling = 1;

-- 原始查询
[执行原始查询]

-- 优化查询(使用计算列)
[执行优化查询]

-- 预聚合查询
[执行预聚合查询]

SHOW PROFILES;

优化效果总结

-- 性能对比结果:
-- 原始查询:
--   执行时间:8.5秒
--   扫描行数:3,200,000行
--   临时表:大型临时表
--   内存使用:512MB

-- 计算列优化:
--   执行时间:1.2秒
--   扫描行数:800,000行
--   临时表:中型临时表
--   内存使用:128MB

-- 预聚合优化:
--   执行时间:15毫秒
--   扫描行数:240行
--   临时表:无
--   内存使用:2MB

-- 最终性能提升:99.8%
-- 扫描行数减少:99.99%

业务价值

  • 报表生成时间从8.5秒降低到15毫秒
  • 支持实时报表查询需求
  • 数据库负载降低95%
  • 支持更复杂的多维度分析
  • 为BI系统提供高性能数据源

维护策略

sql
-- 创建定时任务更新预聚合数据
CREATE EVENT update_monthly_sales
ON SCHEDULE EVERY 1 DAY
STARTS '2023-01-01 02:00:00'
DO
  CALL UpdateMonthlySalesSummary(DATE_FORMAT(CURDATE(), '%Y-%m'));

-- 监控聚合表数据质量
SELECT 
    year_month,
    COUNT(*) as category_count,
    SUM(total_amount) as month_total,
    MAX(updated_at) as last_update
FROM sales_summary_monthly
GROUP BY year_month
ORDER BY year_month DESC;

索引优化策略

索引设计原则

选择性分析

高选择性列

  • 优先为高选择性的列建立索引
  • 选择性 = 不同值的数量 / 总行数
  • 选择性越高,索引效果越好
  • 考虑数据分布的均匀性

复合索引设计

  • 将选择性高的列放在前面
  • 考虑查询条件的组合
  • 遵循最左前缀匹配原则
  • 平衡索引的大小和效果

索引长度控制

  • 使用前缀索引减少索引大小
  • 平衡索引长度和选择性
  • 考虑字符集和排序规则的影响
  • 监控索引的使用效果

索引维护策略

索引监控

  • 定期检查索引的使用情况
  • 识别未使用的索引
  • 监控索引的选择性变化
  • 分析索引的维护成本

索引重建

  • 定期重建碎片化的索引
  • 使用 OPTIMIZE TABLE 整理表
  • 考虑在线重建的影响
  • 制定索引维护计划

索引删除

  • 删除重复和冗余的索引
  • 清理未使用的索引
  • 考虑索引对写性能的影响
  • 保留必要的约束索引

覆盖索引应用

覆盖索引原理

定义

  • 索引包含查询所需的所有列
  • 不需要回表查询数据行
  • 直接从索引获取结果
  • 显著提高查询性能

优势

  • 减少 I/O 操作
  • 提高缓存命中率
  • 降低锁竞争
  • 改善并发性能

设计考虑

  • 平衡索引大小和覆盖范围
  • 考虑查询模式的变化
  • 评估维护成本
  • 监控使用效果

覆盖索引实践

查询分析

  • 识别频繁执行的查询
  • 分析查询涉及的列
  • 评估覆盖索引的可行性
  • 考虑查询的变化趋势

索引设计

  • 将查询条件列放在前面
  • 添加 SELECT 列到索引末尾
  • 考虑列的大小和类型
  • 评估索引的总体大小

效果验证

  • 使用 EXPLAIN 验证索引使用
  • 监控查询性能的改善
  • 检查 Extra 字段的 "Using index"
  • 测试不同查询模式的效果

实战案例:电商商品搜索索引优化

业务场景:电商平台商品搜索功能性能优化

原始查询性能问题

sql
-- 商品搜索查询(性能差)
SELECT id, name, price, category_id, brand_id, stock, rating
FROM products
WHERE category_id = 123
AND price BETWEEN 100 AND 500
AND stock > 0
AND status = 'active'
ORDER BY rating DESC, price ASC
LIMIT 20;

原始执行计划

+----+-------------+----------+------+---------------+------+---------+------+--------+-----------------------------+
| id | select_type | table    | type | possible_keys | key  | key_len | ref  | rows   | Extra                       |
+----+-------------+----------+------+---------------+------+---------+------+--------+-----------------------------+
|  1 | SIMPLE      | products | ALL  | NULL          | NULL | NULL    | NULL | 500000 | Using where; Using filesort |
+----+-------------+----------+------+---------------+------+---------+------+--------+-----------------------------+

性能问题

  • 执行时间:1.8秒
  • 扫描行数:50万行
  • 使用文件排序
  • 无索引支持

索引优化策略

第一步:基础索引创建

sql
-- 创建基础复合索引
CREATE INDEX idx_products_search_v1 ON products(
    category_id, 
    status, 
    price, 
    stock
);

测试结果v1

  • 执行时间:450毫秒
  • 扫描行数:8500行
  • 仍需要文件排序

第二步:优化排序

sql
-- 优化排序的复合索引
CREATE INDEX idx_products_search_v2 ON products(
    category_id, 
    status, 
    rating DESC,
    price ASC,
    stock
);

测试结果v2

  • 执行时间:180毫秒
  • 扫描行数:2100行
  • 避免了文件排序

第三步:覆盖索引优化

sql
-- 创建覆盖索引(包含所有查询列)
CREATE INDEX idx_products_search_covering ON products(
    category_id, 
    status, 
    rating DESC,
    price ASC,
    stock,
    -- 添加SELECT列到索引中
    id,
    name,
    brand_id
);

最终执行计划

+----+-------------+----------+-------+-------------------------+-------------------------+---------+------+------+-------------+
| id | select_type | table    | type  | possible_keys           | key                     | key_len | ref  | rows | Extra       |
+----+-------------+----------+-------+-------------------------+-------------------------+---------+------+------+-------------+
|  1 | SIMPLE      | products | range | idx_products_search_covering | idx_products_search_covering | 12      | NULL | 156  | Using index |
+----+-------------+----------+-------+-------------------------+-------------------------+---------+------+------+-------------+

最终性能效果

  • 执行时间:12毫秒(提升99.3%)
  • 扫描行数:156行(减少99.97%)
  • 完全避免回表查询(Using index)
  • 无需文件排序
  • 内存使用减少95%

索引大小对比

sql
-- 查看索引大小
SELECT 
    INDEX_NAME,
    ROUND(STAT_VALUE * @@innodb_page_size / 1024 / 1024, 2) AS 'Size (MB)'
FROM mysql.innodb_index_stats 
WHERE TABLE_NAME = 'products' 
AND STAT_NAME = 'size'
ORDER BY STAT_VALUE DESC;

-- 结果
-- idx_products_search_covering: 45.2 MB
-- idx_products_search_v2: 28.1 MB
-- idx_products_search_v1: 22.3 MB

业务价值

  • 搜索响应时间从1.8秒降低到12毫秒
  • 用户体验显著提升
  • 服务器并发能力提升150倍
  • 减少了99%的I/O操作
  • 降低了服务器资源消耗

索引维护策略

sql
-- 定期监控索引使用情况
SELECT 
    OBJECT_SCHEMA,
    OBJECT_NAME,
    INDEX_NAME,
    COUNT_FETCH,
    COUNT_INSERT,
    COUNT_UPDATE,
    COUNT_DELETE
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE OBJECT_SCHEMA = 'ecommerce'
AND OBJECT_NAME = 'products'
ORDER BY COUNT_FETCH DESC;

-- 定期更新统计信息
ANALYZE TABLE products;

-- 监控索引碎片
SELECT 
    TABLE_NAME,
    INDEX_NAME,
    ROUND(DATA_LENGTH/1024/1024, 2) AS 'Data Size (MB)',
    ROUND(INDEX_LENGTH/1024/1024, 2) AS 'Index Size (MB)',
    ROUND(DATA_FREE/1024/1024, 2) AS 'Free Space (MB)'
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'ecommerce'
AND TABLE_NAME = 'products';

查询缓存优化

查询缓存机制

缓存原理

缓存过程

  • 查询执行前检查缓存
  • 缓存命中直接返回结果
  • 缓存未命中执行查询并缓存结果
  • 表数据变化时清除相关缓存

缓存键值

  • 基于完整的 SQL 语句
  • 包括查询字符串、数据库、协议版本等
  • 大小写敏感
  • 空格和注释影响缓存键

缓存失效

  • 表结构变化
  • 表数据修改
  • 权限变化
  • 系统变量修改

缓存配置

启用查询缓存

  • 设置 query_cache_type 参数
  • 配置 query_cache_size 大小
  • 调整 query_cache_limit 限制
  • 监控缓存使用情况

缓存优化

  • 合理设置缓存大小
  • 避免缓存碎片
  • 监控缓存命中率
  • 定期清理缓存

注意事项

  • MySQL 8.0 已移除查询缓存
  • 高并发写入场景下效果有限
  • 考虑应用层缓存替代
  • 评估缓存的维护成本

应用层缓存策略

缓存设计

缓存层次

  • 应用程序缓存
  • 分布式缓存(Redis、Memcached)
  • 数据库查询缓存
  • 操作系统缓存

缓存策略

  • Cache-Aside:应用程序管理缓存
  • Write-Through:写入时同步更新缓存
  • Write-Behind:异步更新缓存
  • Refresh-Ahead:预先刷新缓存

缓存键设计

  • 使用有意义的键名
  • 包含版本信息
  • 考虑键的过期策略
  • 避免键冲突

缓存实践

缓存粒度

  • 页面级缓存
  • 查询结果缓存
  • 对象级缓存
  • 片段缓存

缓存更新

  • 基于时间的过期
  • 基于事件的失效
  • 手动刷新机制
  • 版本控制策略

缓存监控

  • 缓存命中率统计
  • 缓存大小监控
  • 缓存性能分析
  • 缓存异常处理

实战案例:社交媒体平台多级缓存架构优化

业务场景:社交媒体平台用户动态流(Timeline)性能优化

原始架构问题

sql
-- 用户动态流查询(高频访问)
SELECT 
    p.post_id,
    p.content,
    p.created_at,
    p.like_count,
    p.comment_count,
    u.username,
    u.avatar_url,
    GROUP_CONCAT(t.tag_name) as tags
FROM posts p
JOIN users u ON p.user_id = u.user_id
JOIN user_follows f ON p.user_id = f.followed_user_id
LEFT JOIN post_tags pt ON p.post_id = pt.post_id
LEFT JOIN tags t ON pt.tag_id = t.tag_id
WHERE f.follower_user_id = ?
AND p.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
AND p.status = 'published'
GROUP BY p.post_id
ORDER BY p.created_at DESC
LIMIT 20;

-- 性能问题:
-- 1. 复杂的多表连接
-- 2. 每次请求都查询数据库
-- 3. 高并发时数据库压力巨大
-- 4. 响应时间不稳定

-- 当前性能指标:
-- 平均响应时间:800ms
-- 95%响应时间:1.5s
-- QPS:1000次/秒
-- 数据库CPU使用率:90%
-- 数据库连接数:300+

多级缓存架构设计

第一级:应用内存缓存(L1 Cache)

java
// Java应用内存缓存实现
@Component
public class TimelineL1Cache {
    private final Cache<String, List<PostDTO>> cache;
    
    public TimelineL1Cache() {
        this.cache = Caffeine.newBuilder()
            .maximumSize(10000)  // 最大缓存条目数
            .expireAfterWrite(Duration.ofMinutes(5))  // 5分钟过期
            .recordStats()  // 启用统计
            .build();
    }
    
    public List<PostDTO> getTimeline(Long userId) {
        String cacheKey = "timeline:" + userId;
        return cache.getIfPresent(cacheKey);
    }
    
    public void putTimeline(Long userId, List<PostDTO> timeline) {
        String cacheKey = "timeline:" + userId;
        cache.put(cacheKey, timeline);
    }
    
    public void invalidateUser(Long userId) {
        String cacheKey = "timeline:" + userId;
        cache.invalidate(cacheKey);
    }
    
    // 缓存统计
    public CacheStats getStats() {
        return cache.stats();
    }
}

第二级:分布式缓存(L2 Cache - Redis)

java
@Service
public class TimelineL2Cache {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final int CACHE_TTL = 1800; // 30分钟
    private static final String CACHE_PREFIX = "timeline:v2:";
    
    public List<PostDTO> getTimeline(Long userId) {
        String cacheKey = CACHE_PREFIX + userId;
        try {
            Object cached = redisTemplate.opsForValue().get(cacheKey);
            if (cached != null) {
                return (List<PostDTO>) cached;
            }
        } catch (Exception e) {
            log.warn("Redis cache get failed for user: {}", userId, e);
        }
        return null;
    }
    
    public void putTimeline(Long userId, List<PostDTO> timeline) {
        String cacheKey = CACHE_PREFIX + userId;
        try {
            redisTemplate.opsForValue().set(cacheKey, timeline, CACHE_TTL, TimeUnit.SECONDS);
        } catch (Exception e) {
            log.warn("Redis cache put failed for user: {}", userId, e);
        }
    }
    
    public void invalidateUser(Long userId) {
        String cacheKey = CACHE_PREFIX + userId;
        try {
            redisTemplate.delete(cacheKey);
        } catch (Exception e) {
            log.warn("Redis cache invalidate failed for user: {}", userId, e);
        }
    }
    
    // 批量失效(当用户发布新动态时)
    public void invalidateFollowers(Long userId) {
        try {
            // 获取关注者列表
            List<Long> followers = getFollowers(userId);
            
            // 批量删除关注者的时间线缓存
            List<String> keysToDelete = followers.stream()
                .map(followerId -> CACHE_PREFIX + followerId)
                .collect(Collectors.toList());
                
            if (!keysToDelete.isEmpty()) {
                redisTemplate.delete(keysToDelete);
            }
        } catch (Exception e) {
            log.warn("Batch cache invalidation failed for user: {}", userId, e);
        }
    }
}

第三级:数据库查询优化

sql
-- 优化后的查询(减少JOIN)
-- 第一步:获取用户关注的动态ID列表
SELECT p.post_id, p.created_at
FROM posts p
JOIN user_follows f ON p.user_id = f.followed_user_id
WHERE f.follower_user_id = ?
AND p.created_at >= ?
AND p.status = 'published'
ORDER BY p.created_at DESC
LIMIT 20;

-- 第二步:批量获取动态详情
SELECT 
    p.post_id,
    p.content,
    p.created_at,
    p.like_count,
    p.comment_count,
    p.user_id
FROM posts p
WHERE p.post_id IN (?, ?, ?, ...)
ORDER BY p.created_at DESC;

-- 第三步:批量获取用户信息
SELECT user_id, username, avatar_url
FROM users
WHERE user_id IN (?, ?, ?, ...);

-- 第四步:批量获取标签信息
SELECT 
    pt.post_id,
    GROUP_CONCAT(t.tag_name) as tags
FROM post_tags pt
JOIN tags t ON pt.tag_id = t.tag_id
WHERE pt.post_id IN (?, ?, ?, ...)
GROUP BY pt.post_id;

缓存服务整合

java
@Service
public class TimelineService {
    @Autowired
    private TimelineL1Cache l1Cache;
    
    @Autowired
    private TimelineL2Cache l2Cache;
    
    @Autowired
    private TimelineRepository repository;
    
    public List<PostDTO> getUserTimeline(Long userId) {
        // L1缓存查询
        List<PostDTO> timeline = l1Cache.getTimeline(userId);
        if (timeline != null) {
            recordCacheHit("L1", userId);
            return timeline;
        }
        
        // L2缓存查询
        timeline = l2Cache.getTimeline(userId);
        if (timeline != null) {
            recordCacheHit("L2", userId);
            // 回填L1缓存
            l1Cache.putTimeline(userId, timeline);
            return timeline;
        }
        
        // 数据库查询
        recordCacheMiss(userId);
        timeline = repository.getUserTimeline(userId);
        
        // 回填缓存
        if (timeline != null && !timeline.isEmpty()) {
            l2Cache.putTimeline(userId, timeline);
            l1Cache.putTimeline(userId, timeline);
        }
        
        return timeline;
    }
    
    // 发布新动态时的缓存失效
    @Async
    public void onPostPublished(Long userId, Long postId) {
        // 异步失效相关缓存
        CompletableFuture.runAsync(() -> {
            // 失效发布者的关注者缓存
            l2Cache.invalidateFollowers(userId);
            
            // 预热热门用户的缓存
            if (isHotUser(userId)) {
                preWarmFollowersCache(userId);
            }
        });
    }
    
    // 缓存预热
    @Async
    public void preWarmFollowersCache(Long userId) {
        List<Long> activeFollowers = getActiveFollowers(userId);
        
        activeFollowers.parallelStream()
            .limit(100)  // 限制预热数量
            .forEach(followerId -> {
                try {
                    // 异步预热缓存
                    getUserTimeline(followerId);
                } catch (Exception e) {
                    log.warn("Cache pre-warm failed for user: {}", followerId, e);
                }
            });
    }
}

缓存监控和指标

java
@Component
public class CacheMetrics {
    private final MeterRegistry meterRegistry;
    private final Counter l1Hits;
    private final Counter l2Hits;
    private final Counter cacheMisses;
    private final Timer queryTimer;
    
    public CacheMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.l1Hits = Counter.builder("cache.hits")
            .tag("level", "L1")
            .register(meterRegistry);
        this.l2Hits = Counter.builder("cache.hits")
            .tag("level", "L2")
            .register(meterRegistry);
        this.cacheMisses = Counter.builder("cache.misses")
            .register(meterRegistry);
        this.queryTimer = Timer.builder("timeline.query.duration")
            .register(meterRegistry);
    }
    
    public void recordL1Hit() {
        l1Hits.increment();
    }
    
    public void recordL2Hit() {
        l2Hits.increment();
    }
    
    public void recordCacheMiss() {
        cacheMisses.increment();
    }
    
    // 计算缓存命中率
    @Scheduled(fixedRate = 60000) // 每分钟计算一次
    public void calculateHitRates() {
        double l1HitRate = l1Hits.count() / (l1Hits.count() + l2Hits.count() + cacheMisses.count());
        double l2HitRate = l2Hits.count() / (l2Hits.count() + cacheMisses.count());
        double totalHitRate = (l1Hits.count() + l2Hits.count()) / 
            (l1Hits.count() + l2Hits.count() + cacheMisses.count());
        
        Gauge.builder("cache.hit.rate")
            .tag("level", "L1")
            .register(meterRegistry, () -> l1HitRate);
        
        Gauge.builder("cache.hit.rate")
            .tag("level", "L2")
            .register(meterRegistry, () -> l2HitRate);
            
        Gauge.builder("cache.hit.rate")
            .tag("level", "total")
            .register(meterRegistry, () -> totalHitRate);
    }
}

缓存配置优化

yaml
# application.yml
spring:
  redis:
    host: redis-cluster.internal
    port: 6379
    password: ${REDIS_PASSWORD}
    timeout: 2000ms
    lettuce:
      pool:
        max-active: 200
        max-idle: 20
        min-idle: 5
        max-wait: 1000ms

# 缓存配置
cache:
  l1:
    max-size: 10000
    expire-after-write: 5m
    record-stats: true
  l2:
    ttl: 30m
    max-memory: 2gb
    eviction-policy: allkeys-lru

# 监控配置
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  metrics:
    export:
      prometheus:
        enabled: true

性能测试结果

-- 优化前:
-- 平均响应时间:800ms
-- 95%响应时间:1.5s
-- QPS:1000次/秒
-- 数据库CPU使用率:90%
-- 数据库连接数:300+
-- 缓存命中率:0%

-- 优化后(多级缓存):
-- 平均响应时间:45ms
-- 95%响应时间:120ms
-- QPS:8000次/秒
-- 数据库CPU使用率:25%
-- 数据库连接数:50+
-- L1缓存命中率:75%
-- L2缓存命中率:20%
-- 总缓存命中率:95%

-- 性能提升:
-- 响应时间提升:94.4%
-- 吞吐量提升:700%
-- 数据库负载降低:72%

业务价值

  • 用户体验显著提升,页面加载速度提升94%
  • 系统并发能力提升8倍
  • 数据库资源成本降低70%
  • 系统稳定性和可用性大幅提升
  • 支持业务快速增长和流量突发

运维和监控

bash
# Redis监控脚本
#!/bin/bash
# redis_monitor.sh

REDIS_CLI="redis-cli -h redis-cluster.internal -p 6379"

echo "=== Redis Cache Statistics ==="
echo "Memory Usage:"
$REDIS_CLI info memory | grep used_memory_human

echo "\nHit Rate:"
$REDIS_CLI info stats | grep keyspace_hits
$REDIS_CLI info stats | grep keyspace_misses

echo "\nConnected Clients:"
$REDIS_CLI info clients | grep connected_clients

echo "\nCache Keys Count:"
$REDIS_CLI eval "return #redis.call('keys', 'timeline:*')" 0

# 应用缓存监控
echo "\n=== Application Cache Statistics ==="
curl -s http://localhost:8080/actuator/metrics/cache.hits | jq '.measurements[0].value'
curl -s http://localhost:8080/actuator/metrics/cache.misses | jq '.measurements[0].value'

最佳实践总结

  1. 分层缓存:L1本地缓存 + L2分布式缓存 + 数据库优化
  2. 缓存策略:根据数据特性选择合适的缓存策略
  3. 失效机制:建立完善的缓存失效和更新机制
  4. 监控告警:实时监控缓存性能和命中率
  5. 容错设计:缓存故障时的降级和恢复机制
  6. 容量规划:根据业务增长合理规划缓存容量
  7. 预热策略:关键数据的缓存预热机制

性能监控与诊断

性能指标监控

关键性能指标

查询性能指标

  • 查询响应时间
  • 查询吞吐量(QPS)
  • 慢查询数量和比例
  • 查询错误率

资源使用指标

  • CPU 使用率
  • 内存使用率
  • I/O 使用率
  • 网络使用率

数据库内部指标

  • 缓冲池命中率
  • 索引使用情况
  • 锁等待时间
  • 连接数和并发度

监控工具

MySQL 内置工具

  • Performance Schema
  • Information Schema
  • SHOW STATUS 命令
  • 慢查询日志

第三方监控工具

  • Percona Monitoring and Management (PMM)
  • MySQL Enterprise Monitor
  • Zabbix
  • Prometheus + Grafana

系统监控工具

  • top、htop
  • iostat、iotop
  • vmstat
  • netstat

实战案例:电商平台数据库性能监控体系建设

业务背景:某大型电商平台需要建立完善的数据库性能监控体系,确保系统稳定运行

监控需求

  • 实时监控数据库性能指标
  • 及时发现和预警性能问题
  • 提供详细的性能分析报告
  • 支持历史数据分析和趋势预测

监控架构设计

第一层:数据采集层

sql
-- 1. 启用Performance Schema
SET GLOBAL performance_schema = ON;

-- 2. 配置关键监控项
UPDATE performance_schema.setup_instruments 
SET ENABLED = 'YES', TIMED = 'YES' 
WHERE NAME LIKE '%statement/%' 
   OR NAME LIKE '%stage/%'
   OR NAME LIKE '%wait/io/%'
   OR NAME LIKE '%wait/lock/%';

-- 3. 启用关键监控表
UPDATE performance_schema.setup_consumers 
SET ENABLED = 'YES' 
WHERE NAME IN (
    'events_statements_current',
    'events_statements_history',
    'events_statements_history_long',
    'events_waits_current',
    'events_waits_history',
    'events_waits_history_long'
);

-- 4. 配置慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.5;
SET GLOBAL log_queries_not_using_indexes = 'ON';
SET GLOBAL log_slow_admin_statements = 'ON';
SET GLOBAL log_slow_slave_statements = 'ON';

第二层:指标计算层

python
# Python监控脚本
import pymysql
import time
import json
from datetime import datetime
import redis

class MySQLMonitor:
    def __init__(self, host, port, user, password, database):
        self.connection = pymysql.connect(
            host=host, port=port, user=user, 
            password=password, database=database
        )
        self.redis_client = redis.Redis(host='localhost', port=6379, db=0)
        
    def collect_performance_metrics(self):
        """收集性能指标"""
        metrics = {}
        
        # 1. 基础性能指标
        basic_metrics = self._get_basic_metrics()
        metrics.update(basic_metrics)
        
        # 2. 查询性能指标
        query_metrics = self._get_query_metrics()
        metrics.update(query_metrics)
        
        # 3. 资源使用指标
        resource_metrics = self._get_resource_metrics()
        metrics.update(resource_metrics)
        
        # 4. 锁和等待指标
        lock_metrics = self._get_lock_metrics()
        metrics.update(lock_metrics)
        
        return metrics
    
    def _get_basic_metrics(self):
        """获取基础指标"""
        cursor = self.connection.cursor()
        
        # QPS和TPS
        cursor.execute("""
            SELECT 
                VARIABLE_NAME, 
                VARIABLE_VALUE 
            FROM performance_schema.global_status 
            WHERE VARIABLE_NAME IN (
                'Queries', 'Com_select', 'Com_insert', 
                'Com_update', 'Com_delete', 'Connections',
                'Threads_connected', 'Threads_running'
            )
        """)
        
        status_vars = dict(cursor.fetchall())
        
        # 计算QPS(需要与上次采集时间对比)
        current_time = time.time()
        current_queries = int(status_vars.get('Queries', 0))
        
        last_time = self.redis_client.get('last_collect_time')
        last_queries = self.redis_client.get('last_queries')
        
        qps = 0
        if last_time and last_queries:
            time_diff = current_time - float(last_time)
            query_diff = current_queries - int(last_queries)
            qps = query_diff / time_diff if time_diff > 0 else 0
        
        # 保存当前值
        self.redis_client.set('last_collect_time', current_time)
        self.redis_client.set('last_queries', current_queries)
        
        return {
            'qps': round(qps, 2),
            'connections': int(status_vars.get('Connections', 0)),
            'threads_connected': int(status_vars.get('Threads_connected', 0)),
            'threads_running': int(status_vars.get('Threads_running', 0))
        }
    
    def _get_query_metrics(self):
        """获取查询性能指标"""
        cursor = self.connection.cursor()
        
        # 慢查询统计
        cursor.execute("""
            SELECT 
                COUNT(*) as slow_query_count,
                AVG(TIMER_WAIT/1000000000) as avg_duration,
                MAX(TIMER_WAIT/1000000000) as max_duration
            FROM performance_schema.events_statements_history_long 
            WHERE TIMER_WAIT > 500000000  -- 0.5秒
            AND EVENT_NAME LIKE 'statement/sql/%'
        """)
        
        slow_query_result = cursor.fetchone()
        
        # 查询类型分布
        cursor.execute("""
            SELECT 
                SQL_TEXT,
                COUNT(*) as execution_count,
                AVG(TIMER_WAIT/1000000000) as avg_duration,
                SUM(TIMER_WAIT/1000000000) as total_duration
            FROM performance_schema.events_statements_history_long 
            WHERE EVENT_NAME LIKE 'statement/sql/%'
            GROUP BY LEFT(SQL_TEXT, 100)
            ORDER BY total_duration DESC
            LIMIT 10
        """)
        
        top_queries = cursor.fetchall()
        
        return {
            'slow_query_count': slow_query_result[0] or 0,
            'avg_query_duration': round(slow_query_result[1] or 0, 3),
            'max_query_duration': round(slow_query_result[2] or 0, 3),
            'top_queries': list(top_queries)
        }
    
    def _get_resource_metrics(self):
        """获取资源使用指标"""
        cursor = self.connection.cursor()
        
        # InnoDB缓冲池命中率
        cursor.execute("""
            SELECT 
                VARIABLE_NAME, 
                VARIABLE_VALUE 
            FROM performance_schema.global_status 
            WHERE VARIABLE_NAME IN (
                'Innodb_buffer_pool_read_requests',
                'Innodb_buffer_pool_reads',
                'Innodb_buffer_pool_pages_total',
                'Innodb_buffer_pool_pages_free',
                'Innodb_buffer_pool_pages_dirty'
            )
        """)
        
        innodb_vars = dict(cursor.fetchall())
        
        # 计算缓冲池命中率
        read_requests = int(innodb_vars.get('Innodb_buffer_pool_read_requests', 0))
        reads = int(innodb_vars.get('Innodb_buffer_pool_reads', 0))
        hit_rate = (1 - reads / read_requests) * 100 if read_requests > 0 else 0
        
        # 缓冲池使用率
        total_pages = int(innodb_vars.get('Innodb_buffer_pool_pages_total', 0))
        free_pages = int(innodb_vars.get('Innodb_buffer_pool_pages_free', 0))
        dirty_pages = int(innodb_vars.get('Innodb_buffer_pool_pages_dirty', 0))
        
        used_pages = total_pages - free_pages
        usage_rate = (used_pages / total_pages) * 100 if total_pages > 0 else 0
        dirty_rate = (dirty_pages / total_pages) * 100 if total_pages > 0 else 0
        
        return {
            'buffer_pool_hit_rate': round(hit_rate, 2),
            'buffer_pool_usage_rate': round(usage_rate, 2),
            'buffer_pool_dirty_rate': round(dirty_rate, 2)
        }
    
    def _get_lock_metrics(self):
        """获取锁等待指标"""
        cursor = self.connection.cursor()
        
        # 当前锁等待
        cursor.execute("""
            SELECT 
                COUNT(*) as lock_waits,
                AVG(TIMER_WAIT/1000000000) as avg_wait_time
            FROM performance_schema.events_waits_current 
            WHERE EVENT_NAME LIKE 'wait/lock/%'
            AND TIMER_WAIT IS NOT NULL
        """)
        
        lock_result = cursor.fetchone()
        
        # 死锁统计
        cursor.execute("""
            SELECT VARIABLE_VALUE 
            FROM performance_schema.global_status 
            WHERE VARIABLE_NAME = 'Innodb_deadlocks'
        """)
        
        deadlocks = cursor.fetchone()
        
        return {
            'current_lock_waits': lock_result[0] or 0,
            'avg_lock_wait_time': round(lock_result[1] or 0, 3),
            'total_deadlocks': int(deadlocks[0]) if deadlocks else 0
        }
    
    def generate_alert(self, metrics):
        """生成告警"""
        alerts = []
        
        # QPS异常告警
        if metrics['qps'] > 5000:
            alerts.append({
                'level': 'warning',
                'metric': 'qps',
                'value': metrics['qps'],
                'threshold': 5000,
                'message': 'QPS过高,可能影响系统性能'
            })
        
        # 缓冲池命中率告警
        if metrics['buffer_pool_hit_rate'] < 95:
            alerts.append({
                'level': 'critical',
                'metric': 'buffer_pool_hit_rate',
                'value': metrics['buffer_pool_hit_rate'],
                'threshold': 95,
                'message': '缓冲池命中率过低,需要优化查询或增加内存'
            })
        
        # 慢查询告警
        if metrics['slow_query_count'] > 100:
            alerts.append({
                'level': 'warning',
                'metric': 'slow_query_count',
                'value': metrics['slow_query_count'],
                'threshold': 100,
                'message': '慢查询数量过多,需要优化SQL'
            })
        
        # 连接数告警
        if metrics['threads_connected'] > 200:
            alerts.append({
                'level': 'warning',
                'metric': 'threads_connected',
                'value': metrics['threads_connected'],
                'threshold': 200,
                'message': '数据库连接数过多'
            })
        
        return alerts

# 监控主程序
def main():
    monitor = MySQLMonitor(
        host='localhost',
        port=3306,
        user='monitor_user',
        password='monitor_password',
        database='performance_schema'
    )
    
    while True:
        try:
            # 收集指标
            metrics = monitor.collect_performance_metrics()
            metrics['timestamp'] = datetime.now().isoformat()
            
            # 生成告警
            alerts = monitor.generate_alert(metrics)
            
            # 输出结果
            print(f"Metrics: {json.dumps(metrics, indent=2)}")
            if alerts:
                print(f"Alerts: {json.dumps(alerts, indent=2)}")
            
            # 存储到时序数据库(如InfluxDB)
            # store_to_influxdb(metrics)
            
            # 发送告警(如钉钉、邮件等)
            # send_alerts(alerts)
            
            time.sleep(60)  # 每分钟采集一次
            
        except Exception as e:
            print(f"监控异常: {e}")
            time.sleep(10)

if __name__ == '__main__':
    main()

第三层:可视化展示层

yaml
# Grafana Dashboard配置
apiVersion: 1
datasources:
  - name: MySQL-Performance
    type: mysql
    url: mysql://monitor_user:password@localhost:3306/performance_schema
    access: proxy
    
  - name: InfluxDB-Metrics
    type: influxdb
    url: http://localhost:8086
    database: mysql_metrics
    access: proxy

dashboards:
  - title: "MySQL性能监控大盘"
    panels:
      - title: "QPS趋势"
        type: graph
        targets:
          - expr: "mysql_qps"
            legendFormat: "QPS"
        
      - title: "缓冲池命中率"
        type: singlestat
        targets:
          - expr: "mysql_buffer_pool_hit_rate"
        thresholds: "90,95"
        
      - title: "慢查询Top10"
        type: table
        targets:
          - expr: "mysql_slow_queries"
        
      - title: "连接数监控"
        type: graph
        targets:
          - expr: "mysql_threads_connected"
            legendFormat: "已连接"
          - expr: "mysql_threads_running"
            legendFormat: "运行中"

第四层:告警通知层

python
# 告警通知服务
import requests
import json
from datetime import datetime

class AlertManager:
    def __init__(self):
        self.dingtalk_webhook = "https://oapi.dingtalk.com/robot/send?access_token=xxx"
        self.email_api = "http://internal-email-service/send"
    
    def send_dingtalk_alert(self, alerts):
        """发送钉钉告警"""
        if not alerts:
            return
        
        message = "🚨 MySQL数据库告警\n\n"
        for alert in alerts:
            level_emoji = "🔴" if alert['level'] == 'critical' else "🟡"
            message += f"{level_emoji} {alert['message']}\n"
            message += f"   指标: {alert['metric']}\n"
            message += f"   当前值: {alert['value']}\n"
            message += f"   阈值: {alert['threshold']}\n\n"
        
        message += f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
        
        payload = {
            "msgtype": "text",
            "text": {
                "content": message
            }
        }
        
        try:
            response = requests.post(
                self.dingtalk_webhook,
                json=payload,
                timeout=10
            )
            print(f"钉钉告警发送结果: {response.status_code}")
        except Exception as e:
            print(f"钉钉告警发送失败: {e}")
    
    def send_email_alert(self, alerts):
        """发送邮件告警"""
        if not alerts:
            return
        
        critical_alerts = [a for a in alerts if a['level'] == 'critical']
        if not critical_alerts:
            return
        
        subject = f"[CRITICAL] MySQL数据库严重告警 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
        
        body = "<h2>MySQL数据库严重告警</h2>\n"
        body += "<table border='1'>\n"
        body += "<tr><th>指标</th><th>当前值</th><th>阈值</th><th>描述</th></tr>\n"
        
        for alert in critical_alerts:
            body += f"<tr>"
            body += f"<td>{alert['metric']}</td>"
            body += f"<td>{alert['value']}</td>"
            body += f"<td>{alert['threshold']}</td>"
            body += f"<td>{alert['message']}</td>"
            body += f"</tr>\n"
        
        body += "</table>\n"
        
        payload = {
            "to": ["dba@company.com", "ops@company.com"],
            "subject": subject,
            "body": body,
            "type": "html"
        }
        
        try:
            response = requests.post(
                self.email_api,
                json=payload,
                timeout=10
            )
            print(f"邮件告警发送结果: {response.status_code}")
        except Exception as e:
            print(f"邮件告警发送失败: {e}")

监控效果验证

sql
-- 验证监控系统效果
-- 1. 模拟慢查询
SELECT SLEEP(2), COUNT(*) FROM orders WHERE create_time > '2023-01-01';

-- 2. 模拟高并发连接
-- 使用压测工具创建大量连接

-- 3. 查看监控数据
SELECT 
    EVENT_NAME,
    SQL_TEXT,
    TIMER_WAIT/1000000000 as duration_seconds
FROM performance_schema.events_statements_history_long 
WHERE TIMER_WAIT > 1000000000  -- 1秒以上
ORDER BY TIMER_WAIT DESC
LIMIT 10;

-- 4. 验证告警触发
-- 检查告警日志和通知记录

监控体系价值

-- 监控覆盖率:
-- 性能指标覆盖率:95%
-- 告警准确率:92%
-- 故障发现时间:平均2分钟
-- 误报率:<5%

-- 业务价值:
-- 故障发现时间缩短80%
-- 系统可用性提升到99.9%
-- 运维效率提升60%
-- 避免了多次重大故障
-- 为容量规划提供数据支撑

最佳实践总结

  1. 分层监控:数据采集、指标计算、可视化展示、告警通知
  2. 关键指标:QPS、响应时间、缓冲池命中率、慢查询、连接数
  3. 实时告警:设置合理的告警阈值,避免误报和漏报
  4. 历史分析:保留历史数据,支持趋势分析和容量规划
  5. 自动化运维:结合监控数据实现自动化运维操作
  6. 持续优化:根据监控数据持续优化数据库性能

慢查询分析

慢查询日志

配置慢查询日志

  • 启用 slow_query_log
  • 设置 long_query_time 阈值
  • 配置 log_queries_not_using_indexes
  • 设置日志文件路径

日志分析

  • 使用 mysqldumpslow 工具
  • 分析查询模式和频率
  • 识别性能瓶颈
  • 制定优化计划

日志管理

  • 定期轮转日志文件
  • 控制日志文件大小
  • 备份重要的日志数据
  • 自动化日志分析

实战案例:生产环境慢查询诊断与优化

业务背景:某在线教育平台学生成绩查询系统性能问题

问题现象

  • 用户反馈成绩查询页面加载缓慢
  • 数据库CPU使用率持续90%以上
  • 应用服务器连接池经常耗尽
  • 高峰期系统响应超时

第一步:启用慢查询日志

sql
-- 配置慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = 'ON';
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

-- 查看当前配置
SHOW VARIABLES LIKE '%slow%';

第二步:分析慢查询日志

bash
# 使用mysqldumpslow分析慢查询
mysqldumpslow -s c -t 10 /var/log/mysql/slow.log

# 输出结果(前3个最频繁的慢查询)
# Count: 2847  Time=3.21s (9140s)  Lock=0.00s (0s)  Rows=156.3 (444891)
# SELECT s.student_id, s.student_name, c.course_name, sc.score, sc.exam_date 
# FROM student_scores sc 
# JOIN students s ON sc.student_id = s.id 
# JOIN courses c ON sc.course_id = c.id 
# WHERE s.class_id = N AND sc.exam_date >= 'S' 
# ORDER BY sc.exam_date DESC, sc.score DESC

# Count: 1923  Time=2.87s (5518s)  Lock=0.00s (0s)  Rows=89.2 (171476)
# SELECT AVG(score) as avg_score, COUNT(*) as total_count 
# FROM student_scores 
# WHERE course_id = N AND exam_date BETWEEN 'S' AND 'S'

# Count: 1456  Time=4.12s (5999s)  Lock=0.00s (0s)  Rows=234.1 (340849)
# SELECT * FROM student_scores WHERE student_id IN (N,N,N...)

第三步:详细分析问题查询

sql
-- 分析第一个慢查询
EXPLAIN FORMAT=JSON 
SELECT s.student_id, s.student_name, c.course_name, sc.score, sc.exam_date 
FROM student_scores sc 
JOIN students s ON sc.student_id = s.id 
JOIN courses c ON sc.course_id = c.id 
WHERE s.class_id = 101 
AND sc.exam_date >= '2023-01-01' 
ORDER BY sc.exam_date DESC, sc.score DESC;

-- 执行计划显示问题
{
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "89234.56"
    },
    "ordering_operation": {
      "using_filesort": true,
      "cost_info": {
        "sort_cost": "12456.78"
      },
      "nested_loop": [
        {
          "table": {
            "table_name": "sc",
            "access_type": "ALL",
            "rows_examined_per_scan": 450000,
            "filtered": "11.11"
          }
        }
      ]
    }
  }
}

第四步:性能监控分析

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

-- 查看表的统计信息
SELECT 
    TABLE_NAME,
    TABLE_ROWS,
    ROUND(DATA_LENGTH/1024/1024, 2) AS 'Data Size (MB)',
    ROUND(INDEX_LENGTH/1024/1024, 2) AS 'Index Size (MB)',
    ROUND((DATA_LENGTH + INDEX_LENGTH)/1024/1024, 2) AS 'Total Size (MB)'
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'education_system'
ORDER BY (DATA_LENGTH + INDEX_LENGTH) DESC;

-- 结果显示
-- student_scores: 450,000行, 数据85MB, 索引12MB
-- students: 15,000行, 数据3.2MB, 索引1.8MB  
-- courses: 500行, 数据0.1MB, 索引0.05MB

第五步:索引优化方案

sql
-- 分析现有索引
SHOW INDEX FROM student_scores;
SHOW INDEX FROM students;
SHOW INDEX FROM courses;

-- 创建优化索引
-- 1. 为student_scores表创建复合索引
CREATE INDEX idx_scores_student_date ON student_scores(student_id, exam_date DESC, score DESC);

-- 2. 为students表创建复合索引
CREATE INDEX idx_students_class ON students(class_id, id);

-- 3. 优化查询语句
SELECT s.student_id, s.student_name, c.course_name, sc.score, sc.exam_date 
FROM students s
INNER JOIN student_scores sc ON s.id = sc.student_id 
INNER JOIN courses c ON sc.course_id = c.id 
WHERE s.class_id = 101 
AND sc.exam_date >= '2023-01-01' 
ORDER BY sc.exam_date DESC, sc.score DESC
LIMIT 50;

第六步:优化效果验证

sql
-- 优化后的执行计划
EXPLAIN FORMAT=JSON [优化后的查询];

-- 性能对比测试
SET profiling = 1;
[执行优化后的查询]
SHOW PROFILES;
SHOW PROFILE FOR QUERY 1;

-- 结果对比
-- 优化前:执行时间3.21秒,扫描45万行
-- 优化后:执行时间45毫秒,扫描156行
-- 性能提升:98.6%

第七步:监控和维护

sql
-- 创建性能监控视图
CREATE VIEW slow_query_summary AS
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,
    ROUND(AVG_ROWS_EXAMINED, 0) as avg_rows_examined
FROM performance_schema.events_statements_summary_by_digest
WHERE AVG_TIMER_WAIT > 1000000000  -- 大于1秒的查询
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 20;

-- 定期检查慢查询
SELECT * FROM slow_query_summary;

-- 自动化监控脚本
#!/bin/bash
# slow_query_monitor.sh
MYSQL_USER="monitor"
MYSQL_PASS="password"
MYSQL_HOST="localhost"
LOG_FILE="/var/log/mysql_monitor.log"

# 检查慢查询数量
SLOW_COUNT=$(mysql -u$MYSQL_USER -p$MYSQL_PASS -h$MYSQL_HOST -e \
"SELECT COUNT(*) FROM performance_schema.events_statements_summary_by_digest 
WHERE AVG_TIMER_WAIT > 2000000000;" -N)

if [ $SLOW_COUNT -gt 10 ]; then
    echo "$(date): Warning - $SLOW_COUNT slow queries detected" >> $LOG_FILE
    # 发送告警邮件或钉钉通知
fi

优化成果总结

  • 查询性能:平均响应时间从3.2秒降低到45毫秒
  • 系统负载:数据库CPU使用率从90%降低到15%
  • 并发能力:支持并发用户数从500提升到5000
  • 用户体验:页面加载时间从8秒降低到1秒
  • 资源成本:避免了硬件升级,节省成本30万元

实时性能分析

Performance Schema

  • 实时监控查询执行
  • 分析等待事件
  • 跟踪资源使用
  • 诊断性能问题

SHOW PROCESSLIST

  • 查看当前执行的查询
  • 识别长时间运行的查询
  • 分析锁等待情况
  • 监控连接状态

EXPLAIN ANALYZE

  • 获取实际执行统计
  • 比较估算和实际值
  • 分析执行时间分布
  • 识别性能瓶颈

问题诊断方法

性能问题分类

查询性能问题

  • 慢查询识别和优化
  • 索引使用分析
  • 执行计划优化
  • SQL 语句重写

并发性能问题

  • 锁竞争分析
  • 死锁检测和处理
  • 连接池配置优化
  • 事务设计优化

资源瓶颈问题

  • I/O 瓶颈诊断
  • 内存使用优化
  • CPU 使用分析
  • 网络性能优化

诊断流程

问题定位

  1. 收集性能指标和日志
  2. 识别性能瓶颈的类型
  3. 分析问题的根本原因
  4. 制定解决方案

解决方案实施

  1. 制定详细的优化计划
  2. 在测试环境验证方案
  3. 逐步在生产环境实施
  4. 监控优化效果

效果评估

  1. 对比优化前后的性能指标
  2. 验证问题是否得到解决
  3. 评估优化的副作用
  4. 总结经验和教训

总结

MySQL 查询优化是一个系统性的工程,需要从多个层面进行综合考虑:

  1. 理解原理:深入了解查询优化器的工作机制
  2. 分析执行计划:熟练使用 EXPLAIN 等工具
  3. 优化 SQL 语句:掌握各种 SQL 优化技巧
  4. 设计合适的索引:根据查询模式设计索引策略
  5. 监控和诊断:建立完善的性能监控体系

最佳实践建议

  • 在设计阶段就考虑性能因素
  • 建立性能基准和监控体系
  • 定期进行性能审查和优化
  • 保持对新特性和工具的学习
  • 结合业务需求制定优化策略

查询优化是一个持续的过程,需要根据业务发展和数据增长不断调整和改进。通过系统性的学习和实践,可以构建高性能、高可靠的数据库系统。