MySQL 查询优化核心面试知识点
🎯 一、索引优化(最核心)
1.1 索引类型
【B+Tree 索引】(InnoDB 默认)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
结构:
[Root]
/ \
[Branch] [Branch]
/ \ / \
[L] [L] [L] [L] ← 叶子节点存储数据
特点:
✅ 叶子节点有序排列
✅ 叶子节点之间有指针(范围查询快)
✅ 所有数据在叶子节点
✅ 查询时间复杂度 O(log n)
适用场景:
• =、>、<、>=、<=、BETWEEN
• LIKE 'abc%'(前缀匹配)
• ORDER BY、GROUP BY
【Hash 索引】(Memory 引擎)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
特点:
✅ 等值查询极快 O(1)
❌ 不支持范围查询
❌ 不支持排序
❌ 不支持最左前缀
适用场景:
• 仅等值查询(=、IN)
• 缓存表
【全文索引】(FULLTEXT)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
适用场景:
• 文章内容搜索
• MATCH... AGAINST 语法
【空间索引】(SPATIAL)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
适用场景:
• 地理位置查询
1.2 索引分类
【按列数分类】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
单列索引:
CREATE INDEX idx_name ON users(name);
联合索引(组合索引):
CREATE INDEX idx_name_age ON users(name, age);
【按功能分类】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
主键索引(PRIMARY KEY):
• 唯一 + 非空
• 聚簇索引(InnoDB)
• 一张表只能有一个
唯一索引(UNIQUE):
• 列值唯一,允许 NULL
CREATE UNIQUE INDEX idx_email ON users(email);
普通索引(INDEX):
• 最基本的索引
CREATE INDEX idx_age ON users(age);
前缀索引:
• 对字符串前缀建索引
CREATE INDEX idx_name_prefix ON users(name(10));
【按存储方式分类】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
聚簇索引(Clustered Index):
• 数据和索引存储在一起
• InnoDB 主键就是聚簇索引
• 一张表只有一个
非聚簇索引(Secondary Index):
• 索引和数据分开存储
• 普通索引、唯一索引
• 需要回表查询
1.3 索引优化规则(必背)
最左前缀原则
索引:idx_name_age_city (name, age, city)
✅ 生效的查询:
WHERE name = 'Alice'
WHERE name = 'Alice' AND age = 25
WHERE name = 'Alice' AND age = 25 AND city = 'Beijing'
WHERE name = 'Alice' AND city = 'Beijing' (name 生效)
❌ 不生效的查询:
WHERE age = 25
WHERE city = 'Beijing'
WHERE age = 25 AND city = 'Beijing'
口诀:带头大哥不能死,中间兄弟不能断
索引失效场景(必背)
1️⃣ 在索引列上使用函数
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
❌ SELECT * FROM users WHERE YEAR(birthday) = 1990;
✅ SELECT * FROM users WHERE birthday BETWEEN '1990-01-01' AND '1990-12-31';
2️⃣ 隐式类型转换
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
表结构:phone VARCHAR(11)
❌ SELECT * FROM users WHERE phone = 13800138000; (数字)
✅ SELECT * FROM users WHERE phone = '13800138000'; (字符串)
3️⃣ LIKE 以 % 开头
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
❌ SELECT * FROM users WHERE name LIKE '%Alice';
❌ SELECT * FROM users WHERE name LIKE '%Alice%';
✅ SELECT * FROM users WHERE name LIKE 'Alice%';
4️⃣ OR 条件
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
❌ SELECT * FROM users WHERE name = 'Alice' OR age = 25;
✅ SELECT * FROM users WHERE name = 'Alice'
UNION ALL
SELECT * FROM users WHERE age = 25;
或者给两个字段都建索引,MySQL 会优化
5️⃣ 不等于操作
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
❌ SELECT * FROM users WHERE age != 25;
❌ SELECT * FROM users WHERE age <> 25;
6️⃣ IS NULL / IS NOT NULL
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
❌ SELECT * FROM users WHERE name IS NULL;
⚠️ SELECT * FROM users WHERE name IS NOT NULL; (可能失效)
7️⃣ NOT IN、NOT EXISTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
❌ SELECT * FROM users WHERE id NOT IN (1,2,3);
✅ SELECT * FROM users WHERE id IN (4,5,6); (取反逻辑)
8️⃣ 索引列参与计算
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
❌ SELECT * FROM users WHERE age + 1 = 26;
✅ SELECT * FROM users WHERE age = 25;
索引覆盖(Covering Index)
定义:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
查询的列都在索引中,不需要回表
示例:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
索引:idx_name_age (name, age)
✅ 索引覆盖:
SELECT name, age FROM users WHERE name = 'Alice';
(Extra: Using index)
❌ 需要回表:
SELECT name, age, email FROM users WHERE name = 'Alice';
(email 不在索引中,需要回表)
优势:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
• 避免回表,减少 IO
• 查询速度更快
索引下推(Index Condition Pushdown)
MySQL 5.6+ 特性
示例:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
索引:idx_name_age (name, age)
SELECT * FROM users WHERE name LIKE 'A%' AND age = 25;
【没有索引下推】
1. 根据 name LIKE 'A%' 查询索引,找到所有 name 以 A 开头的数据
2. 回表获取完整行数据
3. 过滤 age = 25
【有索引下推】
1. 在索引中同时过滤 name LIKE 'A%' AND age = 25
2. 只回表获取符合条件的数据
优势:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
• 减少回表次数
• 减少 Server 层数据过滤
🎯 二、SQL 语句优化
2.1 SELECT 优化
【避免 SELECT *】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
❌ SELECT * FROM users WHERE id = 1;
✅ SELECT id, name, email FROM users WHERE id = 1;
原因:
• 传输数据量大
• 无法使用索引覆盖
• 增加网络开销
• 应用解析慢
【小表驱动大表】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
场景:用户表 100 万,订单表 10 万
❌ IN(大表驱动小表):
SELECT * FROM orders WHERE user_id IN (
SELECT id FROM users WHERE age > 25
);
(先查 users,可能百万条,再查 orders)
✅ EXISTS(小表驱动大表):
SELECT * FROM orders o WHERE EXISTS (
SELECT 1 FROM users u WHERE u.id = o.user_id AND u.age > 25
);
(先查 orders 10 万条,再去 users 验证)
【避免子查询,用 JOIN】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
❌ 子查询:
SELECT * FROM users WHERE id IN (
SELECT user_id FROM orders WHERE status = 'paid'
);
✅ JOIN:
SELECT DISTINCT u.* FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.status = 'paid';
原因:
• 子查询会创建临时表
• JOIN 可以使用索引
【分页优化】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
❌ 深分页(OFFSET 大):
SELECT * FROM users ORDER BY id LIMIT 1000000, 10;
(需要扫描 1000010 行,丢弃前 1000000 行)
✅ 延迟关联:
SELECT * FROM users
WHERE id >= (
SELECT id FROM users ORDER BY id LIMIT 1000000, 1
)
ORDER BY id LIMIT 10;
✅ 记录上次最大 ID:
SELECT * FROM users WHERE id > 1000000 ORDER BY id LIMIT 10;
【批量操作】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
❌ 逐条插入:
for (User user : users) {
INSERT INTO users VALUES (...);
}
✅ 批量插入:
INSERT INTO users VALUES
(1, 'Alice', ... ),
(2, 'Bob', ...),
(3, 'Charlie', ...);
Java 代码:
String sql = "INSERT INTO users(name, age) VALUES (?, ?)";
PreparedStatement ps = conn.prepareStatement(sql);
for (User user : users) {
ps.setString(1, user.getName());
ps.setInt(2, user. getAge());
ps.addBatch();
}
ps.executeBatch();
2.2 JOIN 优化
【JOIN 类型选择】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
INNER JOIN(首选):
• 只返回匹配的数据
• 性能最好
LEFT JOIN:
• 返回左表所有数据
• 右表没有匹配则 NULL
• 注意 WHERE 条件位置
RIGHT JOIN:
• 不推荐(改用 LEFT JOIN)
【JOIN 优化原则】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 小表驱动大表
• 小表作为驱动表(左表)
• 大表作为被驱动表(右表)
2. 被驱动表的 JOIN 字段建索引
✅ CREATE INDEX idx_order_user ON orders(user_id);
SELECT * FROM users u
INNER JOIN orders o ON u.id = o.user_id;
3. 避免 JOIN 过多表
• 阿里规范:不超过 3 个表
• JOIN 越多,优化器选择越难
【LEFT JOIN 陷阱】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
❌ WHERE 条件放错位置:
SELECT * FROM users u
LEFT JOIN orders o ON u. id = o.user_id
WHERE o.status = 'paid';
(变成 INNER JOIN 了)
✅ 正确写法:
SELECT * FROM users u
LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid';
【STRAIGHT_JOIN 强制驱动顺序】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SELECT * FROM users u
STRAIGHT_JOIN orders o ON u.id = o.user_id;
(强制 users 作为驱动表)
2.3 排序优化
【利用索引排序(Using index)】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
索引:idx_age_create_time (age, create_time)
✅ 走索引排序:
SELECT * FROM users WHERE age = 25 ORDER BY create_time;
(Extra: Using index)
❌ 不走索引(Using filesort):
SELECT * FROM users ORDER BY create_time;
(没有 WHERE age)
SELECT * FROM users WHERE age = 25 ORDER BY name;
(name 不在索引中)
【避免 Using filesort】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
filesort 表示无法利用索引排序,需要额外排序
方案 1:建立合适的索引
方案 2:增大 sort_buffer_size
方案 3:减少返回字段(索引覆盖)
【ORDER BY 优化】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 单字段排序:
ORDER BY create_time
✅ 多字段同向排序:
ORDER BY age ASC, create_time ASC
❌ 多字段反向排序:
ORDER BY age ASC, create_time DESC
(MySQL 8.0+ 支持倒序索引)
【LIMIT 与 ORDER BY】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 利用索引:
SELECT * FROM users ORDER BY id LIMIT 10;
(主键有序,直接取前 10 条)
❌ 全表排序:
SELECT * FROM users ORDER BY create_time LIMIT 10;
(如果没有索引,需要全表排序)
2.4 GROUP BY 优化
【GROUP BY 优化原则】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 分组字段建索引
CREATE INDEX idx_age ON users(age);
SELECT age, COUNT(*) FROM users GROUP BY age;
2. WHERE 过滤在 GROUP BY 之前
✅ SELECT age, COUNT(*) FROM users
WHERE status = 1 GROUP BY age;
❌ SELECT age, COUNT(*) FROM users
GROUP BY age HAVING status = 1;
3. 避免 Using temporary
(需要创建临时表,性能差)
【索引覆盖 + GROUP BY】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
索引:idx_age_status (age, status)
✅ 索引覆盖:
SELECT age, status, COUNT(*) FROM users
GROUP BY age, status;
(Extra: Using index)
❌ 需要临时表:
SELECT age, name, COUNT(*) FROM users
GROUP BY age;
(name 不在索引中)
🎯 三、EXPLAIN 执行计划(必会)
3.1 EXPLAIN 输出字段
sql
EXPLAIN SELECT * FROM users WHERE name = 'Alice';
【关键字段解释】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. id
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
• 查询序列号
• id 越大越先执行
• id 相同从上往下执行
2. select_type(查询类型)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SIMPLE:简单查询(不含子查询和 UNION)
PRIMARY:主查询
SUBQUERY:子查询
DERIVED:衍生表(FROM 子句中的子查询)
UNION:UNION 后面的查询
3. table
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
查询的表名
4. type(访问类型)⭐ 重要
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
性能从好到差:
system:表只有一行(系统表)
const:主键或唯一索引查询,最多返回 1 行
WHERE id = 1
eq_ref:唯一索引扫描,JOIN 时使用
ON u.id = o.user_id(user_id 是主键)
ref:非唯一索引扫描
WHERE name = 'Alice'
range:索引范围扫描
WHERE age > 25
WHERE id BETWEEN 1 AND 100
index:全索引扫描
SELECT id FROM users
ALL:全表扫描 ❌
SELECT * FROM users(没有索引)
优化目标:至少达到 range,最好 ref
5. possible_keys
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
可能使用的索引
6. key ⭐ 重要
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
实际使用的索引
NULL 表示没有使用索引 ❌
7. key_len
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
使用的索引长度(字节数)
越小越好
计算:
INT:4 字节
BIGINT:8 字节
VARCHAR(n):n * 字符集字节数 + 2(存长度)
允许 NULL:+1
8. ref
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
与索引比较的列或常量
const:常量
db. table.column:其他表的列
9. rows ⭐ 重要
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
预计扫描的行数
越少越好
10. Extra ⭐ 重要
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
额外信息
✅ Using index:索引覆盖
✅ Using index condition:索引下推
⚠️ Using where:WHERE 过滤
⚠️ Using temporary:使用临时表(GROUP BY)
⚠️ Using filesort:文件排序(ORDER BY)
❌ Using join buffer:JOIN 缓冲(缺少索引)
3.2 EXPLAIN 示例分析
sql
-- 示例 1:理想的查询
EXPLAIN SELECT id, name FROM users WHERE name = 'Alice';
结果:
id: 1
select_type: SIMPLE
type: ref ✅ 好
key: idx_name ✅ 使用索引
rows: 1 ✅ 少
Extra: Using index ✅ 索引覆盖
-- 示例 2:需要优化的查询
EXPLAIN SELECT * FROM users WHERE YEAR(birthday) = 1990;
结果:
id: 1
select_type: SIMPLE
type: ALL ❌ 全表扫描
key: NULL ❌ 没用索引
rows: 1000000 ❌ 扫描百万行
Extra: Using where ⚠️ 需要过滤
优化:
CREATE INDEX idx_birthday ON users(birthday);
SELECT * FROM users
WHERE birthday BETWEEN '1990-01-01' AND '1990-12-31';
-- 示例 3:联合索引部分使用
索引:idx_name_age_city (name, age, city)
EXPLAIN SELECT * FROM users WHERE name = 'Alice' AND city = 'Beijing';
结果:
key: idx_name_age_city
key_len: 202 ⚠️ 只用了 name(跳过了 age)
Extra: Using index condition
优化:
WHERE name = 'Alice' AND age IS NOT NULL AND city = 'Beijing'
(不推荐,改变查询逻辑)
更好的方案:
调整索引顺序 idx_name_city_age
🎯 四、表结构设计优化
4.1 字段类型选择
【选择合适的数据类型】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 整数类型
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TINYINT:1 字节,-128 ~ 127
SMALLINT:2 字节,-32768 ~ 32767
INT:4 字节,-21 亿 ~ 21 亿
BIGINT:8 字节
✅ 状态字段用 TINYINT
✅ 年龄用 TINYINT UNSIGNED
✅ 自增 ID 用 INT 或 BIGINT
❌ 不要都用 BIGINT
2. 字符串类型
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CHAR(n):定长,最大 255
• 适合:性别、状态码
• 优点:查询快
• 缺点:浪费空间
VARCHAR(n):变长,最大 65535
• 适合:姓名、地址
• 优点:节省空间
• 缺点:需要额外 1-2 字节存长度
TEXT:大文本
• 适合:文章内容
• 缺点:不能建索引(或前缀索引)
选择建议:
✅ 长度固定用 CHAR
✅ 长度可变用 VARCHAR
✅ 大文本用 TEXT
✅ n 设置为最大实际长度,不要过大
3. 时间类型
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DATETIME:8 字节,'1000-01-01' ~ '9999-12-31'
• 与时区无关
TIMESTAMP:4 字节,'1970-01-01' ~ '2038-01-19'
• 与时区有关
• 自动更新(ON UPDATE CURRENT_TIMESTAMP)
INT:4 字节,存时间戳
• 需要转换
选择建议:
✅ 推荐 DATETIME(范围大,不受时区影响)
✅ 创建/更新时间用 TIMESTAMP
❌ 不推荐 INT(可读性差)
4. 枚举类型
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ENUM('male', 'female')
✅ 优点:存储空间小(1-2 字节)
❌ 缺点:修改枚举值需要 ALTER TABLE
推荐:用 TINYINT + 注释
status TINYINT COMMENT '0-待支付 1-已支付 2-已取消'
【字段长度优化】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
❌ VARCHAR(255) name (实际最长 20)
✅ VARCHAR(50) name
原因:
• 索引长度限制(767 或 3072 字节)
• 占用更多内存和磁盘
4.2 表设计规范
【三大范式】(了解)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第一范式(1NF):字段不可再分
第二范式(2NF):非主键字段完全依赖主键
第三范式(3NF):非主键字段不能相互依赖
实际应用:适度反范式化
【垂直拆分】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
原表:users (id, name, email, avatar_url, intro TEXT)
拆分:
users (id, name, email)
user_profiles (user_id, avatar_url, intro)
优势:
• 常用字段放一起(查询快)
• 大字段单独存储(不影响主表)
【水平拆分】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
orders 表 1 亿数据
拆分:
orders_2023 (1000 万)
orders_2024 (1000 万)
或按 ID 取模:
orders_0, orders_1, ... orders_9
优势:
• 单表数据量小(查询快)
• 降低锁粒度
【字段设计规范】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 主键自增 BIGINT
✅ 非空字段设置 NOT NULL
✅ 合理设置默认值 DEFAULT
✅ 添加注释 COMMENT
✅ 时间字段:created_at, updated_at
✅ 逻辑删除:is_deleted TINYINT DEFAULT 0
示例:
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '用户 ID',
name VARCHAR(50) NOT NULL DEFAULT '' COMMENT '姓名',
age TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '年龄',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态 0-禁用 1-正常',
is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除 0-否 1-是',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_name (name),
INDEX idx_status (status, is_deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
🎯 五、慢查询优化流程
5.1 定位慢查询
【开启慢查询日志】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-- 查看慢查询配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 1 秒
-- 配置文件(永久生效)
[mysqld]
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow. log
long_query_time = 1
【分析慢查询日志】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
使用 mysqldumpslow 工具:
# 最慢的 10 条
mysqldumpslow -s t -t 10 /var/log/mysql/slow. log
# 访问次数最多的 10 条
mysqldumpslow -s c -t 10 /var/log/mysql/slow.log
# 平均执行时间最多的 10 条
mysqldumpslow -s at -t 10 /var/log/mysql/slow.log
【慢查询日志示例】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Time: 2024-01-01T10:00:00.123456Z
# User@Host: root[root] @ localhost []
# Query_time: 3.123456 Lock_time: 0.000123 Rows_sent: 100 Rows_examined: 1000000
SET timestamp=1704096000;
SELECT * FROM users WHERE name LIKE '%Alice%';
5.2 优化步骤
【慢查询优化流程】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
步骤 1:EXPLAIN 分析
EXPLAIN SELECT * FROM users WHERE name LIKE '%Alice%';
查看:
• type: ALL(全表扫描) ❌
• key: NULL(没用索引) ❌
• rows: 1000000(扫描百万行) ❌
步骤 2:定位问题
• LIKE '%Alice%' 导致索引失效
步骤 3:优化方案
方案 1:改为前缀匹配
WHERE name LIKE 'Alice%'
方案 2:使用全文索引
ALTER TABLE users ADD FULLTEXT idx_name_ft (name);
SELECT * FROM users WHERE MATCH(name) AGAINST('Alice');
方案 3:使用 Elasticsearch(推荐)
专业的搜索引擎
步骤 4:验证优化效果
EXPLAIN SELECT * FROM users WHERE name LIKE 'Alice%';
对比:
• type: range ✅
• key: idx_name ✅
• rows: 10 ✅
步骤 5:监控
• 查看执行时间是否降低
• 监控 CPU、内存使用情况
🎯 七、面试常见问题(必背)
Q1: 如何定位慢查询?
答:
1. 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
2. 使用 mysqldumpslow 分析慢查询日志
找出最慢的 SQL
3. 使用 EXPLAIN 分析执行计划
查看是否使用索引、扫描行数等
4. 使用 SHOW PROFILE 查看详细耗时
(了解各阶段耗时)
5. 应用层监控
• Spring Boot Actuator
• SkyWalking
• Druid 连接池监控
Q2: EXPLAIN 的 type 字段有哪些值?性能如何?
答:
从好到差:
system > const > eq_ref > ref > range > index > ALL
system: 表只有一行(系统表)
const: 主键或唯一索引查询,最多1行(WHERE id = 1)
eq_ref: JOIN时使用主键或唯一索引
ref: 非唯一索引查询(WHERE name = 'Alice')
range: 范围查询(WHERE age > 25)
index: 全索引扫描
ALL: 全表扫描 ❌
优化目标:至少达到 range,最好 ref
Q3: 索引失效的场景有哪些?
答:
1. 在索引列上使用函数
❌ WHERE YEAR(birthday) = 1990
2. 隐式类型转换
❌ WHERE phone = 13800138000 (phone 是 VARCHAR)
3. LIKE 以 % 开头
❌ WHERE name LIKE '%Alice'
4. 使用 OR(两边没都建索引)
❌ WHERE name = 'Alice' OR age = 25
5. 不等于操作
❌ WHERE age != 25
6. IS NULL / IS NOT NULL(可能失效)
7. NOT IN / NOT EXISTS
8. 索引列参与计算
❌ WHERE age + 1 = 26
9. 违反最左前缀原则
索引(name, age)
❌ WHERE age = 25
Q4: 如何优化 LIKE '%keyword%'?
答:
方案 1:改为前缀匹配(如果业务允许)
WHERE name LIKE 'Alice%'
方案 2:使用全文索引
ALTER TABLE users ADD FULLTEXT idx_name_ft (name);
SELECT * FROM users WHERE MATCH(name) AGAINST('Alice');
方案 3:使用 Elasticsearch(推荐)
• 专业的搜索引擎
• 支持模糊搜索、分词、高亮等
• 性能好
方案 4:业务层优化
• 限制最少输入字符数(如至少3个字符)
• 提供搜索建议(自动补全)
Q5: 如何优化分页查询?
答:
问题:深分页慢
SELECT * FROM users ORDER BY id LIMIT 1000000, 10;
(需要扫描 1000010 行,丢弃前 1000000 行)
方案 1:延迟关联
SELECT * FROM users u
INNER JOIN (
SELECT id FROM users ORDER BY id LIMIT 1000000, 10
) t ON u.id = t.id;
(子查询走索引覆盖,快)
方案 2:记录上次最大 ID
SELECT * FROM users WHERE id > 1000000 ORDER BY id LIMIT 10;
(利用主键索引,快)
方案 3:业务优化
• 禁止跳转到指定页(只能上一页/下一页)
• 限制最大页数(如最多查看前 100 页)
Q6: JOIN 查询如何优化?
答:
1. 小表驱动大表
• 小表作为驱动表(左表)
• 减少循环次数
2. 被驱动表的 JOIN 字段建索引
CREATE INDEX idx_user_id ON orders(user_id);
SELECT * FROM users u
INNER JOIN orders o ON u.id = o.user_id;
3. 避免 JOIN 过多表
• 阿里规范:不超过 3 个
• JOIN 越多,优化器选择越复杂
4. 使用 STRAIGHT_JOIN 强制驱动顺序(必要时)
5. 避免 SELECT *
• 只查询需要的字段
• 减少数据传输
6. 索引覆盖
• 查询字段都在索引中
• 避免回表
Q7: COUNT(*) 和 COUNT(1) 和 COUNT(字段) 的区别?
答:
COUNT(*):
• 统计行数(包括 NULL)
• MySQL 已优化,推荐
COUNT(1):
• 统计行数(包括 NULL)
• 性能与 COUNT(*) 相同
COUNT(字段):
• 统计非 NULL 值的行数
• 需要判断 NULL,性能差
• 如果字段是主键,性能接近 COUNT(*)
性能对比(InnoDB):
COUNT(*) ≈ COUNT(1) > COUNT(主键) > COUNT(非主键字段)
推荐:使用 COUNT(*)
Q8: 如何设计索引?
答:
1. 选择合适的列
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ WHERE、JOIN、ORDER BY、GROUP BY 的列
✅ 区分度高的列(如 user_id、email)
❌ 区分度低的列(如性别、状态)
❌ 频繁更新的列
2. 联合索引设计
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
• 遵循最左前缀原则
• 区分度高的列在前
• 查询频繁的列在前
示例:idx_name_age_city (name, age, city)
• name 区分度最高
• name + age 组合查询最多
3. 前缀索引
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CREATE INDEX idx_email ON users(email(20));
• 节省空间
• 但无法 ORDER BY
4. 索引数量控制
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
• 单表索引不超过 5 个(阿里规范)
• 联合索引不超过 5 个字段
• 索引越多,写入越慢
5. 定期维护
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
• 删除无用索引
• 分析索引使用情况
SHOW INDEX FROM users;
🎯 八、优化总结(背诵版)
核心优化手段
【索引优化】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 建立合适的索引
2. 避免索引失效(8 大场景)
3. 利用索引覆盖
4. 利用索引下推
5. 遵循最左前缀原则
【SQL 优化】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 避免 SELECT *
2. 小表驱动大表
3. 避免子查询,用 JOIN
4. 分页优化(延迟关联、记录ID)
5. 批量操作(批量插入、批量更新)
【表设计优化】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 选择合适的字段类型
2. 字段设置 NOT NULL + DEFAULT
3. 垂直拆分 / 水平拆分
4. 合理冗余字段(反范式化)
【执行计划分析】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 使用 EXPLAIN 分析
2. 关注 type(至少 range)
3. 关注 key(是否用索引)
4. 关注 rows(扫描行数)
5. 关注 Extra(Using index 最好)
【其他优化】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 开启慢查询日志
2. 使用连接池
3. 读写分离
4. 分库分表
5. 使用缓存(Redis)
面试话术模板
面试官:如何优化 MySQL 查询?
你:MySQL 查询优化主要从四个方面入手:
1. 索引优化
首先要建立合适的索引,比如在 WHERE、JOIN、ORDER BY 的字段上建索引。
使用联合索引时要遵循最左前缀原则。
还要注意避免索引失效,比如不在索引列上使用函数、避免隐式类型转换、
LIKE 不以 % 开头等。
2. SQL 语句优化
避免 SELECT *,只查询需要的字段。
遵循小表驱动大表原则,用 JOIN 代替子查询。
深分页要使用延迟关联或记录上次 ID。
批量操作用 PreparedStatement. addBatch()。
3. 执行计划分析
使用 EXPLAIN 分析 SQL,重点关注:
• type:至少达到 range,最好 ref
• key:是否使用了索引
• rows:扫描行数是否合理
• Extra:Using index 表示索引覆盖,性能最好
4. 表结构优化
选择合适的字段类型,比如状态用 TINYINT,时间用 DATETIME。
字段设置 NOT NULL 和 DEFAULT 值。
数据量大时考虑垂直拆分或水平拆分。
在实际项目中,我们还会配合:
• 开启慢查询日志定位问题
• 使用 Druid 连接池监控 SQL
• 读写分离降低主库压力
• Redis 缓存热点数据
背诵要点:
- 索引失效 8 大场景
- EXPLAIN 四大关键字段(type、key、rows、Extra)
- 三种优化手段(索引、SQL、表设计)
- 常见问题解决方案(LIKE、分页、JOIN、COUNT)