【面试突击】MySQL 查询优化核心面试知识点

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 缓存热点数据

背诵要点:

  1. 索引失效 8 大场景
  2. EXPLAIN 四大关键字段(type、key、rows、Extra)
  3. 三种优化手段(索引、SQL、表设计)
  4. 常见问题解决方案(LIKE、分页、JOIN、COUNT)
相关推荐
1024肥宅2 小时前
JS复杂去重一定要先排序吗?深度解析与性能对比
前端·javascript·面试
-dcr2 小时前
45.MySQL
mysql
大学生资源网2 小时前
基于Javaweb技术的宠物用品商城的设计与实现(源码+文档)
java·mysql·毕业设计·源码·springboot
小无名呀2 小时前
视图(View)
数据库·mysql
星光一影2 小时前
教育培训机构消课管理系统智慧校园艺术舞蹈美术艺术培训班扣课时教务管理系统
java·spring boot·mysql·vue·mybatis·uniapp
lkbhua莱克瓦243 小时前
MySQL介绍
java·开发语言·数据库·笔记·mysql
蜂蜜黄油呀土豆3 小时前
MySQL 锁机制全解析:从锁的分类到并发更新是否阻塞
mysql·innodb·并发控制·数据库锁·事务隔离级别
鱼鱼块3 小时前
二叉搜索树:让数据在有序中生长的智慧之树
javascript·数据结构·面试
努力学算法的蒟蒻3 小时前
day44(12.25)——leetcode面试经典150
面试·职场和发展