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核心原理:关键技术深度解析

引言 通过前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

查询优化器:生成最优执行计划的艺术

查询优化器概述 查询优化器(Optimizer) 的作用是为SQL生成最优执行计划。 -- 原始SQL SELECT * FROM orders o JOIN users u ON o.user_id = u.id WHERE u.name = 'Alice' AND o.status = 'PAID'; -- 优化器决策 1. 表连接顺序:先users还是orders? 2. 索引选择:使用哪个索引? 3. 连接算法:Nested Loop、Hash Join、Sort-Merge Join? 4. 条件下推:WHERE条件何时过滤? 优化器类型 1. 基于规则的优化器(RBO) Rule-Based Optimizer:根据预定义规则选择执行计划。 -- 规则示例 1. 有索引优于无索引 2. 主键索引优于二级索引 3. 小表驱动大表 -- 缺点: 不考虑数据分布,可能选择非最优计划 2. 基于成本的优化器(CBO) Cost-Based Optimizer(MySQL使用):根据成本评估选择最优计划。 -- 成本因素 1. IO成本:磁盘读取次数 2. CPU成本:记录比较次数 3. 内存成本:临时表、排序开销 -- 优点: 考虑数据分布,选择更优计划 成本评估 1. 成本模型 总成本 = IO成本 + CPU成本 IO成本 = 页数 × IO_BLOCK_READ_COST CPU成本 = 行数 × ROW_EVALUATE_COST -- 默认值(可调整) IO_BLOCK_READ_COST = 1.0 ROW_EVALUATE_COST = 0.1 2. 索引扫描成本 -- 全表扫描(Table Scan) 成本 = 表页数 × 1.0 + 表行数 × 0.1 -- 索引扫描(Index Scan) 成本 = 索引页数 × 1.0 + 回表行数 × 0.1 + 回表IO × 1.0 -- 示例 表:100万行,10000页 索引:1000页,回表10万行 全表扫描成本 = 10000 × 1.0 + 1000000 × 0.1 = 110000 索引扫描成本 = 1000 × 1.0 + 100000 × 0.1 + 100000 × 1.0 = 111000 -- 优化器选择:全表扫描(成本更低) 优化策略 1. 条件化简 -- 原始条件 WHERE id > 0 AND id < 100 AND id = 50 -- 化简后 WHERE id = 50 -- 原始条件 WHERE (status = 'PAID' OR status = 'PAID') AND user_id = 1 -- 化简后 WHERE status = 'PAID' AND user_id = 1 2. 常量折叠 -- 原始 WHERE price > 100 * 2 -- 优化后 WHERE price > 200 -- 原始 WHERE YEAR(created_at) = 2025 -- 无法优化(函数作用于列,索引失效) 3. 谓词下推(Predicate Pushdown) -- 原始SQL SELECT * FROM orders o JOIN users u ON o.user_id = u.id WHERE u.name = 'Alice'; -- 优化后(谓词下推) SELECT * FROM orders o JOIN (SELECT * FROM users WHERE name = 'Alice') u ON o.user_id = u.id; -- WHERE条件先过滤users表,减少JOIN的数据量 4. 外连接消除 -- 原始SQL SELECT * FROM orders o LEFT JOIN users u ON o.user_id = u.id WHERE u.id IS NOT NULL; -- 优化后(LEFT JOIN → INNER JOIN) SELECT * FROM orders o JOIN users u ON o.user_id = u.id; -- WHERE条件保证u.id不为NULL,可以消除LEFT JOIN 表连接顺序 1. 小表驱动大表 -- 示例 users表:100行 orders表:100万行 -- ✅ 好:users驱动orders FOR EACH user IN users (100次) FOR EACH order IN orders WHERE order.user_id = user.id END FOR -- 总循环次数:100次(users) × 平均100次(orders) = 10000次 -- ❌ 不好:orders驱动users FOR EACH order IN orders (100万次) FOR EACH user IN users WHERE user.id = order.user_id END FOR -- 总循环次数:100万次(效率低) 2. 连接算法 Nested Loop Join(嵌套循环): ...

2025-01-15 · maneng

MySQL启动与关闭流程详解

MySQL启动流程 启动命令: mysqld --defaults-file=/etc/my.cnf 完整流程(8个阶段): 1. 读取配置文件 2. 初始化文件系统 3. 初始化InnoDB存储引擎 4. 加载系统表 5. 执行崩溃恢复(如需要) 6. 启动后台线程 7. 监听客户端连接 8. 完成启动 1. 读取配置文件 查找顺序 # MySQL配置文件查找顺序(Linux) 1. /etc/my.cnf 2. /etc/mysql/my.cnf 3. /usr/etc/my.cnf 4. ~/.my.cnf 5. --defaults-file指定的文件 # 查看实际读取的配置 mysqld --verbose --help | grep -A 1 "Default options" 关键配置 [mysqld] # 数据目录 datadir = /var/lib/mysql # 端口 port = 3306 # 字符集 character-set-server = utf8mb4 # Buffer Pool大小 innodb_buffer_pool_size = 8G # 日志文件 log-error = /var/log/mysql/error.log 2. 初始化文件系统 # 检查数据目录 1. 检查datadir是否存在 2. 检查datadir权限(mysql用户可读写) 3. 创建临时目录(tmpdir) # 检查必要文件 1. ibdata1(系统表空间) 2. ib_logfile0/ib_logfile1(redo log) 3. mysql/(系统数据库目录) # 如果文件不存在 → 初始化数据库:mysqld --initialize 3. 初始化InnoDB存储引擎 步骤: 1. 分配Buffer Pool内存 ├─ 默认128MB,生产环境建议50-80%内存 └─ innodb_buffer_pool_size = 8G 2. 打开表空间文件 ├─ ibdata1(系统表空间) └─ *.ibd(独立表空间) 3. 打开redo log文件 ├─ ib_logfile0 └─ ib_logfile1 4. 打开undo表空间(MySQL 8.0+) ├─ undo_001 └─ undo_002 5. 初始化Change Buffer、Adaptive Hash Index 4. 加载系统表 -- 加载系统数据库 mysql/ ├─ user.frm/ibd -- 用户表 ├─ db.frm/ibd -- 数据库权限 ├─ tables_priv.frm/ibd -- 表权限 └─ ... -- 加载存储引擎信息 information_schema/ performance_schema/ sys/ 5. 崩溃恢复(Crash Recovery) 检查是否需要恢复 判断条件: IF MySQL未正常关闭(如kill -9、断电) THEN 执行崩溃恢复 ELSE 跳过恢复 END IF -- 判断依据:redo log的checkpoint LSN 恢复流程 1. 读取redo log ├─ 从checkpoint开始读取redo log └─ 找到所有未刷盘的数据页修改 2. 重做(Redo) ├─ 对于已提交的事务 └─ 应用redo log,恢复数据 3. 回滚(Undo) ├─ 对于未提交的事务 └─ 使用undo log回滚 4. 清理 ├─ 清理临时表 └─ 清理未完成的DDL操作 -- 恢复时间:取决于redo log大小和修改量 -- 通常:几秒到几分钟 示例: ...

2025-01-15 · maneng

Double Write Buffer:防止页损坏的保障机制

部分写问题(Partial Page Write) 问题 页大小16KB > 磁盘块大小4KB,写入过程中系统崩溃,导致页损坏。 写入16KB页(4个磁盘块): ┌──────┬──────┬──────┬──────┐ │ 4KB │ 4KB │ 4KB │ 4KB │ └──────┴──────┴──────┴──────┘ ✅ ✅ ❌ ❌ 已写入 已写入 未写入 未写入 ↑ 系统崩溃 结果:页损坏(部分写) redo log无法恢复: redo log记录的是页的逻辑修改(如"将id=1的name改为Alice") 如果页本身损坏,无法应用redo log Double Write Buffer解决方案 原理 先写双写缓冲区(连续空间),再写数据文件(离散空间)。 写入流程: 1. 脏页 → Double Write Buffer(共享表空间,连续2MB) 2. Double Write Buffer → 磁盘(刷盘,原子操作) 3. 脏页 → 数据文件(离散写入) 崩溃恢复: IF 数据文件页损坏 THEN 从Double Write Buffer恢复页 ELSE 使用redo log恢复 END IF Double Write Buffer结构 1. 内存结构 Buffer Pool ├─ 脏页1(16KB) ├─ 脏页2(16KB) └─ ... Double Write Buffer(内存,2MB) ├─ 页1(16KB) ├─ 页2(16KB) ├─ ... └─ 页128(16KB) 2. 磁盘结构 共享表空间(ibdata1) ┌─────────────────────────────────┐ │ 数据字典 │ ├─────────────────────────────────┤ │ Double Write Buffer(2MB,连续) │ ← 双写缓冲区 │ ├─ 区1(1MB = 64页) │ │ └─ 区2(1MB = 64页) │ ├─────────────────────────────────┤ │ Undo Log │ └─────────────────────────────────┘ 写入流程 1. 正常写入 1. 脏页刷新触发 ├─ Buffer Pool中的脏页批量刷新(如128个页) 2. 写入Double Write Buffer(内存) ├─ 128个页复制到Double Write Buffer 3. 写入共享表空间(磁盘,顺序写) ├─ fsync刷盘(原子操作) ├─ 2MB连续空间,1次IO 4. 写入数据文件(磁盘,随机写) ├─ 128个页分别写入各自的表空间文件 ├─ 随机IO,多次寻址 5. 清空Double Write Buffer(内存) 关键: ...

2025-01-15 · maneng

Adaptive Hash Index:自适应哈希索引

Adaptive Hash Index概述 Adaptive Hash Index(AHI,自适应哈希索引) 是InnoDB的自动优化机制,在Buffer Pool上层构建哈希索引,加速等值查询。 查询流程对比: 传统B+树查询: SELECT * FROM users WHERE id = 100; → B+树查找(3-4次页访问) AHI查询: SELECT * FROM users WHERE id = 100; → Hash查找(1次内存访问) AHI的工作原理 1. 自动构建 -- InnoDB监控查询模式 SELECT * FROM users WHERE id = 1; -- 第1次 SELECT * FROM users WHERE id = 2; -- 第2次 SELECT * FROM users WHERE id = 3; -- 第3次 ... SELECT * FROM users WHERE id = 100; -- 第N次 -- 当检测到等值查询频繁访问同一索引前缀 -- InnoDB自动构建哈希索引 AHI[100] → Buffer Pool页地址 2. 哈希索引结构 B+树索引(磁盘) 10 / \ 5 15 / \ / \ 1 7 12 17 → 数据页 AHI(内存) Hash表: id=1 → 页地址0x1000 id=5 → 页地址0x1001 id=7 → 页地址0x1002 id=10 → 页地址0x1003 ... 3. 查询流程 -- 查询 SELECT * FROM users WHERE id = 100; -- 流程 1. 检查AHI是否有id=100的条目 2. ✅ 有:直接通过Hash查找,获取页地址(O(1)) 3. ❌ 没有:走B+树查找(O(log N)) AHI的优势 1. 加速等值查询 -- 等值查询(适合AHI) SELECT * FROM users WHERE id = 100; -- ✅ AHI加速 SELECT * FROM orders WHERE order_id = 1001; -- ✅ AHI加速 -- B+树查询:3-4次页访问(假设树高3) -- AHI查询:1次Hash查找(快3-4倍) 2. 减少CPU开销 -- B+树查找需要: 1. 二分查找(多次比较) 2. 页间跳转(多次指针访问) 3. 最终定位数据行 -- AHI查找需要: 1. Hash计算(一次) 2. 直接获取页地址(一次) AHI的限制 1. 只适用于等值查询 -- ✅ 适用 SELECT * FROM users WHERE id = 100; SELECT * FROM users WHERE id IN (1, 2, 3); -- ❌ 不适用 SELECT * FROM users WHERE id > 100; -- 范围查询 SELECT * FROM users WHERE id BETWEEN 1 AND 100; -- 范围查询 SELECT * FROM users WHERE name LIKE 'A%'; -- 模糊查询 2. 只针对热点数据 -- AHI只缓存频繁访问的索引前缀 -- 例如:id=100访问了1000次 → 自动加入AHI -- 而:id=999999只访问1次 → 不会加入AHI 3. 有维护开销 -- 更新/删除数据时,需要维护AHI UPDATE users SET name = 'Alice' WHERE id = 100; -- 流程: 1. 更新B+树索引 2. 更新Buffer Pool 3. 更新AHI(如果id=100在AHI中) AHI配置 1. 查看状态 -- 查看AHI开关 SHOW VARIABLES LIKE 'innodb_adaptive_hash_index'; -- ON(默认开启) -- 查看AHI分区数(MySQL 5.7+) SHOW VARIABLES LIKE 'innodb_adaptive_hash_index_parts'; -- 8(默认,减少锁竞争) 2. 开关配置 -- 关闭AHI(不推荐) SET GLOBAL innodb_adaptive_hash_index = OFF; -- 开启AHI(默认) SET GLOBAL innodb_adaptive_hash_index = ON; 注意:动态修改需要重建AHI,会短暂影响性能。 ...

2025-01-15 · maneng

如约数科科技工作室

浙ICP备2025203501号

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