Compare commits
3 Commits
ebf9db844d
...
2e84683d9c
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e84683d9c | |||
| e176563c0b | |||
| 115c952200 |
@@ -338,8 +338,8 @@ mysqlbinlog --start-position=154 --stop-position=892 mysql-bin.000001 | mysql -u
|
||||
|
||||
### MySQL 系列
|
||||
- [[hhs/MySQL/README]] — MySQL 知识库总目录
|
||||
- [[hhs/MySQL/02-安装与初始化]] — 安装方法与初始化配置
|
||||
- [[hhs/MySQL/34-备份与恢复]] — mysqldump、xtrabackup 完整指南
|
||||
- [[hhs/MySQL/29-Binary Log]] — binlog 原理与 mysqlbinlog 深入
|
||||
- [[hhs/MySQL/39-安全加固]] — 账号权限、SSL、审计最佳实践
|
||||
- [[hhs/MySQL/01-MySQL 架构与进程模型]] — 理解客户端与服务端通信基础
|
||||
- [[hhs/MySQL/01-入门基础/02-安装与初始化]] — 安装方法与初始化配置
|
||||
- [[hhs/MySQL/07-高可用与分布式/34-备份与恢复]] — mysqldump、xtrabackup 完整指南
|
||||
- [[hhs/MySQL/07-高可用与分布式/29-Binary Log]] — binlog 原理与 mysqlbinlog 深入
|
||||
- [[hhs/MySQL/08-工程实践/39-安全加固]] — 账号权限、SSL、审计最佳实践
|
||||
- [[hhs/MySQL/01-入门基础/01-MySQL 架构与入门]] — 理解客户端与服务端通信基础
|
||||
@@ -267,6 +267,6 @@ ALTER DATABASE app_db CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||||
|
||||
- [[hhs/Redis/02-核心数据类型]] — Redis 的 STRING 类型对多字节字符的处理
|
||||
- [[hhs/GORM/02-模型定义]] — GORM 建模时的字符串长度设置要点
|
||||
- [[hhs/MySQL/10-DDL 建表与结构变更]] — 大表 DDL 的在线变更策略
|
||||
- [[hhs/MySQL/40-常见踩坑]] — 隐式转换等更多常见问题
|
||||
- [[hhs/MySQL/29-Binary Log]] — Binlog Format(ROW vs STATEMENT)对字符集的影响
|
||||
- [[hhs/MySQL/02-SQL核心/06-DDL 建表与结构变更]] — 大表 DDL 的在线变更策略
|
||||
- [[hhs/MySQL/08-工程实践/40-常见踩坑]] — 隐式转换等更多常见问题
|
||||
- [[hhs/MySQL/07-高可用与分布式/29-Binary Log]] — Binlog Format(ROW vs STATEMENT)对字符集的影响
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
tags: [MySQL, Database, 入门]
|
||||
create time: 2026-05-20 23:55
|
||||
---
|
||||
|
||||
# 一、入门基础
|
||||
|
||||
## 概述
|
||||
|
||||
本章从零开始介绍 MySQL:它的整体架构长什么样、怎么装起来、用什么工具连接、以及数据类型和字符集这些最基础但最容易踩坑的知识点。
|
||||
|
||||
## 本章文档
|
||||
|
||||
| # | 主题 | 说明 |
|
||||
|---|------|------|
|
||||
| 1 | [[hhs/MySQL/01-入门基础/01-MySQL 架构与入门]] | MySQL 分几层(客户端 → SQL → 引擎 → 存储);`mysqld` 是怎么启动的 |
|
||||
| 2 | [[hhs/MySQL/01-入门基础/02-安装与初始化]] | 本地安装、Docker Compose 快速启动、基本配置文件 |
|
||||
| 3 | [[hhs/MySQL/01-入门基础/03-客户端工具]] | 命令行 `mysql`、DBeaver、Workbench,选一个顺手的就行 |
|
||||
| 4 | [[hhs/MySQL/01-入门基础/04-数据类型全景]] | 整型、浮点、字符串、日期时间、JSON——每种类型适合存什么 |
|
||||
| 5 | [[hhs/MySQL/01-入门基础/05-字符集与排序规则]] | 为什么一定要用 `utf8mb4`、中文搜索不出来的坑 |
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/README]] — MySQL 知识库总目录
|
||||
@@ -0,0 +1,477 @@
|
||||
---
|
||||
tags: [MySQL, DDL, ALTER TABLE, CREATE TABLE, DATATYPE, NULL, ONLINE DDL, INDEX, CHARACTER SET, SNOWFLAKE, PRIMARY KEY]
|
||||
create time: 2026-05-16 00:00
|
||||
update time: 2026-05-20 12:00
|
||||
---
|
||||
|
||||
# DDL — 建表与结构变更
|
||||
|
||||
## 概述
|
||||
|
||||
DDL(Data Definition Language)用于定义和修改数据库对象的结构。本章从最基础的 `CREATE TABLE` 语法出发,覆盖数据类型选型、主键策略、索引设计原则与常见反模式,然后深入 `ALTER TABLE` 操作与在线 DDL 机制,最后给出生产环境的完整 DDL 检查清单。
|
||||
|
||||
## CREATE TABLE
|
||||
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL COMMENT '用户名',
|
||||
email VARCHAR(255) NOT NULL COMMENT '邮箱唯一',
|
||||
password_hash VARCHAR(64) NOT NULL COMMENT 'bcrypt hash',
|
||||
status TINYINT NOT NULL DEFAULT 1 COMMENT '1=active 0=disabled',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE KEY uk_email (email),
|
||||
INDEX idx_username (username),
|
||||
INDEX idx_status_created (status, created_at)
|
||||
) ENGINE=InnoDB
|
||||
DEFAULT CHARSET=utf8mb4
|
||||
COLLATE=utf8mb4_0900_ai_ci
|
||||
COMMENT='用户表';
|
||||
```
|
||||
|
||||
### 关键语法解析
|
||||
|
||||
| 子句 | 作用 |
|
||||
|------|------|
|
||||
| `ENGINE=InnoDB` | 显式指定存储引擎(8.0 默认值)|
|
||||
| `DEFAULT CHARSET` | 数据库级字符集覆盖 |
|
||||
| `ON UPDATE CURRENT_TIMESTAMP` | 自动维护更新时间戳 |
|
||||
| `UNIQUE KEY` | 唯一索引,同时约束数据唯一性 |
|
||||
| `INDEX idx_name (col)` | 普通索引,辅助查询 |
|
||||
| `COMMENT` | 表和字段注释,可通过 `SHOW FULL COLUMNS` 查看 |
|
||||
|
||||
> [!TIP] 命名规范约定
|
||||
> - **索引名**:`idx_表名_列名`(普通索引)、`uk_表名_列名`(唯一索引)——避免超过 64 字符
|
||||
> - **时间字段**:统一用 `DATETIME` 而非 `TIMESTAMP`。后者范围仅限 `1970~2038`,遇到闰秒时会报错
|
||||
> - **自增主键**:务必用 `UNSIGNED`,正数区间翻倍(0 ~ 42.9 亿),且能略微节省空间
|
||||
|
||||
> [!QUESTION] 思考:为什么 `password_hash` 要用 `VARCHAR(64)` 而不是固定长度?
|
||||
> 答案取决于你使用的哈希算法。bcrypt 输出为 60 字符(如 `$2b$12$...`),预留 64 留有扩展余量。如果改用 SHA-256 则恰好 64 字节,此时 `CHAR(64)` 反而更高效——因为固定长度无需额外存储长度前缀。
|
||||
|
||||
---
|
||||
|
||||
## 数据类型选择指南
|
||||
|
||||
建表时数据类型直接决定磁盘占用和查询性能。以下是常见场景的经验推荐:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["需要整数?"] -->|"是"| B["选择尺寸"]
|
||||
B --> C["TINYINT<br/>-128 ~ 127"]
|
||||
B --> D["SMALLINT<br/>-3.2 万 ~ 3.2 万"]
|
||||
B --> E["MEDIUMINT<br/>-838 万 ~ 838 万"]
|
||||
B --> F["INT<br/>-21 亿 ~ 21 亿"]
|
||||
B --> G["BIGINT<br/>±922 亿亿"]
|
||||
|
||||
A -->|"否"| H["字符串?"]
|
||||
H --> I["变长 < 255"]
|
||||
I --> J["VARCHAR(N)"]
|
||||
H --> K["定长 (密码/签名/IP)"]
|
||||
K --> L["CHAR(N)"]
|
||||
H --> M["超长 (文章/JSON)"]
|
||||
M --> N["TEXT / LONGTEXT"]
|
||||
```
|
||||
|
||||
| 类型家族 | 适用场景 | 避坑提示 |
|
||||
|----------|---------|---------|
|
||||
| `TINYINT` | 状态标记、布尔值 | 建议加 `UNSIGNED`,0~255 足够 |
|
||||
| `INT` | 计数器、外键引用 | 数字型 ID 优先 `INT UNSIGNED`,超 42 亿再用 `BIGINT` |
|
||||
| `VARCHAR(N)` | 用户名、邮箱、地址 | N 按实际 +20% 余量;不要盲目设 255 |
|
||||
| `DATETIME` | 所有时间戳 | MySQL 8.0 推荐;避免 `TIMESTAMP` 的 2038 年瓶颈 |
|
||||
| `DECIMAL(M,D)` | 金额 | **绝对禁止**用 `FLOAT/DOUBLE` 存储财务数据 |
|
||||
|
||||
#### 一个常见的反例
|
||||
|
||||
```sql
|
||||
-- ❌ 错误:用 FLOAT 存价格,会出现精度丢失
|
||||
price FLOAT DEFAULT 0.00,
|
||||
|
||||
-- ✅ 正确:DECIMAL 精确表示货币,M 总位数 D 小数位
|
||||
price DECIMAL(10, 2) DEFAULT 0.00 COMMENT '单位:元',
|
||||
```
|
||||
|
||||
> [!NOTE] DECIMAL(10,2) 表示最多 10 位数字,其中 2 位在小数点后,即最大值为 `99999999.99`。在 InnoDB 内部以二进制紧凑存储,性能接近整数类型。
|
||||
|
||||
---
|
||||
|
||||
## 主键设计选型
|
||||
|
||||
InnoDB 是 **索引组织表(IOT)**——数据和聚簇索引存于一体。主键的选择直接影响磁盘布局、二级索引大小和分库分表的可行性。
|
||||
|
||||
| 方案 | 长度 | 有序性 | 可预测性 | 适用场景 |
|
||||
|------|------|--------|---------|---------|
|
||||
| `BIGINT AUTO_INCREMENT` | 8 字节 | ✅ 天然有序 | ❌ 单调递增暴露业务量 | 单库单表、中小型项目 |
|
||||
| `BIGINT UNSIGNED` + 外部生成器 | 8 字节 | ❌ 随机 | ✅ 不可预测 | 高并发分布式场景 |
|
||||
| `CHAR(36) UUID()` | 36 字节 | ❌ 完全随机 | ✅ 不可预测 | 需要全局唯一 ID 的场景 |
|
||||
| `CHAR(26) ULID / Base62 Snowflake` | 26 字节 | ✅ 时间有序 | ✅ 不可预测 | **推荐**:分布式 + 有序 + 紧凑 |
|
||||
|
||||
```sql
|
||||
-- ❌ 不推荐:直接用 UUID() 作主键(8.0.13 之前)
|
||||
id CHAR(36) PRIMARY KEY DEFAULT (UUID())
|
||||
-- 问题:① 36 字节膨胀二级索引;② 随机写入导致页分裂严重;
|
||||
-- ③ B+ 树高度增加,缓冲池命中率下降
|
||||
|
||||
-- ✅ 推荐:Snowflake 风格 64 位整数
|
||||
-- 1 bit 符号位 + 41 bit 毫秒时间戳 + 10 bit 机器标识 + 12 bit 序列号
|
||||
-- 最大 ID = 2^63 - 1 ≈ 922 亿亿,可支撑到公元 2262 年
|
||||
id BIGINT UNSIGNED NOT NULL PRIMARY KEY
|
||||
```
|
||||
|
||||
> [!QUESTION] 为什么 Snowflake 比 UUID 更受青睐?
|
||||
> 核心在于 **B+ 树的插入局部性**。Snowflake ID 大致有序,InnoDB 可以追加写入最后一页,开销极小。而 UUID v4 完全随机,每次插入都可能触发页面分裂和页迁移,在高并发写入时性能差距可达 **5~10 倍**。
|
||||
|
||||
> [!TIP] 自增主键的安全隐患
|
||||
> 连续递增的 ID 会泄露业务量级信息(竞争对手可通过 API 返回的 ID 推测日活)。若需隐藏业务数据,可在应用层用 AES 加密 ID([Pseudo Random ID](https://github.com/yookoala/pagination-adapter#prandomid)),或在数据库层添加 `INSERT` 前 `RAND()` 扰序。
|
||||
|
||||
---
|
||||
|
||||
## 索引设计与反模式
|
||||
|
||||
索引是 DDL 中最值得投入精力、也最容易被滥用的部分。一个好的索引可以让查询从全表扫描降到 O(log N)。
|
||||
|
||||
### 基本原则
|
||||
|
||||
```sql
|
||||
-- ✅ 最左前缀原则:联合索引 (a, b, c)
|
||||
INDEX idx_abc (a, b, c)
|
||||
|
||||
-- 以下查询都能命中索引
|
||||
WHERE a = 1 -- ✓ (a,)
|
||||
WHERE a = 1 AND b = 2 -- ✓ (a,b)
|
||||
WHERE a = 1 AND b = 2 AND c = 3 -- ✓ (a,b,c)
|
||||
WHERE a = 1 AND c = 3 -- ✓ (a,) + filescan 查 c
|
||||
|
||||
-- 以下查询只能命中部分索引
|
||||
WHERE b = 2 AND c = 3 -- ✗ 只用了 (a,b,c) 中的 c,b 无法利用
|
||||
WHERE a > 1 AND b = 2 -- ✓ 只有 b 能用到索引,c 不能(> 后断链)
|
||||
```
|
||||
|
||||
> [!TIP] 最左前缀匹配口诀
|
||||
> **「从左往右,碰到范围就停」**——一旦遇到 `>`、`<`、`BETWEEN`、`LIKE 'abc%'` 这类范围条件,后面的列就不再参与索引查找。这也是为什么把等值列放在联合索引前面的原因。
|
||||
|
||||
### 覆盖索引 & 回表
|
||||
|
||||
```sql
|
||||
-- 假设表有 PRIMARY KEY (id), INDEX idx_status (status)
|
||||
|
||||
-- ❌ 需要回表:查出不在索引中的列
|
||||
SELECT id, username, email FROM users WHERE status = 1;
|
||||
-- → 先查 idx_status 找到 id,再按 id 回聚簇索引拿 username/email
|
||||
|
||||
-- ✅ 覆盖索引:所有所需列都在索引中,无需回表
|
||||
SELECT id FROM users WHERE status = 1;
|
||||
-- → 直接从 idx_status 取 id,零回表
|
||||
```
|
||||
|
||||
> [!NOTE] 覆盖索引(Covering Index)是性能优化的杀手锏。当查询只需的列全部包含在某个二级索引中时,InnoDB 可以直接从索引树提取数据,无需再访问聚簇索引,大幅减少 I/O。
|
||||
|
||||
### 常见反模式
|
||||
|
||||
```sql
|
||||
-- ❌ 函数包裹导致索引失效
|
||||
SELECT * FROM users WHERE YEAR(created_at) = 2025;
|
||||
-- ✅ 改写为范围查询,利用索引
|
||||
SELECT * FROM users
|
||||
WHERE created_at >= '2025-01-01' AND created_at < '2026-01-01';
|
||||
|
||||
-- ❌ 隐式类型转换
|
||||
CREATE TABLE products (sku VARCHAR(20) NOT NULL);
|
||||
SELECT * FROM products WHERE sku = 12345; -- 字符串被转成数字
|
||||
-- ✅ 保持类型一致
|
||||
SELECT * FROM products WHERE sku = '12345';
|
||||
|
||||
-- ❌ LIKE 通配符在前
|
||||
SELECT * FROM users WHERE username LIKE '%admin%';
|
||||
-- ✅ 改写为全文搜索或调整匹配策略
|
||||
SELECT * FROM users WHERE username LIKE 'admin%'; -- 可以用索引
|
||||
```
|
||||
|
||||
> [!WARNING] 隐式类型转换是最隐蔽的性能杀手
|
||||
> 当列定义为 `VARCHAR` 但传入的是数值(反之亦然),MySQL 会在每一行上做隐式转换,**导致索引直接失效**。执行 `EXPLAIN` 看到 `type=ALL` 且无 `Using where` 优化时,第一反应就是检查类型是否匹配。
|
||||
|
||||
---
|
||||
|
||||
## 字符集与排序规则
|
||||
|
||||
字符集决定了数据如何编码存储,排序规则决定了字符串比较的顺序。选错会导致乱码、排序错误甚至查询走不了索引。
|
||||
|
||||
| 排序规则 | 特点 | 适用场景 |
|
||||
|----------|------|---------|
|
||||
| `utf8mb4_general_ci` | 速度快但不精确,已废弃 | 旧系统兼容 |
|
||||
| `utf8mb4_bin` | 精确的二进制比较,区分大小写 | 密码、Token 等敏感字段 |
|
||||
| `utf8mb4_0900_ai_ci` | 8.0 默认,Unicode 9.0 准确排序 | **通用推荐** |
|
||||
| `utf8mb4_zh_pinyin_ci` | 按拼音排序中文 | 中文名排序场景 |
|
||||
|
||||
> [!TIP] utf8 不是真正的 UTF-8!
|
||||
> MySQL 中的 `utf8` 只是一个缩写,最大只支持 3 字节的字符(缺少 Emoji 等 4 字节 Unicode)。**始终使用 `utf8mb4`**,哪怕你确定没有 Emoji。
|
||||
|
||||
> [!QUESTION] 排序规则影响索引吗?
|
||||
> 是的。如果表 A 用 `utf8mb4_0900_ai_ci` 而表 B 用 `utf8mb4_bin`,两个表做 JOIN 时 MySQL 会在运行时做临时转换,可能导致无法使用索引。关联表的字符集和排序规则应保持一致。
|
||||
|
||||
---
|
||||
|
||||
## MySQL 8.0 新增能力
|
||||
|
||||
### 不可见索引(Invisible Indexes)
|
||||
|
||||
上线前验证索引有效性的利器,无需真正删除索引即可测试其影响:
|
||||
|
||||
```sql
|
||||
-- 将索引设为不可见(优化器不再使用它)
|
||||
ALTER TABLE orders ALTER INDEX idx_status INVISIBLE;
|
||||
|
||||
-- 验证性能影响后,确认无用再删除
|
||||
DROP INDEX idx_status ON orders;
|
||||
|
||||
-- 恢复可见
|
||||
ALTER TABLE orders ALTER INDEX idx_status VISIBLE;
|
||||
```
|
||||
|
||||
> [!TIP] 渐进式下线索引流程
|
||||
> 1. 设置 `INVISIBLE` → 观察慢查询和错误率 1~7 天
|
||||
> 2. 确认无负面影响 → `DROP INDEX`
|
||||
> 3. 此方法比直接 `DROP` 更安全,相当于一次「灰度发布」
|
||||
|
||||
### 表达式索引(Functional Indexes)
|
||||
|
||||
8.0.13+ 支持对函数结果建立索引,无需冗余列:
|
||||
|
||||
```sql
|
||||
-- 对 JSON 字段内的嵌套属性建立索引
|
||||
ALTER TABLE events ADD INDEX json_idx ((JSON_UNQUOTE(JSON_EXTRACT(data, '$.type'))));
|
||||
|
||||
-- 对大小写不敏感的搜索建索引
|
||||
ALTER TABLE users ADD INDEX lower_email_idx ((LOWER(email)));
|
||||
```
|
||||
|
||||
### 窗口函数与 CTE
|
||||
|
||||
DDL 本身不涉及这些,但它们改变了我们设计表结构的方式——有了分析函数后,某些聚合维度表可以简化:
|
||||
|
||||
```sql
|
||||
-- 传统做法:额外建一张月粒度汇总表
|
||||
CREATE TABLE sales_monthly (
|
||||
month DATE NOT NULL,
|
||||
total DECIMAL(12,2),
|
||||
PRIMARY KEY (month)
|
||||
);
|
||||
|
||||
-- 8.0 有了窗口函数后,很多场景可以直接在原表上计算
|
||||
SELECT sales_date, amount,
|
||||
SUM(amount) OVER (ORDER BY sales_date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS moving_avg_7d
|
||||
FROM daily_sales;
|
||||
```
|
||||
|
||||
## ALTER TABLE — 常见操作
|
||||
|
||||
### 增删改列
|
||||
|
||||
`CHANGE` 与 `MODIFY` 的区别在于:**CHANGE 必须同时写出列名(可改名)**,而 MODIFY 只改属性不更名。这是初学最容易混淆的地方。
|
||||
|
||||
```sql
|
||||
-- 添加列
|
||||
ALTER TABLE users ADD COLUMN phone VARCHAR(20) AFTER email;
|
||||
ALTER TABLE users ADD COLUMN last_login_ip VARCHAR(45); -- IPv4/IPv6 通用
|
||||
|
||||
-- 修改列类型(不改名)
|
||||
ALTER TABLE users MODIFY COLUMN status TINYINT UNSIGNED DEFAULT 0;
|
||||
|
||||
-- 重命名列(改名 + 可选改类型)
|
||||
ALTER TABLE users CHANGE COLUMN phone phone_number VARCHAR(20);
|
||||
|
||||
-- 删除列
|
||||
ALTER TABLE users DROP COLUMN last_login_ip;
|
||||
|
||||
-- 删除索引
|
||||
ALTER TABLE users DROP INDEX idx_username;
|
||||
```
|
||||
|
||||
> [!WARNING] ALTER TABLE DROP COLUMN 不可逆
|
||||
> 列一旦删除,其中的数据和统计信息全部消失。**生产环境执行 DDL 前务必确认已有备份**。现代运维流程中应通过迁移工具(见关联笔记)做版本化管理,而非手动执行 ALTER。
|
||||
|
||||
### 在线 DDL(ALGORITHM / LOCK)
|
||||
|
||||
MySQL 5.6+ 支持 Online DDL,允许在结构变更期间持续处理读写请求。
|
||||
|
||||
```sql
|
||||
-- 三种算法对比
|
||||
ALTER TABLE users ADD COLUMN bio TEXT, ALGORITHM=INPLACE;
|
||||
ALTER TABLE users ADD COLUMN bio TEXT, ALGORITHM=COPY;
|
||||
ALTER TABLE users ADD COLUMN bio TEXT; -- 8.0 默认 INPLACE
|
||||
|
||||
-- 三种锁策略对比
|
||||
ALTER TABLE ... LOCK=NONE; -- 不阻塞任何读/写(最安全)
|
||||
ALTER TABLE ... LOCK=SHARED; -- 允许并发读,阻塞写
|
||||
ALTER TABLE ... LOCK=EXCLUSIVE; -- 阻塞所有其他操作(最快)
|
||||
```
|
||||
|
||||
> [!NOTE] INPLACE vs COPY 的本质区别
|
||||
> - **INPLACE**:直接在原表上重建索引或新增索引,不需要全表拷贝数据。支持的场景包括:增加/删除索引、改变列默认值、新增列等。
|
||||
> - **COPY**:创建临时表 → 逐行拷贝数据 → 原子替换。适用于改变列定义导致格式不兼容的场景,比如 `VARCHAR` 改 `INT`。
|
||||
>
|
||||
> **经验法则**:8.0 默认的 `INPLACE` 对大多数操作都够用。若不确定,先跑 `pt-online-schema-change --dry-run` 看预估耗时。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Start["ALTER TABLE 开始"] --> Alg{"ALGORITHM"}
|
||||
|
||||
Alg -->|INPLACE| Inplace["原地重建索引"]
|
||||
Alg -->|COPY| Copy["创建临时表拷贝数据"]
|
||||
Alg -->|DEFAULT(8.0)| Inplace
|
||||
|
||||
Inplace --> Lock{"LOCK"}
|
||||
Copy --> Lock
|
||||
|
||||
Lock|NONE| Online["在线<br/>读写不阻塞"]
|
||||
Lock|SHARED| ReadConc["并发读<br/>写入等待"]
|
||||
Lock|EXCLUSIVE| BlockAll["排他锁<br/>速度最快"]
|
||||
|
||||
style Online fill:#00D866,color:#fff
|
||||
style ReadConc fill:#FF9F43,color:#000
|
||||
style BlockAll fill:#EE5A24,color:#fff
|
||||
```
|
||||
|
||||
### 大表 DDL 实战技巧
|
||||
|
||||
对百万行以上的表执行 `ALTER TABLE` 时:
|
||||
|
||||
```bash
|
||||
# ❌ 危险:直接在线上执行
|
||||
ALTER TABLE big_table ADD INDEX idx_col (col);
|
||||
|
||||
# ✅ 方案一:pt-online-schema-change(Percona Toolkit)
|
||||
pt-online-schema-change \
|
||||
--user=root --password=xxx \
|
||||
--alter "ADD INDEX idx_col (col)" \
|
||||
D=mydb,t=big_table \
|
||||
--execute
|
||||
|
||||
# ✅ 方案二:gh-ost(GitHub 开源)
|
||||
gh-ost \
|
||||
--user=root --password=xxx \
|
||||
--host=127.0.0.1 --port=3306 \
|
||||
--database=mydb \
|
||||
--table=big_table \
|
||||
--alter "ADD INDEX idx_col (col)" \
|
||||
--allow-on-master \
|
||||
--execute
|
||||
```
|
||||
|
||||
> [!TIP] pt-osc 原理三步走
|
||||
> 1. **创建新表**:与原表结构一致 + 目标变更
|
||||
> 2. **触发器同步**:创建 INSERT/UPDATE/DELETE 触发器,将原表的变更实时同步到新表
|
||||
> 3. **原子替换**:加排他锁极短时间,swap 两张表名后删除触发器和旧表
|
||||
|
||||
> [!QUESTION] gh-ost 和 pt-osc 怎么选?
|
||||
> - **pt-osc** 依赖触发器,对高并发写密集型表有较大开销
|
||||
> - **gh-ost** 基于 binlog 解析,无需触发器,对线上影响更小
|
||||
> - **结论**:新项目优先 gh-ost;老项目已有 pt-toolkit 积累也可以用 pt-osc
|
||||
|
||||
### 生产环境 DDL Checklist
|
||||
|
||||
在执行任何 `ALTER TABLE` 之前,逐项核对这份清单。养成习惯可以避免 90% 的线上事故。
|
||||
|
||||
```text
|
||||
□ 1. 确认操作有对应的应用代码上线计划
|
||||
└─ DDL 与应用逻辑必须同步上线,否则可能出现字段不存在但代码已读写该字段的竞态
|
||||
|
||||
□ 2. 预估耗时(先在预发/从库验证)
|
||||
└─ SELECT COUNT(*) FROM big_table; → 根据行数估算 COPY 耗时
|
||||
└─ 或使用 --dry-run 模式(pt-osc / gh-ost 均支持)
|
||||
|
||||
□ 3. 选择 ALGORITHM=INPLACE + LOCK=NONE
|
||||
└─ 只有在无法在线完成时才考虑 LOCK=EXCLUSIVE + 低峰期执行
|
||||
|
||||
□ 4. 检查磁盘空间 ≥ 当前表大小 × 2
|
||||
└─ INPLACE 通常只需少量额外空间;COPY 需要整份表的临时空间
|
||||
|
||||
□ 5. 确认主从复制延迟可控
|
||||
└─ DDL 在主库完成前从库会一直阻塞,DDL 完成后可能瞬间追赶大量事件
|
||||
└─ 建议在监控中关注 Seconds_Behind_Master
|
||||
|
||||
□ 6. 评估二级索引数量上限
|
||||
└─ 每张表建议不超过 5~7 个索引(含主键)
|
||||
└─ 每个索引都会拖慢 INSERT/UPDATE/DELETE
|
||||
└─ 使用不可见索引先下线无用索引,再删除
|
||||
|
||||
□ 7. 准备回滚方案
|
||||
└─ 记录 ALTER 前的 CREATE TABLE 语句(SHOW CREATE TABLE)
|
||||
└─ 如果新结构有问题,可以用原语句重建表
|
||||
|
||||
□ 8. 更新文档和迁移脚本
|
||||
└─ 关联笔记中的 GORM model、migration 文件需要同步修改
|
||||
```
|
||||
|
||||
> [!WARNING] 最危险的操作顺序
|
||||
> 1. **先发代码改逻辑读旧列名** → 此时新列还不存在,查询报 `Unknown column`
|
||||
> 2. **再执行 ALTER TABLE ADD COLUMN** → 解决第一步的问题,但已有脏数据
|
||||
> 3. **最后清理旧列** → 第三次发布才能安全 DROP
|
||||
>
|
||||
> **最佳做法**:先用 `ADD COLUMN` + 双写(新旧并存),然后发代码切换到新字段,最后再 `DROP OLD_COLUMN`——三步走,每次只做一个动作。
|
||||
|
||||
### NULL vs NOT NULL — 选型指南
|
||||
|
||||
这是 DDL 中最常见的争论之一。核心原则:**能用 NOT NULL 就不用 NULL**。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Q1["是否允许未知状态?"] -->|否| NN["NOT NULL + DEFAULT"]
|
||||
Q1 -->|是| Biz{"业务语义?"}
|
||||
|
||||
Biz -->|逻辑删除/软删| SoftDel["TINYINT DEFAULT 0<br/>0=正常 1=已删除"]
|
||||
Biz -->| truly optional | AllowNull["允许 NULL<br/>但加注释说明含义"]
|
||||
|
||||
style NN fill:#00D866,color:#fff
|
||||
style SoftDel fill:#FF9F43,color:#000
|
||||
style AllowNull fill:#EE5A24,color:#fff
|
||||
```
|
||||
|
||||
| 维度 | NOT NULL + DEFAULT | 允许 NULL |
|
||||
|------|-------------------|----------|
|
||||
| **索引效率** | InnoDB 二级索引不存储纯 NULL 值,用默认值占位可减少索引碎片 | 包含 NULL 标记的索引需要额外 1 bit |
|
||||
| **查询安全** | `WHERE col = ?` 不会遗漏任何行 | `WHERE col = 'value'` **排除了 NULL 行**(需用 `IS NULL`) |
|
||||
| **聚合函数** | SUM/COUNT 直接可用 | COUNT(col) 忽略 NULL 行,容易误判总行数 |
|
||||
| **可读性** | `status = 0` 一目了然 | `status IS NULL` 语义模糊——到底是"还没填"还是"被清除了"? |
|
||||
|
||||
> [!WARNING] NULL 的三个常见误区
|
||||
> 1. **"NULL 占空间更小"**:InnoDB 中 NULL 也需要在行格式中标记,固定为每 8 个字节 1 bit 的 null bitmap。比存一个空字符串或零值并不节省什么。
|
||||
> 2. **"NULL = NULL"**:在 SQL 三值逻辑中,`NULL = NULL` 结果为 UNKNOWN,永远不等于 TRUE。判断必须用 `IS NULL` / `IS NOT NULL`。
|
||||
> 3. **"外键可以为 NULL"**:技术上可以,但会导致孤儿记录难以追踪。建议用显式的 `deleted_at` 时间戳代替外键 NULL 做软删除。
|
||||
|
||||
## 常用 DDL 查询
|
||||
|
||||
这些 SQL 命令帮助你查看当前数据库的「结构全貌」,在排查索引失效、表空间膨胀时尤其有用。
|
||||
|
||||
```sql
|
||||
-- 查看表完整创建语句(含所有索引、注释、引擎配置)
|
||||
SHOW CREATE TABLE users\G
|
||||
|
||||
-- 查看所有索引(含索引类型、列顺序、唯一性)
|
||||
SHOW INDEX FROM users\G
|
||||
|
||||
-- 查看表统计信息(Rows 为估算值,非精确计数)
|
||||
SHOW TABLE STATUS LIKE 'users'\G
|
||||
-- 重点关注 Rows(估算行数)、Data_length、Index_length
|
||||
|
||||
-- 查看分区情况
|
||||
SELECT PARTITION_NAME, PARTITION_EXPRESSION, PARTITION_DESCRIPTION
|
||||
FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_NAME = 'logs';
|
||||
```
|
||||
|
||||
> [!TIP] 快速定位慢查询相关的结构问题
|
||||
> ```sql
|
||||
> -- 检查表是否存在大量碎片的索引(数据删除后未回收的空间)
|
||||
> SELECT TABLE_NAME, INDEX_NAME, CARDINALITY
|
||||
> FROM INFORMATION_SCHEMA.STATISTICS
|
||||
> WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users'
|
||||
> ORDER BY CARDINALITY ASC;
|
||||
>
|
||||
> -- 低基数(CARDINALITY 接近 0)的索引通常效果不佳,考虑移除
|
||||
> ```
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/GORM/02-模型定义]] — GORM AutoMigrate 等价于 DDL 的自动化版本
|
||||
- [[hhs/GORM/17-迁移工具]] — Schema 迁移管理最佳实践
|
||||
@@ -7,7 +7,19 @@ create time: 2026-05-16 00:00
|
||||
|
||||
## 概述
|
||||
|
||||
SELECT 是 MySQL 中使用频率最高的语句,也是最容易被误解的语句。理解其内部执行顺序是写出高性能查询的第一步。
|
||||
SELECT 是 MySQL 中使用频率最高的语句,也是最容易被误解的语句。理解其内部执行顺序是写出**正确且高效**查询的第一步。
|
||||
|
||||
本文覆盖 SELECT 从基础语法到进阶优化的所有核心场景:
|
||||
|
||||
| 模块 | 内容 | 关键词 |
|
||||
|------|------|--------|
|
||||
| 执行顺序 | 书写顺序 vs 执行顺序、完整示例 | `WHERE` / `GROUP BY` / `HAVING` / `ORDER BY` |
|
||||
| 关键字详解 | DISTINCT、GROUP BY 优化、条件过滤 | 索引利用、聚合 |
|
||||
| 条件表达式 | CASE 分支、IF 三目运算 | 数据变形、行转列 |
|
||||
| 窗口函数 | 排名、前后行访问、累计计算、帧子句 | `PARTITION BY` / `ROWS BETWEEN` |
|
||||
| 子查询与 CTE | 标量子查询、EXISTS、WITH、递归 CTE | 可读性、复用性 |
|
||||
| 深分页优化 | 延迟关联、游标分页、性能对比 | LIMIT 陷阱 |
|
||||
| 性能贴士 | 常见反模式与修复方案 | Covering Index、索引失效 |
|
||||
|
||||
## SQL 书写顺序 vs 执行顺序
|
||||
|
||||
@@ -105,6 +117,74 @@ HAVING hire_date >= '2024-01-01' -- 错!HAVING 不能用非聚合
|
||||
AND AVG(salary) > 15000;
|
||||
```
|
||||
|
||||
### 条件表达式 — CASE / IF / IFNULL
|
||||
|
||||
在 SELECT 中插入"逻辑判断",是做数据变形(Pivot、区间分组)的核心技能。
|
||||
|
||||
#### CASE 表达式
|
||||
|
||||
SQL 中的 switch-case——标准 SQL 可移植性最好的分支语法:
|
||||
|
||||
```sql
|
||||
-- ✅ CASE WHEN:多路分支
|
||||
SELECT name, salary,
|
||||
CASE
|
||||
WHEN salary >= 20000 THEN 'L5+'
|
||||
WHEN salary >= 15000 THEN 'L4'
|
||||
WHEN salary >= 10000 THEN 'L3'
|
||||
ELSE 'L2-'
|
||||
END AS level
|
||||
FROM employees;
|
||||
|
||||
-- ✅ CASE WHEN:实现行转列(Pivot)
|
||||
-- 统计各部门各职级的员工数
|
||||
SELECT department,
|
||||
SUM(CASE WHEN level = 'P' THEN 1 ELSE 0 END) AS individual_count,
|
||||
SUM(CASE WHEN level = 'M' THEN 1 ELSE 0 END) AS manager_count,
|
||||
SUM(CASE WHEN level = 'D' THEN 1 ELSE 0 END) AS director_count
|
||||
FROM employees
|
||||
GROUP BY department;
|
||||
```
|
||||
|
||||
> [!TIP] CASE 位置决定影响范围
|
||||
> - **WHERE/CASE** → 逐行过滤,走普通索引
|
||||
> - **HAVING/CASE** → 需要先聚合再过滤,效率较低
|
||||
> - 能用 WHERE 解决的,不要推到 HAVING
|
||||
> ```sql
|
||||
> -- ❌ 把能写在 WHERE 的判断放到 HAVING
|
||||
> SELECT status, COUNT(*) FROM orders GROUP BY status HAVING status IN ('paid', 'shipped');
|
||||
> -- ✅ WHERE 先缩小范围,再聚合
|
||||
> SELECT status, COUNT(*) FROM orders WHERE status IN ('paid', 'shipped') GROUP BY status;
|
||||
> ```
|
||||
|
||||
#### IF / IFNULL / COALESCE
|
||||
|
||||
MySQL 专属的快捷函数,适合简单场景:
|
||||
|
||||
```sql
|
||||
-- IF(condition, true_value, false_value) — 三目运算
|
||||
SELECT name,
|
||||
IF(status = 'active', '在职', '离职') AS label,
|
||||
IF(salary IS NULL, 0, salary) AS pay
|
||||
FROM employees;
|
||||
|
||||
-- IFNULL(val, default) — 空值替换
|
||||
SELECT order_id, IFNULL(comments, '暂无评价') AS review
|
||||
FROM orders;
|
||||
|
||||
-- COALESCE(v1, v2, ..., vn) — 返回第一个非 NULL 值
|
||||
-- MySQL 8.0.19+ 支持多个参数(之前只支持 2 个)
|
||||
SELECT name, COALESCE(alias, nickname, name) AS display_name;
|
||||
-- 优先级:别名 > 昵称 > 真实姓名
|
||||
```
|
||||
|
||||
> [!NOTE] CASE vs IF 的选择
|
||||
> | 场景 | 推荐 | 原因 |
|
||||
> |------|------|------|
|
||||
> | 三路以上分支 | `CASE WHEN` | 可读性好,易扩展 |
|
||||
> | 二选一简单判断 | `IF()` | 简洁,但仅限 MySQL |
|
||||
> | 空值兜底 | `COALESCE()` | 标准 SQL,比多个 `IFNULL` 嵌套更优雅 |
|
||||
|
||||
## 窗口函数 (WINDOW FUNCTIONS)
|
||||
|
||||
窗口函数是 MySQL 8.0+ 引入的分析利器——它能在**不减少行数**的前提下进行聚合计算。
|
||||
@@ -146,7 +226,7 @@ SELECT order_date, amount,
|
||||
FROM daily_sales;
|
||||
```
|
||||
|
||||
### 累计计算
|
||||
### 累计计算 — Running Total / Percentage
|
||||
|
||||
```sql
|
||||
-- 累计求和 (Running Total)
|
||||
@@ -160,7 +240,23 @@ SELECT department, name, salary,
|
||||
FROM employees;
|
||||
```
|
||||
|
||||
> [!NOTE] 窗口函数执行时机
|
||||
### 窗口函数执行流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["FROM / JOIN<br/>确定数据源"] --> B["WHERE<br/>逐行过滤"]
|
||||
B --> C["GROUP BY<br/>分组聚合"]
|
||||
C --> D["HAVING<br/>分组后过滤"]
|
||||
D --> E["SELECT 列计算<br/>包括窗口函数"]
|
||||
E --> F["ORDER BY<br/>排序结果"]
|
||||
F --> G["LIMIT / OFFSET<br/>截断输出"]
|
||||
|
||||
style E fill:#00B6BC,color:#fff
|
||||
|
||||
linkStyle 4 stroke-width:3px,fill:none,stroke:#00B6BC
|
||||
```
|
||||
|
||||
> [!NOTE] 窗口函数的"隐形"特性
|
||||
> - 执行顺序在 WHERE、GROUP BY、HAVING **之后**,ORDER BY **之前**
|
||||
> - 因此不能用 WHERE 直接过滤窗口函数的结果——需要套一层子查询:
|
||||
> ```sql
|
||||
@@ -170,6 +266,30 @@ FROM employees;
|
||||
> ) ranked WHERE rn = 1;
|
||||
> -- 作用:每个用户的最新一条订单记录
|
||||
> ```
|
||||
>
|
||||
> 更详细的帧子句说明见下方「### 帧子句 — ROWS BETWEEN」小节。
|
||||
|
||||
### 帧子句 — ROWS BETWEEN
|
||||
|
||||
默认帧范围是 `RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`。用 `ROWS BETWEEN` 可以更精确控制:
|
||||
|
||||
```sql
|
||||
-- 近 7 天滑动窗口均值
|
||||
SELECT order_date, amount,
|
||||
ROUND(AVG(amount) OVER(
|
||||
ORDER BY order_date
|
||||
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
|
||||
), 2) AS avg_7day
|
||||
FROM daily_sales;
|
||||
```
|
||||
|
||||
> [!NOTE] 帧子句速查
|
||||
> | 语法 | 含义 |
|
||||
> |------|------|
|
||||
> | `ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW` | 从分区起点到当前行(**默认**) |
|
||||
> | `ROWS BETWEEN 6 PRECEDING AND CURRENT ROW` | 当前行及前 6 行,共 7 行 |
|
||||
> | `ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING` | 整个分区 |
|
||||
> | `ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING` | 前后各 2 行的滑动窗口 |
|
||||
|
||||
## 子查询与 CTE
|
||||
|
||||
@@ -233,61 +353,40 @@ SELECT * FROM org_chart ORDER BY level, name;
|
||||
> - **性能**:MySQL 会将非递归 CTE 优化为临时表或内联展开——多数情况下两者性能一致
|
||||
> - **复用**:同一个 CTE 可在一个语句中多次引用(派生表不行)
|
||||
|
||||
---
|
||||
## 深分页陷阱
|
||||
|
||||
## 深分页问题
|
||||
|
||||
这是 MySQL 最著名的性能陷阱之一。
|
||||
`LIMIT offset, size` 在 offset 很大时性能急剧下降——MySQL 仍需扫描并跳过前面所有行。
|
||||
|
||||
```sql
|
||||
-- ❌ 灾难级写法:扫描 100 万行后丢弃前 999,990 行
|
||||
-- ❌ 灾难级:扫描 100 万行后丢弃前 999,990 行
|
||||
SELECT * FROM orders LIMIT 999990, 10;
|
||||
-- MySQL 需要先定位到第 999990 行,才返回接下来的 10 行
|
||||
|
||||
-- ✅ 方案一:延迟关联(Deferred Join)
|
||||
-- ✅ 方案一:延迟关联(扫主键索引,只回表 10 次)
|
||||
SELECT o.* FROM orders o
|
||||
INNER JOIN (
|
||||
SELECT id FROM orders ORDER BY id LIMIT 999990, 10
|
||||
) AS tmp ON o.id = tmp.id;
|
||||
-- 子查询只扫主键索引(极紧凑),外层再 JOIN 拿完整数据
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph "传统 LIMIT 999990,10"
|
||||
A1["扫描聚簇索引<br/>跳过 999990 行"] --> A2["取出 10 行数据"]
|
||||
end
|
||||
|
||||
subgraph "延迟关联方案"
|
||||
B1["扫描聚簇索引<br/>跳过 999990 行"] --> B2["仅提取 10 个主键"]
|
||||
B2 --> B3["JOIN 回聚簇索引<br/>精确查找 10 个主键"]
|
||||
B3 --> B4["返回结果"]
|
||||
end
|
||||
|
||||
A1 --> A2
|
||||
style B1 fill:#00B6BC,color:#fff
|
||||
style B2 fill:#00D866,color:#fff
|
||||
```
|
||||
|
||||
### 游标分页(推荐)
|
||||
|
||||
```sql
|
||||
-- 上一页最后一条记录的 id = 999985
|
||||
-- ✅✅ 方案二:游标分页(推荐,O(log N) 恒定性能)
|
||||
SELECT * FROM orders
|
||||
WHERE id > 999985
|
||||
WHERE id > 999985 -- 上一页最后一条的 ID
|
||||
ORDER BY id ASC
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
> [!TIP] 为什么游标分页更优?
|
||||
> - `WHERE id > ?` 走索引范围扫描,复杂度 O(log N) 而非 O(N)
|
||||
> - 无论在第几页,查询时间恒定
|
||||
> - 需要前端传「上一页最后一个 ID」作为下一页的游标
|
||||
> [!TIP] 分页方案选型
|
||||
> | 方案 | 复杂度 | 支持跳页 | 适用场景 |
|
||||
> |------|--------|---------|---------|
|
||||
> | 传统 `LIMIT n, m` | O(N) | ✅ | 小数据量 (< 1 万行) |
|
||||
> | 延迟关联 | O(log N + m) | ✅ | 大数据量、需要精确页码 |
|
||||
> | 游标分页 | O(log N) | ❌ | 瀑布流、"加载更多" |
|
||||
>
|
||||
> **局限性**:不支持跳页(不能直接跳到第 100 页),但这对瀑布流场景足够。
|
||||
> **更完整的分析与调优策略**请见 → [[hhs/MySQL/03-索引与查询优化/18-深分页优化]]
|
||||
|
||||
## 性能小贴士
|
||||
|
||||
### SELECT * 的反面教材
|
||||
|
||||
```sql
|
||||
-- ❌ 避免 SELECT *
|
||||
SELECT * FROM users WHERE status = 1;
|
||||
@@ -295,18 +394,45 @@ SELECT * FROM users WHERE status = 1;
|
||||
-- ✅ 只查需要的列
|
||||
SELECT id, username, email FROM users WHERE status = 1;
|
||||
-- 好处:减少网络传输、提高 Buffer Pool 命中率、可能触发 Covering Index
|
||||
```
|
||||
|
||||
-- ❌ 函数包裹索引列
|
||||
### 函数包裹索引列
|
||||
|
||||
```sql
|
||||
-- ❌ 函数包裹导致索引失效
|
||||
SELECT * FROM users WHERE YEAR(created_at) = 2026;
|
||||
|
||||
-- ✅ 用范围替代函数
|
||||
-- ✅ 用范围替代函数(走索引范围扫描)
|
||||
SELECT * FROM users
|
||||
WHERE created_at >= '2026-01-01'
|
||||
AND created_at < '2027-01-01';
|
||||
```
|
||||
|
||||
### ORDER BY 与 Using filesort
|
||||
|
||||
```sql
|
||||
-- ✅ 排序列有索引 → 直接按索引顺序输出,无需额外排序
|
||||
SELECT * FROM orders ORDER BY user_id;
|
||||
-- EXPLAIN Extra: NULL (无 filesort)
|
||||
|
||||
-- ⚠️ 混合 ASC/DESC → 无法利用普通 B+Tree 索引排序
|
||||
SELECT * FROM orders ORDER BY user_id ASC, created_at DESC;
|
||||
-- EXPLAIN Extra: Using filesort — 需要额外的内存/磁盘排序
|
||||
```
|
||||
|
||||
> [!TIP] 覆盖索引 (Covering Index)
|
||||
> 当 SELECT 的列全部包含在某个索引中时,InnoDB 可以直接从索引树返回结果,**无需回表**。
|
||||
> ```sql
|
||||
> -- 创建覆盖索引:id + status + updated_at 都在 idx 中
|
||||
> CREATE INDEX idx_cover ON users(status, updated_at);
|
||||
> -- 下面这个查询完全走索引扫描,不回表
|
||||
> SELECT status, updated_at FROM users WHERE status = 1;
|
||||
> ```
|
||||
>
|
||||
> **验证方法**:看 EXPLAIN 的 `Extra` 列是否出现 `Using index`。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/12-JOIN 原理与优化]] — 深入理解 JOIN 的内部执行机制
|
||||
- [[hhs/MySQL/13-子查询与派生表]] — 与 SELECT 密切相关的子查询技术
|
||||
- [[hhs/GORM/06-排序与分页]] — GORM 分页 API 与原生 SQL 的差异
|
||||
- [[hhs/MySQL/02-SQL核心/09-JOIN 原理与优化]] — JOIN 的内部执行机制与驱动表选择
|
||||
- [[hhs/MySQL/02-SQL核心/10-子查询与派生表]] — EXISTS / IN 子查询与 CTE 的性能对比
|
||||
- [[hhs/MySQL/03-索引与查询优化/15-EXPLAIN 完全指南]] — 通过执行计划识别查询瓶颈
|
||||
@@ -9,6 +9,114 @@ create time: 2026-05-16 00:00
|
||||
|
||||
JOIN 是关系型数据库的核心能力,也是性能问题的主要来源。理解 MySQL 的 JOIN 执行算法才能写出高效的关联查询。
|
||||
|
||||
## 什么是 JOIN
|
||||
|
||||
在关系型数据库中,数据通常分散在多张表中——用户信息一张表,订单记录另一张表。**JOIN 就是把这些分散的表按某种规则"拼"在一起,形成一张完整的视图。**
|
||||
|
||||
> [!QUESTION] 💡 思考一下
|
||||
> 想象你在 Excel 里有两张表:左边是学生名单(含学号),右边是成绩表(也有学号)。你想看到"每个学生对应的成绩"——你会怎么操作?
|
||||
> **答**:你会通过"学号"这一列,把两行的数据对应起来。SQL 中的 JOIN 就是这个操作的自动化版本。
|
||||
|
||||
### 一个具体的例子
|
||||
|
||||
假设有两张表:
|
||||
|
||||
| users 表 | | orders 表 | |
|
||||
|-----------|---|------------|---|
|
||||
| **id** | **name** | **order_id** | **user_id** | **amount** |
|
||||
| 1 | Alice | 101 | 1 | ¥200 |
|
||||
| 2 | Bob | 103 | 2 | ¥80 |
|
||||
| | | 102 | 3 | ¥150 |
|
||||
|
||||
我们想查 **"每个订单对应用户的名字"** ——单张表做不到,必须同时看 users 和 orders:
|
||||
|
||||
```sql
|
||||
SELECT u.name, o.order_id, o.amount
|
||||
FROM users u JOIN orders o ON u.id = o.user_id;
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
| name | order_id | amount |
|
||||
|------|----------|--------|
|
||||
| Alice | 101 | ¥200 |
|
||||
| Bob | 103 | ¥80 |
|
||||
|
||||
> ⚠️ 注意:order_id=102(user_id=3)没有出现在结果中,因为 users 表里没有 id=3 的记录。这就是 JOIN 的匹配逻辑——**只有两边都能对上的行才会被选中**。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph users 表
|
||||
A1["id=1 / Alice"]
|
||||
A2["id=2 / Bob"]
|
||||
A3["id=3(不存在)"]
|
||||
end
|
||||
|
||||
subgraph orders 表
|
||||
B1["order_id=101<br/>user_id=1 ¥200"]
|
||||
B2["order_id=102<br/>user_id=3 ¥150"]
|
||||
B3["order_id=103<br/>user_id=2 ¥80"]
|
||||
end
|
||||
|
||||
A1 -->|"ON id = user_id"| B1
|
||||
A2 -->|"ON id = user_id"| B3
|
||||
A3 -.->|"users 表中无 id=3"| B2
|
||||
|
||||
style A3 fill:#EE5A24,color:#fff
|
||||
style B2 fill:#EE5A24,color:#fff
|
||||
```
|
||||
|
||||
> [!NOTE] 为什么要拆成多张表?
|
||||
> 你可能会问:为什么不把所有数据存在一张大表里?这涉及数据库设计的核心原则——**避免冗余**。
|
||||
> - 如果订单表直接存用户名,用户改名时要更新几万条订单记录
|
||||
> - 分表后只改 users 表一条记录,订单表通过 user_id 引用即可
|
||||
> - 这就是「范式」(Normal Form)的思想——详见 [[hhs/MySQL/05-表设计/21-表结构设计三范式]]
|
||||
|
||||
### 一句话理解 JOIN 的本质
|
||||
|
||||
> **JOIN 就是"按条件逐行匹配两张表的数据"**。有索引时能跳过大部分不匹配的行(快),没索引时只能一行一行比对(慢)——后续所有优化都是围绕这一点展开的。
|
||||
|
||||
## 驱动表与被驱动表
|
||||
|
||||
> [!TIP] 为什么先讲这个?
|
||||
> 在深入 JOIN 的执行算法之前,必须先理解**驱动表**和**被驱动表**的概念——它们是所有 JOIN 优化决策的基础。
|
||||
|
||||
当一个 JOIN 语句被执行时,MySQL 会将两张表区分角色:
|
||||
|
||||
| 角色 | 职责 | 通俗理解 |
|
||||
|------|------|---------|
|
||||
| **驱动表(Driving Table)** | 先读取数据,提供"查找的关键词" | 手持"点名册"的人 |
|
||||
| **被驱动表(Driven Table)** | 根据驱动表提供的每行数据去匹配 | 拿着点名册逐一核对的人 |
|
||||
|
||||
上面的例子中:
|
||||
|
||||
```sql
|
||||
-- users 是驱动表,orders 是被驱动表
|
||||
SELECT u.name, o.order_id
|
||||
FROM users u -- 👈 驱动表:先读它
|
||||
JOIN orders o ON u.id = o.user_id; -- 👆 被驱动表:每次拿 u.id 来匹配
|
||||
```
|
||||
|
||||
### 谁当驱动表重要吗?
|
||||
|
||||
**非常重要。** 核心原则:**用小表驱动大表**——驱动表行数越少,被驱动表被扫描的次数就越少。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S["小表:100 行"] -->|"驱动"| L["大表:100 万行<br/>匹配 100 次 ✅"]
|
||||
|
||||
L2["大表:100 万行"] -->|"驱动"| S2["小表:100 行<br/>匹配 100 万次 ❌"]
|
||||
|
||||
style L fill:#00D866,color:#fff
|
||||
style S2 fill:#EE5A24,color:#fff
|
||||
```
|
||||
|
||||
MySQL 的 Optimizer(优化器)会**尝试自动选择最优顺序**——它会统计每张表的行数,评估成本后决定谁做驱动表。但当统计信息不准确或数据分布特殊时,优化器可能选错,这时就需要手动干预。
|
||||
|
||||
> [!TIP] 黄金法则
|
||||
> **驱动表可以全表扫描,但被驱动表必须走索引。**
|
||||
> 任何 JOIN 优化的目标都是确保被驱动表的访问方式足够高效。我们会在后面的「驱动表选择」深入学习如何判断和优化。
|
||||
|
||||
## JOIN 类型速览
|
||||
|
||||
```sql
|
||||
@@ -77,13 +185,25 @@ flowchart TD
|
||||
SELECT * FROM users u JOIN orders o ON u.tag = o.tag;
|
||||
```
|
||||
|
||||
```
|
||||
Step 1: 从 users 表读 N 行 → Join Buffer (默认 256KB)
|
||||
Step 2: 扫描 orders 表,对每一行与 Buffer 中的所有行比较
|
||||
Step 3: Buffer 满了就输出匹配结果,清空再加载
|
||||
Step 4: 重复直到读完 users 表
|
||||
> [!QUESTION] 💡 思考
|
||||
> 如果被驱动表有百万行数据,每读一行都要跟驱动表的所有行做比较——这比数据库慢的原因更接近"人脑"的逻辑。你觉得 MySQL 在这种极端情况下会怎么优化?
|
||||
> **答**:不要逐行带进来比较!先把驱动表的 N 行拼成一大块放到内存缓冲区里,然后对被驱动表只扫一遍就完事。这就是 **Block** Nested-Loop Join 的核心思想。
|
||||
|
||||
总成本 = ceil(rows_users / buffer_rows) × rows_orders
|
||||
```mermaid
|
||||
flowchart LR
|
||||
S["从驱动表读 N 行"] --> B["写入 Join Buffer<br/>默认 256KB"]
|
||||
B --> P{"Buffer 满了?"}
|
||||
P -->|否| R["扫描被驱动表<br/>与 Buffer 中每行比较"]
|
||||
P -->|是| O["输出已匹配结果"]
|
||||
O --> C["清空 Buffer"]
|
||||
C --> R
|
||||
R --> S
|
||||
```
|
||||
|
||||
```sql
|
||||
-- BNLJ 成本估算公式
|
||||
-- 总操作数 ≈ ceil(驱动表行数 / 每Buffer能存行数) × 被驱动表行数
|
||||
-- 每Buffer行数 = floor(join_buffer_size / 单行字节数)
|
||||
```
|
||||
|
||||
> [!NOTE] Join Buffer 大小
|
||||
@@ -94,7 +214,8 @@ Step 4: 重复直到读完 users 表
|
||||
> -- 注意:它不参与排序也不去重,纯粹做行数据缓存
|
||||
> ```
|
||||
|
||||
**BNLJ 的致命弱点**:被驱动表无论多大都必须全扫一次。如果两张都是百万级大表,代价呈爆炸式增长。
|
||||
> [!WARNING] BNLJ 是最后的手段
|
||||
> 被驱动表无论多大都必须全扫一次。两张百万级大表无索引 JOIN 的成本可达 **百亿级行比较**。
|
||||
|
||||
### 2. Index Nested-Loop Join(INLJ,索引嵌套循环)
|
||||
|
||||
@@ -125,8 +246,9 @@ sequenceDiagram
|
||||
|
||||
> [!NOTE] INLJ 的成本拆解
|
||||
> - 单次查找代价 = log₂(索引页数),通常 ≈ 3~4 次磁盘随机读
|
||||
> - 若驱动表 1000 行、被驱动表 100 万行:**1000 × log₂(1M) ≈ 1000 × 20 = 20000 次索引查找**
|
||||
> - 对比 BNLJ:若走 BNLJ 则需 **1000 / buffer_rows × 1M** 次行比较 —— 差距巨大
|
||||
> - 若驱动表 1000 行、被驱动表 100 万行:**1000 × log₂(1M) ≈ 20,000 次索引查找**
|
||||
> - 对比 BNLJ(Buffer 每批存 50 行):ceil(1000/50) × 1,000,000 = **20,000,000 次行比较**
|
||||
> - 差距达 **三个数量级**
|
||||
|
||||
**INLJ 的两个子分类**:
|
||||
|
||||
@@ -135,6 +257,30 @@ sequenceDiagram
|
||||
| Unique NLJ | 主键 / 唯一索引 | 恰好 0 或 1 行 | `Using index condition` |
|
||||
| Non-Unique NLJ | 普通二级索引 | 可能 0…N 行 | `Using index condition` |
|
||||
|
||||
### 成本对比:加一个索引能带来什么?
|
||||
|
||||
假设场景:**驱动表 1,000 行,被驱动表 100 万行**
|
||||
|
||||
```mermaid
|
||||
quadrantChart
|
||||
title JOIN 算法性能分布
|
||||
x-axis "低效 ← → 高效"
|
||||
y-axis "高成本 ← → 低成本"
|
||||
"Block NLJ (无索引)": [0.08, 0.1]
|
||||
"Non-Unique NLJ": [0.25, 0.75]
|
||||
"Unique NLJ (主键)": [0.95, 0.95]
|
||||
```
|
||||
|
||||
```sql
|
||||
-- 量化对比 (操作次数)
|
||||
-- Unique NLJ (主键): 1000 × log2(1000000) ≈ 20,000 次查找
|
||||
-- Non-Unique NLJ (二级): 1000 × log2(1M) × 10 ≈ 200,000 次查找 (假设每 key 平均 10 匹配)
|
||||
-- Block NLJ (无索引): ceil(1000/50) × 1,000,000 = 20,000,000 次行比较
|
||||
```
|
||||
|
||||
> [!SUMMARY] 💡 一句话总结
|
||||
> **同一个 JOIN,有索引和无索引相差三个数量级——这就是 MySQL 优化的核心杠杆。**
|
||||
|
||||
## 驱动表选择
|
||||
|
||||
MySQL 在解析 SQL 时,**默认从左到右**确定驱动表——左边第一张表就是驱动表。但 Optimizer 会在评估成本后决定是否交换表的顺序(以最小化被驱动表的扫描行数)。
|
||||
@@ -238,7 +384,40 @@ JOIN products p ON o.product_id = p.id;
|
||||
|
||||
### Multi-Join 处理策略(3+ 表)
|
||||
|
||||
生产中最常见的是 3 表以上 JOIN。优化思路升级:
|
||||
生产中最常见的是 3 表以上 JOIN。MySQL 采用 **链式驱动**——前一张表的输出结果作为下一张被驱动表的输入。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant T1 as 表A (驱动)
|
||||
participant T2 as 表B (被驱动)
|
||||
participant T3 as 表C (被驱动)
|
||||
participant T4 as 表D (被驱动)
|
||||
|
||||
Note over T1: 第一步:全扫 A<br/>得到 N1 行中间结果
|
||||
|
||||
loop N1 行
|
||||
T1->>T2: B ON A.x = B.x (走索引)
|
||||
T2-->>T1: M1 行匹配
|
||||
end
|
||||
Note over T1,T2: 第二步:得到 M1 行中间结果
|
||||
|
||||
loop M1 行
|
||||
T1->>T3: C ON B.y = C.y (走索引)
|
||||
T3-->>T1: M2 行匹配
|
||||
end
|
||||
Note over T1,T3: 第三步:得到 M2 行中间结果
|
||||
|
||||
loop M2 行
|
||||
T1->>T4: D ON C.z = D.z (走索引)
|
||||
T4-->>T1: 最终结果
|
||||
end
|
||||
```
|
||||
|
||||
> [!QUESTION] 💡 思考
|
||||
> 如果有 4 张表按顺序 JOIN,每张表都有合适的索引——那中间结果的行数会逐级递减还是递增?为什么?
|
||||
> **答**:取决于 JOIN 条件的选择性。WHERE 过滤和精确匹配会让行数逐层减少;而一对多关联则可能逐层膨胀。**关键是要确保每一层的被驱动表都走索引。**
|
||||
|
||||
优化思路升级:
|
||||
|
||||
1. **确保第 2 张及之后的每张表都有索引支撑 JOIN 条件**——只有第 1 张表可以全表扫描
|
||||
2. **用小表驱动大表**:EXPLAIN 输出从上到下依次是被驱动表,上面的驱动下面的
|
||||
@@ -274,9 +453,83 @@ SELECT * FROM users u JOIN orders o ON u.user_id = o.user_id;
|
||||
| **隐式转换** | `WHERE varchar_col = 123` | 显式字符串比较 |
|
||||
| **缺少复合索引** | 多条件 JOIN 只用单列索引 | 创建覆盖联合索引 |
|
||||
|
||||
## 进阶优化技巧
|
||||
|
||||
### 1. Index Condition Pushdown(ICP,索引条件下推)
|
||||
|
||||
传统扫描方式:二级索引找到 ROWID → 回表取完整行 → WHERE 过滤。ICP 将部分 WHERE 条件的过滤下沉到存储引擎层,在二级索引上就直接完成,减少回表次数。
|
||||
|
||||
```sql
|
||||
-- 假设 (category, status) 上有联合索引
|
||||
SELECT * FROM products WHERE category = 'electronics' AND status = 'active';
|
||||
|
||||
-- ICP 开启时 (SHOW STATUS LIKE 'Last_query_cost...'):
|
||||
-- 普通模式: 二级索引扫描所有 category='electronics' 的 ROWID → 全部回表 → 再过滤 status
|
||||
-- ICP 模式: 二级索引上同时匹配 category + status → 只回表符合两条件的行
|
||||
```
|
||||
|
||||
> [!TIP] 如何确认 ICP 生效?
|
||||
> EXPLAIN 的 Extra 列出现 `Using index condition`——这表示 MySQL 5.6+ 的 ICP 已启用。
|
||||
|
||||
### 2. Loose Scan(松散扫描)
|
||||
|
||||
当聚合函数(COUNT/DISTINCT/MAX/MIN)配合有序索引使用时,Optimizer 可以跳过中间重复值,直接"跳跃"到每组第一个记录。
|
||||
|
||||
```sql
|
||||
-- 假设 (category, subcategory) 上有联合索引
|
||||
-- 传统 GROUP BY: 扫描所有 10 万行 → 分组 → 每组选第一条
|
||||
SELECT category, COUNT(DISTINCT subcategory) FROM products GROUP BY category;
|
||||
|
||||
-- Loose Scan: 直接从二级索引树中抽取每组的唯一子键
|
||||
-- 代价从全表扫描降到仅遍历索引的第一层 ≈ O(unique_categories)
|
||||
```
|
||||
|
||||
> [!NOTE] 触发条件
|
||||
> - 聚合函数必须是 `MIN()/MAX()` 或 `COUNT(DISTINCT)`
|
||||
> - GROUP BY 的列必须紧跟联合索引的最左前缀
|
||||
> - 查询结果中不能有其他需要额外处理的列
|
||||
|
||||
### 3. Semi Join(半连接)
|
||||
|
||||
将子查询转化为 JOIN 的一种优化策略。MySQL 会对 IN / EXISTS 子查询自动做半连接转换。
|
||||
|
||||
```sql
|
||||
-- 原始子查询
|
||||
SELECT * FROM orders WHERE user_id IN (SELECT id FROM users WHERE level = 'vip');
|
||||
|
||||
-- 等价于半连接:orders 只需要知道"是否存在匹配",不需要返回 users 的所有匹配行
|
||||
-- MySQL 内部可能选择以下策略之一:
|
||||
-- DuplicateWeedout: 先做 JOIN 再去重
|
||||
-- FirstMatch: 被驱动表找到第一行匹配就停止
|
||||
-- Loosescan: 对被驱动表做 Loose Scan
|
||||
```
|
||||
|
||||
> [!WARNING] 注意去重开销
|
||||
> 半连接需要额外的去重步骤(DuplicateWeedout)。如果驱动表本身 SELECT DISTINCT,可以考虑先对驱动表去重再做 JOIN。
|
||||
|
||||
### 4. Derived Table Materialization(派生表物化)
|
||||
|
||||
复杂子查询作为 FROM 子句时,MySQL 会将其物化为临时表。过度物化会带来额外开销。
|
||||
|
||||
```sql
|
||||
-- 派生表会被物化为临时表
|
||||
SELECT * FROM (SELECT user_id, SUM(amount) FROM orders GROUP BY user_id) t
|
||||
JOIN users u ON t.user_id = u.id;
|
||||
|
||||
-- 如果 optimizer_switch='derived_merge=on'(MySQL 5.7+ 默认),
|
||||
-- 优化器会尝试将子查询合并到外层查询中,避免物化开销
|
||||
```
|
||||
|
||||
> [!TIP] 调优建议
|
||||
> ```sql
|
||||
> -- 查看当前优化器开关状态
|
||||
> SHOW VARIABLES LIKE 'optimizer_switch';
|
||||
> -- derived_merge=on 是推荐的默认配置,能让 MySQL 自动展开简单派生表
|
||||
> ```
|
||||
|
||||
## 性能对比速查表
|
||||
|
||||
| 算法 | 最优场景 | 最坏场景 | 典型代价公式 |
|
||||
| **算法** | **最优场景** | **最坏场景** | **典型代价公式** |
|
||||
|------|---------|---------|-------------|
|
||||
| **Unique NLJ** | 被驱动表是主键 / 唯一索引 | 驱动表大量行无匹配(返回 NULL) | 驱动表行数 × log(被驱动表) |
|
||||
| **Non-Unique NLJ** | 被驱动表有普通二级索引 | 索引选择性差,大量重复值 | 驱动表行数 × log(被驱动表) × 平均匹配数 |
|
||||
@@ -290,6 +543,6 @@ SELECT * FROM users u JOIN orders o ON u.user_id = o.user_id;
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/14-子查询与派生表]] — EXISTS / IN 子查询与 JOIN 的替代关系
|
||||
- [[hhs/MySQL/16-B+Tree 索引原理]] — JOIN 如何利用二级索引加速
|
||||
- [[hhs/MySQL/19-EXPLAIN 完全指南]] — 通过 type 字段识别 JOIN 质量问题
|
||||
- [[hhs/MySQL/02-SQL核心/10-子查询与派生表]] — EXISTS / IN 子查询与 JOIN 的替代关系
|
||||
- [[hhs/MySQL/03-索引与查询优化/12-B+Tree 索引原理]] — JOIN 如何利用二级索引加速
|
||||
- [[hhs/MySQL/03-索引与查询优化/15-EXPLAIN 完全指南]] — 通过 type 字段识别 JOIN 质量问题
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
tags: [MySQL, 子查询, EXISTS, IN, 派生表]
|
||||
tags: [MySQL, 子查询, EXISTS, IN, 派生表, CTE]
|
||||
create time: 2026-05-16 00:00
|
||||
---
|
||||
|
||||
@@ -32,6 +32,8 @@ graph BT
|
||||
|
||||
## 标量子查询
|
||||
|
||||
### 基本用法
|
||||
|
||||
```sql
|
||||
-- 用法 1:在 SELECT 中调用
|
||||
SELECT
|
||||
@@ -48,10 +50,39 @@ INSERT INTO reports (month, order_total)
|
||||
VALUES ('2026-05', (SELECT SUM(amount) FROM orders WHERE MONTH(created_at) = 5));
|
||||
```
|
||||
|
||||
### 相关 vs 无关子查询
|
||||
|
||||
这是理解子查询性能的基石:**子查询是否引用了外层查询的列?**
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
UC["子查询"] --> UCX{"是否引用外层表的列?"}
|
||||
UCX -->|否:无关子查询<br/>Uncorrelated| UCN["先执行一次<br/>结果供外层复用 ✅"]
|
||||
UCX -->|是:相关子查询<br/>Correlated| UCR["对每一行外层记录<br/>都要重新执行 ⚠️"]
|
||||
|
||||
style UCN fill:#00B6BC,color:#fff
|
||||
style UCR fill:#FF9F43,color:#000
|
||||
```
|
||||
|
||||
> [!EXAMPLE] 对比示例
|
||||
> ```sql
|
||||
> -- 🟢 无关子查询:只执行一次
|
||||
> -- 子查询完全不依赖 users 表
|
||||
> SELECT * FROM products
|
||||
> WHERE price > (SELECT AVG(price) FROM products);
|
||||
>
|
||||
> -- 🔴 相关子查询:执行 N 次(N = users 行数)
|
||||
> -- 子查询引用了外层 u.id
|
||||
> SELECT u.username,
|
||||
> (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS cnt
|
||||
> FROM users u;
|
||||
> -- 10 万用户 → 子查询执行 10 万次
|
||||
> ```
|
||||
|
||||
> [!WARNING] 标量子查询的性能隐患
|
||||
> MySQL 8.0.21 之前,标量子查询**无法被物化**,会导致每次执行都重新计算(N+1 问题)。
|
||||
> ```sql
|
||||
> -- ❌ 慢:每个用户都要查一次 orders 表
|
||||
> -- ❌ 慢:每个用户都要查一次 orders 表(相关子查询)
|
||||
> SELECT u.username,
|
||||
> (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS cnt
|
||||
> FROM users u;
|
||||
@@ -193,19 +224,30 @@ EXPLAIN SELECT u.id, SUB.total
|
||||
FROM users u
|
||||
JOIN (SELECT user_id, SUM(amount) AS total FROM orders GROUP BY user_id) AS SUB ON u.id = SUB.user_id;
|
||||
|
||||
-- id | select_type | table | type | Extra
|
||||
-- ---|----------------|------------|-------|----------------------------
|
||||
-- 1 | PRIMARY | <derived2> | ALL | NULL
|
||||
-- 1 | PRIMARY | u | ALL | Using where
|
||||
-- 2 | DERIVED | orders | ALL | Using temporary; Using filesort
|
||||
|
||||
-- ✅ 可 Flattening → 无临时表,Extra 干净
|
||||
EXPLAIN SELECT u.id, o.amount
|
||||
FROM users u JOIN (SELECT user_id, amount FROM orders) AS o ON u.id = o.user_id;
|
||||
```
|
||||
|
||||
-- id | select_type | table | type | Extra
|
||||
-- 1 | SIMPLE | orders| ALL | NULL
|
||||
-- 1 | SIMPLE | u | index | Primary key
|
||||
对比两种情况的执行计划:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph "❌ 物化型派生表"
|
||||
T1["select_type: DERIVED"] -->|orders | T2["type: ALL<br/>Extra: Using temporary, Using filesort"]
|
||||
P1["PRIMARY → <derived2>"] -->|全表扫描| P2["type: ALL"]
|
||||
P3["PRIMARY → u"] -->|全表扫描| P4["Extra: Using where"]
|
||||
end
|
||||
|
||||
subgraph "✅ 可展开型派生表"
|
||||
F1["select_type: SIMPLE"] -->|orders | F2["type: ALL<br/>Extra: NULL(无额外开销)"]
|
||||
F3["SIMPLE → u"] -->|索引扫描| F4["type: index<br/>Extra: Using index"]
|
||||
end
|
||||
|
||||
style T2 fill:#FF9F43,color:#000
|
||||
style P2 fill:#EE5A24,color:#fff
|
||||
style F2 fill:#00D866,color:#fff
|
||||
style F4 fill:#00D866,color:#fff
|
||||
```
|
||||
|
||||
> [!TIP] 性能优化技巧
|
||||
@@ -213,6 +255,52 @@ FROM users u JOIN (SELECT user_id, amount FROM orders) AS o ON u.id = o.user_id;
|
||||
> - `optimizer_switch='derived_merge=on'`(默认开启)控制是否允许 Flattening
|
||||
> - 对于超大结果集物化,注意 `tmp_table_size` / `max_heap_table_size` 限制
|
||||
|
||||
### 常见陷阱与最佳实践
|
||||
|
||||
| 坑 | 症状 | 解法 |
|
||||
|----|------|------|
|
||||
| **缺少别名** | `Every derived table must have its own alias` | 派生表必须起别名:`FROM (...) AS t` |
|
||||
| **自引用冲突** | `Table 'xxx' is specified twice` | UPDATE/DELETE 中的派生表需用嵌套包裹 |
|
||||
| **NULL 传播** | NOT IN 返回空集 | 改用 `NOT EXISTS` 或加 `IS NOT NULL` 过滤 |
|
||||
| **隐式类型转换** | 索引失效,全表扫描 | 确保比较列类型一致(如 `VARCHAR` vs `INT`)|
|
||||
| **大结果集物化** | `Using temporary; Using filesort` | 用 JOIN 重写或拆分为多次查询 |
|
||||
|
||||
## 实战:性能排查 Checklist
|
||||
|
||||
遇到慢的子查询,按以下顺序逐项检查:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S["慢查询:含子查询"] --> C1{"EXPLAIN 看了吗?"}
|
||||
C1 -->|没看| STOP["🛑 先跑 EXPLAIN<br/>别盲猜,数据说话"]
|
||||
C1 -->|看了| C2{"Extra 有 Using temporary 吗?"}
|
||||
|
||||
C2 -->|"是:有临时表"| D1["派生表物化了 → 考虑改 JOIN 或提升为 CTE"]
|
||||
C2 -->|"否"| C3{"有 Using filesort 吗?"}
|
||||
|
||||
C3 -->|"是"| D2["排序开销大 → 检查是否有可用索引"]
|
||||
C3 -->|"否"| C4{"子查询是否在 SELECT 列表中?"}
|
||||
|
||||
C4 -->|"是:标量子查询"| D3["N+1 问题 → 改写为 LEFT JOIN + GROUP BY"]
|
||||
C4 -->|"否"| C5{"用了 NOT IN 吗?"}
|
||||
|
||||
C5 -->|"是"| D4["NULL 陷阱 → 换 NOT EXISTS"]
|
||||
C5 -->|"否"| C6{"关联列类型一致吗?"}
|
||||
|
||||
C6 -->|"不一致"| D5["隐式类型转换导致索引失效 → 统一类型"]
|
||||
C6 -->|"一致"| DONE["✅ 执行计划合理,可能是业务逻辑本身复杂"]
|
||||
|
||||
style STOP fill:#EE5A24,color:#fff
|
||||
style D1 fill:#FF9F43,color:#000
|
||||
style D2 fill:#FF9F43,color:#000
|
||||
style D3 fill:#FF9F43,color:#000
|
||||
style D4 fill:#EE5A24,color:#fff
|
||||
style D5 fill:#FF9F43,color:#000
|
||||
style DONE fill:#00D866,color:#fff
|
||||
```
|
||||
|
||||
> 核心原则:**先看 EXPLAIN,再动手改**。很多情况下不是子查询的问题,而是缺了索引或类型不匹配。
|
||||
|
||||
## ANY / ALL / SOME
|
||||
|
||||
```sql
|
||||
@@ -245,15 +333,134 @@ graph TB
|
||||
style R2 fill:#C44569,color:#fff
|
||||
```
|
||||
|
||||
## UPDATE / DELETE 中的子查询
|
||||
|
||||
子查询不仅限于 SELECT,UPDATE 和 DELETE 同样可以使用。
|
||||
|
||||
```sql
|
||||
-- UPDATE:用子查询结果更新列
|
||||
UPDATE employees e
|
||||
SET salary = (
|
||||
SELECT avg_salary FROM (
|
||||
SELECT AVG(salary) AS avg_salary
|
||||
FROM employees WHERE department = e.department
|
||||
) tmp
|
||||
)
|
||||
WHERE dept_level = 'manager';
|
||||
-- ⚠️ MySQL 要求嵌套一层:不能直接在 UPDATE 中引用被更新的表
|
||||
-- 所以要用派生表包裹一下(tmp)来绕过这个限制
|
||||
|
||||
-- DELETE:条件过滤删除记录
|
||||
DELETE FROM sessions
|
||||
WHERE user_id IN (
|
||||
SELECT id FROM users WHERE status = 'banned'
|
||||
);
|
||||
|
||||
-- 关联删除:保留每个分组中最新的一条
|
||||
DELETE t1 FROM logs t1
|
||||
INNER JOIN logs t2
|
||||
ON t1.category = t2.category
|
||||
AND t1.created_at < t2.created_at;
|
||||
-- 虽然这不是子查询写法,但效果等价于上面的子查询逻辑
|
||||
-- 实际场景中更推荐 JOIN 写法,性能更好
|
||||
```
|
||||
|
||||
## CTE:子查询的现代写法(MySQL 8.0+)
|
||||
|
||||
`WITH` 子句(Common Table Expression)是 MySQL 8.0 引入的新语法,本质上是给派生表起了一个「有名字的、可复用的」外壳。
|
||||
|
||||
```sql
|
||||
-- 传统派生表写法
|
||||
SELECT dept, avg_salary
|
||||
FROM (
|
||||
SELECT department AS dept, AVG(salary) AS avg_salary
|
||||
FROM employees WHERE status = 'active'
|
||||
GROUP BY department
|
||||
) AS dt
|
||||
WHERE avg_salary > 15000;
|
||||
|
||||
-- ✅ 等价 CTE 写法
|
||||
WITH dept_stats AS (
|
||||
SELECT department AS dept, AVG(salary) AS avg_salary
|
||||
FROM employees WHERE status = 'active'
|
||||
GROUP BY department
|
||||
)
|
||||
SELECT dept, avg_salary
|
||||
FROM dept_stats
|
||||
WHERE avg_salary > 15000;
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph "传统派生表"
|
||||
D1["FROM (...)"] -->|可读性差| D2["重复写的逻辑难以复用"]
|
||||
D3["嵌套深时层级混乱 😵"]
|
||||
end
|
||||
|
||||
subgraph "CTE 写法"
|
||||
C1["WITH name AS (...)"] -->|语义清晰| C2["可在主查询中多次引用"]
|
||||
C3["链式 CTE 层层递进 🧩"]
|
||||
end
|
||||
|
||||
D1 -.->|"功能等价"| C1
|
||||
D2 -.->|"可读性提升"| C2
|
||||
D3 -.-> "|简化复杂查询|" C3
|
||||
|
||||
style C1 fill:#00D866,color:#fff
|
||||
style C2 fill:#00B6BC,color:#fff
|
||||
style C3 fill:#00B6BC,color:#fff
|
||||
```
|
||||
|
||||
### 递归 CTE
|
||||
|
||||
这是子查询体系无法做到的能力——自我引用的 CTE 可以遍历树形结构。
|
||||
|
||||
```sql
|
||||
-- 递归 CTE:生成从 1 到 10 的序列
|
||||
WITH RECURSIVE nums AS (
|
||||
SELECT 1 AS n -- 锚点成员(递归起点)
|
||||
UNION ALL
|
||||
SELECT n + 1 FROM nums WHERE n < 10 -- 递归成员
|
||||
)
|
||||
SELECT * FROM nums;
|
||||
|
||||
-- 实战:查询组织的完整汇报线
|
||||
WITH RECURSIVE org_chain AS (
|
||||
SELECT id, name, manager_id, 1 AS level
|
||||
FROM employees WHERE manager_id IS NULL -- 根节点:CEO
|
||||
UNION ALL
|
||||
SELECT e.id, e.name, e.manager_id, oc.level + 1
|
||||
FROM employees e
|
||||
INNER JOIN org_chain oc ON e.manager_id = oc.id -- 递归:逐级下钻
|
||||
)
|
||||
SELECT * FROM org_chain ORDER BY level, name;
|
||||
```
|
||||
|
||||
> [!WARNING] 递归 CTE 的注意事项
|
||||
> - MySQL 默认递归深度上限为 **1000**(`cte_max_recursion_depth`),超出会报错
|
||||
> - 务必设置合理的终止条件,否则会无限递归直到达到上限
|
||||
> - 递归部分不能直接用 `LIMIT` 来截断结果
|
||||
|
||||
> [!TIP] 何时用 CTE vs 派生表?
|
||||
> | 场景 | 推荐 |
|
||||
> |------|------|
|
||||
> | 只需引用一次,逻辑简单 | 派生表(更轻量) |
|
||||
> | 需要多次引用同一段逻辑 | CTE(DRY 原则) |
|
||||
> | 需要嵌套多层 | CTE(可读性碾压) |
|
||||
> | 需要递归查询(树形结构) | 只能 CTE |
|
||||
> | 兼容 MySQL 5.7 | 只能派生表 |
|
||||
|
||||
## 总结:核心要点回顾
|
||||
|
||||
| 主题 | 一句话 |
|
||||
|------|--------|
|
||||
| 标量子查询 | MySQL 8.0.21 之前不可物化,百万行数据必踩 N+1 陷阱,优先改写为 JOIN |
|
||||
| 相关 vs 无关 | 相关子查询引用了外层列,每行都要重新执行——性能杀手 |
|
||||
| IN vs EXISTS | 语义上 IN 看值、EXISTS 看存在性;不确定时选 EXISTS,更安全的默认选项 |
|
||||
| NOT IN 陷阱 | 子查询出现 NULL 即返回空集,生产中几乎永远该用 `NOT EXISTS` 替代 |
|
||||
| 派生表物化 | 有 GROUP BY / LIMIT / UNION 等操作的子查询无法被展开,必然产生临时表开销 |
|
||||
| ANY / ALL | 对应 SQL 的 OR / AND 累加,ALL 在空子查询结果时恒返回 TRUE(反直觉,需注意) |
|
||||
| CTE | MySQL 8.0+ 现代写法,可读性和可维护性优于匿名派生表,唯一支持递归的子查询形式 |
|
||||
|
||||
> [!TIP] 核心心法
|
||||
> **子查询不是万能的——它首先是为了表达清晰,其次才是性能。**
|
||||
@@ -261,5 +468,5 @@ graph TB
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/12-DQL SELECT 全解析]] — SELECT 执行顺序与子查询的关系
|
||||
- [[hhs/MySQL/13-JOIN 原理与优化]] — EXISTS 子查询常可重写为 JOIN
|
||||
- [[hhs/MySQL/02-SQL核心/08-DQL SELECT 全解析]] — SELECT 执行顺序与子查询的关系
|
||||
- [[hhs/MySQL/02-SQL核心/09-JOIN 原理与优化]] — EXISTS 子查询常可重写为 JOIN
|
||||
@@ -0,0 +1,364 @@
|
||||
---
|
||||
tags: [MySQL, UNION, UNION ALL, 集合运算]
|
||||
create time: 2026-05-16 00:00
|
||||
---
|
||||
|
||||
# UNION / UNION ALL
|
||||
|
||||
## 概述
|
||||
|
||||
UNION 是 SQL 标准中的集合运算,用于将多个 SELECT 的结果纵向合并为一个结果集。核心应用场景包括:**分表数据汇总**、**多源 Feed 流合并**、以及**需要排序去重的跨查询聚合**。
|
||||
|
||||
## 基本语法
|
||||
|
||||
```sql
|
||||
-- 两种形式
|
||||
SELECT col1, col2 FROM table_a
|
||||
UNION -- 去重(内部排序 + 去重)
|
||||
SELECT col1, col2 FROM table_b;
|
||||
|
||||
SELECT col1, col2 FROM table_a
|
||||
UNION ALL -- 不去重(直接拼接,性能更高)
|
||||
SELECT col1, col2 FROM table_b;
|
||||
```
|
||||
|
||||
## UNION vs UNION ALL
|
||||
|
||||
| 特性 | UNION | UNION ALL |
|
||||
|------|-------|-----------|
|
||||
| **去重** | ✅ 内部去重 | ❌ 保留所有行 |
|
||||
| **性能** | 低(需排序去重) | 高(直接追加) |
|
||||
| **ORDER BY 位置** | 只能放在最后一个 SELECT | 同上 |
|
||||
| **适用场景** | 需要唯一结果的合并 | 已知不重复的合并 |
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A1["数据源 A"] --> U
|
||||
B1["数据源 B"] --> U
|
||||
|
||||
U{"UNION"} --> R1["全部数据去重后输出"]
|
||||
|
||||
U2{"UNION ALL"} --> R2["全部数据直接拼接"]
|
||||
|
||||
style R1 fill:#FF9F43,color:#000
|
||||
style R2 fill:#00D866,color:#fff
|
||||
```
|
||||
|
||||
> [!TIP] 首选 UNION ALL
|
||||
> 如果你能通过业务逻辑保证各部分结果不重复(比如按日期区间划分),**一律用 UNION ALL**。UNION 的去重操作需要 Sort/Dedup 阶段,在大结果集上是昂贵操作。
|
||||
|
||||
## 实战场景
|
||||
|
||||
### 场景一:多表同构合并
|
||||
|
||||
```sql
|
||||
-- ❌ 错误写法:WHERE / ORDER BY / LIMIT 只作用于最后一个 SELECT
|
||||
SELECT * FROM logs_202601
|
||||
UNION ALL
|
||||
SELECT * FROM logs_202602
|
||||
UNION ALL
|
||||
SELECT * FROM logs_202603
|
||||
UNION ALL
|
||||
SELECT * FROM logs_202604
|
||||
UNION ALL
|
||||
SELECT * FROM logs_202605
|
||||
WHERE status = 'error' -- ⚠️ 只过滤 logs_202605!
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 50;
|
||||
|
||||
-- ✅ 正确写法:如果需要对每个分表单独过滤,用子查询包裹
|
||||
SELECT * FROM
|
||||
(SELECT * FROM logs_202601 WHERE status = 'error') AS t1
|
||||
UNION ALL
|
||||
SELECT * FROM
|
||||
(SELECT * FROM logs_202602 WHERE status = 'error') AS t2
|
||||
UNION ALL
|
||||
SELECT * FROM
|
||||
(SELECT * FROM logs_202603 WHERE status = 'error') AS t3
|
||||
UNION ALL
|
||||
SELECT * FROM
|
||||
(SELECT * FROM logs_202604 WHERE status = 'error') AS t4
|
||||
UNION ALL
|
||||
SELECT * FROM
|
||||
(SELECT * FROM logs_202605 WHERE status = 'error') AS t5
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 50;
|
||||
```
|
||||
|
||||
> [!NOTE] 分表合并的注意事项
|
||||
> - `WHERE / ORDER BY / LIMIT` **仅作用于 UNION 中最后一个 SELECT**。前面的分表查询不做过滤,全部返回后再合并排序。
|
||||
> - 如果每个分表数据量很大(百万级),建议**用子查询保护每个表的局部 ORDER BY + LIMIT**,先各取 Top-N 再全局排。这大幅减少中间结果集大小。
|
||||
> - UNION ALL 不保证顺序,最终 ORDER BY 必不可少。
|
||||
|
||||
### 场景二:多表分页合并(Feed 流)
|
||||
|
||||
```sql
|
||||
-- 需求: 用户的动态 Feed 由关注的人和发布的文章混合组成, 按时间排序分页
|
||||
-- ❌ 应用层先查再排: 至少两次 DB 往返 + 内存归并排序
|
||||
-- ✅ UNION ALL 一次搞定
|
||||
|
||||
SELECT user_id AS source_id, content, 'follow' AS source_type, created_at
|
||||
FROM follow_feed WHERE user_id = 42
|
||||
UNION ALL
|
||||
SELECT article_id AS source_id, summary AS content, 'article' AS source_type, published_at
|
||||
FROM articles WHERE author_id = 42
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 20 OFFSET 0;
|
||||
```
|
||||
|
||||
> [!TIP] 何时用 UNION vs CASE WHEN?
|
||||
> - **数据来源是不同表或不同结构**: UNION ALL 是不二之选
|
||||
> - **同表不同条件的聚合计数**: `SUM(CASE WHEN ...)` 单次扫描更高效
|
||||
> - **经验法则**: UNION 的 SQL 可读性显著优于多个 OR 条件叠加时, 优先选 UNION
|
||||
|
||||
### 场景三:搜索多字段权重排序
|
||||
|
||||
```sql
|
||||
-- 搜索商品:标题匹配权重 > 描述匹配 > 两者都匹配
|
||||
SELECT id, title, description,
|
||||
3 AS rank_score -- 标题命中,权重最高
|
||||
FROM products
|
||||
WHERE title LIKE '%runners%'
|
||||
UNION ALL
|
||||
SELECT id, title, description,
|
||||
2 AS rank_score -- 描述命中,权重次之
|
||||
FROM products
|
||||
WHERE description LIKE '%runners%'
|
||||
AND title NOT LIKE '%runners%'; -- 排除已在上面出现的
|
||||
ORDER BY rank_score DESC, id;
|
||||
```
|
||||
|
||||
> [!NOTE] 多条件搜索的 UNION ALL 策略
|
||||
> - 通过不同 SELECT 赋予不同权重(`rank_score`),合并后统一 `ORDER BY` 即可实现"加权排序"
|
||||
> - **注意去重边界**:第二个 SELECT 加 `NOT LIKE` 过滤可避免重复输出同一商品。如果无法在前端精确排重,可以考虑外层套一层 `DISTINCT`(代价是会退化为 UNION)或改用 `GROUP BY id`
|
||||
|
||||
```sql
|
||||
-- ✅ 正确
|
||||
SELECT id, name FROM products WHERE price > 100
|
||||
UNION ALL
|
||||
SELECT id, name FROM products WHERE category = 'sale';
|
||||
|
||||
-- ❌ 错误:列数不一致
|
||||
SELECT id, name FROM products
|
||||
UNION ALL
|
||||
SELECT id FROM discounts;
|
||||
|
||||
-- ❌ 错误:ORDER BY 放在中间(MySQL 可能忽略或报错)
|
||||
SELECT id FROM table_a
|
||||
ORDER BY id
|
||||
UNION ALL
|
||||
SELECT id FROM table_b;
|
||||
```
|
||||
|
||||
### UNION 进阶:ORDER BY 与 LIMIT 的行为
|
||||
|
||||
```sql
|
||||
-- ⚠️ 问题:每个 SELECT 的 ORDER BY 在合并后无效
|
||||
SELECT id FROM orders WHERE status = 'pending' ORDER BY created_at DESC
|
||||
UNION ALL
|
||||
SELECT id FROM orders WHERE status = 'shipped' ORDER BY created_at DESC;
|
||||
-- 上面的 ORDER BY 基本被 MySQL 忽略
|
||||
|
||||
-- ✅ 正确解法:用子包装保护每个查询的排序
|
||||
SELECT * FROM
|
||||
(
|
||||
SELECT id, created_at FROM orders WHERE status = 'pending'
|
||||
ORDER BY created_at DESC LIMIT 50
|
||||
) AS a
|
||||
UNION ALL
|
||||
SELECT * FROM
|
||||
(
|
||||
SELECT id, created_at FROM orders WHERE status = 'shipped'
|
||||
ORDER BY created_at DESC LIMIT 50
|
||||
) AS b
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
> [!QUESTION] 为什么子查询能保护 ORDER BY?
|
||||
> MySQL 优化器发现 UNION 外还有 ORDER BY + LIMIT 时, 会认为内部排序有用,从而保留它。
|
||||
> 但官方文档并不保证这种行为——这是基于执行计划的经验结论, 生产环境务必确认。
|
||||
|
||||
---
|
||||
|
||||
## UNION 的执行流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["查询 1"] --> R1["结果集 1"]
|
||||
B["查询 2"] --> R2["结果集 2"]
|
||||
C["查询 N"] --> RN["结果集 N"]
|
||||
|
||||
R1 --> Temp["临时表 + 唯一索引<br/>UNION 有, UNION ALL 无"]
|
||||
R2 --> Temp
|
||||
RN --> Temp
|
||||
|
||||
Temp --> Dedup{"需要去重?"}
|
||||
Dedup -->|是| Sort["排序 + 去重"]
|
||||
Dedup -->|否| Direct["直接输出"]
|
||||
|
||||
Sort --> Output["最终结果集"]
|
||||
Direct --> Output
|
||||
|
||||
style Dedup fill:#FF9F43,color:#000
|
||||
style Sort fill:#EE5A24,color:#fff
|
||||
```
|
||||
|
||||
## 执行计划特征(EXPLAIN)
|
||||
|
||||
用 `EXPLAIN` 观察 UNION 的执行特征,能直观看到 Optimizer 的处理策略:
|
||||
|
||||
```sql
|
||||
CREATE TABLE users_a (id INT PRIMARY KEY, name VARCHAR(64), city_id INT);
|
||||
CREATE TABLE users_b (id INT PRIMARY KEY, name VARCHAR(64), city_id INT);
|
||||
|
||||
EXPLAIN SELECT * FROM users_a WHERE city_id = 10
|
||||
UNION ALL
|
||||
SELECT * FROM users_b WHERE city_id = 10;
|
||||
```
|
||||
|
||||
期望的 EXPLAIN 输出:
|
||||
|
||||
```
|
||||
+----+--------------+------------+------+---------------+------+---------+-------+-------+------+
|
||||
| id | select_type | table | type | key | extra| rows | ... | ref | |
|
||||
+----+--------------+------------+------+---------------+------+---------+-------+-------+------+
|
||||
| 1 | PRIMARY | users_a | ref | idx_city_id | NULL | 100 | ... | const | |
|
||||
| 2 | UNION | users_b | ref | idx_city_id | NULL | 100 | ... | const | |
|
||||
+----+--------------+------------+------+---------------+------+---------+-------+-------+------+
|
||||
```
|
||||
|
||||
关键解读:
|
||||
|
||||
| 字段 | 含义 | UNION 中的表现 |
|
||||
|------|------|--------------|
|
||||
| **select_type** | 查询类型 | `PRIMARY`(第一个 SELECT) + 每个后续 SELECT 标记为 `UNION` |
|
||||
| **table** | 涉及的表 | 每个 UNION 分支独立显示一行 |
|
||||
| **Extra** | 附加信息 | UNION ALL 无额外信息;使用去重版 UNION 时 Extra 会出现 `Using temporary; Using filesort` |
|
||||
|
||||
> [!WARNING] UNION 去重的隐藏代价
|
||||
>
|
||||
> ```sql
|
||||
> -- 对比两个版本的 Extra
|
||||
> EXPLAIN SELECT * FROM users_a WHERE city_id = 10
|
||||
> UNION ALL -- Extra: (空)
|
||||
> SELECT * FROM users_b WHERE city_id = 10;
|
||||
>
|
||||
> EXPLAIN SELECT * FROM users_a WHERE city_id = 10
|
||||
> UNION -- Extra: Using temporary; Using filesort
|
||||
> SELECT * FROM users_b WHERE city_id = 10;
|
||||
> ```
|
||||
>
|
||||
> MySQL 内部会将所有 UNION 分支的结果收集到一个**临时表**中,然后对这个临时表做**全表扫描 + 文件排序**来完成去重。当结果集很大时,这就是性能瓶颈所在。
|
||||
>
|
||||
> 生产排查建议:如果 UNION 查询慢,先用 `EXPLAIN` 确认 Extra 是否包含 `Using temporary`;再用 `EXPLAIN ANALYZE`(MySQL 8.0.16+)查看各分支的实际行数和耗时。
|
||||
|
||||
---
|
||||
|
||||
## 与 JOIN 的选择
|
||||
|
||||
```sql
|
||||
-- 场景:获取每个部门的员工总数 + 总监姓名
|
||||
-- JOIN 方案(交叉维度,适合取不同列)
|
||||
SELECT d.name, COUNT(e.id) AS emp_count, mgr.name AS manager
|
||||
FROM departments d
|
||||
LEFT JOIN employees e ON d.id = e.dept_id
|
||||
LEFT JOIN employees mgr ON d.manager_id = mgr.id
|
||||
GROUP BY d.id;
|
||||
|
||||
-- UNION 方案(平行维度,适合合并同类数据)
|
||||
SELECT dept_id, 'total' AS metric, COUNT(*) AS value FROM employees GROUP BY dept_id
|
||||
UNION ALL
|
||||
SELECT dept_id, 'managers' AS metric, COUNT(*) AS value
|
||||
FROM employees WHERE is_manager = 1 GROUP BY dept_id;
|
||||
```
|
||||
|
||||
> [!QUESTION] UNION 还是多个查询?
|
||||
> 很多场景中 UNION 看起来方便,但背后可能有更好的解法:
|
||||
> - **应用层合并**:在 Go/Java 中发两次查询然后合并数组(零 DB 压力)
|
||||
> - **STORED PROCEDURE**:存储过程中多次查询 + 临时表
|
||||
> - **视图**:封装 UNION 逻辑供多次复用
|
||||
>
|
||||
> 核心原则:**能不在数据库做的就不做**。UNION 的代价是排序、去重、临时表。
|
||||
|
||||
## 性能对比:UNION vs 替代方案
|
||||
|
||||
| 方案 | DB 往返次数 | 临时表开销 | 排序开销 | 适用规模 |
|
||||
|------|------------|-----------|---------|---------|
|
||||
| **UNION ALL** | 1 次 | 有(结果集缓冲) | 仅外层的 ORDER BY | 百万行级 |
|
||||
| **UNION(去重)** | 1 次 | 有(唯一索引) | Sort + Dedup | 十万行以内 |
|
||||
| **应用层 N 次查询** | N 次 | 无 | 内存归并 | 任意,受网络影响 |
|
||||
| **SUM(CASE WHEN)** | 1 次 | 无 | 无 | 单表聚合计数 |
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["数据源数量 > 1"] --> B{"能否用单次扫描解决?"}
|
||||
B -->|是: 单表多条件| C["SUM CASE WHEN<br/>最优"]
|
||||
B -->|否| D{"结果集是否已知不重复?"}
|
||||
D -->|是| E["UNION ALL<br/>推荐"]
|
||||
D -->|否| F["UNION<br/>去重"]
|
||||
style C fill:#00D866,color:#fff
|
||||
style E fill:#74C0FC,color:#000
|
||||
style F fill:#FF9F43,color:#000
|
||||
```
|
||||
|
||||
## 常见陷阱与排查
|
||||
|
||||
| 陷阱 | 症状 | 解法 |
|
||||
|------|------|------|
|
||||
| **列数不匹配** | `The used SELECT statements have a different number of columns` | 确保每个 SELECT 的列数完全相同;用 `NULL` 补齐 |
|
||||
| **类型隐式转换** | UNION 中对应列类型不一致导致全表扫描或结果错误 | 手动 CAST 到一致类型(如 `CAST(0 AS CHAR)`) |
|
||||
| **ORDER BY 位置错误** | MySQL 忽略分支内的 ORDER BY 或报错 1221 | ORDER BY / LIMIT 放整个 UNION 的最后;需要保护内部排序请用派生表包装 |
|
||||
| **UNION ALL 产生重复行** | 业务期望唯一结果但实际有重复 | 检查是否真的有去重需求 — 有的话用 UNION;无法避免时用 DISTINCT/GROUP BY |
|
||||
| **分表 WHERE 漏写** | WHERE 只过滤最后一个分表,前面全部数据入库 | 每个分表子查询独立加 WHERE,或用视图封装 |
|
||||
| **分页错位** | OFFSET 导致合并后第一页显示第二页数据 | 先合并再整体 OFFSET,不能用局部 LIMIT + OFFSET 代替全局分页 |
|
||||
|
||||
### 实战排查 Checklist
|
||||
|
||||
遇到 UNION 相关慢查询或异常时,按以下顺序逐项检查:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S["UNION 查询异常/慢"] --> C1{"EXPLAIN 看了吗?"}
|
||||
C1 -->|没看| STOP["🛑 先跑 EXPLAIN<br/>别盲猜,看 select_type 确认分支数"]
|
||||
C1 -->|看了| C2{"Extra 有 Using temporary 吗?"}
|
||||
|
||||
C2 -->|"是:UNION 去重"| D1["换 UNION ALL + 业务侧保证不重复"]
|
||||
C2 -->|"否"| C3{"列数/列类型一致吗?"}
|
||||
|
||||
C3 -->|"不一致"| D2["CAST 统一类型<br/>否则可能隐式转换导致索引失效"]
|
||||
C3 -->|"一致"| C4{"ORDER BY 在最终位置吗?"}
|
||||
|
||||
C4 -->|"否,放在中间"| D3["移动到最后<br/>或在每个分支外加子查询包裹"]
|
||||
C4 -->|"是"| C5{"WHERE 对所有分支都生效吗?"}
|
||||
|
||||
C5 -->|"否"| D4["每个分子查询独立加条件"]
|
||||
C5 -->|"是"| DONE["✅ SQL 结构正确,<br/>继续排查索引和数据分布"]
|
||||
|
||||
style STOP fill:#EE5A24,color:#fff
|
||||
style D1 fill:#FF9F43,color:#000
|
||||
style D2 fill:#FF9F43,color:#000
|
||||
style D3 fill:#FF9F43,color:#000
|
||||
style D4 fill:#EE5A24,color:#fff
|
||||
style DONE fill:#00D866,color:#fff
|
||||
```
|
||||
|
||||
## 核心要点回顾
|
||||
|
||||
| 主题 | 一句话 |
|
||||
|------|--------|
|
||||
| UNION ALL vs UNION | 能用 ALL 就绝不用 UNION — 去重的临时表+文件排序代价远超想象 |
|
||||
| ORDER BY/LIMIT 作用域 | 只在 UNION 最后一个 SELECT 生效;跨表排序必须外层包装子查询 |
|
||||
| 字段匹配规则 | 列数必须相同,类型应尽量兼容 — 列名取自第一个 SELECT |
|
||||
| Feed 流/多源合并 | UNION ALL 的经典场景,一次 DB 往返搞定多数据源混合 |
|
||||
| 分表汇总 | 同构分表最理想的聚合方式,但注意 WHERE 只对最后一个分支生效 |
|
||||
| 执行计划标识 | PRIMARY + UNION 双行 — Extra 中出现 `Using temporary` 就是去重开销的信号 |
|
||||
|
||||
> [!TIP] 核心心法
|
||||
> **UNION ALL 是你最好的朋友,UNION 是你的最后手段。**
|
||||
> 写 UNION 查询前问自己三个问题:① 结果集真的需要去重吗?② 能不能用 CASE WHEN 单表扫描代替?③ 如果拆成 N 次简单查询,应用层合并是不是更清晰?大部分时候答案会让你回到 UNION ALL。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/02-SQL核心/08-DQL SELECT 全解析]] — SELECT 基础语法与 UNION 的结合使用
|
||||
- [[hhs/MySQL/02-SQL核心/07-DML 增删改]] — DML 中的批量操作与 UNION 的互补关系
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
tags: [MySQL, SQL, DDL, DML, DQL]
|
||||
create time: 2026-05-20 23:55
|
||||
---
|
||||
|
||||
# 二、SQL 核心
|
||||
|
||||
## 概述
|
||||
|
||||
本章覆盖日常开发中最常用的 SQL 操作:建表改表(DDL)、增删改(DML)、查询(DQL),以及多表关联(JOIN)、子查询、集合运算等进阶写法。掌握这些就能完成 90% 的日常数据库操作。
|
||||
|
||||
## 本章文档
|
||||
|
||||
| # | 主题 | 说明 |
|
||||
|---|------|------|
|
||||
| 6 | [[hhs/MySQL/02-SQL核心/06-DDL 建表与结构变更]] | 怎么创建和修改表结构、大表加字段会不会锁表 |
|
||||
| 7 | [[hhs/MySQL/02-SQL核心/07-DML 增删改]] | INSERT / UPDATE / DELETE 的常用技巧和坑 |
|
||||
| 8 | [[hhs/MySQL/02-SQL核心/08-DQL SELECT 全解析]] | SELECT 的执行顺序、去重、分组、分页 |
|
||||
| 9 | [[hhs/MySQL/02-SQL核心/09-JOIN 原理与优化]] | 多张表怎么连起来查、Nested Loop 是怎么工作的 |
|
||||
| 10 | [[hhs/MySQL/02-SQL核心/10-子查询与派生表]] | WHERE / FROM 里的子查询什么时候用、EXISTS 和 IN 有什么区别 |
|
||||
| 11 | [[hhs/MySQL/02-SQL核心/11-UNION 与集合运算]] | 把多个查询结果合并在一起,去重 vs 保留重复怎么处理 |
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/README]] — MySQL 知识库总目录
|
||||
@@ -73,11 +73,11 @@ flowchart TD
|
||||
|
||||
## 为什么树这么矮?
|
||||
|
||||
InnoDB 一页 16KB,假设:
|
||||
InnoDB 一页 16KB(可配置),扣除页头 / 页尾等元数据开销后,**实际可用空间约 14~15 KB**。假设:
|
||||
- 主键 BIGINT = 8 bytes
|
||||
- 指针 = 6 bytes
|
||||
- 每个内部节点 Entry ≈ 14 bytes
|
||||
- 一页可存 ≈ 16KB / 14 bytes ≈ **1170 个子节点**
|
||||
- 指针(Page Pointer)= 6 bytes
|
||||
- 每个内部节点 Entry(Key + Pointer + 冗余信息)≈ **16 bytes**
|
||||
- 一页可存 ≈ 14.5 KB / 16 bytes ≈ **900 ~ 1170 个子节点**(取保守值 1170)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
@@ -99,25 +99,27 @@ flowchart LR
|
||||
> ——退化成一棵二叉树(红黑树的高度)。所以 B+ Tree 的**阶数越高,树越矮,IO 越少**。
|
||||
>
|
||||
> [!TIP] 直观感受
|
||||
> 用 SHOW INDEXES 查看某个表的索引信息,关注 Cardinality(基数)——
|
||||
> 通过 `information_schema.statistics` 查看索引基数,关注 CARDINALITY(基数)——
|
||||
> 基数接近总行数说明区分度高,索引效果好:
|
||||
> ```sql
|
||||
> -- 查看表索引及基数估计值
|
||||
> SHOW INDEXES FROM users;
|
||||
> -- 查看表的索引基数统计
|
||||
> SELECT INDEX_NAME, NON_UNIQUE, CARDINALITY
|
||||
> FROM information_schema.statistics
|
||||
> WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users';
|
||||
> ```
|
||||
|
||||
## 索引查找过程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Q as "查询id等于42"
|
||||
participant R as "根节点IO1"
|
||||
participant I as "内部节点IO2"
|
||||
participant L as "叶子节点IO3"
|
||||
participant Q as "查询"
|
||||
participant R as "根节点"
|
||||
participant I as "内部节点"
|
||||
participant L as "叶子节点"
|
||||
participant D as "数据行"
|
||||
|
||||
Q->>R: "id大于30走向右分支"
|
||||
R->>I: "指向第二层右子节点"
|
||||
Q->>R: "id > 30 → 右分支"
|
||||
R->>I: "定位到第二层右子节点"
|
||||
I->>L: "命中对应叶子节点"
|
||||
L->>D: "读取完整行数据"
|
||||
|
||||
@@ -155,7 +157,7 @@ SELECT email FROM users WHERE email = 'test@example.com';
|
||||
> - `Extra = Using index condition` → 索引下推(ICP),部分过滤在索引层面完成
|
||||
> - Extra 中**没有** `Using index` → 触发了回表
|
||||
>
|
||||
> 💡 更多细节(回表代价、ICP 原理、空间对比)详见 [[hhs/MySQL/17-聚簇索引与二级索引]]
|
||||
> 💡 更多细节(回表代价、ICP 原理、空间对比)详见 [[hhs/MySQL/03-索引与查询优化/13-聚簇索引与二级索引]]
|
||||
|
||||
## 聚簇索引 vs 二级索引
|
||||
|
||||
@@ -216,6 +218,32 @@ flowchart TB
|
||||
|
||||
联合索引 `(a, b, c)` 的本质是:**先按 a 排序,a 相同时按 b 排序,a、b 都相同时按 c 排序**。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "B+ Tree 叶子节点中数据的实际存储顺序"
|
||||
R1["(10, 'pending')"]
|
||||
R2["(10, 'paid')"]
|
||||
R3["(10, 'shipped')"]
|
||||
R4["(20, 'pending')"]
|
||||
R5["(20, 'paid')"]
|
||||
R6["(30, 'pending')"]
|
||||
end
|
||||
|
||||
Q1["WHERE user_id = 10<br/>✅ 精确匹配一层"] --> S1["命中: R1, R2, R3"]
|
||||
Q2["WHERE user_id = 10 AND status = 'paid'<br/>✅ 精确匹配两层"] --> S2["命中: R2"]
|
||||
Q3["WHERE status = 'paid'<br/>❌ 缺少最左列 user_id"] --> S3["全表扫描"]
|
||||
Q4["WHERE user_id = 10 AND created_at > ...<br/>⚠️ 只能用到 user_id"] --> S4["R1,R2,R3 + 逐行过滤"]
|
||||
|
||||
style S1 fill:#00D866,color:#fff
|
||||
style S2 fill:#00D866,color:#fff
|
||||
style S3 fill:#EE5A24,color:#fff
|
||||
style S4 fill:#FF9F43,color:#000
|
||||
```
|
||||
|
||||
> [!NOTE] 核心直觉
|
||||
> - 联合索引的 B+ Tree **不是按列独立排序**的,而是把所有列拼成一条记录整体排序。
|
||||
> - 跳过最左列 → 相当于跳过了目录的第一层,直接翻到第二层找,找不到就退化全表扫描。
|
||||
|
||||
```sql
|
||||
CREATE TABLE orders (
|
||||
id BIGINT PRIMARY KEY,
|
||||
@@ -310,10 +338,10 @@ SELECT * FROM users WHERE phone = '13800138000';
|
||||
```sql
|
||||
INSERT INTO users (id, email, name, age) VALUES (1, 'a@x.com', 'Tom', 25);
|
||||
-- InnoDB 需要同时更新:
|
||||
-- ① 聚簇索引 idx__PRIMARY → 1 次 IO
|
||||
-- ② 二级索引 idx_email → 1 次 IO
|
||||
-- ③ 如果有更多二级索引... → N 次 IO
|
||||
-- 总写入成本 = 1 + 二级索引数量
|
||||
-- ① 聚簇索引 idx__PRIMARY → 1 次 IO(顺序插入在末尾)
|
||||
-- ② 二级索引 idx_email → 1 次 IO(定位 + 可能的页分裂)
|
||||
-- ③ 每多一个二级索引 → 各加 1 次 IO
|
||||
-- 总写入成本 = 1(聚簇)+ N(N 个二级索引)
|
||||
```
|
||||
|
||||
> [!NOTE] 直观理解
|
||||
@@ -340,7 +368,7 @@ INSERT INTO users (id, email, name, age) VALUES (1, 'a@x.com', 'Tom', 25);
|
||||
| **INSERT** | 叶子节点末尾追加(顺序 IO) | 需定位正确位置 + 可能的页分裂(随机 IO) |
|
||||
| **UPDATE** | 若主键不变则无影响;变化则删除+重建 | 所有涉及列变化的索引都需要更新 |
|
||||
| **DELETE** | 标记删除或合并页 | 同上,且可能有页合并开销 |
|
||||
| **页分裂** | 高水位上升时发生 | 频率更高(二级索引更密集) |
|
||||
| **页分裂** | 顺序写满一页后可能触发 | 频率更高(写入定位分散 + 热点行密集) |
|
||||
|
||||
> [!TIP] 经验法则
|
||||
> - 写密集型系统(如日志、订单创建),索引数量控制在 **3 个以内**
|
||||
@@ -374,6 +402,6 @@ INSERT INTO users (id, email, name, age) VALUES (1, 'a@x.com', 'Tom', 25);
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/GORM/02-模型定义]] — GORM 创建索引的 struct tag 映射到 B+ Tree
|
||||
- [[hhs/MySQL/17-聚簇索引与二级索引]] — 深入解析聚簇索引的物理存储结构与二级索引的回表优化
|
||||
- [[hhs/MySQL/18-联合索引与最左前缀]] — 联合索引的设计原则与实践
|
||||
- [[hhs/MySQL/19-EXPLAIN 完全指南]] — type=index / range 的物理含义与索引执行计划解读
|
||||
- [[hhs/MySQL/03-索引与查询优化/13-聚簇索引与二级索引]] — 深入解析聚簇索引的物理存储结构与二级索引的回表优化
|
||||
- [[hhs/MySQL/03-索引与查询优化/14-联合索引与最左前缀]] — 联合索引的设计原则与实践
|
||||
- [[hhs/MySQL/03-索引与查询优化/15-EXPLAIN 完全指南]] — type=index / range 的物理含义与索引执行计划解读
|
||||
@@ -1,14 +1,32 @@
|
||||
---
|
||||
tags: [MySQL, 聚簇索引, 二级索引, Covering Index, 回表]
|
||||
create time: 2026-05-16 00:00
|
||||
create time: 2026-05-20 14:00
|
||||
---
|
||||
|
||||
# 聚簇索引 vs 二级索引
|
||||
# 13-聚簇索引与二级索引
|
||||
|
||||
## 概述
|
||||
|
||||
InnoDB 使用 B+ Tree 组织所有索引,但根据「索引叶子节点存什么」的不同,分为两类:**聚簇索引**和**二级索引**。理解这两者的差异,是你设计高效查询和执行计划的起点。
|
||||
|
||||
为了方便说明,本文档将始终使用以下 `users` 表作为示例:
|
||||
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
status TINYINT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_email (email),
|
||||
INDEX idx_status_created (status, created_at)
|
||||
) ENGINE=InnoDB;
|
||||
```
|
||||
|
||||
这张表中:
|
||||
- **聚簇索引**:`id`(PRIMARY KEY)
|
||||
- **二级索引**:`idx_email`(email)、`idx_status_created`(status, created_at)
|
||||
|
||||
> [!QUESTION] 思考
|
||||
> 假设一张用户表按 `id` 排序存储在磁盘上——
|
||||
> 现在要查 `WHERE email = 'alice@test.com'`,数据库需要做什么?
|
||||
@@ -16,6 +34,39 @@ InnoDB 使用 B+ Tree 组织所有索引,但根据「索引叶子节点存什
|
||||
|
||||
带着这个问题往下看。
|
||||
|
||||
## 一张图看懂索引全貌
|
||||
|
||||
在深入细节之前,先看这张示意图——它展示了同一个表上**聚簇索引**和**二级索引**如何共存:
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
columns 2
|
||||
|
||||
block:ClusteredIndex["聚簇索引 — idx_id (PK = id)"]
|
||||
columns 1
|
||||
CI_L1["枝节点: id=5 | id=18"]
|
||||
CI_L2["枝节点: id=12 | id=25"]
|
||||
CI_L3["叶子节点: 整行数据(id=5, name=Bob, email=bob@x.com, …)"]
|
||||
end
|
||||
|
||||
block:SecondaryIndex["二级索引 — idx_email (email)"]
|
||||
columns 1
|
||||
SI_L1["枝节点: email < 'k'"]
|
||||
SI_L2["枝节点: email >= 'k'"]
|
||||
SI_L3["叶子节点: (email='alice@x.com' | PK=12)"]
|
||||
SI_L4["叶子节点: (email='bob@x.com' | PK=5)"]
|
||||
end
|
||||
|
||||
style CI_L3 fill:#00B6BC,color:#fff
|
||||
style SI_L3 fill:#E8DFF5,color:#333
|
||||
style SI_L4 fill:#E8DFF5,color:#333
|
||||
```
|
||||
|
||||
> [!HIGHLIGHT] 核心区别一目了然
|
||||
> - **聚簇索引叶子层**存的是**完整行数据** → 查到就结束
|
||||
> - **二级索引叶子层**存的是**「索引列 + 主键」** → 拿到主键后还要去聚簇索引里再查一次(回表)
|
||||
> - 所有索引共享同一份数据(存储在主键聚簇索引中),二级索引只是"额外的查找路径"
|
||||
|
||||
## 聚簇索引(Clustered Index)
|
||||
|
||||
聚簇索引就是数据本身。InnoDB 表中**只能有一个聚簇索引**。
|
||||
@@ -52,6 +103,8 @@ flowchart TD
|
||||
style G fill:#00B6BC,color:#fff
|
||||
```
|
||||
|
||||
理解了聚簇索引为什么快,我们反过来想——如果不是按主键查,而是通过其他字段(比如 `email`)查询,又会发生什么?这就是二级索引的故事。
|
||||
|
||||
## 二级索引(Secondary Index)
|
||||
|
||||
除了聚簇索引外的所有索引都是二级索引。叶子节点存储的是:**索引列的值 + 主键值**。
|
||||
@@ -97,60 +150,106 @@ sequenceDiagram
|
||||
Note over SI,DB: 至少 2 次 IO:1次二级索引 + 1次回表
|
||||
```
|
||||
|
||||
### 减少回表的策略
|
||||
### 减少回表的策略(Covering Index)
|
||||
|
||||
要减少回表,最直接的方法是让查询**完全在二级索引中完成**——这就是 Covering Index。
|
||||
|
||||
```sql
|
||||
-- ❌ 差:Covering Index 未命中,需要回表拿 name 字段
|
||||
SELECT name, email FROM users WHERE email = 'alice@test.com';
|
||||
-- idx_email 能匹配 WHERE,但 name 不在索引里 → 需要回表
|
||||
EXPLAIN SELECT name, email FROM users WHERE email = 'alice@test.com'\G
|
||||
*************************** 1. row ***************************
|
||||
id: 1
|
||||
select_type: SIMPLE
|
||||
table: users
|
||||
type: ref ← 通过 idx_email 找到匹配行
|
||||
possible_keys: idx_email
|
||||
key: idx_email
|
||||
key_len: 769
|
||||
ref: const
|
||||
rows: 1
|
||||
filtered: 100.00
|
||||
Extra: Using where ← 注意:无 "Using index",说明走了回表
|
||||
|
||||
-- ✅ 好:覆盖索引,无需回表
|
||||
ALTER TABLE users ADD INDEX idx_email_name (email, name);
|
||||
SELECT name, email FROM users WHERE email = 'alice@test.com';
|
||||
-- EXPLAIN Extra: Using index ← 完美!
|
||||
EXPLAIN SELECT name, email FROM users WHERE email = 'alice@test.com'\G
|
||||
*************************** 1. row ***************************
|
||||
id: 1
|
||||
select_type: SIMPLE
|
||||
table: users
|
||||
type: ref
|
||||
possible_keys: idx_email,idx_email_name
|
||||
key: idx_email_name ← 优化器选择了更优的联合索引
|
||||
key_len: 769
|
||||
ref: const
|
||||
rows: 1
|
||||
filtered: 100.00
|
||||
Extra: Using index ← 完美!Index Only Scan,无需回表
|
||||
```
|
||||
|
||||
> [!SUCCESS] Covering Index 的黄金法则
|
||||
> **把 SELECT 中的列 + WHERE 中的列放到同一个联合索引中**,就能实现 Index Only Scan。
|
||||
> - 适合:高频查询、固定列选择
|
||||
> - 不适合:SELECT *(永远无法覆盖)、列变化频繁的查询
|
||||
> - 不适合:`SELECT *`(永远无法覆盖)、列变化频繁的查询
|
||||
|
||||
## 回表 vs 索引下推(ICP)
|
||||
### 当 Covering Index 不够用时:索引下推(ICP)
|
||||
|
||||
MySQL 5.6 引入的 Index Condition Pushdown 优化了部分回表场景。
|
||||
并非所有查询都能被覆盖索引解决。比如 `SELECT *` 必须回表,或者部分过滤条件无法放入索引中。这时 MySQL 5.6 引入的 **索引下推**(Index Condition Pushdown)能进一步减少无效回表。
|
||||
|
||||
假设 `users` 表有联合索引 `idx_status_created (status, created_at)`:
|
||||
|
||||
```sql
|
||||
-- 假设联合索引 idx_name_status = (name, status)
|
||||
-- 查询:WHERE name LIKE '张%' AND status = 1
|
||||
|
||||
-- ❌ 无 ICP:所有匹配的 name 都要回表查 status
|
||||
SELECT * FROM users WHERE name LIKE '张%' AND status = 1;
|
||||
-- 步骤:1) 找到所有 '张%' 的行 2) 逐条回表查 status 3) 过滤
|
||||
|
||||
-- ✅ 有 ICP:在二级索引中就先过滤 status
|
||||
-- 引擎层直接读二级索引页,提取 name 和 status,先判断 status=1
|
||||
-- 只有满足条件的才回表
|
||||
-- 减少了大量无效回表
|
||||
|
||||
-- EXPLAIN 验证
|
||||
EXPLAIN SELECT * FROM users WHERE name LIKE '张%' AND status = 1\G
|
||||
-- Extra 显示: Using index condition
|
||||
-- 查询:WHERE status = 1 AND created_at > '2024-01-01'
|
||||
SELECT id, name FROM users WHERE status = 1 AND created_at > '2024-01-01';
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
N["无 ICP"] --> A["查到 1000 条 '张%' 的记录"]
|
||||
A --> B["1000 次回表检查 status"]
|
||||
B --> C["最终只有 10 条符合"]
|
||||
N["无 ICP"] --> A["在 idx_status_created<br/>查到 1000 条 status=1 的记录"]
|
||||
A --> B["1000 次回表检查 created_at"]
|
||||
B --> C["最终只有 50 条满足时间条件"]
|
||||
|
||||
Y["有 ICP"] --> D["在索引中预检 status"]
|
||||
D --> E["1000 条中筛出 10 条"]
|
||||
E --> F["仅 10 次回表"]
|
||||
Y["有 ICP"] --> D["在二级索引中直接判断<br/>created_at > '2024-01-01'"]
|
||||
D --> E["1000 条中筛出 50 条"]
|
||||
E --> F["仅 50 次回表"]
|
||||
|
||||
style B fill:#EE5A24,color:#fff
|
||||
style F fill:#00D866,color:#fff
|
||||
```
|
||||
|
||||
> [!HIGHLIGHT] ICP 的本质
|
||||
> 把 **Server 层**的过滤条件(如 `created_at > ...`)**下推到 Handler 层**,在读取二级索引页时就完成判断,命中了才回表。
|
||||
>
|
||||
> **适用条件:**
|
||||
> - 必须是二级索引(聚簇索引不走 ICP)
|
||||
> - 过滤条件中能利用到的部分必须在索引列范围内
|
||||
> - MySQL 5.6+ 默认开启,无需额外配置
|
||||
|
||||
```sql
|
||||
-- 验证 ICP 是否生效
|
||||
EXPLAIN SELECT * FROM users WHERE status = 1 AND created_at > '2024-01-01'\G
|
||||
*************************** 1. row ***************************
|
||||
id: 1
|
||||
select_type: SIMPLE
|
||||
table: users
|
||||
type: range
|
||||
possible_keys: idx_status_created
|
||||
key: idx_status_created
|
||||
key_len: 5 -- TINYINT(1) + DATETIME(5)
|
||||
ref: NULL
|
||||
rows: 1000
|
||||
filtered: 10.00
|
||||
Extra: Using where ← 注意这里
|
||||
-- 如果开启了 ICP,实际执行时会先判断 created_at,减少回表次数
|
||||
|
||||
-- 可以通过开关观察差异
|
||||
SET optimizer_switch = 'index_condition_pushdown=off';
|
||||
-- EXPLAIN Extra 仍为 "Using where",但执行计划变差(更多回表)
|
||||
SET optimizer_switch = 'index_condition_pushdown=on';
|
||||
```
|
||||
|
||||
## 两种索引的空间对比
|
||||
|
||||
```mermaid
|
||||
@@ -195,6 +294,6 @@ graph TB
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/16-B+Tree 索引原理]] — B+ Tree 作为底层数据结构的工作原理
|
||||
- [[hhs/MySQL/18-联合索引与最左前缀]] — 联合索引的搜索模式对回表的影响
|
||||
- [[hhs/MySQL/03-索引与查询优化/12-B+Tree 索引原理]] — B+ Tree 作为底层数据结构的工作原理
|
||||
- [[hhs/MySQL/03-索引与查询优化/14-联合索引与最左前缀]] — 联合索引的搜索模式对回表的影响
|
||||
- [[hhs/GORM/15-性能优化]] — GORM 场景下的 Covering Index 实践
|
||||
@@ -191,7 +191,7 @@ GROUP BY type;
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/17-聚簇索引与二级索引]] — 聚簇索引本身就是特殊的联合索引 (PK)
|
||||
- [[hhs/MySQL/19-EXPLAIN 完全指南]] — 用 EXPLAIN 验证联合索引是否按预期生效
|
||||
- [[hhs/MySQL/20-慢查询日志分析]] — 如何从 slow log 中识别索引未命中
|
||||
- [[hhs/MySQL/21-查询改写技巧]] — 将失效的查询改写为可利用索引的形式
|
||||
- [[hhs/MySQL/03-索引与查询优化/13-聚簇索引与二级索引]] — 聚簇索引本身就是特殊的联合索引 (PK)
|
||||
- [[hhs/MySQL/03-索引与查询优化/15-EXPLAIN 完全指南]] — 用 EXPLAIN 验证联合索引是否按预期生效
|
||||
- [[hhs/MySQL/03-索引与查询优化/16-慢查询日志分析]] — 如何从 slow log 中识别索引未命中
|
||||
- [[hhs/MySQL/03-索引与查询优化/17-查询改写技巧]] — 将失效的查询改写为可利用索引的形式
|
||||
@@ -323,6 +323,6 @@ SET GLOBAL innodb_stats_auto_recalc = ON;
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/16-B+Tree 索引原理]] — type=ALL vs type=range 的底层差异
|
||||
- [[hhs/MySQL/19-慢查询日志分析]] — 如何用 pt-query-digest 配合 EXPLAIN
|
||||
- [[hhs/MySQL/18-联合索引与最左前缀]] — 索引设计不当导致的 EXPLAIN 问题
|
||||
- [[hhs/MySQL/03-索引与查询优化/12-B+Tree 索引原理]] — type=ALL vs type=range 的底层差异
|
||||
- [[hhs/MySQL/03-索引与查询优化/16-慢查询日志分析]] — 如何用 pt-query-digest 配合 EXPLAIN
|
||||
- [[hhs/MySQL/03-索引与查询优化/14-联合索引与最左前缀]] — 索引设计不当导致的 EXPLAIN 问题
|
||||
@@ -307,7 +307,7 @@ ORDER BY created_at DESC LIMIT 10;
|
||||
- `table->rows`(预估扫描行数)
|
||||
- `query_block->filesort` / `temporary_table` 是否存在
|
||||
|
||||
更详细的 EXPLAIN 解读见 [[hhs/MySQL/19-EXPLAIN 完全指南]]。
|
||||
更详细的 EXPLAIN 解读见 [[hhs/MySQL/03-索引与查询优化/15-EXPLAIN 完全指南]]。
|
||||
|
||||
#### Step 4: 针对性优化
|
||||
|
||||
@@ -335,9 +335,9 @@ flowchart LR
|
||||
```
|
||||
|
||||
详见:
|
||||
- [[hhs/MySQL/21-查询改写技巧]] — 常见 SQL 改写方案
|
||||
- [[hhs/MySQL/22-深分页优化]] — OFFSET 深分页的替代方案
|
||||
- [[hhs/MySQL/18-联合索引与最左前缀]] — 索引设计原则
|
||||
- [[hhs/MySQL/03-索引与查询优化/17-查询改写技巧]] — 常见 SQL 改写方案
|
||||
- [[hhs/MySQL/03-索引与查询优化/18-深分页优化]] — OFFSET 深分页的替代方案
|
||||
- [[hhs/MySQL/03-索引与查询优化/14-联合索引与最左前缀]] — 索引设计原则
|
||||
|
||||
#### Step 5: 回归验证 & 监控接入
|
||||
|
||||
@@ -358,9 +358,9 @@ flowchart LR
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/19-EXPLAIN 完全指南]] — EXPLAIN 各字段的详细解读与执行计划分析
|
||||
- [[hhs/MySQL/21-查询改写技巧]] — SQL 反模式改写方案
|
||||
- [[hhs/MySQL/22-深分页优化]] — OFFSET 深分页的替代方案
|
||||
- [[hhs/MySQL/16-B+Tree 索引原理]] — 索引底层结构,理解为何某些写法会让索引失效
|
||||
- [[hhs/MySQL/26-锁机制总览]] — 行锁/表锁/间隙锁,排查 Lock_time 偏高问题
|
||||
- [[hhs/MySQL/40-常见踩坑]] — MySQL 日常使用中的典型陷阱
|
||||
- [[hhs/MySQL/03-索引与查询优化/15-EXPLAIN 完全指南]] — EXPLAIN 各字段的详细解读与执行计划分析
|
||||
- [[hhs/MySQL/03-索引与查询优化/17-查询改写技巧]] — SQL 反模式改写方案
|
||||
- [[hhs/MySQL/03-索引与查询优化/18-深分页优化]] — OFFSET 深分页的替代方案
|
||||
- [[hhs/MySQL/03-索引与查询优化/12-B+Tree 索引原理]] — 索引底层结构,理解为何某些写法会让索引失效
|
||||
- [[hhs/MySQL/06-事务与并发控制/26-锁机制总览]] — 行锁/表锁/间隙锁,排查 Lock_time 偏高问题
|
||||
- [[hhs/MySQL/08-工程实践/40-常见踩坑]] — MySQL 日常使用中的典型陷阱
|
||||
@@ -327,9 +327,9 @@ mindmap
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/19-EXPLAIN 完全指南]] — 改写后验证效果的必备工具
|
||||
- [[hhs/MySQL/20-慢查询日志分析]] — 从慢日志中发现需要改写的 SQL
|
||||
- [[hhs/MySQL/22-深分页优化]] — 游标分页的专项深入
|
||||
- [[hhs/MySQL/16-B+Tree 索引原理]] — 理解索引走与否的根本原因
|
||||
- [[hhs/MySQL/40-常见踩坑]] — 生产环境中的典型错误写法合集
|
||||
- [[hhs/MySQL/03-索引与查询优化/15-EXPLAIN 完全指南]] — 改写后验证效果的必备工具
|
||||
- [[hhs/MySQL/03-索引与查询优化/16-慢查询日志分析]] — 从慢日志中发现需要改写的 SQL
|
||||
- [[hhs/MySQL/03-索引与查询优化/18-深分页优化]] — 游标分页的专项深入
|
||||
- [[hhs/MySQL/03-索引与查询优化/12-B+Tree 索引原理]] — 理解索引走与否的根本原因
|
||||
- [[hhs/MySQL/08-工程实践/40-常见踩坑]] — 生产环境中的典型错误写法合集
|
||||
- [[hhs/GORM/15-性能优化]] — GORM 层的批量操作优化
|
||||
@@ -276,6 +276,6 @@ flowchart TD
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/12-DQL SELECT 全解析]] — LIMIT 基础语法与 OFFSET 的定义
|
||||
- [[hhs/MySQL/19-EXPLAIN 完全指南]] — 分页查询的 EXPLAIN 分析
|
||||
- [[hhs/MySQL/02-SQL核心/08-DQL SELECT 全解析]] — LIMIT 基础语法与 OFFSET 的定义
|
||||
- [[hhs/MySQL/03-索引与查询优化/15-EXPLAIN 完全指南]] — 分页查询的 EXPLAIN 分析
|
||||
- [[hhs/GORM/06-排序与分页]] — GORM 分页 API 与各方案的 Go 实现
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
tags: [MySQL, 索引, B+Tree, EXPLAIN, 查询优化]
|
||||
create time: 2026-05-20 23:55
|
||||
---
|
||||
|
||||
# 三、索引与查询优化
|
||||
|
||||
## 概述
|
||||
|
||||
学会写 SQL 之后,下一步是让 SQL 跑得快。本章从 B+ Tree 底层原理讲起,覆盖聚簇索引、联合索引、EXPLAIN 执行计划分析、慢查询排查、查询改写和深分页优化——这些是解决线上性能问题的核心技能。
|
||||
|
||||
## 本章文档
|
||||
|
||||
| # | 主题 | 说明 |
|
||||
|---|------|------|
|
||||
| 12 | [[hhs/MySQL/03-索引与查询优化/12-B+Tree 索引原理]] | MySQL 为什么选 B+ Tree、它比 Hash / 红黑树好在哪里 |
|
||||
| 13 | [[hhs/MySQL/03-索引与查询优化/13-聚簇索引与二级索引]] | 数据存在哪棵树上、查二级索引为什么要"回表" |
|
||||
| 14 | [[hhs/MySQL/03-索引与查询优化/14-联合索引与最左前缀]] | 多个字段一起建索引怎么用、为什么跳列就用不上了 |
|
||||
| 15 | [[hhs/MySQL/03-索引与查询优化/15-EXPLAIN 完全指南]] | 怎么看 SQL 的执行计划、type 从优到差排哪些 |
|
||||
| 16 | [[hhs/MySQL/03-索引与查询优化/16-慢查询日志分析]] | 抓出执行慢的 SQL、用工具分析根因 |
|
||||
| 17 | [[hhs/MySQL/03-索引与查询优化/17-查询改写技巧]] | OR → UNION ALL、JOIN → EXISTS,同样的结果写法影响性能 |
|
||||
| 18 | [[hhs/MySQL/03-索引与查询优化/18-深分页优化]] | `LIMIT 1000000, 20` 为什么慢、怎么优化 |
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/README]] — MySQL 知识库总目录
|
||||
@@ -454,26 +454,26 @@ flowchart LR
|
||||
## 关联笔记
|
||||
|
||||
### 索引相关
|
||||
- [[hhs/MySQL/16-B+Tree 索引原理]] — B+Tree 数据结构基础
|
||||
- [[hhs/MySQL/17-聚簇索引与二级索引]] — 聚簇索引与二级索引的深度对比
|
||||
- [[hhs/MySQL/18-联合索引与最左前缀]] — 覆盖索引与最左前缀原则的关系
|
||||
- [[hhs/MySQL/19-EXPLAIN 完全指南]] — 用 EXPLAIN 验证是否命中覆盖索引(Using index)
|
||||
- [[hhs/MySQL/03-索引与查询优化/12-B+Tree 索引原理]] — B+Tree 数据结构基础
|
||||
- [[hhs/MySQL/03-索引与查询优化/13-聚簇索引与二级索引]] — 聚簇索引与二级索引的深度对比
|
||||
- [[hhs/MySQL/03-索引与查询优化/14-联合索引与最左前缀]] — 覆盖索引与最左前缀原则的关系
|
||||
- [[hhs/MySQL/03-索引与查询优化/15-EXPLAIN 完全指南]] — 用 EXPLAIN 验证是否命中覆盖索引(Using index)
|
||||
|
||||
### 事务与并发控制
|
||||
- [[hhs/MySQL/23-ACID 与原子性实现]] — Undo Log 保证原子性的核心机制
|
||||
- [[hhs/MySQL/24-隔离级别与可见性]] — 四种隔离级别的实际区别
|
||||
- [[hhs/MySQL/25-MVCC 原理]] — Read View + Undo Version Chain 的完整实现
|
||||
- [[hhs/MySQL/26-锁机制总览]] — InnoDB 行锁与 Buffer Pool 的协同
|
||||
- [[hhs/MySQL/28-一致性读与当前读]] — 二次读与快照读的触发条件
|
||||
- [[hhs/MySQL/06-事务与并发控制/23-ACID 与原子性实现]] — Undo Log 保证原子性的核心机制
|
||||
- [[hhs/MySQL/06-事务与并发控制/24-隔离级别与可见性]] — 四种隔离级别的实际区别
|
||||
- [[hhs/MySQL/06-事务与并发控制/25-MVCC 原理]] — Read View + Undo Version Chain 的完整实现
|
||||
- [[hhs/MySQL/06-事务与并发控制/26-锁机制总览]] — InnoDB 行锁与 Buffer Pool 的协同
|
||||
- [[hhs/MySQL/06-事务与并发控制/28-一致性读与当前读]] — 二次读与快照读的触发条件
|
||||
|
||||
### 日志与恢复
|
||||
- [[hhs/MySQL/29-Binary Log]] — Binlog 与 Redo Log 的两阶段提交(2PC)
|
||||
- [[hhs/MySQL/34-备份与恢复]] — 全量备份 + Redo Log 恢复策略
|
||||
- [[hhs/MySQL/07-高可用与分布式/29-Binary Log]] — Binlog 与 Redo Log 的两阶段提交(2PC)
|
||||
- [[hhs/MySQL/07-高可用与分布式/34-备份与恢复]] — 全量备份 + Redo Log 恢复策略
|
||||
|
||||
### 性能调优
|
||||
- [[hhs/MySQL/38-监控指标]] — 生产环境关键监控项(Buffer Pool命中率、脏页比例等)
|
||||
- [[hhs/MySQL/40-常见踩坑]] — InnoDB 常见性能陷阱及排查方法
|
||||
- [[hhs/MySQL/08-工程实践/38-监控指标]] — 生产环境关键监控项(Buffer Pool命中率、脏页比例等)
|
||||
- [[hhs/MySQL/08-工程实践/40-常见踩坑]] — InnoDB 常见性能陷阱及排查方法
|
||||
|
||||
### 整体架构
|
||||
- [[hhs/MySQL/01-MySQL 架构与进程模型]] — Server 层与 InnoDB 引擎的分层协作
|
||||
- [[hhs/MySQL/07-其他存储引擎概览]] — MyISAM / Memory / Archive 的特点对比
|
||||
- [[hhs/MySQL/01-入门基础/01-MySQL 架构与入门]] — Server 层与 InnoDB 引擎的分层协作
|
||||
- [[hhs/MySQL/04-存储引擎/20-其他存储引擎概览]] — MyISAM / Memory / Archive 的特点对比
|
||||
@@ -217,5 +217,5 @@ flowchart TD
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/06-InnoDB 深度解析]] — InnoDB 五大核心组件完整文档
|
||||
- [[hhs/MySQL/04-存储引擎/19-InnoDB 深度解析]] — InnoDB 五大核心组件完整文档
|
||||
- [[hhs/GORM/13-多数据库支持]] — GORM 在不同数据库间的移植注意事项
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
tags: [MySQL, InnoDB, 存储引擎]
|
||||
create time: 2026-05-20 23:55
|
||||
---
|
||||
|
||||
# 四、存储引擎
|
||||
|
||||
## 概述
|
||||
|
||||
学会了怎么用,来看看底层是怎么存的。本章深入解析 InnoDB 的存储机制(聚簇索引、Buffer Pool、Redo/Undo Log)以及 MyISAM、Memory 等其他引擎的适用场景。理解这部分会让你在排查性能问题时更有底气。
|
||||
|
||||
## 本章文档
|
||||
|
||||
| # | 主题 | 说明 |
|
||||
|---|------|------|
|
||||
| 19 | [[hhs/MySQL/04-存储引擎/19-InnoDB 深度解析]] | InnoDB 是怎么存数据的、聚簇索引的工作原理、缓冲机制概览 |
|
||||
| 20 | [[hhs/MySQL/04-存储引擎/20-其他存储引擎概览]] | MyISAM / Memory / Archive 的适用场景,为什么生产几乎只用 InnoDB |
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/README]] — MySQL 知识库总目录
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
tags: [MySQL, 表设计, 范式, 主键]
|
||||
create time: 2026-05-20 23:55
|
||||
---
|
||||
|
||||
# 五、表设计
|
||||
|
||||
## 概述
|
||||
|
||||
设计先行,写好的表结构能省去后面大量的麻烦。本章介绍数据库设计的三范式(以及什么时候该故意违反范式),以及不同主键策略(自增 ID、UUID、雪花算法)对性能的影响。
|
||||
|
||||
## 本章文档
|
||||
|
||||
| # | 主题 | 说明 |
|
||||
|---|------|------|
|
||||
| 21 | [[hhs/MySQL/05-表设计/21-表结构设计三范式]] | 什么是 1NF / 2NF / 3NF、什么时候该故意违反范式 |
|
||||
| 22 | [[hhs/MySQL/05-表设计/22-主键策略对比]] | 自增 ID、UUID、雪花算法——各自对性能的影响 |
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/README]] — MySQL 知识库总目录
|
||||
@@ -291,6 +291,6 @@ sequenceDiagram
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/24-隔离级别与可见性]] — ACID 中 Isolation 的具体实现
|
||||
- [[hhs/MySQL/25-MVCC 原理]] — Undo Log 如何支撑多版本并发控制
|
||||
- [[hhs/MySQL/06-事务与并发控制/24-隔离级别与可见性]] — ACID 中 Isolation 的具体实现
|
||||
- [[hhs/MySQL/06-事务与并发控制/25-MVCC 原理]] — Undo Log 如何支撑多版本并发控制
|
||||
- [[hhs/GORM/08-事务管理]] — GORM 的事务 API 与 MySQL 引擎层的对应关系
|
||||
@@ -112,7 +112,7 @@ SELECT COUNT(*) FROM orders WHERE amount > 100;
|
||||
```
|
||||
|
||||
> [!QUESTION] 为什么 InnoDB 能在 RR 下挡住幻影?
|
||||
> RC 每次 SELECT 都创建新的 Read View,所以能看到后来提交的新行。而 RR 下第一个 SELECT 就创建了 Read View,整个事务期间都用这个视图——后来的 INSERT 在 Read View 中不可见。再加上 Next-Key Lock 锁住插入间隙,INSERT 也会被阻塞。详见 [[hhs/MySQL/25-MVCC 原理]]。
|
||||
> RC 每次 SELECT 都创建新的 Read View,所以能看到后来提交的新行。而 RR 下第一个 SELECT 就创建了 Read View,整个事务期间都用这个视图——后来的 INSERT 在 Read View 中不可见。再加上 Next-Key Lock 锁住插入间隙,INSERT 也会被阻塞。详见 [[hhs/MySQL/06-事务与并发控制/25-MVCC 原理]]。
|
||||
|
||||
## 各隔离级别下的 Read View 行为
|
||||
|
||||
@@ -213,6 +213,6 @@ Next-Key Lock 并非万能,有几种场景 RR 仍无法阻止幻影读:
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/23-ACID 与原子性实现]] — ACID 的基础概念和 Redo/Undo Log
|
||||
- [[hhs/MySQL/25-MVCC 原理]] — Read View 的详细构造算法
|
||||
- [[hhs/MySQL/26-锁机制总览]] — 隔离级别与锁类型的对应关系
|
||||
- [[hhs/MySQL/06-事务与并发控制/23-ACID 与原子性实现]] — ACID 的基础概念和 Redo/Undo Log
|
||||
- [[hhs/MySQL/06-事务与并发控制/25-MVCC 原理]] — Read View 的详细构造算法
|
||||
- [[hhs/MySQL/06-事务与并发控制/26-锁机制总览]] — 隔离级别与锁类型的对应关系
|
||||
@@ -281,7 +281,7 @@ SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/24-隔离级别与可见性]] — 隔离级别的定义及 RC vs RR 的选择
|
||||
- [[hhs/MySQL/23-ACID 与原子性实现]] — Undo Log 的结构和生命周期
|
||||
- [[hhs/MySQL/26-锁机制总览]] — MVCC 与锁的配合(一致性读 vs 当前读)
|
||||
- [[hhs/MySQL/28-一致性读与当前读]] — 什么情况下走 MVCC、什么情况下需要加锁
|
||||
- [[hhs/MySQL/06-事务与并发控制/24-隔离级别与可见性]] — 隔离级别的定义及 RC vs RR 的选择
|
||||
- [[hhs/MySQL/06-事务与并发控制/23-ACID 与原子性实现]] — Undo Log 的结构和生命周期
|
||||
- [[hhs/MySQL/06-事务与并发控制/26-锁机制总览]] — MVCC 与锁的配合(一致性读 vs 当前读)
|
||||
- [[hhs/MySQL/06-事务与并发控制/28-一致性读与当前读]] — 什么情况下走 MVCC、什么情况下需要加锁
|
||||
@@ -311,7 +311,7 @@ sequenceDiagram
|
||||
> ```
|
||||
>
|
||||
> > [!NOTE] 深入阅读
|
||||
> > 想进一步了解 MVCC 与锁的配合原理,参见 [[hhs/MySQL/25-MVCC 原理]] — 其中详细解释了 Read View 如何与锁协同工作。
|
||||
> > 想进一步了解 MVCC 与锁的配合原理,参见 [[hhs/MySQL/06-事务与并发控制/25-MVCC 原理]] — 其中详细解释了 Read View 如何与锁协同工作。
|
||||
|
||||
## 锁的场景矩阵
|
||||
|
||||
@@ -386,7 +386,7 @@ sequenceDiagram
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/23-ACID 与原子性实现]] — Redo Log 和 Undo Log 的基础知识
|
||||
- [[hhs/MySQL/25-MVCC 原理]] — MVCC 与锁的配合(一致性读 vs 当前读)
|
||||
- [[hhs/MySQL/26-死锁与排查]] — 完整的死锁诊断流程
|
||||
- [[hhs/MySQL/06-事务与并发控制/23-ACID 与原子性实现]] — Redo Log 和 Undo Log 的基础知识
|
||||
- [[hhs/MySQL/06-事务与并发控制/25-MVCC 原理]] — MVCC 与锁的配合(一致性读 vs 当前读)
|
||||
- [[hhs/MySQL/06-事务与并发控制/27-死锁与排查]] — 完整的死锁诊断流程
|
||||
- [[hhs/GORM/08-事务管理]] — GORM 事务中的锁获取时机
|
||||
@@ -311,6 +311,6 @@ innodb_print_all_deadlocks = ON # 记录所有死锁到错误日志
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/26-锁机制总览]] — 各种锁类型及其交互规则
|
||||
- [[hhs/MySQL/25-MVCC 原理]] — MVCC 如何通过一致性读避免不必要的锁
|
||||
- [[hhs/MySQL/06-事务与并发控制/26-锁机制总览]] — 各种锁类型及其交互规则
|
||||
- [[hhs/MySQL/06-事务与并发控制/25-MVCC 原理]] — MVCC 如何通过一致性读避免不必要的锁
|
||||
- [[hhs/DEV/Go-Database]] — Go 中数据库事务的最佳实践
|
||||
@@ -329,6 +329,6 @@ SELECT * FROM orders WHERE user_id = 100 FOR UPDATE;
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/25-MVCC 原理]] — ReadView 的构造和版本链查找
|
||||
- [[hhs/MySQL/26-锁机制总览]] — 当前读涉及的 S 锁和 X 锁
|
||||
- [[hhs/MySQL/06-事务与并发控制/25-MVCC 原理]] — ReadView 的构造和版本链查找
|
||||
- [[hhs/MySQL/06-事务与并发控制/26-锁机制总览]] — 当前读涉及的 S 锁和 X 锁
|
||||
- [[hhs/GORM/08-事务管理]] — GORM 中的 FirstForUpdate / SetLock 用法
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
tags: [MySQL, 事务, ACID, MVCC, 锁, 并发]
|
||||
create time: 2026-05-20 23:55
|
||||
---
|
||||
|
||||
# 六、事务与并发控制
|
||||
|
||||
## 概述
|
||||
|
||||
当多个用户同时操作数据时,如何保证数据不出错?本章从 ACID 原理出发,逐步深入隔离级别、MVCC 多版本并发控制、锁机制和死锁排查——这是理解 MySQL 并发行为的核心章节,也是面试的高频考点。
|
||||
|
||||
## 本章文档
|
||||
|
||||
| # | 主题 | 说明 |
|
||||
|---|------|------|
|
||||
| 23 | [[hhs/MySQL/06-事务与并发控制/23-ACID 与原子性实现]] | 什么是 ACID、Redo Log + Undo Log 怎么保证不出错 |
|
||||
| 24 | [[hhs/MySQL/06-事务与并发控制/24-隔离级别与可见性]] | 四个隔离级别各是什么意思,MySQL 默认的是哪个 |
|
||||
| 25 | [[hhs/MySQL/06-事务与并发控制/25-MVCC 原理]] | 不加锁也能并发读的秘密——多版本并发控制 |
|
||||
| 26 | [[hhs/MySQL/06-事务与并发控制/26-锁机制总览]] | 从全局锁到行级锁,MySQL 在不同场景下锁什么 |
|
||||
| 27 | [[hhs/MySQL/06-事务与并发控制/27-死锁与排查]] | 什么时候会发生死锁、怎么定位和避免 |
|
||||
| 28 | [[hhs/MySQL/06-事务与并发控制/28-一致性读与当前读]] | 普通 SELECT 读到什么、加 FOR UPDATE 又读到什么 |
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/README]] — MySQL 知识库总目录
|
||||
@@ -23,7 +23,7 @@ flowchart LR
|
||||
```
|
||||
|
||||
> [!TIP] 读这一页之前建议先了解
|
||||
> 对 Binlog 的理解建立在 InnoDB 存储引擎基础之上。**建议先看**: [[hhs/MySQL/21-InnoDB 架构]] → 本文档 → [[hhs/MySQL/30-主从复制]]
|
||||
> 对 Binlog 的理解建立在 InnoDB 存储引擎基础之上。**建议先看**: [[hhs/MySQL/04-存储引擎/19-InnoDB 深度解析]] → 本文档 → [[hhs/MySQL/07-高可用与分布式/30-主从复制详解]]
|
||||
|
||||
## Binlog 与 Redo Log 的根本区别
|
||||
|
||||
@@ -286,6 +286,6 @@ mysqlbinlog --start-position=1234 --stop-position=5678 mysql-bin.000001
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/30-主从复制]] — Binlog 是主从复制的基石
|
||||
- [[hhs/MySQL/23-ACID 与原子性实现]] — Binlog 与 Redo Log 的两阶段提交协议
|
||||
- [[hhs/MySQL/32-备份与恢复]] — 基于 binlog 的 PITR 时间点恢复
|
||||
- [[hhs/MySQL/07-高可用与分布式/30-主从复制详解]] — Binlog 是主从复制的基石
|
||||
- [[hhs/MySQL/06-事务与并发控制/23-ACID 与原子性实现]] — Binlog 与 Redo Log 的两阶段提交协议
|
||||
- [[hhs/MySQL/07-高可用与分布式/34-备份与恢复]] — 基于 binlog 的 PITR 时间点恢复
|
||||
@@ -350,7 +350,6 @@ flowchart LR
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/29-Binary Log]] — Binlog 是主从复制的数据源
|
||||
- [[hhs/MySQL/31-MHA / Orchestrator]] — 自动故障切换工具
|
||||
- [[hhs/MySQL/33-Group Replication]] — Paxos 多主复制进阶方案
|
||||
- [[hhs/MySQL/32-读写分离与代理]] — 基于主从架构的流量分发
|
||||
- [[hhs/MySQL/07-高可用与分布式/29-Binary Log]] — Binlog 是主从复制的数据源
|
||||
- [[hhs/MySQL/07-高可用与分布式/31-MHA与Orchestrator]] — 自动故障切换工具
|
||||
- [[hhs/MySQL/07-高可用与分布式/32-Group Replication]] — Paxos 多主复制进阶方案
|
||||
@@ -340,6 +340,6 @@ flowchart LR
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/30-主从复制]] — 主从复制的配置和监控
|
||||
- [[hhs/MySQL/33-Group Replication]] — 内置多主复制,不需要外部 HA 工具
|
||||
- [[hhs/MySQL/07-高可用与分布式/30-主从复制详解]] — 主从复制的配置和监控
|
||||
- [[hhs/MySQL/07-高可用与分布式/32-Group Replication]] — 内置多主复制,不需要外部 HA 工具
|
||||
- [[hhs/Redis/06-主从与哨兵]] — Redis Sentinel 与 MySQL HA 方案的对比
|
||||
@@ -270,6 +270,6 @@ SELECT * FROM performance_schema.replication_group_connections;
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/30-主从复制]] — GR 的主干是改进后的主从复制
|
||||
- [[hhs/MySQL/31-MHA与Orchestrator]] — 为什么 GR 不需要外部 HA 工具
|
||||
- [[hhs/MySQL/35-监控指标]] — GR 特有的监控指标
|
||||
- [[hhs/MySQL/07-高可用与分布式/30-主从复制详解]] — GR 的主干是改进后的主从复制
|
||||
- [[hhs/MySQL/07-高可用与分布式/31-MHA与Orchestrator]] — 为什么 GR 不需要外部 HA 工具
|
||||
- [[hhs/MySQL/08-工程实践/38-监控指标]] — GR 特有的监控指标
|
||||
@@ -325,6 +325,6 @@ try (Connection conn = DriverManager.getConnection(url, props)) {
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/32-Group Replication]] — InnoDB Cluster 底层就是 GR
|
||||
- [[hhs/MySQL/31-MHA与Orchestrator]] — 不同 HA 方案的对比
|
||||
- [[hhs/MySQL/07-高可用与分布式/32-Group Replication]] — InnoDB Cluster 底层就是 GR
|
||||
- [[hhs/MySQL/07-高可用与分布式/31-MHA与Orchestrator]] — 不同 HA 方案的对比
|
||||
- [[hhs/GORM/13-多数据库支持]] — GORM 在集群环境中的使用注意事项
|
||||
@@ -273,6 +273,6 @@ echo "$(date +%F %T) | full | ${DATE} | OK" >> "${BACKUP_DIR}/backup.log"
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/29-Binary Log]] — Binlog 是 PITR 的基础
|
||||
- [[hhs/MySQL/30-主从复制]] — 从库可以作为只读备份节点
|
||||
- [[hhs/MySQL/07-高可用与分布式/29-Binary Log]] — Binlog 是 PITR 的基础
|
||||
- [[hhs/MySQL/07-高可用与分布式/30-主从复制详解]] — 从库可以作为只读备份节点
|
||||
- [[hhs/DEV/Go-Database]] — Go 应用中处理数据库恢复错误的最佳实践
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
tags: [MySQL, 主从复制, 高可用, Binlog, 集群]
|
||||
create time: 2026-05-20 23:55
|
||||
---
|
||||
|
||||
# 七、高可用与分布式
|
||||
|
||||
## 概述
|
||||
|
||||
单台 MySQL 扛不住了怎么办?本章从 Binlog 原理讲起,覆盖主从复制、自动故障切换(MHA / Orchestrator)、Group Replication、InnoDB Cluster 以及备份恢复策略——这些是保障生产环境数据库可靠性的必备知识。
|
||||
|
||||
## 本章文档
|
||||
|
||||
| # | 主题 | 说明 |
|
||||
|---|------|------|
|
||||
| 29 | [[hhs/MySQL/07-高可用与分布式/29-Binary Log]] | Binlog 记录了所有写操作,主从复制和恢复都靠它 |
|
||||
| 30 | [[hhs/MySQL/07-高可用与分布式/30-主从复制详解]] | 数据怎么从主库同步到从库、半同步 vs 异步的区别 |
|
||||
| 31 | [[hhs/MySQL/07-高可用与分布式/31-MHA与Orchestrator]] | 主库挂了怎么自动切换到从库 |
|
||||
| 32 | [[hhs/MySQL/07-高可用与分布式/32-Group Replication]] | 多主同时写入、冲突怎么处理 |
|
||||
| 33 | [[hhs/MySQL/07-高可用与分布式/33-InnoDB Cluster]] | MySQL 官方的高可用集群方案 |
|
||||
| 34 | [[hhs/MySQL/07-高可用与分布式/34-备份与恢复]] | 全量备份和增量备份、恢复到任意时间点 |
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/README]] — MySQL 知识库总目录
|
||||
@@ -157,7 +157,7 @@ flowchart TD
|
||||
>
|
||||
> 如果你的主键是 AUTO_INCREMENT,每个分库各自从 1 开始计数——一旦需要合并数据或做跨库查询,ID 冲突不可避免。这就是为什么**要做分片就必须先上分布式 ID**。
|
||||
>
|
||||
> 详细的 ID 生成方案请参考:[[hhs/MySQL/09-主键策略对比]](Snowflake、ULID、UUID_TO_BIN 等方案的深度对比)。
|
||||
> 详细的 ID 生成方案请参考:[[hhs/MySQL/05-表设计/22-主键策略对比]](Snowflake、ULID、UUID_TO_BIN 等方案的深度对比)。
|
||||
|
||||
快速选型参考:
|
||||
|
||||
@@ -449,7 +449,7 @@ func CreateOrder(order *Order) error {
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/09-主键策略对比]] — 分布式 ID 生成方案(Snowflake / ULID / UUID)
|
||||
- [[hhs/MySQL/05-表设计/22-主键策略对比]] — 分布式 ID 生成方案(Snowflake / ULID / UUID)
|
||||
- [[hhs/GORM/13-多数据库支持]] — GORM 对多库连接的支持
|
||||
- [[hhs/MySQL/35-Go 连接池配置]] — 多分片场景下的连接池隔离
|
||||
- [[hhs/MySQL/38-监控指标]] — 分片集群的监控指标体系
|
||||
- [[hhs/MySQL/08-工程实践/35-Go 连接池配置]] — 多分片场景下的连接池隔离
|
||||
- [[hhs/MySQL/08-工程实践/38-监控指标]] — 分片集群的监控指标体系
|
||||
@@ -257,6 +257,6 @@ mysqld_exporter \
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/20-慢查询日志分析]] — pt-query-digest 配合监控系统
|
||||
- [[hhs/MySQL/03-索引与查询优化/16-慢查询日志分析]] — pt-query-digest 配合监控系统
|
||||
- [[hhs/Redis/09-高级特性]] — Redis 监控指标的对比
|
||||
- [[hhs/GORM/16-日志与调试]] — GORM 级别的慢查询追踪
|
||||
@@ -316,6 +316,6 @@ sequenceDiagram
|
||||
|
||||
## 11. 关联笔记
|
||||
|
||||
- [[hhs/MySQL/19-EXPLAIN 完全指南]] — 通过 EXPLAIN 识别上述问题
|
||||
- [[hhs/MySQL/18-联合索引与最左前缀]] — 索引失效的典型原因
|
||||
- [[hhs/MySQL/21-查询改写技巧]] — 各种问题的改写方案
|
||||
- [[hhs/MySQL/03-索引与查询优化/15-EXPLAIN 完全指南]] — 通过 EXPLAIN 识别上述问题
|
||||
- [[hhs/MySQL/03-索引与查询优化/14-联合索引与最左前缀]] — 索引失效的典型原因
|
||||
- [[hhs/MySQL/03-索引与查询优化/17-查询改写技巧]] — 各种问题的改写方案
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
tags: [MySQL, 工程实践, 连接池, 监控, 分库分表]
|
||||
create time: 2026-05-20 23:55
|
||||
---
|
||||
|
||||
# 八、工程实践
|
||||
|
||||
## 概述
|
||||
|
||||
部署上线后,连接池怎么配、Schema 怎么迁移、数据量大了怎么拆分、出了问题怎么排查。本章聚焦生产环境中 MySQL 的运维和工程化管理,是"从会用到能管好"的最后一公里。
|
||||
|
||||
## 本章文档
|
||||
|
||||
| # | 主题 | 说明 |
|
||||
|---|------|------|
|
||||
| 35 | [[hhs/MySQL/08-工程实践/35-Go 连接池配置]] | `sql.DB` 几个关键参数的含义和调优思路 |
|
||||
| 36 | [[hhs/MySQL/08-工程实践/36-Schema 迁移管理]] | 版本化的建表脚本、谁在用、怎么保证可回滚 |
|
||||
| 37 | [[hhs/MySQL/08-工程实践/37-分库分表]] | 数据量太大时怎么拆分、跨分片查询怎么办 |
|
||||
| 38 | [[hhs/MySQL/08-工程实践/38-监控指标]] | QPS/TPS、慢查询数、连接数——哪些数据决定数据库健康 |
|
||||
| 39 | [[hhs/MySQL/08-工程实践/39-安全加固]] | 权限管理、加密传输、SQL 注入防护 |
|
||||
| 40 | [[hhs/MySQL/08-工程实践/40-常见踩坑]] | ORDER BY 文件排序、COUNT 的区别、timestamp 自动更新 |
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/README]] — MySQL 知识库总目录
|
||||
@@ -1,251 +0,0 @@
|
||||
---
|
||||
tags: [MySQL, DDL, ALTER TABLE, CREATE TABLE, DATATYPE, NULL, ONLINE DDL]
|
||||
create time: 2026-05-16 00:00
|
||||
---
|
||||
|
||||
# DDL — 建表与结构变更
|
||||
|
||||
## 概述
|
||||
|
||||
DDL(Data Definition Language)用于定义和修改数据库对象的结构。本章聚焦最常用的 `CREATE TABLE`、`ALTER TABLE`,以及 MySQL 8.0 引入的在线 DDL 特性。
|
||||
|
||||
## CREATE TABLE
|
||||
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL COMMENT '用户名',
|
||||
email VARCHAR(255) NOT NULL COMMENT '邮箱唯一',
|
||||
password_hash VARCHAR(64) NOT NULL COMMENT 'bcrypt hash',
|
||||
status TINYINT NOT NULL DEFAULT 1 COMMENT '1=active 0=disabled',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE KEY uk_email (email),
|
||||
INDEX idx_username (username),
|
||||
INDEX idx_status_created (status, created_at)
|
||||
) ENGINE=InnoDB
|
||||
DEFAULT CHARSET=utf8mb4
|
||||
COLLATE=utf8mb4_0900_ai_ci
|
||||
COMMENT='用户表';
|
||||
```
|
||||
|
||||
### 关键语法解析
|
||||
|
||||
| 子句 | 作用 |
|
||||
|------|------|
|
||||
| `ENGINE=InnoDB` | 显式指定存储引擎(8.0 默认值)|
|
||||
| `DEFAULT CHARSET` | 数据库级字符集覆盖 |
|
||||
| `ON UPDATE CURRENT_TIMESTAMP` | 自动维护更新时间戳 |
|
||||
| `UNIQUE KEY` | 唯一索引,同时约束数据唯一性 |
|
||||
| `INDEX idx_name (col)` | 普通索引,辅助查询 |
|
||||
| `COMMENT` | 表和字段注释,可通过 `SHOW FULL COLUMNS` 查看 |
|
||||
|
||||
> [!TIP] 命名规范约定
|
||||
> - **索引名**:`idx_表名_列名`(普通索引)、`uk_表名_列名`(唯一索引)——避免超过 64 字符
|
||||
> - **时间字段**:统一用 `DATETIME` 而非 `TIMESTAMP`。后者范围仅限 `1970~2038`,遇到闰秒时会报错
|
||||
> - **自增主键**:务必用 `UNSIGNED`,正数区间翻倍(0 ~ 42.9 亿),且能略微节省空间
|
||||
|
||||
> [!QUESTION] 思考:为什么 `password_hash` 要用 `VARCHAR(64)` 而不是固定长度?
|
||||
> 答案取决于你使用的哈希算法。bcrypt 输出为 60 字符(如 `$2b$12$...`),预留 64 留有扩展余量。如果改用 SHA-256 则恰好 64 字节,此时 `CHAR(64)` 反而更高效——因为固定长度无需额外存储长度前缀。
|
||||
|
||||
---
|
||||
|
||||
### 数据类型选择指南
|
||||
|
||||
建表时数据类型直接决定磁盘占用和查询性能。以下是常见场景的经验推荐:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["需要整数?" -->|"是"| B{"需要 UNSIGNED?"}
|
||||
B -->|"否"| C["INT — 覆盖 -21 万 ~ 21 万"]
|
||||
B -->|"是"| D["BIGINT UNSIGNED — 最大 1844 亿"]
|
||||
|
||||
A -->|"否"| E["字符串?"]
|
||||
E -->|"变长 < 255"| F["VARCHAR(N)"]
|
||||
E -->|"定长 (密码/签名)"| G["CHAR(N)"]
|
||||
E -->|"超长 (文章/JSON)"| H["TEXT / LONGTEXT"]
|
||||
```
|
||||
|
||||
| 类型家族 | 适用场景 | 避坑提示 |
|
||||
|----------|---------|---------|
|
||||
| `TINYINT` | 状态标记、布尔值 | 建议加 `UNSIGNED`,0~255 足够 |
|
||||
| `INT` | 计数器、外键引用 | 数字型 ID 优先 `INT UNSIGNED`,超 42 亿再用 `BIGINT` |
|
||||
| `VARCHAR(N)` | 用户名、邮箱、地址 | N 按实际 +20% 余量;不要盲目设 255 |
|
||||
| `DATETIME` | 所有时间戳 | MySQL 8.0 推荐;避免 `TIMESTAMP` 的 2038 年瓶颈 |
|
||||
| `DECIMAL(M,D)` | 金额 | **绝对禁止**用 `FLOAT/DOUBLE` 存储财务数据 |
|
||||
|
||||
#### 一个常见的反例
|
||||
|
||||
```sql
|
||||
-- ❌ 错误:用 FLOAT 存价格,会出现精度丢失
|
||||
price FLOAT DEFAULT 0.00,
|
||||
|
||||
-- ✅ 正确:DECIMAL 精确表示货币,M 总位数 D 小数位
|
||||
price DECIMAL(10, 2) DEFAULT 0.00 COMMENT '单位:元',
|
||||
```
|
||||
|
||||
> [!NOTE] DECIMAL(10,2) 表示最多 10 位数字,其中 2 位在小数点后,即最大值为 `99999999.99`。在 InnoDB 内部以二进制紧凑存储,性能接近整数类型。
|
||||
|
||||
## ALTER TABLE — 常见操作
|
||||
|
||||
### 增删改列
|
||||
|
||||
`CHANGE` 与 `MODIFY` 的区别在于:**CHANGE 必须同时写出列名(可改名)**,而 MODIFY 只改属性不更名。这是初学最容易混淆的地方。
|
||||
|
||||
```sql
|
||||
-- 添加列
|
||||
ALTER TABLE users ADD COLUMN phone VARCHAR(20) AFTER email;
|
||||
ALTER TABLE users ADD COLUMN last_login_ip VARCHAR(45); -- IPv4/IPv6 通用
|
||||
|
||||
-- 修改列类型(不改名)
|
||||
ALTER TABLE users MODIFY COLUMN status TINYINT UNSIGNED DEFAULT 0;
|
||||
|
||||
-- 重命名列(改名 + 可选改类型)
|
||||
ALTER TABLE users CHANGE COLUMN phone phone_number VARCHAR(20);
|
||||
|
||||
-- 删除列
|
||||
ALTER TABLE users DROP COLUMN last_login_ip;
|
||||
|
||||
-- 删除索引
|
||||
ALTER TABLE users DROP INDEX idx_username;
|
||||
```
|
||||
|
||||
> [!WARNING] ALTER TABLE DROP COLUMN 不可逆
|
||||
> 列一旦删除,其中的数据和统计信息全部消失。**生产环境执行 DDL 前务必确认已有备份**。现代运维流程中应通过迁移工具(见关联笔记)做版本化管理,而非手动执行 ALTER。
|
||||
|
||||
### 在线 DDL(ALGORITHM / LOCK)
|
||||
|
||||
MySQL 5.6+ 支持 Online DDL,允许在结构变更期间持续处理读写请求。
|
||||
|
||||
```sql
|
||||
-- 三种算法对比
|
||||
ALTER TABLE users ADD COLUMN bio TEXT, ALGORITHM=INPLACE;
|
||||
ALTER TABLE users ADD COLUMN bio TEXT, ALGORITHM=COPY;
|
||||
ALTER TABLE users ADD COLUMN bio TEXT; -- 8.0 默认 INPLACE
|
||||
|
||||
-- 三种锁策略对比
|
||||
ALTER TABLE ... LOCK=NONE; -- 不阻塞任何读/写(最安全)
|
||||
ALTER TABLE ... LOCK=SHARED; -- 允许并发读,阻塞写
|
||||
ALTER TABLE ... LOCK=EXCLUSIVE; -- 阻塞所有其他操作(最快)
|
||||
```
|
||||
|
||||
> [!NOTE] INPLACE vs COPY 的本质区别
|
||||
> - **INPLACE**:直接在原表上重建索引或新增索引,不需要全表拷贝数据。支持的场景包括:增加/删除索引、改变列默认值、新增列等。
|
||||
> - **COPY**:创建临时表 → 逐行拷贝数据 → 原子替换。适用于改变列定义导致格式不兼容的场景,比如 `VARCHAR` 改 `INT`。
|
||||
>
|
||||
> **经验法则**:8.0 默认的 `INPLACE` 对大多数操作都够用。若不确定,先跑 `pt-online-schema-change --dry-run` 看预估耗时。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["ALTER TABLE 开始"] --> B{"ALGORITHM"}
|
||||
B -->|"INPLACE"| C["直接修改数据字典<br/>原地更新索引"]
|
||||
B -->|"COPY"| D["创建临时表拷贝数据后替换"]
|
||||
|
||||
C --> E{"LOCK"}
|
||||
D --> E
|
||||
|
||||
E -->|"NONE"| F["在线执行<br/>读写不阻塞"]
|
||||
E -->|"SHARED"| G["并发读<br/>写入等待"]
|
||||
E -->|"EXCLUSIVE"| H["阻塞全部操作<br/>速度最快"]
|
||||
|
||||
style F fill:#00D866,color:#fff
|
||||
style G fill:#FF9F43,color:#000
|
||||
style H fill:#EE5A24,color:#fff
|
||||
```
|
||||
|
||||
### 大表 DDL 实战技巧
|
||||
|
||||
对百万行以上的表执行 `ALTER TABLE` 时:
|
||||
|
||||
```bash
|
||||
# ❌ 危险:直接在线上执行
|
||||
ALTER TABLE big_table ADD INDEX idx_col (col);
|
||||
|
||||
# ✅ 方案一:pt-online-schema-change(Percona Toolkit)
|
||||
pt-online-schema-change \
|
||||
--user=root --password=xxx \
|
||||
--alter "ADD INDEX idx_col (col)" \
|
||||
D=mydb,t=big_table \
|
||||
--execute
|
||||
|
||||
# ✅ 方案二:gh-ost(GitHub 开源)
|
||||
gh-ost \
|
||||
--user=root --password=xxx \
|
||||
--host=127.0.0.1 --port=3306 \
|
||||
--database=mydb \
|
||||
--table=big_table \
|
||||
--alter "ADD INDEX idx_col (col)" \
|
||||
--allow-on-master \
|
||||
--execute
|
||||
```
|
||||
|
||||
> [!TIP] pt-osc 原理三步走
|
||||
> 1. **创建新表**:与原表结构一致 + 目标变更
|
||||
> 2. **触发器同步**:创建 INSERT/UPDATE/DELETE 触发器,将原表的变更实时同步到新表
|
||||
> 3. **原子替换**:加排他锁极短时间,swap 两张表名后删除触发器和旧表
|
||||
|
||||
> [!QUESTION] gh-ost 和 pt-osc 怎么选?
|
||||
> - **pt-osc** 依赖触发器,对高并发写密集型表有较大开销
|
||||
> - **gh-ost** 基于 binlog 解析,无需触发器,对线上影响更小
|
||||
> - **结论**:新项目优先 gh-ost;老项目已有 pt-toolkit 积累也可以用 pt-osc
|
||||
|
||||
### NULL vs NOT NULL — 选型指南
|
||||
|
||||
这是 DDL 中最常见的争论之一。核心原则:**能用 NOT NULL 就不用 NULL**。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["是否允许未知状态" -->|"否"| B["NOT NULL + DEFAULT"]
|
||||
A -->|"是"| C{"业务语义?"}
|
||||
C -->|"逻辑删除/软删"| D["TINYINT DEFAULT 0<br/>0=正常 1=已删除"]
|
||||
C -->|" truly optional "| E["允许 NULL<br/>但加注释说明含义"]
|
||||
```
|
||||
|
||||
| 维度 | NOT NULL + DEFAULT | 允许 NULL |
|
||||
|------|-------------------|----------|
|
||||
| **索引效率** | InnoDB 二级索引不存储纯 NULL 值,用默认值占位可减少索引碎片 | 包含 NULL 标记的索引需要额外 1 bit |
|
||||
| **查询安全** | `WHERE col = ?` 不会遗漏任何行 | `WHERE col = 'value'` **排除了 NULL 行**(需用 `IS NULL`) |
|
||||
| **聚合函数** | SUM/COUNT 直接可用 | COUNT(col) 忽略 NULL 行,容易误判总行数 |
|
||||
| **可读性** | `status = 0` 一目了然 | `status IS NULL` 语义模糊——到底是"还没填"还是"被清除了"? |
|
||||
|
||||
> [!WARNING] NULL 的三个常见误区
|
||||
> 1. **"NULL 占空间更小"**:InnoDB 中 NULL 也需要在行格式中标记,固定为每 8 个字节 1 bit 的 null bitmap。比存一个空字符串或零值并不节省什么。
|
||||
> 2. **"NULL = NULL"**:在 SQL 三值逻辑中,`NULL = NULL` 结果为 UNKNOWN,永远不等于 TRUE。判断必须用 `IS NULL` / `IS NOT NULL`。
|
||||
> 3. **"外键可以为 NULL"**:技术上可以,但会导致孤儿记录难以追踪。建议用显式的 `deleted_at` 时间戳代替外键 NULL 做软删除。
|
||||
|
||||
## 常用 DDL 查询
|
||||
|
||||
这些 SQL 命令帮助你查看当前数据库的「结构全貌」,在排查索引失效、表空间膨胀时尤其有用。
|
||||
|
||||
```sql
|
||||
-- 查看表完整创建语句(含所有索引、注释、引擎配置)
|
||||
SHOW CREATE TABLE users\G
|
||||
|
||||
-- 查看所有索引(含索引类型、列顺序、唯一性)
|
||||
SHOW INDEX FROM users\G
|
||||
|
||||
-- 查看表统计信息(Rows 为估算值,非精确计数)
|
||||
SHOW TABLE STATUS LIKE 'users'\G
|
||||
-- 重点关注 Rows(估算行数)、Data_length、Index_length
|
||||
|
||||
-- 查看分区情况
|
||||
SELECT PARTITION_NAME, PARTITION_EXPRESSION, PARTITION_DESCRIPTION
|
||||
FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_NAME = 'logs';
|
||||
```
|
||||
|
||||
> [!TIP] 快速定位慢查询相关的结构问题
|
||||
> ```sql
|
||||
> -- 检查表是否存在大量碎片的索引(数据删除后未回收的空间)
|
||||
> SELECT TABLE_NAME, INDEX_NAME, CARDINALITY
|
||||
> FROM INFORMATION_SCHEMA.STATISTICS
|
||||
> WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users'
|
||||
> ORDER BY CARDINALITY ASC;
|
||||
>
|
||||
> -- 低基数(CARDINALITY 接近 0)的索引通常效果不佳,考虑移除
|
||||
> ```
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/GORM/02-模型定义]] — GORM AutoMigrate 等价于 DDL 的自动化版本
|
||||
- [[hhs/GORM/17-迁移工具]] — Schema 迁移管理最佳实践
|
||||
@@ -1,238 +0,0 @@
|
||||
---
|
||||
tags: [MySQL, UNION, UNION ALL, 集合运算]
|
||||
create time: 2026-05-16 00:00
|
||||
---
|
||||
|
||||
# UNION / UNION ALL
|
||||
|
||||
## 概述
|
||||
|
||||
UNION 是 SQL 标准中的集合运算,用于将多个 SELECT 的结果纵向合并为一个结果集。核心应用场景包括:**分表数据汇总**、**多源 Feed 流合并**、以及**需要排序去重的跨查询聚合**。
|
||||
|
||||
## 基本语法
|
||||
|
||||
```sql
|
||||
-- 两种形式
|
||||
SELECT col1, col2 FROM table_a
|
||||
UNION -- 去重(内部排序 + 去重)
|
||||
SELECT col1, col2 FROM table_b;
|
||||
|
||||
SELECT col1, col2 FROM table_a
|
||||
UNION ALL -- 不去重(直接拼接,性能更高)
|
||||
SELECT col1, col2 FROM table_b;
|
||||
```
|
||||
|
||||
## UNION vs UNION ALL
|
||||
|
||||
| 特性 | UNION | UNION ALL |
|
||||
|------|-------|-----------|
|
||||
| **去重** | ✅ 内部去重 | ❌ 保留所有行 |
|
||||
| **性能** | 低(需排序去重) | 高(直接追加) |
|
||||
| **ORDER BY 位置** | 只能放在最后一个 SELECT | 同上 |
|
||||
| **适用场景** | 需要唯一结果的合并 | 已知不重复的合并 |
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A1["数据源 A"] --> U
|
||||
B1["数据源 B"] --> U
|
||||
|
||||
U{"UNION"} --> R1["全部数据去重后输出"]
|
||||
|
||||
U2{"UNION ALL"} --> R2["全部数据直接拼接"]
|
||||
|
||||
style R1 fill:#FF9F43,color:#000
|
||||
style R2 fill:#00D866,color:#fff
|
||||
```
|
||||
|
||||
> [!TIP] 首选 UNION ALL
|
||||
> 如果你能通过业务逻辑保证各部分结果不重复(比如按日期区间划分),**一律用 UNION ALL**。UNION 的去重操作需要 Sort/Dedup 阶段,在大结果集上是昂贵操作。
|
||||
|
||||
## 实战场景
|
||||
|
||||
### 场景一:多表同构合并
|
||||
|
||||
```sql
|
||||
-- 日志系统按月分表:logs_202601, logs_202602, ..., logs_202605
|
||||
SELECT * FROM logs_202601
|
||||
UNION ALL
|
||||
SELECT * FROM logs_202602
|
||||
UNION ALL
|
||||
SELECT * FROM logs_202603
|
||||
UNION ALL
|
||||
SELECT * FROM logs_202604
|
||||
UNION ALL
|
||||
SELECT * FROM logs_202605
|
||||
WHERE status = 'error'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 50;
|
||||
```
|
||||
|
||||
> [!NOTE] 分表合并的注意事项
|
||||
> - 上述写法中,`WHERE / ORDER BY / LIMIT` 仅作用于**最后一个 SELECT**。前面的分表查询不做过滤,全部返回后再合并排序。
|
||||
> - 如果每个分表数据量很大(百万级),建议**用子查询保护每个表的局部 ORDER BY + LIMIT**,先各取 Top-N 再全局排。这大幅减少中间结果集大小。
|
||||
> - UNION ALL 不保证顺序,最终 ORDER BY 必不可少。
|
||||
|
||||
### 场景二:多表分页合并(Feed 流)
|
||||
|
||||
```sql
|
||||
-- 需求: 用户的动态 Feed 由关注的人和发布的文章混合组成, 按时间排序分页
|
||||
-- ❌ 应用层先查再排: 至少两次 DB 往返 + 内存归并排序
|
||||
-- ✅ UNION ALL 一次搞定
|
||||
|
||||
SELECT user_id AS source_id, content, 'follow' AS source_type, created_at
|
||||
FROM follow_feed WHERE user_id = 42
|
||||
UNION ALL
|
||||
SELECT article_id AS source_id, summary AS content, 'article' AS source_type, published_at
|
||||
FROM articles WHERE author_id = 42
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 20 OFFSET 0;
|
||||
```
|
||||
|
||||
> [!TIP] 何时用 UNION vs CASE WHEN?
|
||||
> - **数据来源是不同表或不同结构**: UNION ALL 是不二之选
|
||||
> - **同表不同条件的聚合计数**: `SUM(CASE WHEN ...)` 单次扫描更高效
|
||||
> - **经验法则**: UNION 的 SQL 可读性显著优于多个 OR 条件叠加时, 优先选 UNION
|
||||
|
||||
### 场景三:分页合并多个条件
|
||||
|
||||
```sql
|
||||
-- 搜索商品:标题含关键词 或 描述含关键词
|
||||
SELECT id, title, description, score_title
|
||||
FROM products
|
||||
WHERE title LIKE '%runners%'
|
||||
UNION ALL
|
||||
SELECT id, title, description, score_desc
|
||||
FROM products
|
||||
WHERE description LIKE '%runners%';
|
||||
```
|
||||
|
||||
> [!WARNING] UNION 的字段匹配规则
|
||||
> - 各 SELECT 的**列数必须相同**
|
||||
> - 各 SELECT 对应列的**类型应尽量兼容**(MySQL 会自动转换)
|
||||
> - ORDER BY 和 LIMIT 通常放在**整个 UNION 的最后**
|
||||
> - 列名取第一个 SELECT 的别名
|
||||
> - **注意**: MySQL 不推荐在 UNION 的各个独立 SELECT 中使用 ORDER BY(结果可能被丢弃)
|
||||
|
||||
```sql
|
||||
-- ✅ 正确
|
||||
SELECT id, name FROM products WHERE price > 100
|
||||
UNION ALL
|
||||
SELECT id, name FROM products WHERE category = 'sale';
|
||||
|
||||
-- ❌ 错误:列数不一致
|
||||
SELECT id, name FROM products
|
||||
UNION ALL
|
||||
SELECT id FROM discounts;
|
||||
|
||||
-- ❌ 错误:ORDER BY 放在中间(MySQL 可能忽略或报错)
|
||||
SELECT id FROM table_a
|
||||
ORDER BY id
|
||||
UNION ALL
|
||||
SELECT id FROM table_b;
|
||||
```
|
||||
|
||||
### UNION 进阶:ORDER BY 与 LIMIT 的行为
|
||||
|
||||
```sql
|
||||
-- ⚠️ 问题:每个 SELECT 的 ORDER BY 在合并后无效
|
||||
SELECT id FROM orders WHERE status = 'pending' ORDER BY created_at DESC
|
||||
UNION ALL
|
||||
SELECT id FROM orders WHERE status = 'shipped' ORDER BY created_at DESC;
|
||||
-- 上面的 ORDER BY 基本被 MySQL 忽略
|
||||
|
||||
-- ✅ 正确解法:用子包装保护每个查询的排序
|
||||
SELECT * FROM
|
||||
(
|
||||
SELECT id, created_at FROM orders WHERE status = 'pending'
|
||||
ORDER BY created_at DESC LIMIT 50
|
||||
) AS a
|
||||
UNION ALL
|
||||
SELECT * FROM
|
||||
(
|
||||
SELECT id, created_at FROM orders WHERE status = 'shipped'
|
||||
ORDER BY created_at DESC LIMIT 50
|
||||
) AS b
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
> [!QUESTION] 为什么子查询能保护 ORDER BY?
|
||||
> MySQL 优化器发现 UNION 外还有 ORDER BY + LIMIT 时, 会认为内部排序有用,从而保留它。
|
||||
> 但官方文档并不保证这种行为——这是基于执行计划的经验结论, 生产环境务必确认。
|
||||
|
||||
---
|
||||
|
||||
## UNION 的执行流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["查询 1"] --> R1["结果集 1"]
|
||||
B["查询 2"] --> R2["结果集 2"]
|
||||
C["查询 N"] --> RN["结果集 N"]
|
||||
|
||||
R1 --> Temp["临时表 + 唯一索引<br/>UNION 有, UNION ALL 无"]
|
||||
R2 --> Temp
|
||||
RN --> Temp
|
||||
|
||||
Temp --> Dedup{"需要去重?"}
|
||||
Dedup -->|是| Sort["排序 + 去重"]
|
||||
Dedup -->|否| Direct["直接输出"]
|
||||
|
||||
Sort --> Output["最终结果集"]
|
||||
Direct --> Output
|
||||
|
||||
style Dedup fill:#FF9F43,color:#000
|
||||
style Sort fill:#EE5A24,color:#fff
|
||||
```
|
||||
|
||||
## 与 JOIN 的选择
|
||||
|
||||
```sql
|
||||
-- 场景:获取每个部门的员工总数 + 总监姓名
|
||||
-- JOIN 方案(交叉维度,适合取不同列)
|
||||
SELECT d.name, COUNT(e.id) AS emp_count, mgr.name AS manager
|
||||
FROM departments d
|
||||
LEFT JOIN employees e ON d.id = e.dept_id
|
||||
LEFT JOIN employees mgr ON d.manager_id = mgr.id
|
||||
GROUP BY d.id;
|
||||
|
||||
-- UNION 方案(平行维度,适合合并同类数据)
|
||||
SELECT dept_id, 'total' AS metric, COUNT(*) AS value FROM employees GROUP BY dept_id
|
||||
UNION ALL
|
||||
SELECT dept_id, 'managers' AS metric, COUNT(*) AS value
|
||||
FROM employees WHERE is_manager = 1 GROUP BY dept_id;
|
||||
```
|
||||
|
||||
> [!QUESTION] UNION 还是多个查询?
|
||||
> 很多场景中 UNION 看起来方便,但背后可能有更好的解法:
|
||||
> - **应用层合并**:在 Go/Java 中发两次查询然后合并数组(零 DB 压力)
|
||||
> - **STORED PROCEDURE**:存储过程中多次查询 + 临时表
|
||||
> - **视图**:封装 UNION 逻辑供多次复用
|
||||
>
|
||||
> 核心原则:**能不在数据库做的就不做**。UNION 的代价是排序、去重、临时表。
|
||||
|
||||
## 性能对比:UNION vs 替代方案
|
||||
|
||||
| 方案 | DB 往返次数 | 临时表开销 | 排序开销 | 适用规模 |
|
||||
|------|------------|-----------|---------|---------|
|
||||
| **UNION ALL** | 1 次 | 有(结果集缓冲) | 仅外层的 ORDER BY | 百万行级 |
|
||||
| **UNION(去重)** | 1 次 | 有(唯一索引) | Sort + Dedup | 十万行以内 |
|
||||
| **应用层 N 次查询** | N 次 | 无 | 内存归并 | 任意,受网络影响 |
|
||||
| **SUM(CASE WHEN)** | 1 次 | 无 | 无 | 单表聚合计数 |
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["数据源数量 > 1"] --> B{"能否用单次扫描解决?"}
|
||||
B -->|是: 单表多条件| C["SUM CASE WHEN<br/>最优"]
|
||||
B -->|否| D{"结果集是否已知不重复?"}
|
||||
D -->|是| E["UNION ALL<br/>推荐"]
|
||||
D -->|否| F["UNION<br/>去重"]
|
||||
style C fill:#00D866,color:#fff
|
||||
style E fill:#74C0FC,color:#000
|
||||
style F fill:#FF9F43,color:#000
|
||||
```
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/12-DQL SELECT 全解析]] — SELECT 基础语法与 UNION 的结合使用
|
||||
- [[hhs/MySQL/03-CRUD 操作]] — DML 中的批量操作与 UNION 的互补关系
|
||||
+158
-100
@@ -7,37 +7,116 @@ create time: 2026-05-16 00:00
|
||||
|
||||
## 概述
|
||||
|
||||
本文件夹系统整理 MySQL 的核心知识点,从底层架构到生产实践,覆盖日常开发和高阶场景。MySQL 是世界上最流行的开源关系型数据库,掌握它不仅是后端开发的必备技能,更是理解"数据如何持久化"这一根本问题的钥匙。
|
||||
本文件夹系统整理 MySQL 的核心知识点。MySQL 是目前世界上最流行的关系型数据库,几乎所有后端项目都在用它——存用户数据、订单记录、配置信息……本质上它就是**一个帮你安全存放和查询数据的程序**。
|
||||
|
||||
> [!QUESTION] 为什么选 MySQL?
|
||||
> - **生态成熟**:社区活跃、文档完善、云厂商全覆盖(RDS、PolarDB、TencentDB)
|
||||
> - **存储引擎可插拔**:InnoDB 默认提供事务和行锁,MyISAM 适合只读分析,Memory 用于缓存
|
||||
> - **协议兼容性好**:标准 SQL + 丰富的方言扩展,驱动支持几乎所有主流语言
|
||||
> - **性能可预期**:B+ Tree 索引 + Buffer Pool + Redo Log 三层架构,让写入和查询都有章可循
|
||||
### 从零开始的三条学习路线
|
||||
|
||||
```
|
||||
路线 A(日常开发) 路线 B(深入原理) 路线 C(运维 & 高可用)
|
||||
↓ ↓ ↓
|
||||
基础语法 → SQL 查询 存储引擎 / 索引底层 主从复制 / 备份恢复
|
||||
表设计 → CRUD 操作 事务 / 隔离级别 / MVCC 高可用方案 / 故障切换
|
||||
连接池 → ORM 框架 EXPLAIN / 慢查询优化 监控 / 分库分表
|
||||
```
|
||||
|
||||
> [!QUESTION] 怎么选?
|
||||
> - **如果你是刚入门的后端开发**:走路线 A 就够了,搞定增删改查和表设计就能上手干活
|
||||
> - **如果你想面试大厂或解决线上性能问题**:继续走路线 B,理解索引和事务底层原理
|
||||
> - **如果你要负责生产环境运维**:路线 C 是必修课,主从、备份、故障切换一个不能少
|
||||
> - **理想状态**:A → B → C,循序渐进
|
||||
|
||||
### 核心概念速览(不用现在全懂)
|
||||
|
||||
下面的术语你在后面会逐一遇到,先有个印象就行:
|
||||
|
||||
| 概念 | 一句话理解 |
|
||||
|------|-----------|
|
||||
| **数据库 (Database)** | 存放表的容器,类似一个项目文件夹 |
|
||||
| **表 (Table)** | 按行和列组织的数据,像 Excel 表格 |
|
||||
| **SQL** | 跟数据库沟通的语言,比如"把这条数据查出来" |
|
||||
| **索引 (Index)** | 书的目录,让查找更快,不加索引就像一页页翻书 |
|
||||
| **事务 (Transaction)** | 一组操作要么全部成功、要么全部回滚,保证数据不出错 |
|
||||
| **InnoDB** | MySQL 默认的存储引擎,支持事务、行锁,生产环境几乎都用它 |
|
||||
|
||||
> [!TIP] 学习建议
|
||||
> 不要试图一口气吃成胖子。先学会写 `SELECT` 和 `INSERT`,看到能查出数据,再慢慢往下挖。每一个高级主题(比如事务隔离级别)都建立在前面的基础之上。
|
||||
|
||||
## 知识体系
|
||||
|
||||
### 一、入门基础
|
||||
### [一、入门基础(了解 MySQL 是什么)](./01-入门基础/README.md)
|
||||
|
||||
先装起来、连上去,知道能存什么类型的数据。
|
||||
|
||||
| # | 主题 | 说明 |
|
||||
|---|------|------|
|
||||
| 1.1 | [MySQL 架构与进程模型](./01-MySQL 架构与进程模型.md) | Client-Server 模型、连接层 / SQL 层 / 引擎层 / 存储层;Server 线程 vs Worker 线程;`mysqld` 启动流程 |
|
||||
| 1.2 | [安装与初始化](./02-安装与初始化.md) | Community Edition 安装、Docker Compose、`mysql_install_db`、配置文件层级 (`my.cnf`) |
|
||||
| 1.3 | [客户端工具](./03-客户端工具.md) | `mysql CLI`、`mysqlsh`、Workbench、HeidiSQL、DBeaver;常用 `\G`、`\h` 等快捷命令 |
|
||||
| 1.4 | [数据类型全景](./04-数据类型全景.md) | 整型(TINYINT~BIGINT)、浮点(FLOAT / DOUBLE / DECIMAL)、字符串(CHAR / VARCHAR / TEXT / BLOB)、日期时间(DATE / DATETIME / TIMESTAMP / TIME)、JSON |
|
||||
| 1.5 | [字符集与排序规则](./05-字符集与排序规则.md) | utf8mb4 为什么比 utf8 重要、Collation 的 Binlog / Case 差异、`COLLATE` 关键字 |
|
||||
| 1 | [MySQL 架构与入门](./01-入门基础/01-MySQL 架构与入门.md) | MySQL 分几层(客户端 → SQL → 引擎 → 存储);`mysqld` 是怎么启动的 |
|
||||
| 2 | [安装与初始化](./01-入门基础/02-安装与初始化.md) | 本地安装、Docker Compose 快速启动、基本配置文件 |
|
||||
| 3 | [客户端工具](./01-入门基础/03-客户端工具.md) | 命令行 `mysql`、DBeaver、Workbench,选一个顺手的就行 |
|
||||
| 4 | [数据类型全景](./01-入门基础/04-数据类型全景.md) | 整型、浮点、字符串、日期时间、JSON——每种类型适合存什么 |
|
||||
| 5 | [字符集与排序规则](./01-入门基础/05-字符集与排序规则.md) | 为什么一定要用 `utf8mb4`、中文搜索不出来的坑 |
|
||||
|
||||
> [!TIP] 选择数据类型时,遵循「够用就好」原则
|
||||
> 能用 TINYINT 就别用 INT,能用 DATETIME 就别用 STRING 存日期——这直接影响索引效率和存储空间。
|
||||
|
||||
### 二、存储引擎与表设计
|
||||
### [二、SQL 核心(学会写最常用的语句)](./02-SQL核心/README.md)
|
||||
|
||||
建表、插数据、查数据——这是日常开发 90% 的时间在干的事。
|
||||
|
||||
| # | 主题 | 说明 |
|
||||
|---|------|------|
|
||||
| 2.1 | [InnoDB 深度解析](./06-InnoDB 深度解析.md) | Clustered Index、Change Buffer、Insert Buffer、Adaptive Hash Index |
|
||||
| 2.2 | [其他存储引擎概览](./07-其他存储引擎概览.md) | MyISAM / Memory / Archive 各自适用场景、为什么生产环境几乎只用 InnoDB |
|
||||
| 2.3 | [表结构设计三范式](./08-表结构设计三范式.md) | 1NF ~ 3NF、反范式取舍、冗余字段的设计哲学 |
|
||||
| 2.4 | [主键策略对比](./09-主键策略对比.md) | Auto-increment / UUID / Snowflake / ULID —— 各方案对索引碎片化的影响 |
|
||||
| 6 | [DDL — 建表与结构变更](./02-SQL核心/06-DDL 建表与结构变更.md) | 怎么创建和修改表结构、大表加字段会不会锁表 |
|
||||
| 7 | [DML — 增删改](./02-SQL核心/07-DML 增删改.md) | INSERT / UPDATE / DELETE 的常用技巧和坑 |
|
||||
| 8 | [DQL — SELECT 全解析](./02-SQL核心/08-DQL SELECT 全解析.md) | SELECT 的执行顺序、去重、分组、分页——查对数据是日常开发的核心能力 |
|
||||
| 9 | [JOIN 原理与优化](./02-SQL核心/09-JOIN 原理与优化.md) | 多张表怎么连起来查、Nested Loop 是怎么工作的 |
|
||||
| 10 | [子查询与派生表](./02-SQL核心/10-子查询与派生表.md) | WHERE / FROM 里的子查询什么时候用、EXISTS 和 IN 有什么区别 |
|
||||
| 11 | [UNION 与集合运算](./02-SQL核心/11-UNION 与集合运算.md) | 把多个查询结果合并在一起,去重 vs 保留重复怎么处理 |
|
||||
|
||||
### [三、索引与查询优化(让 SQL 跑得更快)](./03-索引与查询优化/README.md)
|
||||
|
||||
学会写 SQL 之后,下一步是让 SQL 跑得快的——这就是索引的意义。
|
||||
|
||||
| # | 主题 | 说明 |
|
||||
|---|------|------|
|
||||
| 12 | [B+Tree 索引原理](./03-索引与查询优化/12-B+Tree 索引原理.md) | MySQL 为什么选 B+ Tree、它比 Hash / 红黑树好在哪里 |
|
||||
| 13 | [聚簇索引与二级索引](./03-索引与查询优化/13-聚簇索引与二级索引.md) | 数据存在哪棵树上、查二级索引为什么要"回表" |
|
||||
| 14 | [联合索引与最左前缀](./03-索引与查询优化/14-联合索引与最左前缀.md) | 多个字段一起建索引怎么用、为什么跳列就用不上了 |
|
||||
| 15 | [EXPLAIN 完全指南](./03-索引与查询优化/15-EXPLAIN 完全指南.md) | 怎么看 SQL 的执行计划、type 从优到差排哪些 |
|
||||
| 16 | [慢查询日志分析](./03-索引与查询优化/16-慢查询日志分析.md) | 抓出执行慢的 SQL、用工具分析根因 |
|
||||
| 17 | [查询改写技巧](./03-索引与查询优化/17-查询改写技巧.md) | OR → UNION ALL、JOIN → EXISTS,同样的结果写法影响性能 |
|
||||
| 18 | [深分页优化](./03-索引与查询优化/18-深分页优化.md) | `LIMIT 1000000, 20` 为什么慢、怎么优化 |
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["SELECT 语句"] --> B["Parse Tree"]
|
||||
B --> C["Query Optimizer"]
|
||||
C --> D["Execution Plan"]
|
||||
D --> E{type}
|
||||
E -->|"system"<| F["最优:只有1行"]
|
||||
E -->|"const"<| G["常量访问"]
|
||||
E -->|"eq_ref"<| H["唯一索引,每行1次"]
|
||||
E -->|"ref"<| I["非唯一索引,多行匹配"]
|
||||
E -->|"range"<| J["索引范围扫描"]
|
||||
E -->|"index"<| K["全索引扫描"]
|
||||
E -->|"ALL"<| L["全表扫描 ⚠️"]
|
||||
style L fill:#EE5A24,color:#fff
|
||||
style F fill:#00B6BC,color:#fff
|
||||
```
|
||||
|
||||
> [!QUESTION] 什么是最左前缀法则?
|
||||
> 假设联合索引 `(city, age, sex)`,以下情况哪些能用上索引?
|
||||
> - `WHERE city = 'Shanghai'` ✅ 第一列命中
|
||||
> - `WHERE city = 'Shanghai' AND age = 20` ✅ 前两列连续命中
|
||||
> - `WHERE age = 20 AND sex = 'M'` ❌ 跳过了第一列 city
|
||||
> - `WHERE city = 'Shanghai' AND sex = 'M'` ⚠️ 用到 city 部分,sex 需额外过滤
|
||||
|
||||
### [四、存储引擎(InnoDB 到底是怎么存的?)](./04-存储引擎/README.md)
|
||||
|
||||
学会了怎么用,来看看底层是怎么存的。这部分不是必须立刻搞懂,但理解了会受益很多。
|
||||
|
||||
| # | 主题 | 说明 |
|
||||
|---|------|------|
|
||||
| 19 | [InnoDB 深度解析](./04-存储引擎/19-InnoDB 深度解析.md) | InnoDB 是怎么存数据的、聚簇索引的工作原理、缓冲机制概览 |
|
||||
| 20 | [其他存储引擎概览](./04-存储引擎/20-其他存储引擎概览.md) | MyISAM / Memory / Archive 的适用场景,为什么生产几乎只用 InnoDB |
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
@@ -69,68 +148,32 @@ graph TB
|
||||
style H fill:#A0AEC0,color:#fff
|
||||
```
|
||||
|
||||
> [!NOTE] InnoDB 的核心缓冲机制
|
||||
> - **Buffer Pool**:热数据页的内存缓存,命中率应在 99%+
|
||||
> - **Redo Log**:保证 durability,循环写入,崩溃恢复时使用
|
||||
> - **Undo Log**:支持 MVCC 和多版本读,rollback 也用它
|
||||
> [!NOTE] InnoDB 的三个关键机制(先有个印象,后面详解)
|
||||
> - **Buffer Pool**:内存里的"草稿纸",频繁访问的数据先放这里,减少磁盘读取
|
||||
> - **Redo Log**:操作日志,数据库崩溃了靠它恢复数据
|
||||
> - **Undo Log**:撤销日志,回滚和版本控制靠它
|
||||
|
||||
### 三、SQL 精解
|
||||
### [五、表设计(怎么设计一张好表)](./05-表设计/README.md)
|
||||
|
||||
设计先行,写好的表结构能省去后面大量的麻烦。
|
||||
|
||||
| # | 主题 | 说明 |
|
||||
|---|------|------|
|
||||
| 3.1 | [DDL — 建表与结构变更](./10-DDL 建表与结构变更.md) | CREATE TABLE / ALTER TABLE、`ADD COLUMN` 在线变更、`ALGORITHM=INPLACE` |
|
||||
| 3.2 | [DML — 增删改](./11-DML 增删改.md) | INSERT ... ON DUPLICATE KEY UPDATE、REPLACE INTO、DELETE vs TRUNCATE |
|
||||
| 3.3 | [DQL — SELECT 全解析](./12-DQL SELECT 全解析.md) | SELECT 执行顺序、DISTINCT、GROUP BY 优化、LIMIT 深分页问题 |
|
||||
| 3.4 | [JOIN 原理与优化](./13-JOIN 原理与优化.md) | Inner / Left / Right / Cross JOIN、Index Merge、Nested Loop Join、Block Nested Loop |
|
||||
| 3.5 | [子查询与派生表](./14-子查询与派生表.md) | WHERE 子查询 vs FROM 子查询、EXISTS / IN / ANY 语义差异、Derived Table 物化 |
|
||||
| 3.6 | [UNION 与集合运算](./15-UNION 与集合运算.md) | 集合运算、去重 vs 保留重复、合并字段类型推断 |
|
||||
| 21 | [表结构设计三范式](./05-表设计/21-表结构设计三范式.md) | 什么是 1NF / 2NF / 3NF、什么时候该故意违反范式 |
|
||||
| 22 | [主键策略对比](./05-表设计/22-主键策略对比.md) | 自增 ID、UUID、雪花算法——各自对性能的影响 |
|
||||
|
||||
### 四、索引与查询优化
|
||||
### [六、事务与并发控制(保证数据不出错)](./06-事务与并发控制/README.md)
|
||||
|
||||
当多个用户同时操作数据时,如何保证数据不乱?这是进阶的必经之路。
|
||||
|
||||
| # | 主题 | 说明 |
|
||||
|---|------|------|
|
||||
| 4.1 | [B+ Tree 索引原理](./16-B+Tree 索引原理.md) | 为什么不用 B 树 / Hash / 红黑树?非叶子节点只存键、叶子节点链表串联 |
|
||||
| 4.2 | [聚簇索引与二级索引](./17-聚簇索引与二级索引.md) | Secondary Index 回表、Covering Index(覆盖索引)、Index Only Scan |
|
||||
| 4.3 | [联合索引与最左前缀](./18-联合索引与最左前缀.md) | (a,b,c) 的使用模式、为什么 b 单独查不了、隐式类型转换导致索引失效 |
|
||||
| 4.4 | [EXPLAIN 完全指南](./19-EXPLAIN 完全指南.md) | type 字段(ALL/index/ref/range/eq_ref/system)、Extra(Using where/index/filesort/temporary)|
|
||||
| 4.5 | [慢查询日志分析](./20-慢查询日志分析.md) | `slow_query_log` 配置、`pt-query-digest`、`mysqldumpslow` |
|
||||
| 4.6 | [查询改写技巧](./21-查询改写技巧.md) | JOIN → EXISTS 转换、UNION ALL 拆分 OR、避免函数作用于索引列 |
|
||||
| 4.7 | [深分页优化](./22-深分页优化.md) | `LIMIT 1000000, 20` 的陷阱、延迟关联(Deferred Join)、游标分页 |
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["SELECT 语句"] --> B["Parse Tree"]
|
||||
B --> C["Query Optimizer"]
|
||||
C --> D["Execution Plan"]
|
||||
D --> E{type}
|
||||
E -->|"system"<| F["最优:只有1行"]
|
||||
E -->|"const"<| G["常量访问"]
|
||||
E -->|"eq_ref"<| H["唯一索引,每行1次"]
|
||||
E -->|"ref"<| I["非唯一索引,多行匹配"]
|
||||
E -->|"range"<| J["索引范围扫描"]
|
||||
E -->|"index"<| K["全索引扫描"]
|
||||
E -->|"ALL"<| L["全表扫描 ⚠️"]
|
||||
style L fill:#EE5A24,color:#fff
|
||||
style F fill:#00B6BC,color:#fff
|
||||
```
|
||||
|
||||
> [!QUESTION] 什么是最左前缀法则?
|
||||
> 假设联合索引 `(city, age, sex)`,以下情况哪些能用上索引?
|
||||
> - `WHERE city = 'Shanghai'` ✅ 第一列命中
|
||||
> - `WHERE city = 'Shanghai' AND age = 20` ✅ 前两列连续命中
|
||||
> - `WHERE age = 20 AND sex = 'M'` ❌ 跳过了第一列 city
|
||||
> - `WHERE city = 'Shanghai' AND sex = 'M'` ⚠️ 用到 city 部分,sex 需额外过滤
|
||||
|
||||
### 五、事务与并发控制
|
||||
|
||||
| # | 主题 | 说明 |
|
||||
|---|------|------|
|
||||
| 5.1 | [ACID 与原子性实现](./23-ACID 与原子性实现.md) | Redo Log(物理 WAL)+ Undo Log(逻辑回滚)的组合拳 |
|
||||
| 5.2 | [隔离级别与可见性](./24-隔离级别与可见性.md) | Read Uncommitted / Read Committed / Repeatable Read / Serializable |
|
||||
| 5.3 | [MVCC 原理](./25-MVCC 原理.md) | Read View + Undo Log version chain、RC 下每次 SELECT 新建 Read View、RR 下第一次 SELECT 创建 |
|
||||
| 5.4 | [锁机制总览](./26-锁机制总览.md) | 全局锁、表级锁(MDL)、行级锁(Record Lock / Next-Key Lock / Gap Lock)|
|
||||
| 5.5 | [死锁与排查](./27-死锁与排查.md) | `SHOW ENGINE INNODB STATUS`、等待图、减少锁竞争的策略 |
|
||||
| 5.6 | [一致性读 vs 当前读](./28-一致性读与当前读.md) | `SELECT` 一致性读、`LOCK IN SHARE MODE / FOR UPDATE` 当前读 |
|
||||
| 23 | [ACID 与原子性实现](./06-事务与并发控制/23-ACID 与原子性实现.md) | 什么是 ACID、Redo Log + Undo Log 怎么保证不出错 |
|
||||
| 24 | [隔离级别与可见性](./06-事务与并发控制/24-隔离级别与可见性.md) | 四个隔离级别各是什么意思,MySQL 默认的是哪个 |
|
||||
| 25 | [MVCC 原理](./06-事务与并发控制/25-MVCC 原理.md) | 不加锁也能并发读的秘密——多版本并发控制 |
|
||||
| 26 | [锁机制总览](./06-事务与并发控制/26-锁机制总览.md) | 从全局锁到行级锁,MySQL 在不同场景下锁什么 |
|
||||
| 27 | [死锁与排查](./06-事务与并发控制/27-死锁与排查.md) | 什么时候会发生死锁、怎么定位和避免 |
|
||||
| 28 | [一致性读 vs 当前读](./06-事务与并发控制/28-一致性读与当前读.md) | 普通 SELECT 读到什么、加 FOR UPDATE 又读到什么 |
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
@@ -149,20 +192,22 @@ sequenceDiagram
|
||||
T1->>DB: COMMIT;
|
||||
```
|
||||
|
||||
> [!NOTE] RC vs RR 的关键区别
|
||||
> - **RC (Read Committed)**:每次 SELECT 都新建 Read View → 能看到其他事务已提交的修改(不可重复读)
|
||||
> - **RR (Repeatable Read)**:同一个事务内第一次 SELECT 创建 Read View → 整个事务看到一致快照(可重复读),配合 Next-Key Lock 解决幻影读
|
||||
> [!NOTE] RC vs RR 的关键区别(面试常考)
|
||||
> - **RC (Read Committed)**:每次查都能看到别的事务刚提交的数据 → 可能出现"不可重复读"
|
||||
> - **RR (Repeatable Read)**:整个事务内看到的是同一个快照 → 数据一致,这也是 MySQL 的默认设置
|
||||
|
||||
### 六、高可用与分布式
|
||||
### [七、高可用与分布式(让数据库永不宕机)](./07-高可用与分布式/README.md)
|
||||
|
||||
单台 MySQL 扛不住了怎么办?这就涉及到集群和复制。
|
||||
|
||||
| # | 主题 | 说明 |
|
||||
|---|------|------|
|
||||
| 6.1 | [Binary Log(Binlog)](./29-Binary Log.md) | Row / Statement / Mixed 格式、binlog_cache、`mysqlbinlog` 工具 |
|
||||
| 6.2 | [主从复制详解](./30-主从复制详解.md) | Semi-Sync 半同步、GTID 复制、并行复制(MTS)、延迟监控 |
|
||||
| 6.3 | [MHA 与 Orchestrator](./31-MHA与Orchestrator.md) | 自动故障检测与切换、脑裂处理 |
|
||||
| 6.4 | [Group Replication](./32-Group Replication.md) | Paxos 共识、单主/多主模式、冲突检测 |
|
||||
| 6.5 | [InnoDB Cluster](./33-InnoDB Cluster.md) | MySQL Shell + Router + Monitor,官方一键部署方案 |
|
||||
| 6.6 | [备份与恢复](./34-备份与恢复.md) | mysqldump(逻辑)、Percona XtraBackup(物理)、PITR 时间点恢复 |
|
||||
| 29 | [Binary Log(Binlog)](./07-高可用与分布式/29-Binary Log.md) | Binlog 记录了所有写操作,主从复制和恢复都靠它 |
|
||||
| 30 | [主从复制详解](./07-高可用与分布式/30-主从复制详解.md) | 数据怎么从主库同步到从库、半同步 vs 异步的区别 |
|
||||
| 31 | [MHA 与 Orchestrator](./07-高可用与分布式/31-MHA与Orchestrator.md) | 主库挂了怎么自动切换到从库 |
|
||||
| 32 | [Group Replication](./07-高可用与分布式/32-Group Replication.md) | 多主同时写入、冲突怎么处理 |
|
||||
| 33 | [InnoDB Cluster](./07-高可用与分布式/33-InnoDB Cluster.md) | MySQL 官方的高可用集群方案 |
|
||||
| 34 | [备份与恢复](./07-高可用与分布式/34-备份与恢复.md) | 全量备份和增量备份、恢复到任意时间点 |
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
@@ -184,20 +229,21 @@ flowchart LR
|
||||
```
|
||||
|
||||
> [!TIP] 生产环境的主从架构建议
|
||||
> - 至少 **一主两从**,使用 GTID + Semi-Sync 保证数据不丢
|
||||
> - 读写分离时注意 **弱一致性风险**:刚写的数据可能读不到
|
||||
> - 定期做 **灾备演练**,验证切换时间和数据完整性
|
||||
> - 至少 **一主两从**,读写分离时注意刚写的数据可能读不到(有延迟)
|
||||
> - 定期做 **灾备演练**,别等挂了才知道切换不成功
|
||||
|
||||
### 七、工程实践
|
||||
### [八、工程实践(生产环境中怎么管)](./08-工程实践/README.md)
|
||||
|
||||
部署上线后,连接池怎么配、数据大了怎么拆、出了问题怎么排查。
|
||||
|
||||
| # | 主题 | 说明 |
|
||||
|---|------|------|
|
||||
| 7.1 | [Go 连接池配置](./35-Go 连接池配置.md) | `sql.DB` 参数调优:MaxOpenConns、MaxIdleConns、ConnMaxLifetime、ConnMaxIdleTime |
|
||||
| 7.2 | [Schema 迁移管理](./36-Schema 迁移管理.md) | flyway / goose / migrate、版本化迁移脚本、幂等性设计 |
|
||||
| 7.3 | [分库分表](./37-分库分表.md) | ShardingSphere、Vitess、水平切分(Hash / Range / List)、跨分片查询 |
|
||||
| 7.4 | [监控指标](./38-监控指标.md) | QPS/TPS、Slow Query Count、Innodb Buffer Pool Hit Rate、Threads Connected、Replication Lag |
|
||||
| 7.5 | [安全加固](./39-安全加固.md) | 最小权限原则、SSL/TLS 加密传输、审计日志、防止 SQL 注入 |
|
||||
| 7.6 | [常见踩坑](./40-常见踩坑.md) | `ORDER BY` 文件排序、`COUNT(*)` vs `COUNT(1)`、`timestamp` 自动更新 |
|
||||
| 35 | [Go 连接池配置](./08-工程实践/35-Go 连接池配置.md) | `sql.DB` 几个关键参数的含义和调优思路 |
|
||||
| 36 | [Schema 迁移管理](./08-工程实践/36-Schema 迁移管理.md) | 版本化的建表脚本、谁在用、怎么保证可回滚 |
|
||||
| 37 | [分库分表](./08-工程实践/37-分库分表.md) | 数据量太大时怎么拆分、跨分片查询怎么办 |
|
||||
| 38 | [监控指标](./08-工程实践/38-监控指标.md) | QPS/TPS、慢查询数、连接数——哪些数据决定数据库健康 |
|
||||
| 39 | [安全加固](./08-工程实践/39-安全加固.md) | 权限管理、加密传输、SQL 注入防护 |
|
||||
| 40 | [常见踩坑](./08-工程实践/40-常见踩坑.md) | ORDER BY 文件排序、COUNT 的区别、timestamp 自动更新 |
|
||||
|
||||
[[hhs/DEV/Go-Database]] — Go 中 sql.DB 的连接池使用模式
|
||||
|
||||
@@ -205,21 +251,33 @@ flowchart LR
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
BASE["一、入门基础<br/>架构 + 数据类型"] --> CORE["二、存储引擎与表设计"]
|
||||
CORE --> SQL["三、SQL 精解<br/>DDL/DML/DQL"]
|
||||
SQL --> INDEX["四、索引与查询优化"]
|
||||
SQL --> TXN["五、事务与并发控制"]
|
||||
INDEX --> HA["六、高可用与分布式"]
|
||||
BASE["一、入门基础<br/>架构 + 数据类型"] --> SQL["二、SQL 核心<br/>DDL/DML/DQL"]
|
||||
SQL --> INDEX["三、索引与优化<br/>B+Tree/EXPLAIN/改写"]
|
||||
INDEX --> ENGINE["四、存储引擎<br/>InnoDB底层"]
|
||||
INDEX --> DESIGN["五、表设计<br/>范式/主键"]
|
||||
SQL --> TXN["六、事务与并发<br/>ACID/MVCC/锁"]
|
||||
INDEX --> HA["七、高可用与分布式<br/>复制/集群/备份"]
|
||||
TXN --> HA
|
||||
HA --> ENG["七、工程实践<br/>分库分表 + 监控 + 安全"]
|
||||
ENGINE --> PROD["八、工程实践<br/>连接池/监控/分库分表"]
|
||||
DESIGN --> PROD
|
||||
HA --> PROD
|
||||
|
||||
style BASE fill:#00B6BC,color:#fff
|
||||
style SQL fill:#4FC08D,color:#000
|
||||
style INDEX fill:#FF9F43,color:#000
|
||||
style TXN fill:#C44569,color:#fff
|
||||
style HA fill:#4FC08D,color:#fff
|
||||
style ENG fill:#EE5A24,color:#fff
|
||||
style ENGINE fill:#C44569,color:#fff
|
||||
style DESIGN fill:#A0AEC0,color:#000
|
||||
style TXN fill:#EE5A24,color:#fff
|
||||
style HA fill:#0097E6,color:#fff
|
||||
style PROD fill:#4A4A4A,color:#fff
|
||||
```
|
||||
|
||||
**推荐学习顺序**:`一 → 二 → 三 → 六 → 四 → 五 → 七 → 八`
|
||||
|
||||
> - **新手**:一 → 二 → (够用)
|
||||
> - **进阶**:一 → 二 → 三 → 六 → 四 → 五
|
||||
> - **生产环境**:全学,重点在 三、六、七、八
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/GORM/02-模型定义]] — GORM Model 如何映射到 MySQL 表和字段类型
|
||||
|
||||
Reference in New Issue
Block a user