乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 数据库基础与安装

    • MySQL 简介
    • MySQL 5.6 到 5.7 到 8.0 关键差异速查
    • 安装 MySQL 5.7
    • 连接与断开服务器
    • 创建数据库
    • 创建数据表
    • 数据库与数据表
    • 加载数据
    • 获取数据库信息
    • 批处理模式
    • SHOW 语句汇总
    • FLUSH 与 RESET 语句
    • my.cnf 核心参数
    • 字符集与排序规则
  • 第2章 SQL基础查询

    • SELECT
    • WHERE
    • ORDER BY
    • LIMIT
    • COUNT
    • 聚合函数
    • 比较运算符
    • 逻辑运算符
    • 算术运算符
    • 模式匹配
    • NULL 值处理
    • UPDATE
    • DELETE
    • REPLACE
    • SELECT INTO
  • 第3章 数据类型与运算符

    • 数值类型
    • 字符串类型
    • 日期时间类型
    • BIT 类型
    • ENUM 类型
    • SET 类型
    • JSON 类型
    • 类型转换
  • 第4章 函数与表达式

    • 字符串函数
    • 数值函数
    • 日期函数
    • 全文检索函数
  • 第5章 高级查询与子查询

    • JOIN
    • 子查询
    • UNION
    • GROUP BY
    • HAVING
    • DISTINCT
  • 第6章 表与索引

    • 数据定义语言
    • 修改表结构
    • 视图
    • 修改视图与检查选项
    • 外键
    • 索引
    • 唯一索引
    • 复合索引
    • 存储引擎对比
    • 分区表
    • 第一范式与第二范式
    • 第三范式与 BC 范式
    • 反范式设计
  • 第7章 存储过程与函数

    • 存储过程
    • 存储函数
    • 变量
    • 流程控制
    • 游标
    • 预处理语句
  • 第8章 事务与锁

    • 事务
    • 事务隔离级别
    • 锁机制
    • MVCC
    • 死锁专题分析
    • LOCK TABLES
    • XA 事务
  • 第9章 用户管理与安全

    • 用户管理
    • 权限管理
    • 角色
    • SQL 注入防范
  • 第10章 性能优化入门

    • 执行计划
    • 索引优化
    • 查询优化
    • 查询优化器提示
    • 慢查询日志
    • InnoDB 深入机制
    • InnoDB 专项优化
    • Performance Schema
    • sys Schema
  • 第11章 复制与高可用

    • 主从复制原理
    • 半同步复制配置
    • binlog 开启与 point-in-time 恢复
    • mysqldump 全库备份
    • mysqldump 单表与条件备份
    • mysqldump 恢复与导入
    • xtrabackup 全量热备
    • xtrabackup 准备与恢复
    • xtrabackup 增量与流式备份
  • 第12章 触发器与事件

    • 触发器
    • 事件调度器
  • 参考

    • MySQL 5.7 专业术语大全
    • MySQL 5.7 关键字与保留字大全

查询优化

导学

索引解决"找得快"的问题,但 SQL 写法决定"找得对不对"。同样的业务需求,不同的 SQL 写法性能可能相差百倍。本节从 SQL 编写层面讲解查询优化技巧。

定义

查询优化:通过改写 SQL、调整查询策略、利用数据库特性,使查询以更少的资源消耗获得正确结果的过程。

查询优化技巧

技巧一:避免 SELECT *

-- 建表
CREATE TABLE employees (
    emp_id INT PRIMARY KEY AUTO_INCREMENT,
    emp_name VARCHAR(20),
    dept VARCHAR(20),
    score DECIMAL(5,2),
    biography TEXT,
    INDEX idx_dept (dept)
);

INSERT INTO employees (emp_name, dept, score, biography) VALUES
('大翔', '技术部', 100, '热爱编程,喜欢开源技术'),
('白歌', '技术部', NULL, '专注算法研究');

当前数据状态:

emp_idemp_namedeptscorebiography
1大翔技术部100热爱编程,喜欢开源技术
2白歌技术部NULL专注算法研究

示例:SELECT * vs 指定列

糟糕的写法:

EXPLAIN SELECT * FROM employees WHERE dept = '技术部';

执行计划:

idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
1SIMPLEemployeesrefidx_deptidx_dept63const2Using index condition

结果解读:

  • SELECT * 需要返回所有列,包括 biography 等大字段
  • 即使走了索引,也需要回表查询聚簇索引获取所有列的数据
  • 浪费网络带宽和内存

优秀的写法:

EXPLAIN SELECT emp_id, emp_name, score FROM employees WHERE dept = '技术部';

执行计划:

idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
1SIMPLEemployeesrefidx_deptidx_dept63const2Using index condition

结果解读:

  • 只返回需要的列,减少数据传输量
  • 虽然这个例子中仍然需要回表(因为 emp_id、emp_name、score 不在 idx_dept 中),但如果创建了覆盖索引,效果更明显

进一步优化(覆盖索引):

CREATE INDEX idx_dept_name_score ON employees(dept, emp_name, score);

EXPLAIN SELECT emp_id, emp_name, score FROM employees WHERE dept = '技术部';

优化后的执行计划:

idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
1SIMPLEemployeesrefidx_dept,idx_dept_name_scoreidx_dept_name_score63const2Using index

结果解读:

  • Extra = Using index:覆盖索引,无需回表
  • SELECT * 无法利用覆盖索引(因为需要所有列),而指定列可以

技巧二:优化深度分页

-- 准备测试数据
CREATE TABLE employees (
    emp_id INT PRIMARY KEY AUTO_INCREMENT,
    emp_name VARCHAR(20),
    dept VARCHAR(20),
    score DECIMAL(5,2)
);

-- 插入 10 条乐途员工数据
INSERT INTO employees (emp_name, dept, score) VALUES
('大翔', '技术部', 100),
('白歌', '技术部', NULL),
('小崔', '技术部', 85),
('黄俪', '市场部', 90),
('李眉', '市场部', 88),
('孔蓝', '财务部', 92),
('赵鸣', '财务部', NULL),
('孙鹤', '技术部', 78),
('高英', '市场部', 95),
('杨英', '财务部', 80);

示例:深度分页问题

糟糕的写法:

-- 偏移量越大越慢,需要扫描 8 行然后丢弃
EXPLAIN SELECT * FROM employees ORDER BY emp_id LIMIT 5, 5;

执行计划:

idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
1SIMPLEemployeesALLNULLNULLNULLNULL10Using filesort

结果解读:

  • rows = 10:MySQL 需要扫描全部数据,排序后取出第 6-10 行
  • Using filesort:需要额外排序
  • 当数据量达到百万级时,LIMIT 1000000, 10 会极其缓慢

优秀的写法:延迟关联

-- 先 LIMIT 取主键,再 JOIN 取完整行
EXPLAIN SELECT e.* FROM employees e
JOIN (
    SELECT emp_id FROM employees
    ORDER BY emp_id LIMIT 5, 5
) tmp ON e.emp_id = tmp.emp_id;

执行计划:

idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
1PRIMARY<derived2>ALLNULLNULLNULLNULL5NULL
1PRIMARYeeq_refPRIMARYPRIMARY4tmp.emp_id1NULL
2DERIVEDemployeesindexNULLPRIMARY4NULL10Using index

结果解读:

  • 子查询只扫描主键索引(Using index),取出 5 个 id
  • 外层 JOIN 通过主键精确查找(eq_ref),只需 5 次查找
  • 避免了扫描大量完整行数据

更优秀的写法:记录上一页最后 ID

-- 业务层记录上一页最后 ID = 5
-- 下一页查询:
EXPLAIN SELECT * FROM employees
WHERE emp_id > 5
ORDER BY emp_id LIMIT 5;

执行计划:

idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
1SIMPLEemployeesrangePRIMARYPRIMARY4NULL5Using where

结果解读:

  • type = range:直接通过主键范围扫描
  • rows = 5:只需扫描 5 行
  • 这是深度分页的最佳方案,但需要业务层配合记录上一页的最后 ID

技巧三:用 JOIN 替代子查询

-- 准备测试表
CREATE TABLE employees (
    emp_id INT PRIMARY KEY AUTO_INCREMENT,
    emp_name VARCHAR(20),
    dept VARCHAR(20),
    score DECIMAL(5,2)
);

CREATE TABLE scores (
    score_id INT PRIMARY KEY AUTO_INCREMENT,
    emp_id INT,
    score DECIMAL(5,2),
    INDEX idx_emp_id (emp_id)
);

INSERT INTO employees (emp_name, dept, score) VALUES
('大翔', '技术部', 100),
('白歌', '技术部', NULL);

INSERT INTO scores (emp_id, score) VALUES
(1, 90),
(2, 85);

当前数据状态:

employees 表:

emp_idemp_namedeptscore
1大翔技术部100
2白歌技术部NULL

scores 表:

score_idemp_idscore
1190.00
2285.00

示例:子查询 vs JOIN

子查询(相关子查询,性能差):

EXPLAIN SELECT emp_name FROM employees e
WHERE (SELECT AVG(score) FROM scores WHERE emp_id = e.emp_id) > 80;

执行计划:

idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
1PRIMARYeALLNULLNULLNULLNULL2Using where
2DEPENDENT SUBQUERYscoresrefidx_emp_ididx_emp_id5func1NULL

结果解读:

  • DEPENDENT SUBQUERY:相关子查询,对外层表的每一行都执行一次子查询
  • 如果 employees 表有 10 万行,子查询会执行 10 万次

JOIN 改写(性能更好):

EXPLAIN SELECT e.emp_name FROM employees e
JOIN (
    SELECT emp_id, AVG(score) AS avg_score
    FROM scores GROUP BY emp_id HAVING avg_score > 80
) t ON e.emp_id = t.emp_id;

执行计划:

idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
1PRIMARY<derived2>ALLNULLNULLNULLNULL2NULL
1PRIMARYeeq_refPRIMARYPRIMARY4t.emp_id1NULL
2DERIVEDscoresindexidx_emp_ididx_emp_id5NULL2Using index

结果解读:

  • 子查询先执行一次,对 scores 表分组聚合(Using index)
  • 外层 JOIN 通过主键精确匹配(eq_ref)
  • 无论 employees 表多大,子查询只执行一次

查询结果:

emp_name
大翔
白歌

技巧四:批量操作替代循环

-- 准备测试表(使用 employees 表结构)
CREATE TABLE employees (
    emp_id INT PRIMARY KEY AUTO_INCREMENT,
    emp_name VARCHAR(20),
    dept VARCHAR(20),
    score DECIMAL(5,2)
);

示例:循环插入 vs 批量插入

糟糕的写法(循环执行):

-- 伪代码,实际应在应用程序中循环执行
-- for emp in ['小崔', '黄俪', '李眉']:
--     INSERT INTO employees (emp_name, dept, score) VALUES (emp, '技术部', 80);

结果解读:

  • 循环执行多次 INSERT,每次都要进行网络往返、事务日志写入
  • 性能极差

优秀的写法(批量插入):

INSERT INTO employees (emp_name, dept, score) VALUES
    ('小崔', '技术部', 85),
    ('黄俪', '市场部', 90),
    ('李眉', '市场部', 88);

查询结果:

emp_idemp_namedeptscore
3小崔技术部85
4黄俪市场部90
5李眉市场部88

结果解读:

  • 一条 SQL 插入 3 行,只需一次网络往返
  • 批量提交的事务日志也更高效
  • 建议每批 1000-5000 行,根据 max_allowed_packet 调整

技巧五:避免大事务

-- 准备测试表
CREATE TABLE huge_table (
    id INT PRIMARY KEY AUTO_INCREMENT,
    status TINYINT DEFAULT 0,
    created_at DATETIME
);

-- 插入测试数据
INSERT INTO huge_table (status, created_at)
SELECT 0, DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 365) DAY)
FROM (
    SELECT @row := @row + 1 AS seq
    FROM (SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3) t1,
         (SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3) t2,
         (SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3) t3,
         (SELECT @row := 0) t4
) numbers
WHERE seq <= 100;

示例:大事务 vs 分批提交

糟糕的写法:

-- 一个事务更新大量数据
START TRANSACTION;
UPDATE huge_table SET status = 1 WHERE created_at < '2024-01-01';
COMMIT;

结果解读:

  • 如果表中数百万行符合条件,这个事务会持有大量行的锁
  • Undo Log 会膨胀,影响性能
  • 主从复制延迟增大
  • 如果中途失败,回滚成本也很高

优秀的写法:分批提交

-- 分批更新,每批 10 行
SET @batch_size = 10;
SET @affected_rows = 1;

WHILE @affected_rows > 0 DO
    START TRANSACTION;
    UPDATE huge_table SET status = 1
    WHERE status <> 1 AND created_at < '2024-01-01'
    LIMIT 10;
    SET @affected_rows = ROW_COUNT();
    COMMIT;
    SELECT SLEEP(0.1);  -- 给从库复制留时间
END WHILE;

结果解读:

  • 每批只更新 10 行,锁持有时间短
  • 分批提交减少 Undo Log 膨胀
  • SLEEP(0.1) 给主从复制留出缓冲时间
  • 即使某一批失败,已提交的批次不会回滚,需要在应用层处理幂等性

技巧六:EXISTS 替代 IN(某些场景)

-- 使用 IN 子查询
EXPLAIN SELECT * FROM employees WHERE emp_id IN (
    SELECT emp_id FROM scores WHERE score > 80
);

执行计划(MySQL 5.7 优化器可能自动优化为半连接):

idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
1SIMPLEscoresindexidx_emp_ididx_emp_id5NULL2Using where; Using index; Start temporary
1SIMPLEemployeeseq_refPRIMARYPRIMARY4scores.emp_id1Using index condition; End temporary

使用 EXISTS 改写:

EXPLAIN SELECT * FROM employees e
WHERE EXISTS (
    SELECT 1 FROM scores WHERE emp_id = e.emp_id AND score > 80
);

执行计划:

idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
1PRIMARYeALLNULLNULLNULLNULL2Using where
2DEPENDENT SUBQUERYscoresrefidx_emp_ididx_emp_id5func1Using where

结果解读:

  • MySQL 5.7 优化器对 IN 和 EXISTS 都有优化,有时会自动转换
  • 一般来说:子查询结果大、主查询结果小时 EXISTS 更好;子查询结果小、主查询结果大时 IN 更好
  • 应以 EXPLAIN 和实际测试为准,不要迷信"EXISTS 一定比 IN 快"

SQL 示例

场景一:优化 COUNT 查询

-- 需要精确计数(必须扫描索引或表)
EXPLAIN SELECT COUNT(*) FROM huge_table WHERE status = 1;

执行计划:

idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
1SIMPLEhuge_tableALLNULLNULLNULLNULL100Using where

近似计数(快,但不精确):

SHOW TABLE STATUS LIKE 'huge_table';

示例输出:

NameEngineRows
huge_tableInnoDB100

结果解读:

  • SHOW TABLE STATUS 的 Rows 是估算值,来自索引统计信息
  • 对于 InnoDB,这个值可能不准确,但获取极快
  • 适合不需要精确计数的场景(如分页显示"约 X 条")

场景二:优化 GROUP BY

-- 确保 GROUP BY 的列有索引
CREATE INDEX idx_status ON huge_table(status);

-- 使用索引完成 GROUP BY
EXPLAIN SELECT status, COUNT(*) FROM huge_table GROUP BY status;

执行计划:

idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
1SIMPLEhuge_tableindexidx_statusidx_status2NULL100Using index

结果解读:

  • type = index:全索引扫描,但 Extra = Using index 表示覆盖索引
  • MySQL 可以直接使用索引完成 GROUP BY,无需临时表

常见误区

误区正解
优化就是加索引SQL 写法、查询策略、事务控制同样重要。
子查询总是慢的MySQL 5.7 优化器会自动将某些子查询转为半连接。应 EXPLAIN 后判断。
事务越大越好大事务持有锁时间长,增加死锁风险,影响复制延迟。应分批提交。

面试考点

Q:深度分页怎么优化?

  1. 延迟关联(先 LIMIT 取主键,再 JOIN 取完整行);2. 业务层记录上一页最后 ID,用 WHERE id > last_id LIMIT N;3. 限制最大页码。

Q:大事务有什么危害?

  1. 锁持有时间长,阻塞其他事务;2. Undo Log 膨胀,影响性能;3. 主从复制延迟;4. 故障恢复时间长。应分批提交。

Q:EXISTS 一定比 IN 快吗?

不一定。MySQL 5.7 优化器对两者都有优化。一般来说:子查询结果大、主查询结果小时 EXISTS 更好;子查询结果小、主查询结果大时 IN 更好。应以 EXPLAIN 和实际测试为准。

小结

  • 查询优化包括索引优化和 SQL 写法优化
  • 避免 SELECT *、优化分页、用 JOIN 替代子查询、批量操作
  • 控制事务大小,避免大事务带来的锁和复制问题
  • 优化是系统工程,需要 EXPLAIN、慢查询日志和实际测试验证

下一章引子:查询优化让单条 SQL 更快,而事务则让多条 SQL 组合在一起具有原子性和一致性——这是数据库最核心的特性之一。

上一页
索引优化
下一页
查询优化器提示