vault backup: 2026-08-08 19:01:04
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
---
|
||||
tags: [mysql, bplus-tree, clustered-index, secondary-index, page-split]
|
||||
create time: 2026-08-08 18:00
|
||||
update time: 2026-08-08 18:00
|
||||
---
|
||||
|
||||
# B+树索引原理
|
||||
|
||||
## 概述
|
||||
|
||||
InnoDB 存储引擎的默认索引结构是 B+树(Balanced Plus Tree),它是 MySQL 实现高效范围查询和精确匹配的核心数据结构。理解 B+树的设计哲学——为什么选它而不是 B 树或红黑树——是掌握 MySQL 查询性能底层逻辑的第一步。
|
||||
|
||||
## 核心原理
|
||||
|
||||
### B 树与 B+树的本质区别
|
||||
|
||||
B+树并非独立发明,而是 B 树的演化版本。两者的核心差异在于**数据存放位置**:
|
||||
|
||||
- **B 树**:每个节点都存储键值和对应数据记录
|
||||
- **B+树**:非叶子节点只存键(不存数据),所有数据统一在叶子节点
|
||||
|
||||
这意味着同样大小的页(通常 16KB),B+树的非叶子节点可以容纳更多键,从而降低树的高度。
|
||||
|
||||
| 对比维度 | B 树 | B+树 |
|
||||
|---------|------|------|
|
||||
| 数据存储 | 所有节点均存数据 | 仅叶子节点存数据 |
|
||||
| 非叶子节点作用 | 既做路由又存数据 | 纯路由,无数据负担 |
|
||||
| 叶子节点连接 | 无连接 | 双向链表串联 |
|
||||
| 范围查询 | 需中序遍历,效率低 | 直接遍历叶子链表,一步到位 |
|
||||
| 磁盘 IO 次数 | 较高 | 更低(同等数据量下树更矮) |
|
||||
| 查询稳定性 | 不同路径长度可能不同 | 所有查询路径等长 |
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "B 树 - 每个节点含数据"
|
||||
B_Root["根节点\nK1 D1"] -->|"≤K1"| B_L["左子节点\nK2 D2"]
|
||||
B_Root -->|"K1<K≤K2"| B_M["中间节点\nK3 D3"]
|
||||
B_Root -->|">K2"| B_R["右子节点\nK4 D4"]
|
||||
end
|
||||
|
||||
subgraph "B+树 - 仅叶子节点含数据"
|
||||
N_Root["根节点\nK2"] -->|"≤K2"| N_Inter["内节点\nK1 K3"]
|
||||
N_Root -->|">K2"| N_Right["内节点\nK4 K5"]
|
||||
N_Inter -->|"≤K1"| L1["叶子 K1→D1"]
|
||||
N_Inter -->|"K1~K3"| L2["叶子 K2→D2"]
|
||||
N_Inter -->|"K3~K4"| L3["叶子 K3→D3"]
|
||||
N_Right -->|"K4~K5"| L4["叶子 K4→D4"]
|
||||
N_Right -->|">K5"| L5["叶子 K5→D5"]
|
||||
L1 -.->|"双向链表"| L2
|
||||
L2 -.-> L3
|
||||
L3 -.-> L4
|
||||
L4 -.-> L5
|
||||
end
|
||||
```
|
||||
|
||||
> [!NOTE] 关键直觉
|
||||
> 树的高度每降低一层,意味着一次查询减少一次磁盘 IO。B+树通过让内节点只存索引而不存数据,让单个页能放下更多键,从而将 1 亿行数据的树高控制在 3~4 层,而 B 树可能需要 4~5 层。
|
||||
|
||||
### 页分裂与页合并机制
|
||||
|
||||
B+树的每个节点对应 InnoDB 的一个数据页(Page),默认大小为 16KB。当数据插入导致页满时,触发再平衡操作。
|
||||
|
||||
**页分裂规则**:
|
||||
- 原页保留最左侧约一半的记录,新页获得剩余记录
|
||||
- 父节点增加一个分割键(split key),指向新页
|
||||
- 如果父节点也满了,递归向上分裂,直到找到不满的父节点或到达根节点
|
||||
- 根节点分裂时,树的高度 +1
|
||||
|
||||
**页合并规则**:
|
||||
- 删除操作使页填充率低于一定阈值(默认约 50%,可通过 `innodb_fill_factor` 调整)时,尝试与相邻页合并
|
||||
- 合并方向优先选择右侧页;若右侧页也无法合并,则尝试左侧
|
||||
- 合并后,父节点中的分割键被移除
|
||||
|
||||
> [!WARNING] 常见误区
|
||||
> 很多人认为页分裂发生在"页使用率达到 100%"时,实际上 InnoDB 在预分页阶段就会考虑空间利用率。InnoDB 采用"首次分裂占 7/8,后续平分"的策略来预留碎片空间,避免频繁分裂。
|
||||
|
||||
### 聚簇索引与非聚簇索引
|
||||
|
||||
这是 InnoDB 索引体系中最核心的概念之一。
|
||||
|
||||
**聚簇索引(Clustered Index)**:
|
||||
- InnoDB 的数据文件和索引文件是同一份——这就是"聚簇"的含义
|
||||
- 主键索引就是聚簇索引,叶子节点存储整行完整数据
|
||||
- 一张表有且仅有**一个**聚簇索引
|
||||
|
||||
**二级索引(Secondary Index / Non-Clustered Index)**:
|
||||
- 除主键之外的所有索引都是二级索引
|
||||
- 叶子节点存储的是**索引列的值 + 主键值**,而非完整行数据
|
||||
- 通过二级索引查到主键后,再用主键回表查询完整数据
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph "聚簇索引(主键索引)叶子节点"
|
||||
CI1["PK=1 → 完整行"]
|
||||
CI2["PK=5 → 完整行"]
|
||||
CI3["PK=10 → 完整行"]
|
||||
end
|
||||
|
||||
subgraph "二级索引(name字段)叶子节点"
|
||||
SI1["name='Alice' → PK=1"]
|
||||
SI2["name='Bob' → PK=5"]
|
||||
SI3["name='Charlie' → PK=10"]
|
||||
end
|
||||
|
||||
SI1 --> CI1
|
||||
SI2 --> CI2
|
||||
SI3 --> CI3
|
||||
```
|
||||
|
||||
> [!TIP] 面试常考点
|
||||
> 问:"为什么主键要尽量短?" 答案:因为所有二级索引叶子节点都存储了主键值,主键越长,二级索引越大,内存中能缓存的索引页数越少,命中率越低。这也是推荐使用自增 BIGINT 而非 UUID 作为主键的原因之一。
|
||||
|
||||
### 覆盖索引的起点
|
||||
|
||||
B+树的二级索引结构天然支持"覆盖索引"优化——当查询的列全部存在于某个二级索引中时,无需回表即可返回结果。这一话题将在 [[覆盖索引与回表优化]] 中详细展开。
|
||||
|
||||
## 代码示例
|
||||
|
||||
以下 SQL 演示如何通过执行计划观察聚簇索引与二级索引的使用差异:
|
||||
|
||||
```sql
|
||||
-- 建表并建立索引
|
||||
CREATE TABLE users (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
email VARCHAR(128) UNIQUE,
|
||||
age INT NOT NULL,
|
||||
INDEX idx_name_age (name, age)
|
||||
);
|
||||
|
||||
-- 使用覆盖索引:不需要回表
|
||||
EXPLAIN SELECT name FROM users WHERE name = 'Alice';
|
||||
-- Extra: Using index (直接从 idx_name_age 二级索引获取 name)
|
||||
|
||||
-- 需要回表:二级索引查不到所需列
|
||||
EXPLAIN SELECT * FROM users WHERE name = 'Alice';
|
||||
-- Extra: Using where(先走 idx_name_age 拿到 id,再回聚簇索引取全行)
|
||||
```
|
||||
|
||||
## 实践场景
|
||||
|
||||
**场景一:选型主键**
|
||||
- 优先使用自增 BIGINT 或 BIGINT UNSIGNED 作为主键,保持顺序写入减少页分裂
|
||||
- 绝对避免用 UUID 或随机字符串作为主键——乱序插入会导致频繁的页分裂和碎片
|
||||
|
||||
**场景二:联合索引的顺序设计**
|
||||
- 区分度高的列放前面(如 user_id 优于 status)
|
||||
- 等值查询列在前,范围查询列在后(范围查询会中断最左前缀匹配)
|
||||
|
||||
**场景三:监控页分裂**
|
||||
```sql
|
||||
-- 查看表中页的使用情况
|
||||
SELECT table_name, data_length, index_length
|
||||
FROM information_schema.tables
|
||||
WHERE engine = 'InnoDB';
|
||||
|
||||
-- 填充率低说明页分裂频繁,考虑 OPTIMIZE TABLE
|
||||
```
|
||||
|
||||
## 扩展阅读
|
||||
- [[覆盖索引与回表优化]]
|
||||
- [[Explain 执行计划解读]]
|
||||
- [[ACID 与 MVCC 机制]]
|
||||
@@ -0,0 +1,172 @@
|
||||
---
|
||||
tags: [mysql, explain, execution-plan, index-pushdown, filesort]
|
||||
create time: 2026-08-08 18:00
|
||||
update time: 2026-08-08 18:00
|
||||
---
|
||||
|
||||
# Explain 执行计划解读
|
||||
|
||||
## 概述
|
||||
|
||||
`EXPLAIN` 是 MySQL 提供的最重要的性能诊断工具。它在 SQL 真正执行之前,模拟优化器生成 SQL 的执行计划,揭示查询是如何使用索引、如何连接表、是否有临时表和文件排序等关键信息。掌握 EXPLAIN 输出各字段的含义,是将经验式调优转化为精准打击的前提。
|
||||
|
||||
## 核心原理
|
||||
|
||||
### 执行计划各字段详解
|
||||
|
||||
#### type 字段 —— 访问类型(性能由好到差)
|
||||
|
||||
type 描述了 MySQL 如何访问表中的数据,是评估查询质量的首要指标。
|
||||
|
||||
| type | 名称 | 含义 | 性能 |
|
||||
|------|------|------|------|
|
||||
| `system` | 系统表 | 表仅有一行(system 表),常数级读取 | ★★★★★ |
|
||||
| `const` | 常量 | 最多匹配一行,通过主键/唯一索引一次性定位 | ★★★★★ |
|
||||
| `eq_ref` | 等值引用 | 前一个表的每一行,当前表通过唯一索引匹配一行 | ★★★★☆ |
|
||||
| `ref` | 引用 | 通过非唯一索引或主键的前缀匹配多行 | ★★★☆☆ |
|
||||
| `range` | 范围 | 索引范围扫描,如 BETWEEN、IN、>、< | ★★☆☆☆ |
|
||||
| `index` | 全索引扫描 | 遍历整个索引树,不走表数据 | ★★☆☆☆ |
|
||||
| `ALL` | 全表扫描 | 逐行扫描表数据,未使用任何索引 | ☆☆☆☆☆ |
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["system"] -->|"最优"| B["const"]
|
||||
B --> C["eq_ref"]
|
||||
C --> D["ref"]
|
||||
D --> E["range"]
|
||||
E --> F["index"]
|
||||
F --> G["ALL"]
|
||||
G -->|"最差"| H["需要优化"]
|
||||
|
||||
style A fill:#90ee90
|
||||
style B fill:#90ee90
|
||||
style C fill:#90ee90
|
||||
style D fill:#ffeb99
|
||||
style E fill:#ffeb99
|
||||
style F fill:#ffcc99
|
||||
style G fill:#ff9999
|
||||
style H fill:#ff6666
|
||||
```
|
||||
|
||||
> [!TIP] 面试常考点
|
||||
> 为什么 index 比 ALL 好?`index` 虽然是全表扫描级别,但它只扫描索引树(通常为有序排列,可利用顺序 IO),而 `ALL` 需要扫描数据行(随机 IO)。所以 `index < ALL`。
|
||||
|
||||
#### select_type 字段 —— 查询类型
|
||||
|
||||
| select_type | 含义 |
|
||||
|-------------|------|
|
||||
| `SIMPLE` | 简单查询,不包含子查询或 UNION |
|
||||
| `PRIMARY` | 最外层查询(配合 SUBQUERY/UNION 等出现) |
|
||||
| `SUBQUERY` | 子查询中的 SELECT(非 FROM 子句中) |
|
||||
| `DERIVED` | 派生表(FROM 子句中的子查询) |
|
||||
| `UNION` | UNION 中的第二个及以后的 SELECT |
|
||||
| `UNION RESULT` | UNION 的结果集 |
|
||||
|
||||
```sql
|
||||
-- 示例:复杂查询的 select_type
|
||||
SELECT * FROM orders o
|
||||
WHERE o.user_id IN (
|
||||
SELECT u.id FROM users u WHERE u.status = 1
|
||||
)
|
||||
UNION
|
||||
SELECT * FROM orders_archive WHERE archived = 1;
|
||||
-- 输出行的 select_type 依次为:PRIMARY / SUBQUERY / UNION / UNION_RESULT
|
||||
```
|
||||
|
||||
#### possible_keys vs key vs key_len
|
||||
|
||||
- **possible_keys**:优化器认为可能用到的索引列表(只是"候选",不代表最终会选用)
|
||||
- **key**:实际选择的索引
|
||||
- **key_len**:使用的索引长度(字节数),反映索引列参与的程度
|
||||
|
||||
```sql
|
||||
-- 复合索引 (name VARCHAR(64), age INT)
|
||||
-- utf8mb4 每个字符 4 字节,name 的 key_len = 64 * 4 = 256 字节
|
||||
-- 加上变长标记 1 字节 + NULL 标记 1 字节 = 258
|
||||
-- 如果 key_len = 258,说明用了 name 列
|
||||
-- 如果 key_len = 262 (= 258 + 4),说明同时用了 name 和 age
|
||||
|
||||
SELECT * FROM users WHERE name = 'Alice' AND age = 25;
|
||||
-- key: idx_name_age, key_len: 262 → 两个列都用上了
|
||||
```
|
||||
|
||||
#### rows × filtered —— 行数估算
|
||||
|
||||
- **rows**:优化器估算的需要扫描的行数(越小越好)
|
||||
- **filtered**:按表条件筛选后,留存行的百分比(0~100)
|
||||
- **rows × filtered%** ≈ 实际需要处理的行数
|
||||
|
||||
> [!WARNING] 估算偏差
|
||||
> rows 和 filtered 是统计采样估算值,并非精确计数。如果表的统计信息过期,可能导致严重偏差。执行 `ANALYZE TABLE t;` 可以更新统计信息。
|
||||
|
||||
### Extra 常见值解析
|
||||
|
||||
| Extra 值 | 含义 | 建议 |
|
||||
|----------|------|------|
|
||||
| `Using where` | 存储引擎返回数据后,Server 层再做 WHERE 过滤 | 正常,但如果没走索引说明需要加索引 |
|
||||
| `Using index` | 覆盖索引,无需回表 | 最好的情况 |
|
||||
| `Using temporary` | 使用了临时表解决查询 | 通常出现在 GROUP BY 或 DISTINCT,需要优化 |
|
||||
| `Using filesort` | 需要额外排序,无法利用索引顺序 | 考虑加索引消除排序 |
|
||||
| `Using index condition` | 索引下推(ICP),在存储引擎层过滤 | 好消息,说明 MySQL 自动优化了 |
|
||||
| `Using sort_union()` / `Using union()` | 使用了多个索引合并后再排序 | 考虑改造成单索引 |
|
||||
| `Impossible where` | WHERE 条件永远不成立 | 检查 SQL 逻辑 |
|
||||
| `Select tables optimized away` | 最小化优化,无需访问表 | 最优情况 |
|
||||
|
||||
```sql
|
||||
-- 看到 WARNING 时的排查思路
|
||||
EXPLAIN SELECT * FROM orders
|
||||
WHERE YEAR(created_at) = 2025;
|
||||
-- Extra: Using where; Using filesort
|
||||
-- 问题:YEAR() 函数作用于字段 → 无法使用索引
|
||||
-- 优化:改用范围查询
|
||||
-- WHERE created_at >= '2025-01-01' AND created_at < '2026-01-01'
|
||||
```
|
||||
|
||||
## 代码示例
|
||||
|
||||
```sql
|
||||
-- 典型 EXPLAIN 输出解读
|
||||
EXPLAIN SELECT u.name, o.amount
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.status = 1
|
||||
ORDER BY o.created_at DESC
|
||||
LIMIT 10;
|
||||
|
||||
-- 解读要点:
|
||||
-- 第一行:u 表 → select_type=PRIMARY, type=ref (status 上有索引)
|
||||
-- → key=idx_status, rows≈500, filtered=100%
|
||||
-- 第二行:o 表 → type=ref (user_id 是外键索引), eq_ref
|
||||
-- → 对每个 u 的行,通过 user_id 精确匹配一条订单
|
||||
-- Extra: Using index condition → ICP 生效
|
||||
-- Using filesort → created_at 上没有合适索引,需额外排序
|
||||
-- 优化方向:给 orders(created_at) 建索引,消除 filesort
|
||||
```
|
||||
|
||||
## 实践场景
|
||||
|
||||
**场景一:日常慢查询排查**
|
||||
1. 开启慢查询日志(`long_query_time=1`)
|
||||
2. 对慢 SQL 加 `EXPLAIN` 前缀执行
|
||||
3. 检查 type 是否退化到 `ALL` 或 `index`
|
||||
4. 关注 Extra 中的 `Using temporary` 和 `Using filesort`
|
||||
5. 针对问题点调整索引或改写 SQL
|
||||
|
||||
**场景二:判断索引是否被有效利用**
|
||||
```sql
|
||||
-- 如果发现某张表的 key 列为 NULL,说明该查询完全没有使用索引
|
||||
-- 检查 possible_keys 是否有可选索引但未被选中
|
||||
-- 如果是,可能是统计信息过时,执行 ANALYZE TABLE
|
||||
|
||||
ANALYZE TABLE orders;
|
||||
```
|
||||
|
||||
**场景三:JOIN 查询优化**
|
||||
- 小表驱动大表:JOIN 中较小的表放在前面(EXPLAIN 靠上的表先被执行)
|
||||
- 确保 JOIN 条件两侧都有对应的索引
|
||||
- 避免在多列上使用 OR 连接 JOIN 条件,这会迫使优化器退化为全表扫描
|
||||
|
||||
## 扩展阅读
|
||||
- [[B+树索引原理]]
|
||||
- [[覆盖索引与回表优化]]
|
||||
- [[ACID 与 MVCC 机制]]
|
||||
@@ -0,0 +1,174 @@
|
||||
---
|
||||
tags: [mysql, covering-index, index-pushdown, most-left-prefix, composite-index]
|
||||
create time: 2026-08-08 18:00
|
||||
update time: 2026-08-08 18:00
|
||||
---
|
||||
|
||||
# 覆盖索引与回表优化
|
||||
|
||||
## 概述
|
||||
|
||||
覆盖索引(Covering Index)是 MySQL 查询优化的核心技术之一。当查询所需的列全部可以由某个索引直接提供时,引擎无需访问聚簇索引中的完整行数据,从而大幅减少磁盘 IO 和 CPU 开销。理解回表的成本、如何识别覆盖索引以及如何通过索引下推(ICP)进一步降低回表次数,是每个后端工程师调优必会的技能。
|
||||
|
||||
## 核心原理
|
||||
|
||||
### 回表的完整过程
|
||||
|
||||
回表是指通过二级索引查询到主键后,再用主键回到聚簇索引中获取完整行记录的步骤。这个过程涉及两次 B+树查找:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Q as 查询请求
|
||||
participant SI as 二级索引 B+树
|
||||
participant PK as 主键值
|
||||
participant CI as 聚簇索引 B+树
|
||||
participant Row as 完整行数据
|
||||
participant R as 返回结果
|
||||
|
||||
Q->>SI: 根据二级索引条件定位
|
||||
SI-->>Q: 找到满足条件的记录
|
||||
Note over SI,Q: 例:idx_status(status)<br/>status='active' → PK=1001
|
||||
Q->>PK: 提取主键值 PK=1001
|
||||
|
||||
PK->>CI: 以 PK=1001 在聚簇索引中查找
|
||||
CI-->>PK: 定位到对应叶子节点
|
||||
PK->>Row: 取出完整行数据
|
||||
Row->>R: 返回 {id:1001, status:'active', ...}
|
||||
```
|
||||
|
||||
**回表成本分析**:
|
||||
- 每次回表都是一次独立的 B+树搜索,至少涉及 3~4 次磁盘 IO(假设树高为 3~4 层)
|
||||
- 如果二次查询需要过滤大量数据,回表次数成倍增长
|
||||
- 回表造成的随机 IO 比顺序 IO 慢数十倍
|
||||
|
||||
### 覆盖索引的概念与识别
|
||||
|
||||
**覆盖索引**:查询只需要从索引树中就能获取所有需要的数据,无需回表。
|
||||
|
||||
判断是否覆盖索引的方法非常简单:看 EXPLAIN 结果的 `Extra` 列是否出现 **"Using index"**。
|
||||
|
||||
```sql
|
||||
EXPLAIN SELECT id, name FROM users WHERE name = 'Alice';
|
||||
-- Extra: Using index
|
||||
-- 解释:idx_name 索引已包含 name 和隐含的主键 id,无需回表
|
||||
```
|
||||
|
||||
注意:`Using index` 并不等同于"用了覆盖索引",还需要结合具体索引定义来判断。更准确的判断方式是看 `Key` 列使用的索引是否真的覆盖了查询的所有列。
|
||||
|
||||
> [!TIP] MySQL 8.0 新增指示符
|
||||
> - `Using index condition`:索引下推(ICP),部分过滤在存储引擎层完成
|
||||
> - `Using where; Using index`:真正的覆盖索引,无需回表
|
||||
> - `Using index`(不带 where):可能是前缀覆盖,需确认查询列是否完全在索引中
|
||||
|
||||
### 索引下推(Index Condition Pushdown, ICP)
|
||||
|
||||
ICP 是 MySQL 5.6 引入的优化技术,用于减少回表次数。
|
||||
|
||||
**没有 ICP 的情况**:
|
||||
1. 二级索引遍历到满足条件的记录
|
||||
2. 立即回表取完整行
|
||||
3. 在 Server 层用 WHERE 条件过滤
|
||||
|
||||
**有 ICP 的情况**:
|
||||
1. 二级索引遍历到候选记录
|
||||
2. **在存储引擎层先用索引中包含的列做 WHERE 过滤**
|
||||
3. 只有通过过滤的记录才回表
|
||||
|
||||
这省去了不必要的回表 IO,尤其对多条件查询中最后一个不在索引前列的条件非常有效。
|
||||
|
||||
```sql
|
||||
-- 复合索引 (name, age, email)
|
||||
-- WHERE name = 'Alice' AND age > 20
|
||||
-- name 在索引前列,age 也在索引中 → ICP 可发挥作用
|
||||
-- 如果改为 WHERE name = 'Alice' AND email = 'a@x.com'
|
||||
-- email 不在索引前列但仍在索引树中,ICP 仍有效
|
||||
```
|
||||
|
||||
### 最左前缀原则详解
|
||||
|
||||
复合索引 `(col1, col2, col3)` 的匹配规则类似于前缀树匹配:
|
||||
|
||||
```sql
|
||||
INDEX idx_abc (a, b, c);
|
||||
|
||||
a ✅ 走索引全部三段
|
||||
a, b ✅ 走索引前两段
|
||||
a, b, c ✅ 走索引全部三段
|
||||
b ❌ 跳过了 a,不走索引(除非有单独的 b 索引)
|
||||
a, c ⚠️ 只用 a 这一段索引,c 无法使用前缀匹配
|
||||
b, c ❌ 跳过 a,不走索引
|
||||
a, range_on_b, c ⚠️ a 和 b(range) 使用索引,但 c 因 b 的范围查询中断,不使用索引
|
||||
```
|
||||
|
||||
> [!WARNING] 范围查询断链陷阱
|
||||
> 最常见的误解是"a,c 也能用到 c"。实际上,当遇到范围查询(>、<、BETWEEN、LIKE 'prefix%')时,该列之后的索引列失效。例如:`WHERE a = 1 AND b > 10 AND c = 3`,只有 a 和 b 使用了索引,c 无法利用索引过滤。
|
||||
|
||||
### 复合索引设计最佳实践
|
||||
|
||||
1. **区分度高(基数大)的列放前面**
|
||||
- 用户 ID 的区分度远高于性别,前者应放在复合索引的前面
|
||||
- 可以用 `SELECT COUNT(DISTINCT col) / COUNT(*) AS selectivity` 估算
|
||||
|
||||
2. **等值查询在前,范围查询在后**
|
||||
- 范围查询一旦命中,同索引后面的列就无法利用
|
||||
|
||||
3. **避免过度索引**
|
||||
- 每个额外的索引都会增加 INSERT/UPDATE 的成本
|
||||
- 一条 SQL 只能使用一个索引(MySQL 8.0 之前)
|
||||
|
||||
4. **考虑排序需求**
|
||||
- 如果经常按 `(status, created_at)` 排序,可以直接建这个复合索引,避免 filesort
|
||||
|
||||
## 代码示例
|
||||
|
||||
```sql
|
||||
-- 建表示例
|
||||
CREATE TABLE orders (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
status TINYINT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL,
|
||||
amount DECIMAL(10,2),
|
||||
INDEX idx_uid_status (user_id, status),
|
||||
INDEX idx_created (created_at)
|
||||
);
|
||||
|
||||
-- 覆盖索引案例:查询只涉及索引列
|
||||
EXPLAIN SELECT user_id, status FROM orders WHERE user_id = 100 AND status = 1;
|
||||
-- Key: idx_uid_status
|
||||
-- Extra: Using index ← 覆盖索引,不回表
|
||||
|
||||
-- 非覆盖索引案例:需要回表
|
||||
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 1;
|
||||
-- Key: idx_uid_status
|
||||
-- Extra: Using where ← 需要回表
|
||||
|
||||
-- ICP 优化案例:MySQL 5.6+ 自动启用
|
||||
EXPLAIN SELECT id, user_id, status FROM orders
|
||||
WHERE user_id = 100 AND status IN (1, 2, 3)
|
||||
AND amount > 100;
|
||||
-- amount 不在 idx_uid_status 索引中 → 需要回表
|
||||
-- 但 user_id + status 在索引中 → ICP 可以先过滤 status
|
||||
```
|
||||
|
||||
## 实践场景
|
||||
|
||||
**场景一:报表查询优化**
|
||||
- 常见的统计类查询往往只需要几个聚合列,完全可以构建专门的覆盖索引
|
||||
- 例如 `COUNT(user_id)` 只需建 `(status, user_id)` 索引即可覆盖,避免扫描整张表
|
||||
|
||||
**场景二:高频接口缓存穿透防护**
|
||||
- 二级接口返回固定字段列表时,确保这些字段恰好落在某个二级索引上
|
||||
- 这样即使缓存失效,DB 层的查询开销也非常小
|
||||
|
||||
**场景三:监控回表率**
|
||||
```sql
|
||||
-- 通过慢查询日志观察是否需要回表
|
||||
SHOW GLOBAL STATUS LIKE 'Handler_read%';
|
||||
-- Handler_read_next 持续增长且数值很高 → 可能存在大量回表
|
||||
```
|
||||
|
||||
## 扩展阅读
|
||||
- [[B+树索引原理]]
|
||||
- [[Explain 执行计划解读]]
|
||||
- [[ACID 与 MVCC 机制]]
|
||||
@@ -0,0 +1,181 @@
|
||||
---
|
||||
tags: [mysql, acido-mvcc, undo-log, read-view, repeatable-read]
|
||||
create time: 2026-08-08 18:00
|
||||
update time: 2026-08-08 18:00
|
||||
---
|
||||
|
||||
# ACID 与 MVCC 机制
|
||||
|
||||
## 概述
|
||||
|
||||
MVCC(Multi-Version Concurrency Control,多版本并发控制)是 InnoDB 实现幻读隔离的核心机制。它通过维护数据的多个历史版本,使得读写操作互不阻塞,在保证事务隔离性的同时大幅提升并发性能。理解 MVCC 的实现细节——undo log、Read View、可见性判断——对于深入把握 MySQL 事务行为至关重要。
|
||||
|
||||
## 核心原理
|
||||
|
||||
### Undo Log 的结构
|
||||
|
||||
Undo Log 是 InnoDB 为了实现 MVCC 和事务回滚而维护的一种日志,它记录了事务修改前的旧数据版本。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "Buffer Pool — 当前最新版本"
|
||||
RowA["行记录 A<br/>version=3, TRX_ID=T5"]
|
||||
end
|
||||
|
||||
subgraph "Undo Log Chain — 历史版本链"
|
||||
Version3["版本3: val='updated_v3'<br/>TRX_ID=T5, next_undo=Version2"]
|
||||
Version2["版本2: val='updated_v2'<br/>TRX_ID=T4, next_undo=Version1"]
|
||||
Version1["版本1: val='original_val'<br/>TRX_ID=T3, next_undo=NULL"]
|
||||
end
|
||||
|
||||
RowA -->|"DB_TRX_ID 指向"| Version3
|
||||
Version3 --> Version2
|
||||
Version2 --> Version1
|
||||
```
|
||||
|
||||
Undo Log 的两类内容:
|
||||
- **redo log**:物理日志,记录"在某处做了什么修改",用于崩溃恢复
|
||||
- **undo log**:逻辑日志,记录"做了什么事情",用于回滚和 MVCC
|
||||
|
||||
每个行记录隐藏了两列:
|
||||
- **DB_TRX_ID**:最近修改该行的事务 ID(6 字节)
|
||||
- **DB_ROLL_PTR**:回滚指针,指向 undo log 中上一个版本的地址(7 字节)
|
||||
|
||||
> [!NOTE] Rollback Segment 的组织方式
|
||||
> InnoDB 将 undo log 组织在 Rollback Segment 中,分为 undo log header 和 undo log record 两部分。每个事务的修改按时间顺序追加到 segment 尾部,形成版本号链。
|
||||
|
||||
### Read View 的生成规则
|
||||
|
||||
Read View 是 MVCC 的核心数据结构,定义了事务在某一时刻"能看到哪些版本"。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "RC 隔离级别 - ReadView 生成时机"
|
||||
RC_T1["事务开始"] --> RC_Q1["第一次查询时"]
|
||||
RC_Q1 --> RC_ReadView["生成 ReadView"]
|
||||
RC_ReadView --> RC_Q2["第二次查询时"]
|
||||
RC_Q2 --> RC_NewRV["再次生成新的 ReadView"]
|
||||
end
|
||||
|
||||
subgraph "RR 隔离级别 - ReadView 生成时机"
|
||||
RR_T1["事务开始"] --> RR_FirstQ["第一次查询时"]
|
||||
RR_FirstQ --> RR_ReadView["生成 ReadView"]
|
||||
RR_ReadView --> RR_Q2["后续查询"]
|
||||
RR_Q2 --> RR_SameRV["复用同一个 ReadView"]
|
||||
end
|
||||
|
||||
style RC_ReadView fill:#ffeeaa
|
||||
style RC_NewRV fill:#ffeeaa
|
||||
style RR_ReadView fill:#99ffcc
|
||||
style RR_SameRV fill:#99ffcc
|
||||
```
|
||||
|
||||
**RC(Read Committed)和 RR(Repeatable Read)的关键区别**:
|
||||
| 特性 | RC | RR |
|
||||
|------|----|----|
|
||||
| ReadView 生成时机 | 每条 SELECT 语句开始时生成 | 第一次查询时生成,后续复用 |
|
||||
| 快照读可见性 | 每次查询都是最新提交的快照 | 全局一致视图,所有查询所见相同 |
|
||||
| 解决幻读能力 | 不能解决 | 配合 Next-Key Lock 可以解决 |
|
||||
|
||||
**ReadView 的成员变量**:
|
||||
- `m_ids`:生成 ReadView 时当前活跃的事务 ID 列表
|
||||
- `m_low_limit_id`:最小的活跃事务 ID(下一个将要分配的事务 ID)
|
||||
- `m_up_limit_id`:最大活跃事务 ID + 1
|
||||
- `m_trx_id_level`:活跃事务的最小 trx_id
|
||||
|
||||
### 可见性判断流程
|
||||
|
||||
这是 MVCC 最核心的算法:给定一个行版本和一个 ReadView,判断当前事务是否能看到这个版本。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start["开始: 行版本 trx_id + ReadView"] --> Compare1{"trx_id < m_up_limit_id?"}
|
||||
|
||||
Compare1 -->|否| CheckActive{"trx_id ∈ m_ids?"}
|
||||
Compare1 -->|是| Visible["✓ 可见"]
|
||||
|
||||
CheckActive -->|否| Visible
|
||||
CheckActive -->|是| Compare2{"trx_id < m_low_limit_id?"}
|
||||
|
||||
Compare2 -->|是| Invisible["✗ 不可见<br/>事务尚未提交"]
|
||||
Compare2 -->|否| CheckOwn{"trx_id = 当前事务ID?"}
|
||||
|
||||
CheckOwn -->|是| VisibleSelf["✓ 可见<br/>自己修改的版本"]
|
||||
CheckOwn -->|否| Compare3{"trx_id 已提交?"}
|
||||
|
||||
Compare3 -->|是| Visible
|
||||
Compare3 -->|否| Invisible
|
||||
|
||||
style Visible fill:#90ee90
|
||||
style VisibleSelf fill:#90ee90
|
||||
style Invisible fill:#ff9999
|
||||
```
|
||||
|
||||
**简化记忆口诀**:
|
||||
1. **小于 up_limit_id** → 肯定可见(生成 ReadView 前就提交了)
|
||||
2. **大于等于 low_limit_id** → 肯定不可见(生成 ReadView 后才启动的)
|
||||
3. **介于两者之间** → 再看是否在活跃列表中,以及是不是自己
|
||||
|
||||
### Next-Key 与 ReadView 的配合
|
||||
|
||||
RR 隔离级别下,InnoDB 用"MVCC + Next-Key Lock"双管齐下来解决幻读:
|
||||
- **快照读**(普通 SELECT)靠 MVCC/ReadView 解决
|
||||
- **当前读**(SELECT ... FOR UPDATE / LOCK IN SHARE MODE / UPDATE / DELETE)靠 Next-Key Lock 解决
|
||||
|
||||
```sql
|
||||
-- 快照读:不会锁住区间,依赖 ReadView 保证一致性
|
||||
SELECT * FROM orders WHERE status = 1;
|
||||
|
||||
-- 当前读:加锁,防止其他事务插入或修改
|
||||
SELECT * FROM orders WHERE status = 1 FOR UPDATE;
|
||||
```
|
||||
|
||||
## 代码示例
|
||||
|
||||
```sql
|
||||
-- 演示 RC vs RR 在读已提交数据时的差异
|
||||
|
||||
-- 会话 1:设置隔离级别为 RR(默认)
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
|
||||
BEGIN;
|
||||
SELECT balance FROM accounts WHERE id = 1;
|
||||
-- 此时 balance = 1000
|
||||
|
||||
-- 会话 2:修改并提交
|
||||
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
|
||||
BEGIN;
|
||||
UPDATE accounts SET balance = 2000 WHERE id = 1;
|
||||
COMMIT;
|
||||
|
||||
-- 会话 1:再次查询
|
||||
SELECT balance FROM accounts WHERE id = 1;
|
||||
-- RR: 仍然返回 1000(复用了初始 ReadView)
|
||||
-- 如果换成 RC: 返回 2000(重新生成 ReadView)
|
||||
```
|
||||
|
||||
## 实践场景
|
||||
|
||||
**场景一:理解为什么 RR 下"看不到别人刚提交的数据"**
|
||||
- 在 RR 中,事务内的所有快照读看到的是同一个一致性视图
|
||||
- 如果你需要在事务中看到其他事务的最新提交结果,可以在适当时候设置 `SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;`(需单独事务生效)或在应用层做补偿查询
|
||||
|
||||
**场景二:监控 undo log 空间**
|
||||
```sql
|
||||
-- 查看 undo log 使用情况
|
||||
SELECT * FROM sys.innodb_old_tablespaces;
|
||||
|
||||
-- 长时间运行的未提交事务会产生大量 undo 记录
|
||||
-- 影响 buffer pool 的内存占用
|
||||
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id
|
||||
FROM information_schema.innodb_trx
|
||||
WHERE trx_state = 'RUNNING' AND TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60;
|
||||
```
|
||||
|
||||
**场景三:避免长事务导致的 undo 膨胀**
|
||||
- 不要在事务中进行大量无关操作
|
||||
- 批量更新拆分成小批次事务
|
||||
- 定时清理长时间运行的事务:kill 异常休眠的线程
|
||||
|
||||
## 扩展阅读
|
||||
- [[B+树索引原理]]
|
||||
- [[事务隔离级别与锁机制]]
|
||||
@@ -0,0 +1,191 @@
|
||||
---
|
||||
tags: [mysql, transaction-isolation, gap-lock, next-key-lock, deadlock-detection]
|
||||
create time: 2026-08-08 18:00
|
||||
update time: 2026-08-08 18:00
|
||||
---
|
||||
|
||||
# 事务隔离级别与锁机制
|
||||
|
||||
## 概述
|
||||
|
||||
MySQL InnoDB 实现了四种标准事务隔离级别(READ UNCOMMITTED / READ COMMITTED / REPEATABLE READ / SERIALIZABLE),每种级别对应不同的锁策略和并发冲突处理能力。理解行锁、间隙锁(Gap Lock)、临键锁(Next-Key Lock)的工作原理及其交互关系,是分析和解决死锁、幻读、并发更新冲突等问题的基础。
|
||||
|
||||
## 核心原理
|
||||
|
||||
### 行锁、间隙锁与临键锁的关系
|
||||
|
||||
InnoDB 的锁不是单一维度的,而是由多种锁类型组合而成:
|
||||
|
||||
| 锁类型 | 锁定范围 | 适用场景 |
|
||||
|--------|---------|---------|
|
||||
| Record Lock | 索引记录本身 | 单行精确锁定 |
|
||||
| Gap Lock | 索引记录之间的"间隙",不含记录本身 | 防止其他事务在该间隙插入新记录 |
|
||||
| Next-Key Lock | Record Lock + Gap Lock | 锁定区间(前开后闭),默认锁粒度 |
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph "索引序列: 1, 5, 10, 15, 20"
|
||||
G1["(-∞, 1)"] -->|"Gap Lock"| R1["1: Record Lock"]
|
||||
R1 -->|"Gap Lock"| G2["(1, 5)"]
|
||||
G2 -->|"Gap Lock"| R2["5: Record Lock"]
|
||||
R2 -->|"Gap Lock"| G3["(5, 10)"]
|
||||
G3 -->|"Gap Lock"| R3["10: Record Lock"]
|
||||
R3 -->|"Gap Lock"| G4["(10, 15)"]
|
||||
G4 -->|"Gap Lock"| R4["15: Record Lock"]
|
||||
R4 -->|"Gap Lock"| G5["(15, +∞)"]
|
||||
end
|
||||
|
||||
style G1 fill:#ffe0e0
|
||||
style G2 fill:#ffe0e0
|
||||
style G3 fill:#ffe0e0
|
||||
style G4 fill:#ffe0e0
|
||||
style G5 fill:#ffe0e0
|
||||
style R1 fill:#e0ffe0
|
||||
style R2 fill:#e0ffe0
|
||||
style R3 fill:#e0ffe0
|
||||
style R4 fill:#e0ffe0
|
||||
```
|
||||
|
||||
**关键结论**:
|
||||
- 默认情况下(RR 隔离级别),InnoDB 使用 Next-Key Lock,即 Record + Gap 的组合
|
||||
- 如果使用唯一的索引(如主键)等值查询,Gap Lock 会被取消,只剩 Record Lock
|
||||
- GC 隔离级别下,Gap Lock 被关闭(只加 Record Lock),这是它与 RR 的本质区别之一
|
||||
|
||||
### 锁的兼容矩阵
|
||||
|
||||
| 已有锁 ↓ \ 请求锁 → | Record R (共享锁) | Record X (排他锁) | Gap R | Gap X |
|
||||
|---------------------|------------------|------------------|-------|-------|
|
||||
| Record R | ✅ 兼容 | ❌ 冲突 | ✅ 兼容 | ❌ 冲突 |
|
||||
| Record X | ❌ 冲突 | ❌ 冲突 | ❌ 冲突 | ❌ 冲突 |
|
||||
| Gap R | ✅ 兼容 | ❌ 冲突 | ✅ 兼容 | ❌ 冲突 |
|
||||
| Gap X | ❌ 冲突 | ❌ 冲突 | ✅ 兼容* | ❌ 冲突 |
|
||||
|
||||
> [!NOTE] 共享锁与排他锁
|
||||
> 共享锁(S Lock)允许多个事务同时持有,但与其他 S/X 锁都不兼容。排他锁(X Lock)独占资源,不与其他任何锁兼容。INSERT/UPDATE/DELETE 隐式加 X 锁,SELECT ... FOR UPDATE 显式加 X 锁,SELECT ... LOCK IN SHARE MODE 加 S 锁。
|
||||
|
||||
### 死锁检测机制
|
||||
|
||||
InnoDB 使用**等待图(Wait-For Graph)**进行死锁检测:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph "等待图示例"
|
||||
T1["事务 T1"] -->|"等待"| L1["锁 L1"]
|
||||
L1 -->|"持有者"| T2["事务 T2"]
|
||||
T2 -->|"等待"| L2["锁 L2"]
|
||||
L2 -->|"持有者"| T3["事务 T3"]
|
||||
T3 -->|"等待"| L3["锁 L3"]
|
||||
L3 -->|"持有者"| T1
|
||||
end
|
||||
|
||||
CycleCheck{"检测到环<br/>T1→T2→T3→T1"}
|
||||
CycleCheck -->|是| ChooseVictim["选择牺牲者<br/>回滚代价最小的事务"]
|
||||
CycleCheck -->|否| ContinueWait["继续等待"]
|
||||
ChooseVictim --> ReleaseDeadlock["释放死锁<br/>回滚牺牲者的事务"]
|
||||
```
|
||||
|
||||
**死锁参数配置**:
|
||||
| 参数 | 默认值 | 含义 |
|
||||
|------|--------|------|
|
||||
| `innodb_lock_wait_timeout` | 50 秒 | 等待锁的超时时间(不涉及死锁检测) |
|
||||
| `innodb_deadlock_detect` | ON | 是否开启死锁检测 |
|
||||
| `innodb_max_dirty_pages_pct` | 75% | 脏页比例阈值,超过时强制刷新 |
|
||||
|
||||
```
|
||||
死锁处理流程:
|
||||
1. 事务 A 申请锁 B → 被事务 B 持有 → 加入等待队列
|
||||
2. 事务 B 申请锁 A → 被事务 A 持有 → 检测到环路
|
||||
3. InnoDB 选择"回滚代价最小"的事务作为牺牲者
|
||||
4. 回滚牺牲者的当前语句(非整个事务),释放其持有的锁
|
||||
5. 另一事务继续执行
|
||||
```
|
||||
|
||||
> [!WARNING] 牺牲者选择策略
|
||||
> 回滚的是"当前语句"而非"整个事务"。如果语句包含多条 SQL,只回滚最后一条。这意味着事务可以继续重试后续语句,不会中断整个业务流程。
|
||||
|
||||
### Insert Intention Gap Lock
|
||||
|
||||
Insert Intention Gap Lock 是一种特殊的间隙锁,专门用于 INSERT 操作。它表示"我准备在这个间隙插入一条记录"。
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph "索引值: 5 和 10"
|
||||
R5["记录 5"] -->|"Gap (5,10)"| G1["间隙区"]
|
||||
G1 -->|"Gap (5,10)"| R10["记录 10"]
|
||||
end
|
||||
|
||||
T1["事务 T1: INSERT 值=7"] -->|"申请 Insert Intention<br/>Gap Lock on (5,10)"| G1
|
||||
T2["事务 T2: INSERT 值=8"] -->|"申请 Insert Intention<br/>Gap Lock on (5,10)"| G1
|
||||
G1 -->|"Insert Intention 锁互相兼容"| T2
|
||||
```
|
||||
|
||||
两条 INSERT 语句即使目标间隙重叠,它们的 Insert Intention Gap Lock 也是**互相兼容**的。但如果其中一条已经是该间隙上的 Gap Lock(来自 UPDATE/DELETE),则新的 INSERT 会等待。
|
||||
|
||||
## 代码示例
|
||||
|
||||
### 场景分析:更新某区间内不存在记录时会加什么锁
|
||||
|
||||
```sql
|
||||
-- 假设表中有记录: id IN (1, 5, 10, 15, 20)
|
||||
-- 事务 A 执行:
|
||||
UPDATE orders SET status = 1 WHERE id > 5 AND id < 15;
|
||||
|
||||
-- InnoDB 的行为分析:
|
||||
-- 1. 扫描到 id=5 的记录 → Next-Key Lock: (5, 10](Record+Gap)
|
||||
-- 2. 发现 id=10 的记录 → Record Lock: 10 本身(但 id=10 满足条件,被修改)
|
||||
-- 3. 继续扫描,没有 id=11~14 的记录 → Next-Key Lock: (10, 15]
|
||||
-- 4. 扫描到 id=15 → Record Lock: 15 本身(但不满足条件,不加锁)
|
||||
-- 5. 最终锁定的范围:(5, 15],包括间隙和存在的记录
|
||||
|
||||
-- 此时另一个事务执行:
|
||||
-- 事务 B: INSERT INTO orders (id, status) VALUES (12, 0);
|
||||
-- 结果:被阻塞!因为 (10, 15) 间隙已被事务 A 锁定
|
||||
```
|
||||
|
||||
```sql
|
||||
-- 验证锁类型的 SQL 写法
|
||||
-- 会话 1:开始事务并加锁
|
||||
START TRANSACTION;
|
||||
SELECT * FROM orders WHERE id = 10 FOR UPDATE;
|
||||
-- 由于 id 是主键(唯一索引),只加 Record Lock 在 10 上
|
||||
|
||||
-- 会话 2:
|
||||
SELECT * FROM orders WHERE id = 10 FOR UPDATE;
|
||||
-- 阻塞!
|
||||
|
||||
SELECT * FROM orders WHERE id = 5 FOR UPDATE;
|
||||
-- 不会被阻塞(5 ≠ 10,主键等值查询的 Gap Lock 已被去掉)
|
||||
|
||||
-- 如果是非唯一索引:
|
||||
-- 索引 (status) 上有重复值,UPDATE ... WHERE status=1
|
||||
-- 会对所有 status=1 的记录加 Record Lock + Gap Lock
|
||||
```
|
||||
|
||||
## 实践场景
|
||||
|
||||
**场景一:排查生产环境死锁**
|
||||
```sql
|
||||
-- InnoDB 会打印死锁详情到错误日志
|
||||
-- 也可以通过 performance_schema 查看
|
||||
SELECT * FROM performance_schema.data_locks;
|
||||
SELECT * FROM performance_schema.data_lock_waits;
|
||||
|
||||
-- 分析死锁日志的步骤:
|
||||
-- 1. 找到死锁发生的时间点和涉及的 SQL
|
||||
-- 2. 画出等待图,确定循环等待链
|
||||
-- 3. 按照推荐方案调整 SQL 执行顺序或加锁范围
|
||||
```
|
||||
|
||||
**场景二:减少间隙锁的影响**
|
||||
- 如果需要在高并发环境下批量插入数据,可以将隔离级别降到 RC
|
||||
- RC 级别下 InnoDB 不使用 Gap Lock,INSERT 操作不会阻塞
|
||||
- 但要注意:RC 不能解决幻读问题,需评估业务可接受度
|
||||
|
||||
**场景三:合理的索引设计降低锁竞争**
|
||||
- 尽量使用唯一索引(主键)做精确更新,可以避免 Gap Lock
|
||||
- 避免在大宽表的非唯一索引上做大范围 UPDATE/DELETE
|
||||
- 大批量更新时分批次进行,缩短持有锁的时间
|
||||
|
||||
## 扩展阅读
|
||||
- [[B+树索引原理]]
|
||||
- [[ACID 与 MVCC 机制]]
|
||||
Reference in New Issue
Block a user