Skip to content
On this page

MySQL查询优化

查询优化是提高数据库性能的关键技术。通过合理的查询优化,可以显著提升数据库的响应速度和吞吐量。本文档将详细介绍MySQL查询优化的原理、技术和最佳实践。

查询执行过程

理解MySQL如何执行查询是优化的基础:

  1. 连接:客户端连接到MySQL服务器
  2. 解析:解析SQL语句,验证语法
  3. 预处理:检查权限和表结构
  4. 优化:查询优化器生成执行计划
  5. 执行:执行器执行查询计划
  6. 返回结果:将结果返回给客户端

查询优化器

MySQL查询优化器负责选择最优的执行计划,主要考虑:

选择算法

  • 全表扫描:扫描整个表
  • 索引扫描:使用索引访问数据
  • 范围扫描:在索引上进行范围查找
  • 唯一键查找:通过唯一索引查找单行

连接算法

  • 嵌套循环连接(NLJ):最常用的连接算法
  • 索引嵌套循环连接(INLJ):使用索引优化连接
  • 块嵌套循环连接(BNLJ):减少磁盘I/O
  • 哈希连接:MySQL 8.0.20+支持

索引优化

索引类型与选择

B-Tree索引:适用于等值、范围、排序查询

sql
-- 创建B-Tree索引
CREATE INDEX idx_user_email ON users(email);
CREATE INDEX idx_user_created ON users(created_at);

复合索引:多列索引,遵循最左前缀原则

sql
-- 复合索引示例
CREATE INDEX idx_user_status_date ON orders(user_id, status, created_at);

-- 有效使用复合索引的查询
SELECT * FROM orders WHERE user_id = 1 AND status = 'pending';
SELECT * FROM orders WHERE user_id = 1 AND status = 'pending' AND created_at > '2023-01-01';

-- 无法有效使用复合索引的查询
SELECT * FROM orders WHERE status = 'pending'; -- 跳过了最左列

前缀索引:为长字符串列创建索引

sql
-- 为长字符串创建前缀索引
CREATE INDEX idx_title_prefix ON articles(title(20)); -- 只索引前20个字符

-- 计算合适的前缀长度
SELECT 
    COUNT(DISTINCT LEFT(title, 10)) / COUNT(*) AS sel10,
    COUNT(DISTINCT LEFT(title, 20)) / COUNT(*) AS sel20,
    COUNT(DISTINCT LEFT(title, 30)) / COUNT(*) AS sel30
FROM articles;

索引设计原则

  1. 选择性原则:选择性高的列优先创建索引
sql
-- 高选择性列(如邮箱)
CREATE INDEX idx_email ON users(email); -- 好的索引

-- 低选择性列(如性别)
-- CREATE INDEX idx_gender ON users(gender); -- 通常不建议
  1. 查询模式原则:根据查询模式创建索引
sql
-- 如果经常按用户ID和状态查询
CREATE INDEX idx_user_status ON orders(user_id, status);

-- 如果经常按状态统计
CREATE INDEX idx_status ON orders(status);
  1. 覆盖索引原则:索引包含查询所需的所有列
sql
-- 查询只需要索引中的列,无需回表
SELECT user_id, status FROM orders WHERE user_id = 123;

-- 创建覆盖索引
CREATE INDEX idx_covering ON orders(user_id, status, created_at);

查询语句优化

WHERE子句优化

避免在索引列上使用函数

sql
-- 不好的写法
SELECT * FROM users WHERE YEAR(created_at) = 2023;

-- 好的写法
SELECT * FROM users WHERE created_at >= '2023-01-01' AND created_at < '2024-01-01';

使用合适的比较操作符

sql
-- 使用IN代替多个OR
SELECT * FROM users WHERE status IN ('active', 'pending', 'suspended');

-- 不好的写法
-- SELECT * FROM users WHERE status = 'active' OR status = 'pending' OR status = 'suspended';

**避免SELECT ***

sql
-- 好的写法:只选择需要的列
SELECT id, username, email FROM users WHERE active = 1;

-- 不好的写法:选择所有列
-- SELECT * FROM users WHERE active = 1;

JOIN优化

确保JOIN条件上有索引

sql
-- 确保关联列上有索引
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_users_id ON users(id); -- 通常主键已自动创建索引

SELECT u.username, o.total 
FROM users u 
JOIN orders o ON u.id = o.user_id 
WHERE u.active = 1;

选择合适的JOIN类型

sql
-- 内连接:只返回匹配的记录
SELECT u.username, o.total 
FROM users u 
INNER JOIN orders o ON u.id = o.user_id;

-- 左连接:返回左表所有记录
SELECT u.username, o.total 
FROM users u 
LEFT JOIN orders o ON u.id = o.user_id;

使用STRAIGHT_JOIN控制连接顺序

sql
-- 强制按照指定顺序连接表
SELECT /*+ USE_INDEX(orders, idx_orders_user_id) */ u.username, o.total
FROM users u 
STRAIGHT_JOIN orders o ON u.id = o.user_id;

子查询优化

使用JOIN代替子查询

sql
-- 使用EXISTS子查询
SELECT * FROM users u 
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = 'pending');

-- 使用JOIN优化
SELECT DISTINCT u.* 
FROM users u 
JOIN orders o ON u.id = o.user_id 
WHERE o.status = 'pending';

相关子查询vs非相关子查询

sql
-- 非相关子查询(执行一次)
SELECT * FROM users 
WHERE department_id = (SELECT id FROM departments WHERE name = 'IT');

-- 相关子查询(对每行执行)
SELECT u.*, (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) as order_count
FROM users u;

性能分析工具

EXPLAIN命令

使用EXPLAIN分析查询执行计划:

sql
EXPLAIN SELECT * FROM users WHERE email = 'user@example.com';
EXPLAIN FORMAT=JSON SELECT * FROM orders o JOIN users u ON o.user_id = u.id;

EXPLAIN输出字段解释:

  • id:查询序列号
  • select_type:查询类型(SIMPLE, PRIMARY, SUBQUERY等)
  • table:访问的表
  • partitions:匹配的分区
  • type:连接类型(system > const > eq_ref > ref > range > index > ALL)
  • possible_keys:可能使用的索引
  • key:实际使用的索引
  • key_len:使用的索引长度
  • ref:连接条件
  • rows:扫描的行数
  • filtered:通过条件过滤的行百分比
  • Extra:额外信息

Performance Schema

启用Performance Schema监控查询性能:

sql
-- 查看慢查询
SELECT 
    DIGEST_TEXT,
    COUNT_STAR,
    AVG_TIMER_WAIT/1000000000 AS avg_time_ms,
    SUM_ROWS_EXAMINED,
    SUM_ROWS_SENT
FROM performance_schema.events_statements_summary_by_digest 
WHERE DIGEST_TEXT LIKE '%SELECT%' 
ORDER BY AVG_TIMER_WAIT DESC 
LIMIT 10;

慢查询日志

启用和分析慢查询日志:

sql
-- 启用慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 记录执行时间超过1秒的查询

-- 分析慢查询日志
mysqldumpslow /path/to/slow.log

索引优化策略

复合索引优化

最左前缀原则

sql
-- 复合索引 (a, b, c)
CREATE INDEX idx_abc ON table1(a, b, c);

-- 有效使用索引
SELECT * FROM table1 WHERE a = 1;           -- ✓
SELECT * FROM table1 WHERE a = 1 AND b = 2; -- ✓
SELECT * FROM table1 WHERE a = 1 AND b = 2 AND c = 3; -- ✓

-- 无法有效使用索引
SELECT * FROM table1 WHERE b = 2;           -- ✗
SELECT * FROM table1 WHERE c = 3;           -- ✗
SELECT * FROM table1 WHERE b = 2 AND c = 3; -- ✗

索引列顺序

sql
-- 根据查询频率和选择性安排列顺序
CREATE INDEX idx_status_user_created ON orders(status, user_id, created_at);
-- 假设查询模式:WHERE status = ? AND user_id = ? ORDER BY created_at

索引维护

定期分析表统计信息

sql
-- 更新表统计信息
ANALYZE TABLE users, orders;

-- 查看索引使用情况
SHOW INDEX FROM users;

避免过度索引

sql
-- 过多索引会影响INSERT/UPDATE/DELETE性能
-- 一般建议单表索引数量不超过5-7个

查询重写技术

使用UNION优化OR条件

sql
-- 使用OR条件(可能无法使用索引)
SELECT * FROM users WHERE city = 'Beijing' OR age > 30;

-- 使用UNION优化(可以使用不同索引)
SELECT * FROM users WHERE city = 'Beijing'
UNION
SELECT * FROM users WHERE age > 30;

使用EXISTS优化IN

sql
-- 使用IN(可能性能较差)
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE status = 'pending');

-- 使用EXISTS优化(通常性能更好)
SELECT * FROM users u WHERE EXISTS (
    SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = 'pending'
);

分区表优化

sql
-- 按日期分区优化查询
CREATE TABLE sales (
    id INT PRIMARY KEY,
    sale_date DATE NOT NULL,
    amount DECIMAL(10,2)
)
PARTITION BY RANGE (YEAR(sale_date)) (
    PARTITION p2022 VALUES LESS THAN (2023),
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p2024 VALUES LESS THAN (2025)
);

-- 分区裁剪:只扫描相关分区
SELECT * FROM sales WHERE sale_date BETWEEN '2023-01-01' AND '2023-12-31';

配置优化

查询缓存(MySQL 5.7及以下)

sql
-- 启用查询缓存
SET GLOBAL query_cache_type = ON;
SET GLOBAL query_cache_size = 268435456; -- 256MB

排序优化

sql
-- 增加排序缓冲区大小
SET SESSION sort_buffer_size = 2097152; -- 2MB

-- 对于大结果集排序,考虑创建索引
CREATE INDEX idx_created_status ON orders(created_at, status);
SELECT * FROM orders ORDER BY created_at DESC LIMIT 100;

监控和诊断

识别慢查询

sql
-- 使用sys schema识别慢查询
SELECT * FROM sys.statements_with_runtimes_in_95th_percentile LIMIT 10;

-- 查看全表扫描查询
SELECT * FROM sys.statements_with_full_table_scans LIMIT 10;

监控索引使用

sql
-- 查看未使用的索引
SELECT 
    object_schema,
    object_name,
    index_name
FROM performance_schema.table_io_waits_summary_by_index_usage 
WHERE index_name IS NOT NULL 
AND count_star = 0;

优化案例

案例1:优化分页查询

sql
-- 传统分页(在大数据集上性能差)
SELECT * FROM orders ORDER BY id LIMIT 100000, 10;

-- 优化的分页(使用游标)
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 10;

-- 或使用延迟关联
SELECT o.* FROM orders o
INNER JOIN (
    SELECT id FROM orders ORDER BY id LIMIT 100000, 10
) AS lim ON o.id = lim.id;

案例2:优化聚合查询

sql
-- 慢查询:在大表上进行聚合
SELECT status, COUNT(*) FROM orders GROUP BY status;

-- 优化:确保分组列有索引
CREATE INDEX idx_status ON orders(status);

-- 使用覆盖索引优化
CREATE INDEX idx_status_created ON orders(status, created_at);
SELECT status, COUNT(*), MAX(created_at) FROM orders GROUP BY status;

最佳实践总结

  1. 索引策略

    • 为WHERE、JOIN、ORDER BY、GROUP BY子句中的列创建索引
    • 使用复合索引时注意列的顺序
    • 定期检查和删除未使用的索引
  2. 查询编写

    • 避免在索引列上使用函数
    • 使用适当的LIMIT限制结果集
    • 避免不必要的列选择
  3. 表设计

    • 选择合适的数据类型
    • 考虑分区策略
    • 合理使用冗余字段以避免复杂JOIN
  4. 监控维护

    • 定期分析表统计信息
    • 监控慢查询日志
    • 使用性能分析工具

通过遵循这些优化原则和实践,可以显著提升MySQL数据库的查询性能,确保系统高效稳定运行。