MySQL查询优化:从执行计划到性能调优

引言 “过早优化是万恶之源。但当性能问题真正出现时,优化就是救命稻草。” —— Donald Knuth 在前三篇文章中,我们学习了索引、事务、锁的原理。但光有理论还不够,如何定位和优化慢查询? 想象这样的场景: 凌晨3点,你被一通电话吵醒: "数据库快挂了,所有查询都超时!" 你打开监控,发现: - CPU 100% - 慢查询日志爆满 - 某个SQL执行了10秒还没返回 如何快速定位问题?如何优化这个慢查询? 这就是查询优化的核心价值:让慢查询变快,让系统起死回生。 今天,我们从第一性原理出发,深度剖析MySQL的查询优化: SQL执行流程: 客户端 → 连接器 → 解析器 → 优化器 → 执行器 → 存储引擎 ↓ ↓ ↓ ↓ ↓ 权限检查 语法解析 生成计划 执行查询 返回数据 性能优化: 慢查询 → EXPLAIN → 找到瓶颈 → 优化索引 → 改写SQL → 性能飞跃 10秒 分析 全表扫描 建索引 覆盖索引 10ms 我们还将通过10个真实案例,将慢查询从10秒优化到10ms,性能提升1000倍。 一、SQL执行流程:从SQL到结果集 理解查询优化,首先要理解SQL是如何执行的。 1.1 MySQL的架构:两层结构 ┌─────────────────────────────────────────────────────────────┐ │ MySQL Server层 │ ├─────────────────────────────────────────────────────────────┤ │ 连接器 解析器 优化器 执行器 │ │ ↓ ↓ ↓ ↓ │ │ 权限验证 语法解析 生成计划 执行查询 │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 存储引擎层 │ ├─────────────────────────────────────────────────────────────┤ │ InnoDB MyISAM Memory Archive │ │ ↓ ↓ ↓ ↓ │ │ 事务支持 不支持 内存存储 压缩存储 │ └─────────────────────────────────────────────────────────────┘ Server层与存储引擎的职责分工: ...

2025-11-03 · maneng

MySQL查询优化:从执行计划到性能调优

引言 “过早优化是万恶之源。但当性能问题真正出现时,优化就是救命稻草。” —— Donald Knuth 在前三篇文章中,我们学习了索引、事务、锁的原理。但光有理论还不够,如何定位和优化慢查询? 想象这样的场景: 凌晨3点,你被一通电话吵醒: "数据库快挂了,所有查询都超时!" 你打开监控,发现: - CPU 100% - 慢查询日志爆满 - 某个SQL执行了10秒还没返回 如何快速定位问题?如何优化这个慢查询? 这就是查询优化的核心价值:让慢查询变快,让系统起死回生。 今天,我们从第一性原理出发,深度剖析MySQL的查询优化: SQL执行流程: 客户端 → 连接器 → 解析器 → 优化器 → 执行器 → 存储引擎 ↓ ↓ ↓ ↓ ↓ 权限检查 语法解析 生成计划 执行查询 返回数据 性能优化: 慢查询 → EXPLAIN → 找到瓶颈 → 优化索引 → 改写SQL → 性能飞跃 10秒 分析 全表扫描 建索引 覆盖索引 10ms 我们还将通过10个真实案例,将慢查询从10秒优化到10ms,性能提升1000倍。 一、SQL执行流程:从SQL到结果集 理解查询优化,首先要理解SQL是如何执行的。 1.1 MySQL的架构:两层结构 ┌─────────────────────────────────────────────────────────────┐ │ MySQL Server层 │ ├─────────────────────────────────────────────────────────────┤ │ 连接器 解析器 优化器 执行器 │ │ ↓ ↓ ↓ ↓ │ │ 权限验证 语法解析 生成计划 执行查询 │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 存储引擎层 │ ├─────────────────────────────────────────────────────────────┤ │ InnoDB MyISAM Memory Archive │ │ ↓ ↓ ↓ ↓ │ │ 事务支持 不支持 内存存储 压缩存储 │ └─────────────────────────────────────────────────────────────┘ Server层与存储引擎的职责分工: ...

2025-11-03 · maneng

MySQL高可用架构:主从复制与分库分表

引言 单机MySQL的三大瓶颈: 1. 存储瓶颈:单机磁盘容量有限(TB级) 2. 并发瓶颈:单机QPS上限1-2万 3. 可用性瓶颈:单点故障,服务不可用 高可用架构的演进路径: 单机 → 主从复制 → 读写分离 → 分库分表 → 分布式数据库 ↓ ↓ ↓ ↓ ↓ 1台 1主N从 读扩展 存储扩展 NewSQL 一、主从复制:高可用的基石 1.1 主从复制原理 核心组件: 主库(Master) ↓ binlog(二进制日志) 从库(Slave) ↓ relay log(中继日志) ↓ 重放SQL 数据一致 复制流程: -- 主库操作 INSERT INTO users (name, age) VALUES ('Zhang San', 25); -- 主库写binlog -- binlog内容: -- Event_type: Query -- Query: INSERT INTO users (name, age) VALUES ('Zhang San', 25) -- 从库IO线程: -- 1. 连接主库,请求binlog -- 2. 主库binlog dump线程发送binlog -- 3. 从库IO线程写入relay log -- 从库SQL线程: -- 1. 读取relay log -- 2. 解析SQL语句 -- 3. 执行SQL:INSERT INTO users... -- 4. 完成数据同步 三个关键线程: ...

2025-11-03 · maneng

MySQL高可用架构:主从复制与分库分表

引言 单机MySQL的三大瓶颈: 1. 存储瓶颈:单机磁盘容量有限(TB级) 2. 并发瓶颈:单机QPS上限1-2万 3. 可用性瓶颈:单点故障,服务不可用 高可用架构的演进路径: 单机 → 主从复制 → 读写分离 → 分库分表 → 分布式数据库 ↓ ↓ ↓ ↓ ↓ 1台 1主N从 读扩展 存储扩展 NewSQL 一、主从复制:高可用的基石 1.1 主从复制原理 核心组件: 主库(Master) ↓ binlog(二进制日志) 从库(Slave) ↓ relay log(中继日志) ↓ 重放SQL 数据一致 复制流程: -- 主库操作 INSERT INTO users (name, age) VALUES ('Zhang San', 25); -- 主库写binlog -- binlog内容: -- Event_type: Query -- Query: INSERT INTO users (name, age) VALUES ('Zhang San', 25) -- 从库IO线程: -- 1. 连接主库,请求binlog -- 2. 主库binlog dump线程发送binlog -- 3. 从库IO线程写入relay log -- 从库SQL线程: -- 1. 读取relay log -- 2. 解析SQL语句 -- 3. 执行SQL:INSERT INTO users... -- 4. 完成数据同步 三个关键线程: ...

2025-11-03 · maneng

MySQL核心原理:关键技术深度解析

引言 通过前5篇文章,我们已经掌握了MySQL的核心技术。本文将深入MySQL内部,理解关键组件的工作原理。 一、InnoDB核心组件 1.1 Buffer Pool:内存缓存池 作用:缓存数据页和索引页,减少磁盘IO Buffer Pool结构: ┌─────────────────────────────────────┐ │ Buffer Pool (默认128MB) │ ├─────────────────────────────────────┤ │ 数据页缓存 (75%) │ │ ├─ Page 1: users表数据 │ │ ├─ Page 2: orders表数据 │ │ └─ ... │ ├─────────────────────────────────────┤ │ 索引页缓存 (20%) │ │ ├─ Index Page 1: uk_username │ │ ├─ Index Page 2: idx_created_time │ │ └─ ... │ ├─────────────────────────────────────┤ │ Undo页缓存 (5%) │ │ └─ 历史版本数据 │ └─────────────────────────────────────┘ 淘汰算法: LRU (Least Recently Used) 热数据: 靠近链表头部 冷数据: 靠近链表尾部 新数据: 先放入中间位置(避免扫描污染) 关键参数: ...

2025-11-03 · maneng

MySQL核心原理:关键技术深度解析

引言 通过前5篇文章,我们已经掌握了MySQL的核心技术。本文将深入MySQL内部,理解关键组件的工作原理。 一、InnoDB核心组件 1.1 Buffer Pool:内存缓存池 作用:缓存数据页和索引页,减少磁盘IO Buffer Pool结构: ┌─────────────────────────────────────┐ │ Buffer Pool (默认128MB) │ ├─────────────────────────────────────┤ │ 数据页缓存 (75%) │ │ ├─ Page 1: users表数据 │ │ ├─ Page 2: orders表数据 │ │ └─ ... │ ├─────────────────────────────────────┤ │ 索引页缓存 (20%) │ │ ├─ Index Page 1: uk_username │ │ ├─ Index Page 2: idx_created_time │ │ └─ ... │ ├─────────────────────────────────────┤ │ Undo页缓存 (5%) │ │ └─ 历史版本数据 │ └─────────────────────────────────────┘ 淘汰算法: LRU (Least Recently Used) 热数据: 靠近链表头部 冷数据: 靠近链表尾部 新数据: 先放入中间位置(避免扫描污染) 关键参数: ...

2025-11-03 · maneng

MySQL核心原理:关键技术深度解析

引言 通过前5篇文章,我们已经掌握了MySQL的核心技术。本文将深入MySQL内部,理解关键组件的工作原理。 一、InnoDB核心组件 1.1 Buffer Pool:内存缓存池 作用:缓存数据页和索引页,减少磁盘IO Buffer Pool结构: ┌─────────────────────────────────────┐ │ Buffer Pool (默认128MB) │ ├─────────────────────────────────────┤ │ 数据页缓存 (75%) │ │ ├─ Page 1: users表数据 │ │ ├─ Page 2: orders表数据 │ │ └─ ... │ ├─────────────────────────────────────┤ │ 索引页缓存 (20%) │ │ ├─ Index Page 1: uk_username │ │ ├─ Index Page 2: idx_created_time │ │ └─ ... │ ├─────────────────────────────────────┤ │ Undo页缓存 (5%) │ │ └─ 历史版本数据 │ └─────────────────────────────────────┘ 淘汰算法: LRU (Least Recently Used) 热数据: 靠近链表头部 冷数据: 靠近链表尾部 新数据: 先放入中间位置(避免扫描污染) 关键参数: ...

2025-11-03 · maneng

MySQL第一性原理:为什么我们需要数据库?

引言 “如果你不能简单地解释一件事,说明你还没有真正理解它。” —— 理查德·费曼 作为一名后端开发者,你一定写过这样的代码: @Autowired private UserRepository userRepository; public User findUser(Long id) { return userRepository.findById(id); } 这行代码背后,数据库帮你做了什么? 持久化存储:数据写入磁盘,重启不丢失 快速检索:1亿条数据中找到目标行,只需10微秒 并发控制:1万个并发请求,数据不会错乱 事务保证:转账操作要么全成功,要么全失败 故障恢复:系统崩溃后,数据可以完整恢复 但你有没有想过: 为什么不用文件存储数据?(txt、csv、json) 为什么不用内存存储数据?(HashMap、Redis) 为什么一定要用MySQL这样的关系型数据库? 今天,我们从第一性原理出发,通过对比文件、内存、MySQL三种存储方案,深度剖析数据库解决的核心问题。 本文不会教你怎么写SQL,而是回答为什么需要数据库。 一、引子:用户注册功能的三种实现 让我们从一个最简单的需求开始:用户注册功能。 需求: 用户注册时,存储用户名和密码 检查用户名是否已存在 用户登录时,验证用户名和密码 按注册时间范围查询用户 性能要求: 支持100万用户 注册和登录响应时间 < 100ms 支持1000并发 我们将用三种方式实现,看看它们的差异。 1.1 场景A:文件存储(users.txt) 实现思路:将用户数据存储在文本文件中,每行一个用户,逗号分隔字段。 /** * 用户服务 - 文件存储实现 */ public class FileUserService { private static final String FILE_PATH = "users.txt"; /** * 注册用户(追加到文件末尾) */ public void register(String username, String password) { // 1. 检查用户名是否已存在 if (exists(username)) { throw new RuntimeException("用户名已存在"); } // 2. 追加到文件 try (FileWriter fw = new FileWriter(FILE_PATH, true)) { String line = username + "," + hashPassword(password) + "," + System.currentTimeMillis() + "\n"; fw.write(line); } catch (IOException e) { throw new RuntimeException("注册失败", e); } } /** * 检查用户名是否存在(全文件扫描) */ public boolean exists(String username) { try (BufferedReader br = new BufferedReader(new FileReader(FILE_PATH))) { String line; while ((line = br.readLine()) != null) { // O(n) 全表扫描 String[] parts = line.split(","); if (parts[0].equals(username)) { return true; } } } catch (IOException e) { e.printStackTrace(); } return false; } /** * 登录验证(全文件扫描) */ public boolean login(String username, String password) { String hashedPassword = hashPassword(password); try (BufferedReader br = new BufferedReader(new FileReader(FILE_PATH))) { String line; while ((line = br.readLine()) != null) { // O(n) 全表扫描 String[] parts = line.split(","); if (parts[0].equals(username) && parts[1].equals(hashedPassword)) { return true; } } } catch (IOException e) { e.printStackTrace(); } return false; } /** * 按注册时间范围查询(全文件扫描+过滤) */ public List<User> findByRegisteredDateRange(long startTime, long endTime) { List<User> result = new ArrayList<>(); try (BufferedReader br = new BufferedReader(new FileReader(FILE_PATH))) { String line; while ((line = br.readLine()) != null) { // O(n) 全表扫描 String[] parts = line.split(","); long registeredTime = Long.parseLong(parts[2]); if (registeredTime >= startTime && registeredTime <= endTime) { result.add(new User(parts[0], parts[1], registeredTime)); } } } catch (IOException e) { e.printStackTrace(); } return result; } private String hashPassword(String password) { // 简化版,实际应该用BCrypt return Integer.toString(password.hashCode()); } } 文件内容示例: ...

2025-11-03 · maneng

InnoDB架构综合实战:从原理到优化

InnoDB架构回顾 InnoDB架构(5层) ┌────────────────────────────────────┐ │ 1. 内存结构 │ │ ├─ Buffer Pool(最大) │ │ ├─ Change Buffer │ │ ├─ Adaptive Hash Index │ │ └─ Log Buffer │ ├────────────────────────────────────┤ │ 2. 后台线程 │ │ ├─ Master Thread │ │ ├─ IO Thread │ │ ├─ Page Cleaner Thread │ │ └─ Purge Thread │ ├────────────────────────────────────┤ │ 3. 磁盘结构 │ │ ├─ 表空间(Tablespace) │ │ ├─ redo log(持久性) │ │ ├─ undo log(原子性+MVCC) │ │ └─ binlog(复制) │ ├────────────────────────────────────┤ │ 4. 存储结构 │ │ └─ 表空间→段→区(1MB)→页(16KB)→行 │ ├────────────────────────────────────┤ │ 5. 锁机制 │ │ ├─ 表锁、行锁 │ │ ├─ Gap Lock、Next-Key Lock │ │ └─ MVCC │ └────────────────────────────────────┘ 实战案例1:Buffer Pool命中率优化 问题 生产环境查询慢,QPS从5000降到2000。 ...

2025-01-15 · maneng

执行引擎:SQL执行的最后一步

执行引擎概述 执行引擎(Executor) 负责调用存储引擎接口,执行SQL,返回结果。 SQL执行完整流程: ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 连接器 │→│ 分析器 │→│ 优化器 │→│ 执行器 │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ ↓ 调用接口 ┌──────────┐ │ 存储引擎 │ └──────────┘ 执行引擎的职责 1. 权限检查 -- 查询前检查权限 SELECT * FROM users WHERE id = 1; -- 检查流程 1. 当前用户是否有users表的SELECT权限? 2. 如果没有 → 返回错误:Access denied -- 权限存储位置 mysql.user -- 全局权限 mysql.db -- 数据库权限 mysql.tables_priv -- 表权限 2. 调用存储引擎接口 -- 示例SQL SELECT * FROM users WHERE id = 1; -- 执行引擎操作 1. 打开表:handler = open_table("users") 2. 初始化查询:handler->index_init(id索引) 3. 定位第一行:handler->index_read(id=1) 4. 读取行数据:handler->read_row() 5. 关闭表:handler->close() 3. 结果返回 -- 返回流程 1. 存储引擎返回数据行 2. 执行引擎应用WHERE条件过滤 3. 格式化结果(MySQL协议) 4. 通过网络发送给客户端 -- 流式返回(Streaming) -- MySQL边查询边返回,不等全部数据查完 存储引擎接口(Handler API) 1. 打开表 // C代码示例(简化) int handler::ha_open(TABLE *table, const char *name, int mode); // 流程 1. 打开表文件(.ibd) 2. 加载表结构(.frm,MySQL 8.0已废弃) 3. 初始化Handler对象 2. 读取接口 // 全表扫描 int handler::rnd_init(); // 初始化全表扫描 int handler::rnd_next(uchar *buf); // 读取下一行 // 索引扫描 int handler::index_init(uint idx); // 初始化索引扫描 int handler::index_read(uchar *buf, const uchar *key, uint key_len); // 定位第一行 int handler::index_next(uchar *buf); // 读取下一行 3. 写入接口 // 插入 int handler::write_row(uchar *buf); // 更新 int handler::update_row(const uchar *old_data, const uchar *new_data); // 删除 int handler::delete_row(const uchar *buf); 执行流程详解 1. 全表扫描 -- SQL SELECT * FROM users WHERE age > 25; -- 执行流程 1. 打开表:open_table("users") 2. 初始化全表扫描:rnd_init() 3. 循环读取 WHILE (rnd_next(row) == 0) DO IF age > 25 THEN 发送到客户端 END IF END WHILE 4. 关闭表:close() -- 扫描行数:全表所有行 2. 索引扫描 -- SQL SELECT * FROM users WHERE id = 1; -- 执行流程(主键索引) 1. 打开表:open_table("users") 2. 初始化索引扫描:index_init(PRIMARY) 3. 定位第一行:index_read(id=1) 4. 读取行数据:read_row() 5. 发送到客户端 6. 关闭表:close() -- 扫描行数:1行(主键等值查询) 3. 索引范围扫描 -- SQL SELECT * FROM users WHERE id BETWEEN 1 AND 100; -- 执行流程 1. 打开表:open_table("users") 2. 初始化索引扫描:index_init(PRIMARY) 3. 定位第一行:index_read(id=1) 4. 循环读取 WHILE (index_next(row) == 0 AND id <= 100) DO 发送到客户端 END WHILE 5. 关闭表:close() -- 扫描行数:100行(range查询) JOIN执行流程 Nested Loop Join -- SQL SELECT * FROM orders o JOIN users u ON o.user_id = u.id; -- 执行流程(Nested Loop Join) 1. 打开orders表:open_table("orders") 2. 打开users表:open_table("users") 3. 初始化orders全表扫描:orders->rnd_init() 4. 循环orders表 FOR EACH order IN orders DO -- 内层:通过索引查找users表 users->index_read(id = order.user_id) users->read_row() -- 拼接结果,发送客户端 END FOR 5. 关闭表 -- 扫描次数 orders表:全表扫描(M行) users表:索引查找(M次,每次O(log N)) 总复杂度:O(M × log N) 执行器优化 1. 条件下推 -- SQL SELECT * FROM users WHERE id > 100; -- 未优化 1. 存储引擎返回所有行 2. 执行器过滤 id > 100 3. 返回结果 -- ICP优化(Index Condition Pushdown) 1. 存储引擎扫描索引时,直接过滤 id > 100 2. 返回满足条件的行 3. 减少回表次数 2. 批量读取 -- Multi-Range Read(MRR)优化 -- 场景:范围查询 + 回表 -- 未优化 1. 扫描索引:获取10个主键ID(随机顺序) 2. 逐个回表:10次随机IO -- MRR优化 1. 扫描索引:获取10个主键ID 2. 排序主键ID(变为顺序) 3. 批量回表:1次顺序IO 执行器与优化器的配合 -- SQL SELECT * FROM users WHERE name = 'Alice' AND age = 25; -- 优化器决策 1. 选择索引:idx_name_age 2. 生成执行计划: - type: ref - key: idx_name_age - rows: 1 -- 执行器执行 1. 调用存储引擎接口:index_read(idx_name_age, 'Alice', 25) 2. 存储引擎返回数据行 3. 执行器返回客户端 查看执行器操作 1. SHOW PROFILE -- 开启profiling SET profiling = 1; -- 执行SQL SELECT * FROM users WHERE id = 1; -- 查看执行耗时 SHOW PROFILES; -- 详细分析 SHOW PROFILE FOR QUERY 1; -- 输出示例 +----------------------+----------+ | Status | Duration | +----------------------+----------+ | starting | 0.000050 | | checking permissions | 0.000005 | | Opening tables | 0.000020 | | init | 0.000010 | | System lock | 0.000005 | | optimizing | 0.000005 | | statistics | 0.000010 | | preparing | 0.000010 | | executing | 0.000005 | ← 执行器调用存储引擎 | Sending data | 0.000050 | ← 存储引擎返回数据 | end | 0.000005 | | query end | 0.000005 | | closing tables | 0.000005 | | freeing items | 0.000010 | | cleaning up | 0.000010 | +----------------------+----------+ 2. Performance Schema -- 查看执行引擎调用存储引擎的次数 SELECT * FROM performance_schema.events_statements_summary_by_digest WHERE DIGEST_TEXT LIKE 'SELECT%users%' LIMIT 1\G -- 关键指标 COUNT_STAR : 10000 -- 执行次数 SUM_ROWS_EXAMINED : 100000 -- 扫描行数 SUM_ROWS_SENT : 10000 -- 返回行数 执行器性能影响 1. 扫描行数 -- 全表扫描(扫描100万行) SELECT * FROM users; -- 执行器调用100万次 rnd_next() -- 索引查询(扫描1行) SELECT * FROM users WHERE id = 1; -- 执行器调用1次 index_read() -- 优化:减少扫描行数 2. 回表次数 -- 非覆盖索引(需要回表) SELECT * FROM users WHERE name = 'Alice'; -- 1. 索引扫描:index_read(name='Alice') -- 2. 回表:read_row(主键=1) -- 覆盖索引(无需回表) SELECT id, name FROM users WHERE name = 'Alice'; -- 1. 索引扫描:index_read(name='Alice') -- 2. 直接返回(无需回表) -- 优化:使用覆盖索引 实战建议 1. 减少扫描行数 -- ❌ 不好:全表扫描 SELECT * FROM users WHERE age > 25; -- ✅ 好:使用索引 CREATE INDEX idx_age ON users(age); SELECT * FROM users WHERE age > 25; 2. 使用覆盖索引 -- ❌ 不好:需要回表 SELECT * FROM users WHERE name = 'Alice'; -- ✅ 好:覆盖索引 CREATE INDEX idx_name_age ON users(name, age); SELECT name, age FROM users WHERE name = 'Alice'; 3. 避免大结果集 -- ❌ 不好:返回100万行 SELECT * FROM users; -- ✅ 好:分页查询 SELECT * FROM users LIMIT 0, 100; 常见面试题 Q1: 执行引擎的作用是什么? ...

2025-01-15 · maneng

如约数科科技工作室

浙ICP备2025203501号

👀 本站总访问量 ...| 👤 访客数 ...| 📅 今日访问 ...