MySQL 性能优化:慢查询与索引优化实战
性能优化是 DBA 和开发者的核心技能。本文从慢查询分析出发,深入讲解 EXPLAIN 执行计划解读、索引失效场景、覆盖索引优化、ICP 索引条件下推,以及生产环境的性能优化实战技巧。
一、慢查询分析
1.1 开启慢查询日志
sql
-- 查看慢查询配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL slow_query_log_file = '/var/lib/mysql/slow.log';
SET GLOBAL long_query_time = 1; -- 超过1秒记录
SET GLOBAL log_queries_not_using_indexes = 'ON'; -- 记录未使用索引的查询
1.2 my.cnf 配置
ini
[mysqld]
# 慢查询日志
slow_query_log = 1
slow_query_log_file = /var/lib/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
# 通用日志(调试用)
general_log = 0
general_log_file = /var/lib/mysql/general.log
# 性能模式
performance_schema = ON
1.3 慢查询日志分析
bash
# 使用 mysqldumpslow 分析
mysqldumpslow -s t -t 10 /var/lib/mysql/slow.log
# 参数说明:
# -s: 排序方式 (c=次数, t=时间, l=锁时间, r=返回行数)
# -t: 显示前 N 条
# -g: 正则匹配
# 常用命令
mysqldumpslow -s t -t 10 slow.log # 按时间排序前10
mysqldumpslow -s c -t 10 slow.log # 按次数排序前10
mysqldumpslow -s r -t 10 slow.log # 按返回行数排序前10
1.4 pt-query-digest 分析
bash
# 安装 Percona Toolkit
# apt install percona-toolkit
# 分析慢查询
pt-query-digest slow.log
# 输出示例:
# Row 1: 0.053s UPDATE orders SET status='completed' WHERE order_id=12345
# Row 2: 0.089s SELECT * FROM users WHERE email='test@example.com'
1.5 performance_schema
sql
-- 开启监控
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES'
WHERE NAME LIKE 'statement/%';
UPDATE performance_schema.setup_consumers
SET ENABLED = 'YES'
WHERE NAME LIKE 'events_statements_%';
-- 查看慢查询
SELECT
DIGEST_TEXT AS query,
COUNT_STAR AS exec_count,
SUM_TIMER_WAIT/1000000000000 AS total_time,
AVG_TIMER_WAIT/1000000000000 AS avg_time,
SUM_ROWS_EXAMINED AS rows_scanned
FROM performance_schema.events_statements_summary_by_digest
WHERE DIGEST_TEXT LIKE '%SELECT%'
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
二、EXPLAIN 执行计划
2.1 EXPLAIN 输出字段
sql
EXPLAIN SELECT * FROM users WHERE name = '张三';
EXPLAIN字段
EXPLAIN 输出
id
select_type
table
type
possible_keys
key
key_len
ref
rows
Extra
2.2 type 字段详解
| type 值 | 含义 | 性能 |
|---|---|---|
| system | 表只有一行 | 最好 |
| const | 主键或唯一索引等值查询 | 极好 |
| eq_ref | 关联时使用主键或唯一索引 | 好 |
| ref | 非唯一索引等值查询 | 好 |
| range | 索引范围扫描 | 中等 |
| index | 全索引扫描 | 较差 |
| ALL | 全表扫描 | 最差 |
sql
-- system
EXPLAIN SELECT * FROM (SELECT * FROM t WHERE id=1) AS tmp;
-- const
EXPLAIN SELECT * FROM t WHERE id = 1; -- 主键查询
-- eq_ref
EXPLAIN SELECT * FROM t1 JOIN t2 ON t1.id = t2.id; -- 关联主键
-- ref
EXPLAIN SELECT * FROM t WHERE name = '张三'; -- 普通索引
-- range
EXPLAIN SELECT * FROM t WHERE id > 10; -- 范围查询
-- index
EXPLAIN SELECT COUNT(*) FROM t; -- 全索引扫描
-- ALL
EXPLAIN SELECT * FROM t WHERE name LIKE '%张%'; -- 全表扫描
2.3 key_len 计算
sql
-- key_len 表示索引使用的字节数
-- 计算公式:
-- int: 4 bytes
-- bigint: 8 bytes
-- varchar(n): n * 3 (utf8mb4) + 2 (长度) + 1 (nullable)
-- char(n): n * 3 (utf8mb4)
-- 示例
CREATE TABLE t (
id BIGINT, -- 8 bytes
name VARCHAR(50), -- 50 * 3 + 2 = 152 bytes
age INT, -- 4 bytes
INDEX idx (name, age)
);
EXPLAIN SELECT * FROM t WHERE name = '张三' AND age = 25;
-- key_len = 152 + 4 = 156 bytes
2.4 Extra 字段分析
sql
-- Using index: 覆盖索引,不需要回表
EXPLAIN SELECT name, age FROM t WHERE name = '张三';
-- Using where: 使用 WHERE 过滤
EXPLAIN SELECT * FROM t WHERE name = '张三';
-- Using index condition: 索引条件下推 (ICP)
EXPLAIN SELECT * FROM t WHERE name LIKE '张%' AND age > 25;
-- Using MRR: 多范围读,优化磁盘 IO
EXPLAIN SELECT * FROM t WHERE id IN (1, 5, 10);
-- Using filesort: 需要额外排序
EXPLAIN SELECT * FROM t ORDER BY create_time;
-- Using temporary: 使用临时表
EXPLAIN SELECT DISTINCT name FROM t;
-- Range checked for each record: 范围检查
EXPLAIN SELECT * FROM t1, t2 WHERE t1.id > t2.id;
三、索引失效场景
3.1 索引失效一览
索引失效场景
函数操作
WHERE YEAR(create_time) = 2024
隐式类型转换
phone = 13800138000 (phone 是 VARCHAR)
LIKE 前缀通配符
WHERE name LIKE '%张%'
OR 条件
WHERE name = '张三' OR age = 25
NOT 操作
WHERE age != 25
WHERE age NOT IN (20, 25)
范围查询在中间
索引 (a, b, c),查询 b > 5
3.2 函数导致索引失效
sql
-- ❌ 索引失效
SELECT * FROM orders
WHERE YEAR(create_time) = 2024
AND MONTH(create_time) = 6;
-- ✅ 优化方案1: 使用范围查询
SELECT * FROM orders
WHERE create_time >= '2024-06-01'
AND create_time < '2024-07-01';
-- ✅ 优化方案2: 创建函数索引 (MySQL 8.0+)
CREATE INDEX idx_year_month ON orders ((YEAR(create_time)), (MONTH(create_time)));
-- ❌ 索引失效
SELECT * FROM users
WHERE SUBSTRING(phone, 1, 3) = '138';
-- ✅ 优化方案
SELECT * FROM users WHERE phone LIKE '138%';
3.3 隐式类型转换
sql
-- phone 是 VARCHAR(20) 类型
-- 查询传入整数,MySQL 会将字符串转为整数
-- 导致索引失效
-- ❌ 索引失效
SELECT * FROM users WHERE phone = 13800138000;
-- ✅ 正确写法
SELECT * FROM users WHERE phone = '13800138000';
-- 如果无法控制参数类型,使用 CAST
SELECT * FROM users WHERE CAST(phone AS CHAR) = '13800138000';
3.4 OR 条件导致索引失效
sql
-- ❌ OR 导致全表扫描
SELECT * FROM users WHERE name = '张三' OR age = 25;
-- age 列没有索引,导致全表扫描
-- ✅ 优化方案1: 使用 UNION
SELECT * FROM users WHERE name = '张三'
UNION
SELECT * FROM users WHERE age = 25;
-- ✅ 优化方案2: 给 age 添加索引
CREATE INDEX idx_age ON users(age);
-- ✅ 优化方案3: 使用 IN
SELECT * FROM users WHERE name IN ('张三', (SELECT name FROM users WHERE age = 25));
3.5 联合索引失效
sql
-- 联合索引 (name, age, city)
CREATE INDEX idx_user ON users(name, age, city);
-- ✅ 完全使用索引
SELECT * FROM users WHERE name = '张三' AND age = 25 AND city = '北京';
SELECT * FROM users WHERE name = '张三' AND age = 25;
SELECT * FROM users WHERE name = '张三';
-- ❌ 完全不使用索引
SELECT * FROM users WHERE age = 25;
SELECT * FROM users WHERE city = '北京';
SELECT * FROM users WHERE age = 25 AND city = '北京';
-- ⚠️ 部分使用索引
SELECT * FROM users WHERE name = '张三' AND city = '北京';
-- 使用 name 部分,city 需要回表过滤
-- ⚠️ age 范围查询,后面的索引失效
SELECT * FROM users WHERE name = '张三' AND age > 25 AND city = '北京';
-- 使用 name 部分,age 和 city 需要回表过滤
四、索引优化技巧
4.1 覆盖索引
覆盖索引
SELECT name, age FROM users WHERE name='张三'
索引 (name, age) 已包含所需字段
无需回表
只需 1 次 IO
非覆盖索引
SELECT * FROM users WHERE name='张三'
命中 name 索引
获取主键
回表查询聚簇索引
1-2 次额外 IO
4.2 索引下推 (ICP)
sql
-- 索引 (name, age, city)
-- MySQL 5.6+ 支持 ICP
-- 优化前 (不启用 ICP)
SELECT * FROM users WHERE name LIKE '张%' AND age = 25;
-- 不启用 ICP:
-- 1. 使用 name 索引找到所有 name LIKE '张%' 的记录
-- 2. 回表获取完整行
-- 3. 在 Server 层过滤 age = 25
-- 启用 ICP:
-- 1. 使用 name 索引找到所有 name LIKE '张%' 的记录
-- 2. 在 Storage 层过滤 age = 25 (Index Condition Pushdown)
-- 3. 只回表获取符合条件的记录
EXPLAIN 输出: Using index condition (而不是 Using where)
4.3 MRR 优化
sql
-- MRR (Multi-Range Read) 优化
-- 适用于范围查询和 JOIN
-- 优化前
SELECT * FROM orders WHERE id IN (1, 5, 10, 20, 15, 3);
-- 不启用 MRR: 按 id 顺序回表查询,可能随机 IO
-- 启用 MRR:
-- 1. 先获取主键列表 [1, 3, 5, 10, 15, 20]
-- 2. 对主键排序 [1, 3, 5, 10, 15, 20]
-- 3. 按排序后的顺序回表查询,顺序 IO
SET @@optimizer_switch = 'mrr=on,mrr_cost_based=off'; -- 强制启用
EXPLAIN 输出: Using MRR
4.4 前缀索引
sql
-- 对长字段创建前缀索引
ALTER TABLE orders ADD INDEX idx_order_no (order_no(10));
-- 选择合适的前缀长度
SELECT
COUNT(DISTINCT LEFT(order_no, 5)) / COUNT(*) AS sel5,
COUNT(DISTINCT LEFT(order_no, 10)) / COUNT(*) AS sel10,
COUNT(DISTINCT LEFT(order_no, 15)) / COUNT(*) AS sel15,
COUNT(DISTINCT order_no) / COUNT(*) AS sel_all
FROM orders;
-- 选择 sel 接近 sel_all 的最小长度
4.5 索引排序优化
sql
-- 索引用于排序
CREATE INDEX idx_create_time ON orders (create_time);
-- ✅ 可以利用索引排序
SELECT * FROM orders ORDER BY create_time;
SELECT * FROM orders WHERE status = 1 ORDER BY create_time;
-- ❌ 无法利用索引排序 (filesort)
SELECT * FROM orders ORDER BY -create_time; -- 使用表达式
SELECT * FROM orders WHERE status = 1 ORDER BY create_time, id;
-- status 不是等值查询,无法利用索引
-- ✅ 复合索引用于排序
CREATE INDEX idx_status_create ON orders (status, create_time);
-- ✅ 利用复合索引排序
SELECT * FROM orders WHERE status = 1 ORDER BY create_time;
五、实战优化案例
5.1 分页优化
sql
-- ❌ 低效分页
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;
-- 需要扫描 1000010 行
-- ✅ 优化方案1: 延迟关联
SELECT * FROM orders
INNER JOIN (
SELECT id FROM orders ORDER BY id LIMIT 1000000, 10
) AS t USING(id);
-- ✅ 优化方案2: 记录上次位置
-- 第一次查询
SELECT * FROM orders ORDER BY id LIMIT 10;
-- 返回 last_id = 100
-- 后续查询
SELECT * FROM orders
WHERE id > 100
ORDER BY id LIMIT 10;
-- ✅ 优化方案3: 使用主键代替 OFFSET
SELECT * FROM orders
WHERE id > (
SELECT id FROM orders LIMIT 1000000, 1
)
LIMIT 10;
5.2 COUNT 优化
sql
-- ❌ 低效 COUNT
SELECT COUNT(*) FROM orders WHERE status = 1;
-- 全表扫描
-- ✅ 优化方案1: 使用覆盖索引
SELECT COUNT(*) FROM orders WHERE status = 1;
-- 创建索引 (status),覆盖查询
-- ✅ 优化方案2: 使用 EXPLAIN 估算
EXPLAIN SELECT * FROM orders WHERE status = 1;
-- rows 列是估算的行数
-- ✅ 优化方案3: 使用统计表
CREATE TABLE order_stats AS
SELECT status, COUNT(*) AS cnt FROM orders GROUP BY status;
5.3 JOIN 优化
sql
-- 小表驱动大表
-- EXPLAIN 显示的 rows 是估算值,值小的表应作为驱动表
-- ❌ 低效 JOIN
SELECT * FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE u.city = '北京';
-- users 表过滤后可能还有很多记录
-- ✅ 优化方案1: 确保过滤条件在驱动表上
SELECT * FROM users u
INNER JOIN orders o ON o.user_id = u.id
WHERE u.city = '北京';
-- users 先过滤,结果集更小
-- ✅ 优化方案2: 添加合适索引
CREATE INDEX idx_user_city ON users(city);
CREATE INDEX idx_order_user ON orders(user_id);
-- ✅ 优化方案3: 使用 STRAIGHT_JOIN 强制顺序
SELECT STRAIGHT_JOIN * FROM users u
INNER JOIN orders o ON o.user_id = u.id
WHERE u.city = '北京';
5.4 子查询优化
sql
-- ❌ 低效子查询
SELECT * FROM users
WHERE id IN (
SELECT user_id FROM orders WHERE amount > 1000
);
-- MySQL 优化器可能将 IN 转为 EXISTS
-- 可能全表扫描
-- ✅ 优化方案1: 改为 JOIN
SELECT DISTINCT u.* FROM users u
INNER JOIN orders o ON o.user_id = u.id
WHERE o.amount > 1000;
-- ✅ 优化方案2: 使用索引
CREATE INDEX idx_order_amount ON orders(amount, user_id);
-- 覆盖查询,不需要回表
-- ✅ 优化方案3: 提前聚合
SELECT u.* FROM users u
INNER JOIN (
SELECT user_id FROM orders
WHERE amount > 1000
GROUP BY user_id
) o ON o.user_id = u.id;
六、Optimizer 优化器深度解析
6.1 MySQL 查询优化器原理
优化器决策因素
代价估算
扫描行数
索引深度
排序代价
JOIN 顺序
查询优化器工作流程
SQL 语句
- 解析 SQL
- 生成 AST
- 预处理 (验证、权限)
- 查询优化
- 生成执行计划
- 执行计划
6.2 执行计划字段详解
重要字段
id: 查询序号
select_type: 查询类型
table: 涉及哪个表
partitions: 涉及哪个分区
type: 访问类型
possible_keys: 可能使用的索引
key: 实际使用的索引
key_len: 索引长度
ref: 索引比较的列
rows: 估算扫描行数
filtered: 过滤后百分比
Extra: 额外信息
EXPLAIN FORMAT=JSON 输出
cost_info
query_cost: 查询总代价
read_cost: 读取代价
eval_cost: 计算代价
nested_loop: NLJ 代价
6.3 optimizer_trace 分析
sql
-- 开启 optimizer_trace
SET optimizer_trace = 'enabled=on';
SET optimizer_trace_max_mem_size = 1048576;
-- 执行查询
SELECT * FROM orders WHERE status = 1 AND create_time > '2024-01-01';
-- 查看优化器决策
SELECT * FROM information_schema.OPTIMIZER_TRACE;
-- 关闭
SET optimizer_trace = 'enabled=off';
json
{
"join_optimization": {
"considered_execution_plans": [
{
"plan_prefix": [],
"table": "`orders`",
"best_access_path": {
"access_type": "ref",
"key": "idx_status",
"rows": 1000,
"cost": 1200
}
}
]
}
}
6.4 Hint 用法大全
sql
-- 强制使用索引
SELECT * FROM orders USE INDEX (idx_status) WHERE status = 1;
-- 忽略索引
SELECT * FROM orders IGNORE INDEX (idx_status) WHERE status = 1;
-- 强制使用指定索引
SELECT * FROM orders FORCE INDEX (idx_status) WHERE status = 1;
-- 强制使用 JOIN 顺序
SELECT STRAIGHT_JOIN * FROM orders o
INNER JOIN users u ON o.user_id = u.id;
-- 改变优化器策略
SELECT /*+ SET_VAR(optimizer_switch='index_merge=off') */ * FROM orders;
-- 改变成本模型
SELECT /*+ NO_ICP(t) */ * FROM orders t WHERE status = 1 AND amount > 100;
-- 强制 MRR
SELECT /*+ MRR(orders) */ * FROM orders WHERE id IN (1, 2, 3);
6.5 成本模型详解
索引成本计算
使用索引的成本
索引扫描成本
回表成本 (每行一次 IO)
比较成本 (每行一次)
成本因子
总成本 = Σ(行数 × 单行成本) + Σ(IO成本)
IO成本: 读取页面数 × 页面读取成本
页面读取成本 = 1 (磁盘) / 0.1 (SSD)
CPU成本: 行处理成本 × 行数
6.6 索引条件下推 (ICP) 深度
ICP 开启条件
ICP 开启条件
MySQL 5.6+
使用 InnoDB 或 MyISAM
非唯一索引
WHERE 条件可下推
ICP 执行流程
查询: SELECT * FROM t WHERE name LIKE '张%' AND age > 25
Server 层发送查询到 Storage 层
Storage 层使用 name 索引
Storage 层过滤 age > 25 (ICP)
只回表获取符合 age > 25 的行
六·续、性能剖析与实战
6.7 Performance Schema 深度使用
sql
-- 启用所有监控
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES'
WHERE NAME LIKE '%';
UPDATE performance_schema.setup_consumers
SET ENABLED = 'YES';
-- 监控语句延迟
SELECT
DIGEST,
DIGEST_TEXT,
COUNT_STAR,
SUM_TIMER_WAIT / 1000000000000 AS total_sec,
AVG_TIMER_WAIT / 1000000000000 AS avg_sec,
MIN_TIMER_WAIT / 1000000000000 AS min_sec,
MAX_TIMER_WAIT / 1000000000000 AS max_sec
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
-- 监控表级 IO
SELECT
OBJECT_SCHEMA,
OBJECT_NAME,
COUNT_FETCH,
COUNT_INSERT,
COUNT_UPDATE,
COUNT_DELETE
FROM performance_schema.table_io_waits_summary_by_table
WHERE OBJECT_SCHEMA = 'myapp'
ORDER BY SUM_TIMER_WAIT DESC;
-- 监控索引使用
SELECT
OBJECT_SCHEMA,
OBJECT_NAME,
INDEX_NAME,
COUNT_FETCH,
COUNT_INSERT,
COUNT_UPDATE,
COUNT_DELETE,
SUM_TIMER_WAIT as total_wait
FROM performance_schema.table_lock_waits_summary_by_table
WHERE OBJECT_SCHEMA = 'myapp';
-- 监控连接
SELECT
USER,
COUNT_CONNECTIONS,
CURRENT_CONNECTIONS,
TOTAL_CONNECTIONS
FROM performance_schema.accounts
ORDER BY TOTAL_CONNECTIONS DESC;
6.8 慢查询优化案例
sql
-- 原始慢查询
SELECT o.id, o.order_no, u.name, u.phone
FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE o.status = 1
AND o.create_time >= '2024-01-01'
AND o.create_time < '2024-02-01';
-- Step 1: EXPLAIN 分析
EXPLAIN SELECT o.id, o.order_no, u.name, u.phone
FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE o.status = 1
AND o.create_time >= '2024-01-01'
AND o.create_time < '2024-02-01';
-- 问题: type=ALL, rows=1000000
-- Step 2: 添加合适索引
CREATE INDEX idx_order_status_time ON orders(status, create_time);
CREATE INDEX idx_order_user ON orders(user_id);
-- Step 3: 再次分析
EXPLAIN SELECT o.id, o.order_no, u.name, u.phone
FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE o.status = 1
AND o.create_time >= '2024-01-01'
AND o.create_time < '2024-02-01';
-- 优化后: type=range, rows=100, 使用了覆盖索引
-- Step 4: 使用覆盖索引避免回表
SELECT o.id, o.order_no, u.name, u.phone
FROM orders o
INNER JOIN users u USE INDEX(idx_order_user) ON o.user_id = u.id
WHERE o.status = 1
AND o.create_time >= '2024-01-01'
AND o.create_time < '2024-02-01';
6.9 连接池优化
java
// HikariCP 核心参数调优
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/myapp");
config.setUsername("root");
config.setPassword("password");
// 连接池大小
config.setMaximumPoolSize(20); // CPU 核心数 * 2
config.setMinimumIdle(5); // 最小空闲连接
// 超时设置
config.setConnectionTimeout(30000); // 获取连接超时 30s
config.setIdleTimeout(600000); // 空闲连接超时 10min
config.setMaxLifetime(1800000); // 连接最大生命周期 30min
// 性能优化
config.setAutoCommit(true);
config.setCachePrepStmts(true); // 开启预处理语句缓存
config.setPrepStmtCacheSize(250); // 缓存大小
config.setPrepStmtCacheSqlLimit(2048); // 最大 SQL 长度
config.setUseServerPrepStmts(true); // 服务端预处理
config.setRewriteBatchedStatements(true); // 批量写入
// 监控
config.setRegisterMbeans(true);
config.setPoolName("MyAppPool");
六、面试高频问题
6.1 如何分析 SQL 性能?
1. 开启慢查询日志
- 设置 long_query_time
- 记录未使用索引的查询
2. 使用 EXPLAIN 分析执行计划
- type: 优先 const, eq_ref, ref
- key: 确认使用了索引
- rows: 扫描行数越少越好
- Extra: 避免 Using filesort, Using temporary
3. 使用 PROFILING
- SHOW PROFILES;
- 查看每个阶段的耗时
4. 使用 optimizer_trace
- 分析优化器决策过程
5. 使用 Performance Schema
- 实时监控查询性能
6.2 什么是覆盖索引?
覆盖索引是一种优化手段:
定义:索引中包含查询需要的所有字段
示例:
索引 (name, age) 上的查询:
SELECT name, age FROM users WHERE name = '张三';
优势:
- 只需扫描索引,不需要回表
- 减少 IO 操作
- 提高查询性能
验证方法:
EXPLAIN 输出中 Extra 列显示 "Using index"
6.3 什么是索引条件下推?
ICP (Index Condition Pushdown) 是 MySQL 5.6+ 的优化:
原理:
- 将 WHERE 条件下推到存储引擎层
- 在索引层面过滤数据
- 减少回表次数
示例:
索引 (name, age),查询:
SELECT * FROM users WHERE name LIKE '张%' AND age > 25;
无 ICP:
1. 使用 name 索引找到所有 '张%' 的记录
2. 回表获取完整行
3. 在 Server 层过滤 age > 25
有 ICP:
1. 使用 name 索引找到所有 '张%' 的记录
2. 在 Storage 层过滤 age > 25 (ICP)
3. 只回表获取符合 age > 25 的记录
验证方法:
EXPLAIN 输出中 Extra 列显示 "Using index condition"
6.4 如何优化分页查询?
分页查询的性能问题:
- OFFSET 很大时,需要扫描大量行
- LIMIT 1000000, 10 扫描 1000010 行
优化方案:
1. 延迟关联
SELECT * FROM orders
INNER JOIN (
SELECT id FROM orders ORDER BY id LIMIT 1000000, 10
) t USING(id);
2. 记录上次位置
- 第一次: LIMIT 10,返回 last_id
- 后续: WHERE id > last_id LIMIT 10
3. 使用书签
- 记录每页的起始 ID
- 使用 WHERE id > start_id LIMIT 10
4. 使用游标
- 使用上一页的最后一行的某个字段
- WHERE (age, id) > (25, 100) LIMIT 10
6.5 什么情况下索引会失效?
1. 函数操作
WHERE YEAR(create_time) = 2024
WHERE LENGTH(phone) = 11
2. 隐式类型转换
phone VARCHAR,但传入整数
WHERE phone = 13800138000
3. LIKE 前缀通配符
WHERE name LIKE '%张%'
WHERE name LIKE '%张'
4. OR 连接非索引列
WHERE name = '张三' OR age = 25
(age 没有索引)
5. NOT 操作
WHERE age != 25
WHERE age NOT IN (20, 25)
6. 联合索引跳跃
索引 (a, b, c)
查询 WHERE b = 1 (跳过 a)
7. 范围查询在中间
索引 (a, b, c)
查询 WHERE a = 1 AND c = 1 (跳过 b)
七、总结
7.1 优化检查清单
✅ SQL 优化检查清单:
1. 执行计划分析
- type 是否达到 range 以上
- key 是否使用索引
- Extra 是否没有 Using filesort
2. 索引使用
- 是否需要回表
- 是否可以使用覆盖索引
- 是否存在索引失效
3. 慢查询
- 是否开启慢查询日志
- 定期分析慢查询日志
- 优化高频慢查询
4. 监控
- 监控 Query 响应时间
- 监控索引使用情况
- 监控锁等待
7.2 优化口诀
SQL 优化口诀:
全表扫描要避免,索引列上无函数
隐式转换要小心,OR 条件需谨慎
LIKE 百分号在前,排序分页要优化
覆盖索引最理想,查询字段要精简
JOIN 小表来驱动,子查询转 JOIN
EXPLAIN 来看执行计划,性能问题早发现
7.3 性能优化优先级
优化优先级(从高到低):
1. SQL 和索引优化 (效果最明显)
- 改写 SQL
- 创建合适索引
2. 架构优化 (需要较大改动)
- 读写分离
- 分库分表
- 缓存
3. 服务器优化 (效果有限)
- 硬件升级
- 参数调优
- 系统配置