ls -lh backups/tar -tzf backups/backup_*.tar.gz # 查看备份内容
2
SQL 慢查询优化
我们将学习如何分析和优化慢查询,这是数据库性能优化的核心技能。任务分解:
启用慢查询日志
分析慢查询场景
优化查询语句
对比优化前后的性能
Copy
-- ============================================-- Day 05: SQL 慢查询优化实战-- ============================================USE infra_db;-- ============================================-- 1. 启用慢查询日志(MySQL 配置)-- ============================================-- 查看当前慢查询配置SHOW VARIABLES LIKE 'slow_query%';SHOW VARIABLES LIKE 'long_query_time';-- 启用慢查询日志(需要 MySQL 权限)-- SET GLOBAL slow_query_log = 'ON';-- SET GLOBAL long_query_time = 1; -- 超过 1 秒的查询记录为慢查询-- ============================================-- 2. 场景一:全表扫描优化-- ============================================-- 问题查询:使用 != 导致全表扫描-- 优化前:type = ALL(全表扫描)EXPLAIN SELECT * FROM users WHERE status != 1;-- 预期:type = ALL, rows = 全表行数-- 优化方案 1:使用 IN 代替 !=-- 优化后:type = range(范围扫描)EXPLAIN SELECT * FROM users WHERE status IN (0, 2);-- 预期:type = range(如果 status 有索引)-- 优化方案 2:使用 UNION(如果 IN 列表很长)EXPLAIN SELECT * FROM users WHERE status = 0UNIONSELECT * FROM users WHERE status = 2;-- 预期:每个子查询可以使用索引-- ============================================-- 3. 场景二:模糊匹配优化-- ============================================-- 问题查询:左模糊匹配导致索引失效-- 优化前:type = ALL(全表扫描)EXPLAIN SELECT * FROM users WHERE username LIKE '%admin';-- 预期:type = ALL, key = NULL-- 优化方案 1:使用右模糊匹配(可以使用索引)EXPLAIN SELECT * FROM users WHERE username LIKE 'admin%';-- 预期:type = range, key = idx_username-- 优化方案 2:如果必须使用左模糊,考虑全文索引或搜索引擎-- 这里只是演示,实际生产环境可能需要 Elasticsearch 等工具-- ============================================-- 4. 场景三:深分页优化(重要!)-- ============================================-- 问题查询:LIMIT 1000000, 10 会导致极慢-- 原因:MySQL 需要扫描并丢弃前 100 万行-- 优化前:type = ALL, rows = 1000010(扫描 100 万+ 行)EXPLAIN SELECT * FROM users ORDER BY id LIMIT 1000000, 10;-- 预期:非常慢,即使有索引-- 优化方案:利用覆盖索引先查 ID,再回表查询-- 步骤 1:先查询 ID(使用覆盖索引,很快)EXPLAIN SELECT id FROM users ORDER BY id LIMIT 1000000, 10;-- 预期:type = index(全索引扫描,但比全表扫描快)-- 步骤 2:使用子查询或 JOIN 回表查询EXPLAIN SELECT u.*FROM users uINNER JOIN ( SELECT id FROM users ORDER BY id LIMIT 1000000, 10) AS tmp ON u.id = tmp.id;-- 预期:先执行子查询(快),再 JOIN(只 JOIN 10 行,也快)-- ============================================-- 5. 场景四:函数操作优化-- ============================================-- 问题查询:对字段使用函数导致索引失效-- 优化前:type = ALL(全表扫描)EXPLAIN SELECT * FROM users WHERE YEAR(created_at) = 2024;-- 预期:type = ALL, key = NULL-- 优化方案:将函数操作移到常量侧-- 优化后:type = range(范围扫描)EXPLAIN SELECT * FROM users WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';-- 预期:type = range(如果 created_at 有索引)-- ============================================-- 6. 场景五:OR 条件优化-- ============================================-- 问题查询:OR 条件可能导致索引失效-- 优化前:可能 type = ALLEXPLAIN SELECT * FROM users WHERE username = 'Alice' OR email = 'alice@example.com';-- 预期:可能无法使用索引-- 优化方案:使用 UNION 代替 OR-- 优化后:每个子查询可以使用索引EXPLAIN SELECT * FROM users WHERE username = 'Alice'UNIONSELECT * FROM users WHERE email = 'alice@example.com';-- 预期:每个子查询 type = ref(如果字段有索引)-- ============================================-- 7. 性能对比测试-- ============================================-- 测试查询性能(需要实际数据)-- 注意:实际测试时需要大量数据才能看到明显差异-- 慢查询(全表扫描)-- SELECT * FROM users WHERE status != 1;-- 优化后(使用索引)-- SELECT * FROM users WHERE status IN (0, 2);-- 使用时间函数测量(MySQL 8.0+)-- SELECT BENCHMARK(1000000, (SELECT COUNT(*) FROM users WHERE status != 1));
字段类型选择详解:
慢查询优化原则
1. 避免全表扫描:
为 WHERE 条件中的字段添加索引
避免使用 !=、NOT IN、IS NULL(某些情况)
避免对字段使用函数
2. 利用索引:
使用 =、IN、BETWEEN、LIKE 'prefix%'
遵循最左前缀原则
使用覆盖索引减少回表
3. 优化 JOIN:
小表驱动大表
为 JOIN 条件添加索引
避免子查询,使用 JOIN
4. 优化排序:
为 ORDER BY 字段添加索引
避免 ORDER BY 和 WHERE 使用不同索引
深分页优化详解
问题:
LIMIT 1000000, 10 需要扫描并丢弃前 100 万行,非常慢。优化方案:
Copy
-- 步骤 1:先查 ID(使用覆盖索引,很快)SELECT id FROM users ORDER BY id LIMIT 1000000, 10;-- 步骤 2:回表查询(只查询 10 行,很快)SELECT * FROM users WHERE id IN (1000001, 1000002, ..., 1000010);
或者使用 JOIN:
Copy
SELECT u.*FROM users uINNER JOIN ( SELECT id FROM users ORDER BY id LIMIT 1000000, 10) AS tmp ON u.id = tmp.id;
性能提升:
优化前:扫描 100 万+ 行,耗时数秒
优化后:扫描索引 + 查询 10 行,耗时毫秒级
小表驱动大表
原则:小表作为驱动表,大表作为被驱动表。原因:
驱动表会被全表扫描
被驱动表通过索引查找
小表扫描快,大表通过索引查找也快
示例:
Copy
-- 假设 users 表小,orders 表大-- ✅ 正确:users 作为驱动表SELECT * FROM users uINNER JOIN orders o ON u.id = o.user_id;-- ❌ 错误:orders 作为驱动表(如果 MySQL 选择错误)-- 可以通过 STRAIGHT_JOIN 强制顺序SELECT * FROM orders oSTRAIGHT_JOIN users u ON o.user_id = u.id;
验证步骤:
连接 MySQL:
Copy
docker exec -it mysql-learn mysql -uroot -proot
执行 SQL 脚本:
Copy
USE infra_db;source /path/to/04_slow_query_opt.sql;
分析查询:
Copy
-- 对比优化前后的 EXPLAIN 结果EXPLAIN SELECT * FROM users WHERE status != 1;EXPLAIN SELECT * FROM users WHERE status IN (0, 2);-- 对比 type 和 rows 的差异
测试深分页优化:
Copy
-- 如果有足够的数据,测试深分页EXPLAIN SELECT * FROM users ORDER BY id LIMIT 1000, 10;EXPLAIN SELECT u.* FROM users uINNER JOIN (SELECT id FROM users ORDER BY id LIMIT 1000, 10) AS tmp ON u.id = tmp.id;