CC 咖啡猫的工作空间 Coding Space

MySQL 数据库深入原理

MySQL 是最常用的关系型数据库。理解其内部原理,是写出高性能 SQL、排查线上问题的基础。


一、MySQL 架构

1.1 整体架构

连接层
  ↓
SQL 接口 → 解析器 → 优化器 → 执行器
  ↓
存储引擎层(InnoDB / MyISAM / Memory)
  ↓
文件系统层

1.2 连接层

连接池:管理客户端连接
    ↓
连接认证:用户名、密码、连接数限制
    ↓
每个连接一个线程

1.3 Server 层组件

组件 职责
连接器 管理连接、权限校验
查询缓存 缓存查询结果(8.0 已移除)
解析器 词法分析、语法分析,生成 AST
优化器 生成执行计划、选择索引
执行器 调用存储引擎接口执行

1.4 存储引擎

引擎 事务 锁粒度 应用场景
InnoDB ✅ 支持 行级锁 默认,OLTP 首选
MyISAM ❌ 不支持 表级锁 OLAP,读取多
Memory ❌ 不支持 表级锁 临时表、缓存

InnoDB 是 MySQL 默认引擎,原因:

  • 支持事务(ACID)
  • 行级锁,并发性能好
  • MVCC 解决读写冲突
  • Crash Safe(崩溃恢复)
  • 支持外键

二、InnoDB 存储引擎

2.1 内存结构

Buffer Pool(缓冲池)
  ├── 数据页(默认 16KB)
  ├── 索引页
  ├── 自适应哈希索引
  └── 锁信息
      ↓
Change Buffer(写缓冲)
  └── 优化非唯一索引的写入
      ↓
Log Buffer(日志缓冲)
  └── Redo Log 写入前缓冲

2.2 磁盘结构

表空间(ibdata1)
  ├── 数据页
  ├── 索引页
  ├── Double Write Buffer(双写缓冲)
  └── Undo Log(回滚日志)
      ↓
Redo Log(ib_logfile0, ib_logfile1)
  └── 事务提交前的日志

2.3 数据页结构

Page(16KB)
  ├── File Header(38字节):页号、上一页、下一页、checksum
  ├── Page Header(56字节):记录数、空闲空间起始
  ├── Infimum + Supremum:行记录最小/最大值
  ├── User Records:实际数据行
  ├── Free Space:空闲空间
  └── File Trailer(8字节):校验和

三、索引原理

3.1 B+Tree 为什么是 MySQL 的选择

结构 特点
二叉树 可能退化为链表(O(n))
红黑树 高度平衡,但深度仍较大
B-Tree 一个节点可以存多个 key
B+Tree 非叶子节点只存 key,所有数据在叶子节点

B+Tree 优势

  • 叶子节点链表,便于范围查询
  • 非叶子节点不存数据,每页可存更多 key → 更矮、更宽
  • 所有查询最终到叶子节点,性能稳定

3.2 InnoDB 索引实现

聚簇索引(Clustered Index)
  └── 主键索引,叶子节点存完整数据行

二级索引(Secondary Index)
  └── 非主键索引,叶子节点存主键值
        ↓
  查询时:先查二级索引得到主键 → 再查聚簇索引(回表)

3.3 索引匹配规则

-- 假设有索引 idx_name_age (name, age)

-- ✅ 完全匹配
SELECT * FROM users WHERE name = 'zhangsan' AND age = 25;

-- ✅ 最左前缀匹配
SELECT * FROM users WHERE name = 'zhangsan';

-- ✅ 匹配列值前缀
SELECT * FROM users WHERE name LIKE 'zhang%';

-- ❌ 不满足最左前缀
SELECT * FROM users WHERE age = 25;

-- ❌ 中间断层
SELECT * FROM users WHERE name = 'zhangsan' AND city = 'Beijing';
-- city 中断了,无法使用索引

3.4 索引失效场景

-- ❌ 左括号前
SELECT * FROM users WHERE SUBSTR(name, 1, 3) = 'zhang';

-- ❌ 类型转换
SELECT * FROM users WHERE name = 123;  -- name 是 varchar

-- ❌ 函数
SELECT * FROM users WHERE YEAR(created_at) = 2024;

-- ❌ != / NOT IN / NOT EXISTS
SELECT * FROM users WHERE status != 'active';
SELECT * FROM users WHERE name NOT IN ('a', 'b');

-- ✅ 范围查询后失效
SELECT * FROM users WHERE name = 'zhang' AND age > 20;
-- age > 20 后,age 之后的索引列失效

-- ✅ LIMIT 后面仍然可能用索引
SELECT * FROM users WHERE name = 'zhang' LIMIT 100;

3.5 覆盖索引

查询所有列都在索引中,无需回表:

-- 假设有联合索引 idx_name_age (name, age)

-- ✅ 覆盖索引:不需要回表
SELECT name, age FROM users WHERE name = 'zhangsan';

-- ❌ 非覆盖索引:需要回表
SELECT * FROM users WHERE name = 'zhangsan';

3.6 索引下推(Index Condition Pushdown)

MySQL 5.6+ 优化,在索引遍历时过滤数据,减少回表:

-- 有索引 idx_name_age (name, age)

SELECT * FROM users WHERE name LIKE 'zhang%' AND age = 25;

-- 无 ICP:先按 name 查到所有记录,再回表,在 server 层过滤 age
-- 有 ICP:在索引层直接过滤 age,减少回表次数

四、SQL 优化

4.1 EXPLAIN 分析

EXPLAIN SELECT * FROM users WHERE name = 'zhangsan';

-- 关键字段:
-- type: ALL(全表扫描), ref(索引查找), range(范围)
-- key: 实际使用的索引
-- rows: 预计扫描行数
-- Extra: Using index(覆盖索引), Using filesort(文件排序), Using temporary(临时表)

4.2 慢查询优化步骤

-- 1. 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;  -- 超过 1 秒记录

-- 2. 查看慢查询
SHOW FULL PROCESSLIST;

-- 3. 分析执行计划
EXPLAIN SELECT ...

-- 4. 优化方向:
--    - 索引优化
--    - SQL 重写(减少子查询、用 JOIN 替代)
--    - 分库分表

4.3 分页优化

-- ❌ 深度分页:跳过了大量数据
SELECT * FROM orders LIMIT 1000000, 10;

-- ✅ 优化1:延迟关联
SELECT * FROM orders
INNER JOIN (
    SELECT id FROM orders ORDER BY id LIMIT 1000000, 10
) AS t2 USING (id);

-- ✅ 优化2:游标分页(推荐)
SELECT * FROM orders
WHERE id > #{lastId}
ORDER BY id
LIMIT 10;

4.4 JOIN 优化

-- 小表驱动大表(MySQL 优化器自动选择)
-- 但可以用 STRAIGHT_JOIN 强制指定

SELECT * FROM orders STRAIGHT_JOIN users ON orders.user_id = users.id;
-- orders 驱动 users(固定先扫 orders)

-- 索引要求:ON 的列必须有索引
-- 多表 JOIN:控制数量(不超过 5 张)

五、主从复制

5.1 复制原理

主库(Master)
  └── 事务提交 → 写入 Binlog
                       ↓
              Dump Thread 读取 Binlog → 发送给从库
                       ↓
从库(Slave)
  └── I/O Thread 接收 → 写入 Relay Log
                       ↓
              SQL Thread 读取 Relay Log → 重放执行

5.2 复制模式

模式 说明 优点 缺点
异步复制 主库提交后不等待从库 性能好 可能丢数据
半同步复制 至少一个从库写入成功才返回 数据安全 有延迟
全同步复制 所有从库都写入成功才返回 最安全 性能差

5.3 GTID 复制

传统 Binlog 复制:
  - 依赖文件名 + 位置
  - 主从切换后难以定位新主从的同步点

GTID(Global Transaction Identifier):
  - 每个事务有唯一 ID
  - 自动记录已执行的事务
  - 主从切换后自动定位同步点

5.4 读写分离

写入请求 → 主库
读取请求 → 从库(负载均衡)

延迟问题

主库写入 → 从库复制(有延迟)
    ↓
读取到旧数据(主从不一致)

解决方案

  • 强制走主库(重要数据)
  • 延迟读取(业务层延迟)
  • 缓存读取(读请求先查缓存)

六、分库分表

6.1 什么时候需要分库分表

信号 说明
单表超过 2000 万行 索引效果下降
单库超过 1TB 磁盘压力
CPU 持续 100% 查询成为瓶颈
主从延迟严重 复制成为瓶颈

6.2 分片策略

策略 原理 适用场景
哈希分片 user_id % N 均匀分布
范围分片 时间/ID 范围 时序数据
列表分片 地域/分类 地域属性明确

6.3 分片后的问题

问题 解决方案
跨分片 JOIN 应用层处理(多次查询)或 ES
跨分片事务 分布式事务框架(Seata)
分布式 ID 雪花算法、UUID
分页查询 聚合所有分片结果

七、日志系统

7.1 Binlog(逻辑日志)

记录内容:所有 DDL 和 DML(逻辑变化)
  - INSERT/UPDATE/DELETE
  - CREATE/DROP/ALTER

用途:
  - 主从复制
  - 数据恢复
  - 增量备份

格式:
  - STATEMENT:记录 SQL 语句
  - ROW:记录每行变化(推荐)
  - MIXED:混合模式

7.2 Redo Log(物理日志)

记录内容:数据页的物理变化(物理变化)
  - "某偏移量写入 XX 字节"

用途:
  - Crash Safe(崩溃恢复)
  - WAL(Write-Ahead Logging):先写日志再写数据

刷盘策略:
  - innodb_flush_log_at_trx_commit
  - 1:事务提交时刷盘(最安全)
  - 2:提交时写到 OS 缓存
  - 0:每秒刷盘

7.3 Undo Log(回滚日志)

记录内容:数据修改前的值
  - INSERT → 整行记录
  - UPDATE → 修改前的值
  - DELETE → 整行记录

用途:
  - 事务回滚
  - MVCC(读取历史版本)

八、常见问题

8.1 为什么用自增主键

-- 自增主键的好处:
-- 1. 插入时顺序追加,不频繁页分裂
-- 2. 二级索引存储空间小(主键值更小)
-- 3. 范围查询性能好

-- 为什么不建议用 UUID:
-- 1. 插入无序,频繁页分裂
-- 2. 字符串比整数大,索引存储空间大

8.2 CHAR vs VARCHAR

类型 存储 适用
CHAR(N) 固定 N 个字符,不足用空格补 定长:性别、状态、MD5
VARCHAR(N) 可变长度,额外 1-2 字节存长度 变长:姓名、地址、文本

8.3 为什么不建议使用外键

-- 外键的问题:
-- 1. 级联更新/删除影响多表,性能差
-- 2. 并发时容易死锁
-- 3. 水平扩展时外键难处理

-- 建议:
-- 在应用层保证数据一致性
-- 外键用于数据库约束而非业务逻辑

8.4 COUNT(*) vs COUNT(1) vs COUNT(列)

-- InnoDB 优化后三者性能差不多
-- 但 COUNT(*) 会处理 NULL
-- COUNT(列) 跳过 NULL 值

-- 最快:EXPLAIN SELECT COUNT(*) FROM orders;
-- 实际读取最小:主键索引最小

8.5 MySQL 8.0 新特性

特性 说明
隐藏索引 索引可隐藏但不可用,方便在线调整
CTE 公共表达式,简化复杂查询
窗口函数 RANK / ROW_NUMBER 等
Instant ADD COLUMN 加列秒级完成
JSON 增强 JSON_TABLE、JSONPATH