数据库锁
数据库锁用于管理并发访问,确保数据一致性。
锁介绍
共享锁(S锁)
允许多个事务同时读取数据,但阻止其他事务获取排他锁(读读共享)。适用于读操作。
排他锁(X锁)
独占锁,阻止其他事务读取或修改数据(读写互斥)。适用于写操作。
意向锁
表级锁,表明事务将在表中某些行上获取共享锁或排他锁。分为意向共享锁(IS)和意向排他锁(IX)。
行锁
行锁粒度更细,减少锁冲突,使用for update。
表锁
表锁开销小,但并发性低,一般在同步或备份数据使用
死锁处理
通过超时机制或死锁检测(如等待图算法)解决。
锁列分析对比
| 锁类型 | 级别/用途 | 如何手动使用 | 主要优点 | 主要缺点 | 核心场景 |
|---|---|---|---|---|---|
| 共享锁(S锁) | 行级/读操作 | SELECT ... LOCK IN SHARE MODE; | 允许多事务并发读 | 阻塞其他事务的排他锁请求 | 高并发查询 |
| 排他锁(X锁) | 行级/写操作 | SELECT ... FOR UPDATE; | 确保数据修改的独占性 | 阻塞其他所有锁请求 | 更新或删除数据 |
| 意向共享锁(IS) | 表级/预示行级S锁 | 隐式由数据库管理 | 快速判断表内是否有行级S锁 | 增加锁管理开销 | 辅助行级S锁的批量操作 |
| 意向排他锁(IX) | 表级/预示行级X锁 | 隐式由数据库管理 | 快速判断表内是否有行级X锁 | 增加锁管理开销 | 辅助行级X锁的批量操作 |
| 行锁 | 行级/细粒度控制 | UPDATE ... WHERE ...;(默认触发) | 减少锁冲突,提高并发 | 锁管理开销较大 | 高频单行修改 |
| 表锁 | 表级/粗粒度控制 | LOCK TABLES ... READ/WRITE; | 实现简单,开销低 | 并发性差 | 数据迁移或备份 |
关键说明
- 死锁处理:通过设置锁超时(如
innodb_lock_wait_timeout)或启用死锁检测(如InnoDB的等待图算法)自动回滚事务。 - 锁升级:当行锁数量超过阈值时,数据库可能自动升级为表锁(如SQL Server)。
- 隐式锁:某些操作(如DML语句)会自动加锁,无需手动声明。
代码示例(MySQL)
-- 显式加共享锁
SELECT * FROM employees WHERE id = 1 LOCK IN SHARE MODE;
-- 显式加排他锁
SELECT * FROM employees WHERE id = 1 FOR UPDATE;
-- 显式表锁(需手动释放)
LOCK TABLES employees WRITE;
UNLOCK TABLES;
数据库索引
索引总览
1、按存储结构分类(最核心)
| 索引类型 | 数据结构 | 工作原理 | 适用场景 | 优点 | 缺点 | 支持的数据库 |
|---|---|---|---|---|---|---|
| B树/B+树索引 | 平衡多路查找树 | 分层索引,有序存储,支持范围扫描 | 大多数场景:主键、外键、范围查询、排序 | 1. 支持范围查询和排序 2. 高度平衡,查询稳定 3. 适合磁盘存储(减少I/O) 4. 支持前缀匹配 | 1. 插入/删除需维护平衡(成本高) 2. 空间占用较大 3. 全模糊查询( LIKE '%xx')无效 | MySQL(InnoDB)、Oracle、SQL Server、PostgreSQL |
| 哈希索引 | 哈希表 | 计算哈希值,直接定位 | 等值查询、内存表、热点数据加速 | 1. 等值查询极快(O(1)) 2. 内存占用相对较小 | 1. 不支持范围查询 2. 不支持排序 3. 不支持部分索引列查询 4. 哈希冲突影响性能 | MySQL(MEMORY引擎)、PostgreSQL、SQL Server |
| 全文索引 | 倒排索引 | 分词→文档映射,建立词元索引 | 文本搜索、内容检索 | 1. 支持自然语言搜索 2. 支持相关性排序 3. 支持布尔搜索 | 1. 占用空间大 2. 创建维护成本高 3. 停用词处理复杂 | MySQL(MyISAM/InnoDB)、SQL Server、PostgreSQL |
| 空间索引 | R树/R*树 | 空间分区,最小边界矩形 | 地理位置查询、GIS系统 | 1. 高效支持空间查询 2. 支持空间关系计算 3. 适合多维数据 | 1. 实现复杂 2. 仅适用于空间数据 3. 更新成本较高 | MySQL(MyISAM/InnoDB)、PostGIS、Oracle Spatial |
| 位图索引 | 位图向量 | 列值→位图映射,位运算过滤 | 数据仓库、低基数列、OLAP | 1. 空间占用小 2. 多列查询快(位运算) 3. 适合OLAP分析 | 1. 不适合高基数列 2. 更新代价大(锁整个位图) 3. 不适合OLTP高频更新 | Oracle、SQL Server、PostgreSQL |
2、按物理存储分类
| 索引类型 | 存储方式 | 数据与索引关系 | 查询过程 | 优点 | 缺点 | 典型实现 |
|---|---|---|---|---|---|---|
| 聚簇索引 | 数据即索引 | 表数据按索引顺序物理存储 | 直接通过索引找到数据行 | 1. 主键查询极快 2. 范围查询高效 3. 避免回表开销 | 1. 插入依赖主键顺序 2. 更新主键代价高 3. 二级索引需回表 | InnoDB主键索引 |
| 非聚簇索引 | 索引与数据分离 | 索引存储指向数据的指针 | 先查索引,再通过指针找数据 | 1. 索引文件小 2. 创建维护灵活 | 1. 需要回表(二次查找) 2. 查询性能相对较低 | MyISAM索引、InnoDB二级索引 |
3、按逻辑功能分类
| 索引类型 | 唯一性 | 空值约束 | 数量限制 | 主要用途 | 创建语法示例 |
|---|---|---|---|---|---|
| 主键索引 | ✅ 唯一 | ❌ 不允许NULL | 1个/表 | 1. 唯一标识行 2. 建立表关联 | CREATE TABLE t(id INT PRIMARY KEY)ALTER TABLE t ADD PRIMARY KEY(col) |
| 唯一索引 | ✅ 唯一 | ✅ 允许NULL | 多个/表 | 1. 数据完整性约束 2. 避免重复数据 | CREATE UNIQUE INDEX idx_name ON t(col)ALTER TABLE t ADD UNIQUE(col) |
| 普通索引 | ❌ 不唯一 | ✅ 允许NULL | 多个/表 | 1. 加速查询 2. 优化连接查询 | CREATE INDEX idx_name ON t(col)ALTER TABLE t ADD INDEX idx_name(col) |
| 组合索引 | 可选 | 各列分别约束 | 多个/表 | 1. 多列查询优化 2. 覆盖索引 3. 避免回表 | CREATE INDEX idx ON t(col1, col2, col3) |
| 前缀索引 | 可选 | 同普通索引 | 多个/表 | 1. 节约存储空间 2. 长文本字段索引 | CREATE INDEX idx ON t(col(10))(取前10个字符) |
| 函数索引 | 可选 | 同普通索引 | 多个/表 | 1. 优化函数表达式查询 2. 大小写不敏感查询 | CREATE INDEX idx ON t(LOWER(col))CREATE INDEX idx ON t(YEAR(date_col)) |
| 覆盖索引 | 索引类型 | 实现方式 | 查询结果来源 | 性能优势 | 创建要点 |
| 组合索引的特殊应用 | 索引包含所有查询列 | 直接从索引获取,无需访问数据表 | 1. 避免回表操作 2. 极大提升查询性能 | 将SELECT和WHERE中的列都包含在索引中 |
4、索引特性对比矩阵
| 维度 | B+树索引 | 哈希索引 | 全文索引 | 位图索引 | 空间索引 |
|---|---|---|---|---|---|
| 等值查询 | 优秀 | 极佳 | 差(需全文匹配) | 极佳 | 差 |
| 范围查询 | 优秀 | 不支持 | 不支持 | 差(需扫描) | 优秀 |
| 排序支持 | 优秀 | 不支持 | 有限(相关性排序) | 不支持 | 差 |
| 模糊查询 | 前缀匹配优秀 后缀匹配差 | 不支持 | 优秀 | 不支持 | 不支持 |
| 更新性能 | 中等 | 优秀 | 差 | 极差 | 中等 |
| 空间占用 | 中等 | 小 | 极大 | 小(低基数时) | 大 |
| 适用基数 | 高/中/低 | 高/中 | 文本数据 | 低基数 | 空间数据 |
| 典型场景 | OLTP系统 通用查询 | 缓存表 热点数据 | 搜索引擎 内容检索 | 数据仓库 分析系统 | GIS系统 地图应用 |
5、高级索引类型(特殊实现)
| 索引类型 | 数据库 | 说明 | 适用场景 |
|---|---|---|---|
| 自适应哈希索引 | MySQL(InnoDB) | 自动为热点数据创建内存哈希索引 | 热点数据等值查询加速 |
| 倒排索引 | Elasticsearch | 全文索引的分布式实现 | 大规模全文搜索 |
| 布隆过滤器索引 | PostgreSQL | 概率数据结构,快速判断"不存在" | 减少不必要磁盘I/O |
| GiST索引 | PostgreSQL | 通用搜索树,支持多种数据类型 | 自定义数据类型索引 |
| GIN索引 | PostgreSQL | 倒排索引,支持数组、全文搜索 | 多值列、全文搜索 |
| 列存储索引 | SQL Server | 按列存储,高度压缩 | 数据仓库、分析查询 |
6、聚簇索引&非聚簇索引
6.1. 聚簇索引存储(InnoDB)
-- User表:id为主键,name有二级索引
-- 物理存储结构:
聚簇索引B+Tree:
┌─────────────────────────────────┐
│ Root Page │
│ [id:20, id:40, id:60] │
└──────────┬────────┬─────────────┘
│ │ │
┌─────┘ ┌────┘ ┌────┘
▼ ▼ ▼
┌─────────┐┌─────────┐┌─────────┐
│叶子页1 ││叶子页2 ││叶子页3 │ ← 叶子节点=数据页
│id=1-19 ││id=20-39 ││id=40-59 │
│实际数据行││实际数据行││实际数据行│
└─────────┘└─────────┘└─────────┘
关键特性:
- 叶子节点就是数据页,存储完整行数据
- 数据按主键顺序物理存储
- 表数据 = 聚簇索引
6.2. 非聚簇索引存储(InnoDB二级索引)
-- name列的二级索引存储:
非聚簇索引B+Tree:
┌─────────────────────────────────┐
│ Root Page │
│ [name:'David', 'Frank'] │
└──────────┬────────┬─────────────┘
│ │ │
┌─────┘ ┌────┘ ┌────┘
▼ ▼ ▼
┌─────────┐┌─────────┐┌─────────┐
│叶子页1 ││叶子页2 ││叶子页3 │ ← 只存索引键+主键
│'Alice' ││'Bob' ││'Charlie' │
│id:1,5,7 ││id:2,8 ││id:3,9 │
└─────────┘└─────────┘└─────────┘
↓ ↓ ↓
需要回表到聚簇索引获取完整数据!
关键特性:
- 叶子节点存储:索引键值 + 主键值
- 需要回表查询获取完整数据
- 索引文件与数据文件分离
6.3、总结对比表
| 维度 | 聚簇索引 | 非聚簇索引 | 优势方 |
|---|---|---|---|
| 数据组织 | 按主键物理排序 | 按插入顺序存储 | 视场景 |
| 查询性能 | 主键查询极快,范围查询高效 | 所有查询需额外I/O | ✅ 聚簇 |
| 插入性能 | 可能页分裂,较慢 | 追加写入,较快 | ✅ 非聚簇 |
| 更新性能 | 更新主键代价高 | 更新索引列代价高 | 平手 |
| 空间占用 | 节省主键索引空间 | 索引独立存储,可能更多 | ✅ 聚簇 |
| 并发控制 | 行级锁,支持事务 | 表级锁,不支持事务 | ✅ 聚簇 |
| 恢复能力 | 支持崩溃恢复 | 恢复较慢 | ✅ 聚簇 |
| 适用场景 | OLTP,点查询为主 | OLAP,批量加载 | 各有所长 |
索引存储原理分析
主要以B+Tree和B-Tree来分析,核心区别是B-Tree,节点都存数据,B+Tree 内部节点索引,叶子节点底层以双向链表的来处理
1、数据表结构与示例数据
User表结构
CREATE TABLE User (
id INT PRIMARY KEY, -- 主键(聚簇索引)
name VARCHAR(50), -- 用户名
age INT, -- 年龄
sex CHAR(1), -- 性别:'M'/'F'
createDate DATETIME -- 创建时间,
email VARCHAR(100), -- 邮箱
city VARCHAR(50), -- 城市
status TINYINT -- 状态
);
-- 创建联合索引(name, sex)
CREATE INDEX idx_name_sex ON User(name, sex);
模拟数据(7条,更丰富的场景)
| id | name | sex | age | createDate | 实际磁盘地址 |
|---|---|---|---|---|---|
| 1 | Alice | F | 25 | 2024-01-01 10:00:00 | 0x1000 |
| 2 | Bob | M | 30 | 2024-01-02 11:00:00 | 0x2000 |
| 3 | Charlie | M | 28 | 2024-01-03 12:00:00 | 0x3000 |
| 4 | David | M | 35 | 2024-01-04 13:00:00 | 0x4000 |
| 5 | Eve | F | 22 | 2024-01-05 14:00:00 | 0x5000 |
| 6 | Frank | M | 40 | 2024-01-06 15:00:00 | 0x6000 |
| 7 | Alice | M | 26 | 2024-01-07 16:00:00 | 0x7000 |
联合索引 (name, sex) 的索引键值
1>. 索引键值生成与排序规则
联合索引 (name, sex) 的完整键值:(name, sex, id)
先按name排序,name相同按sex排序,都相同按id排序
排序过程:
原始数据 -> 排序后:
(Alice, F, 1) # Alice F组,按id排序
(Alice, M, 7) # Alice M组
(Bob, M, 2) # Bob
(Charlie, M, 3) # Charlie
(David, M, 4) # David
(Eve, F, 5) # Eve
(Frank, M, 6) # Frank
排序键值数组:
1. ('Alice', 'F', 1) → 0x1000
2. ('Alice', 'M', 7) → 0x7000
3. ('Bob', 'M', 2) → 0x2000
4. ('Charlie', 'M', 3) → 0x3000
5. ('David', 'M', 4) → 0x4000
6. ('Eve', 'F', 5) → 0x5000
7. ('Frank', 'M', 6) → 0x6000
2、B-Tree结构存储(假设3阶)
B-Tree结构图(数据分散在各层)
[ (David,M,4) ] ← 根节点(存储数据)
/ \
/ \
[ (Alice,F,1) ] [ (Eve,F,5) ] ← 中间节点(存储数据)
/ | \ / | \
[ ] [(Alice,M,7)] [(Bob,M,2)] [(Charlie,M,3)] [(Frank,M,6)] ← 叶子节点(存储数据)
B-Tree节点详细内容(假设每节点最多3个键值)
节点1:根节点
┌─────────────────────────────────────────────────────┐
│ B-Tree Root Node (Page) │
├─────────┬──────────────┬─────────┬──────────────────┤
│ Ptr0 │ Key1 | DataPtr1│ Ptr1 │
│ (→节点2)│ (David,M,4) │ →0x4000 │ (→节点3) │
└─────────┴──────────────┴─────────┴──────────────────┘
↑ 存储了(David,M,4)的实际数据指针!
节点2:左子树中间节点
┌─────────────────────────────────────────────────────┐
│ B-Tree Non-Leaf Node │
├─────────┬──────────────┬─────────┬──────────────┬───┤
│ Ptr0 │ Key1 | DataPtr1│ Ptr1 │ │
│ (→叶子A)│ (Alice,F,1) │ →0x1000 │ (→叶子B) │ │
└─────────┴──────────────┴─────────┴──────────────┴───┘
↑ 存储了(Alice,F,1)的实际数据指针!
节点3:右子树中间节点
┌─────────────────────────────────────────────────────┐
│ B-Tree Non-Leaf Node │
├─────────┬──────────────┬─────────┬──────────────┬───┤
│ Ptr0 │ Key1 | DataPtr1│ Ptr1 │ │
│ (→叶子C)│ (Eve,F,5) │ →0x5000 │ (→叶子D) │ │
└─────────┴──────────────┴─────────┴──────────────┴───┘
叶子节点A-D
叶子节点A(最左边):
┌─────────────────────────────────────────────────────┐
│ B-Tree Leaf Node │
├─────────┬──────────────┬─────────┬──────────────┬───┤
│ Ptr0 │ Key1 | DataPtr1│ Key2 │ │
│ (NULL) │ (空槽位) │ │ (空槽位) │ │
└─────────┴──────────────┴─────────┴──────────────┴───┘
叶子节点B:
┌─────────────────────────────────────────────────────┐
│ B-Tree Leaf Node │
├─────────┬──────────────┬─────────┬──────────────┬───┤
│ Ptr0 │ Key1 | DataPtr1│ Key2 │ │
│ (→叶子A)│ (Alice,M,7) │ →0x7000 │ (Bob,M,2) │ │
├─────────┼──────────────┼─────────┼──────────────┼───┤
│ DataPtr2│ Ptr1 │ DataPtr3│ │ │
│ →0x2000 │ (→叶子C) │ →0x... │ │ │
└─────────┴──────────────┴─────────┴──────────────┴───┘
叶子节点C:
┌─────────────────────────────────────────────────────┐
│ B-Tree Leaf Node │
├─────────┬──────────────┬─────────┬──────────────┬───┤
│ Ptr0 │ Key1 | DataPtr1│ Key2 │ │
│ (→叶子B)│ (Charlie,M,3)│ →0x3000 │ (空槽位) │ │
└─────────┴──────────────┴─────────┴──────────────┴───┘
叶子节点D:
┌─────────────────────────────────────────────────────┐
│ B-Tree Leaf Node │
├─────────┬──────────────┬─────────┬──────────────┬───┤
│ Ptr0 │ Key1 | DataPtr1│ Key2 │ │
│ (→叶子C)│ (Frank,M,6) │ →0x6000 │ (空槽位) │ │
└─────────┴──────────────┴─────────┴──────────────┴───┘
B-Tree查询示例
-- 查询1:查找 name='Alice', sex='M'
1. 访问根节点:[(David,M,4)]
Alice < David,走左子树(Ptr0)
2. 访问中间节点:[(Alice,F,1)]
找到(Alice,F,1),但我们要找(Alice,M,7)
继续向下走Ptr1(右子树)
3. 到达叶子节点B:[(Alice,M,7), (Bob,M,2)]
✅ 找到(Alice,M,7),直接返回数据指针→0x7000
-- 查询2:范围查询 name BETWEEN 'C' AND 'E'
1. 查找'Charlie':根节点→左子树→叶子节点C
找到(Charlie,M,3) →0x3000
2. 继续找'David':需要回溯到根节点
从根节点开始:(David,M,4)就在根节点!
直接返回→0x4000
3. 找'Eve':从根节点重新开始
根节点:Eve > David,走右子树
中间节点:[(Eve,F,5)],找到→0x5000
⚠️ 多次随机回溯,效率低!
3、B+Tree结构存储(3阶)
B+Tree结构图(数据只在叶子节点)
[ David ] ← 根节点(纯索引)
/ \
/ \
[ Alice ] [ Eve ] ← 非叶子节点(纯索引)
/ \ / \
| | | |
▼ ▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│叶子1 │ │叶子2 │ │叶子3 │ │叶子4 │ ← 叶子节点链表
│Alice │ │Alice │ │Bob │ │Charlie│
│F,1 │ │M,7 │ │M,2 │ │M,3 │
└──────┘ └──────┘ └──────┘ └──────┘
↓ ↓ ↓ ↓
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│David │ │Eve │ │Frank │ │ │
│M,4 │ │F,5 │ │M,6 │ │ │
└──────┘ └──────┘ └──────┘ └──────┘
B+Tree节点详细内容
节点1:根节点(纯索引)
┌─────────────────────────────────────────┐
│ B+Tree Root Node │
├─────────┬─────────┬─────────┬───────────┤
│ Ptr0 │ Key1 │ Ptr1 │ │
│ (→节点2)│ "David" │ (→节点3)│ │
└─────────┴─────────┴─────────┴───────────┘
↑ 只存索引键"David",不存数据指针!
规则:键值 < "David" 走Ptr0,≥ "David" 走Ptr1
节点2:左非叶子节点
┌─────────────────────────────────────────┐
│ B+Tree Non-Leaf Node │
├─────────┬─────────┬─────────┬───────────┤
│ Ptr0 │ Key1 │ Ptr1 │ │
│ (→叶子1)│ "Alice" │ (→叶子2)│ │
└─────────┴─────────┴─────────┴───────────┘
↑ 只存"Alice"作为分隔键
节点3:右非叶子节点
┌─────────────────────────────────────────┐
│ B+Tree Non-Leaf Node │
├─────────┬─────────┬─────────┬───────────┤
│ Ptr0 │ Key1 │ Ptr1 │ │
│ (→叶子3)│ "Eve" │ (→叶子4)│ │
└─────────┴─────────┴─────────┴───────────┘
叶子节点1-4(双向链表连接)
叶子节点1:
┌─────────────────────────────────────────────────────┐
│ B+Tree Leaf Node │
├──────────────┬──────────────┬───────────────────────┤
│ Key1 | DataPtr1 │ Next → 叶子2 │
│ (Alice,F,1) │ →0x1000 │ Prev ← NULL │
├──────────────┼──────────────┼───────────────────────┤
│ Key2 | DataPtr2 │ │
│ (空槽位) │ │ │
└──────────────┴──────────────┴───────────────────────┘
叶子节点2:
┌─────────────────────────────────────────────────────┐
│ B+Tree Leaf Node │
├──────────────┬──────────────┬───────────────────────┤
│ Key1 | DataPtr1 │ Next → 叶子3 │
│ (Alice,M,7) │ →0x7000 │ Prev ← 叶子1 │
├──────────────┼──────────────┼───────────────────────┤
│ Key2 | DataPtr2 │ │
│ (空槽位) │ │ │
└──────────────┴──────────────┴───────────────────────┘
叶子节点3:
┌─────────────────────────────────────────────────────┐
│ B+Tree Leaf Node │
├──────────────┬──────────────┬───────────────────────┤
│ Key1 | DataPtr1 │ Next → 叶子4 │
│ (Bob,M,2) │ →0x2000 │ Prev ← 叶子2 │
├──────────────┼──────────────┼───────────────────────┤
│ Key2 | DataPtr2 │ │
│ (Charlie,M,3)│ →0x3000 │ │
└──────────────┴──────────────┴───────────────────────┘
叶子节点4:
┌─────────────────────────────────────────────────────┐
│ B+Tree Leaf Node │
├──────────────┬──────────────┬───────────────────────┤
│ Key1 | DataPtr1 │ Next → 叶子5 │
│ (David,M,4) │ →0x4000 │ Prev ← 叶子3 │
├──────────────┼──────────────┼───────────────────────┤
│ Key2 | DataPtr2 │ │
│ (Eve,F,5) │ →0x5000 │ │
└──────────────┴──────────────┴───────────────────────┘
叶子节点5:
┌─────────────────────────────────────────────────────┐
│ B+Tree Leaf Node │
├──────────────┬──────────────┬───────────────────────┤
│ Key1 | DataPtr1 │ Next → NULL │
│ (Frank,M,6) │ →0x6000 │ Prev ← 叶子4 │
├──────────────┼──────────────┼───────────────────────┤
│ Key2 | DataPtr2 │ │
│ (空槽位) │ │ │
└──────────────┴──────────────┴───────────────────────┘
B+Tree查询示例
-- 查询1:查找 name='Alice', sex='M'
1. 访问根节点:["David"]
Alice < David,走左子树(Ptr0)
2. 访问非叶子节点:["Alice"]
等于"Alice",需要特殊处理:走右子树(Ptr1)
3. 到达叶子节点2:[(Alice,M,7)]
✅ 找到(Alice,M,7),返回数据指针→0x7000
⚠️ 必须到达叶子节点才能获取数据!
-- 查询2:范围查询 name BETWEEN 'C' AND 'E'
1. 查找'Charlie':
根节点:"David" → 左子树
非叶子节点:"Alice" → 右子树
叶子节点3:找到(Charlie,M,3) →0x3000
2. 沿叶子链表顺序遍历:
叶子节点3:下一个是(David,M,4) →0x4000
叶子节点4:(Eve,F,5) →0x5000(停止)
✅ 无需回溯,顺序扫描效率高!
-- 查询3:所有女性用户(sex='F')
1. 无法有效使用索引(最左前缀原则)
2. 但可以利用叶子链表:
- 从第一个叶子节点开始遍历
- 检查每个键值的sex字段
- 虽然要扫描所有叶子节点,但至少是顺序I/O
4、总结:为什么数据库选择B+Tree
| 特性 | B-Tree表现 | B+Tree表现 | 胜出方 |
|---|---|---|---|
| 等值查询 | 可能更快(少I/O) | 稳定3次I/O | B-Tree(轻微优势) |
| 范围查询 | 多次随机回溯 | 链表顺序扫描 | ✅ B+Tree |
| 全表扫描 | 遍历所有节点 | 只遍历叶子 | ✅ B+Tree |
| 排序查询 | 需要额外排序 | 天然有序 | ✅ B+Tree |
| 插入维护 | 相对简单 | 需要维护链表 | B-Tree |
| 缓存效率 | 数据分散 | 索引集中缓存 | ✅ B+Tree |
索引优化
** 基于User表来演示 **
** 优化层次**
┌─────────────────┐
│ 覆盖索引 │ ← 最有效
├─────────────────┤
│ 索引下推 │
├─────────────────┤
│ 合理索引设计 │
├─────────────────┤
│ 避免索引失效 │
├─────────────────┤
│ SQL重写优化 │
└─────────────────┘
索引设计核心原则
选择合适列建立索引
– ✅ 适合建索引的列:
– 1. WHERE条件中的高频列
CREATE INDEX idx_name ON User(name); – 高频查询条件
CREATE INDEX idx_city_status ON User(city, status); – 组合条件
– 2. JOIN连接列
CREATE INDEX idx_id ON User(id); – 主键已有,用于JOIN
– 3. ORDER BY/GROUP BY列
CREATE INDEX idx_createDate ON User(createDate); – 按时间排序
– 4. 高选择性列(区分度高)
– 先计算选择性
SELECT
COUNT(DISTINCT name)/COUNT() as name_sel, – 0.8 高选择性
COUNT(DISTINCT sex)/COUNT() as sex_sel, – 0.1 低选择性
COUNT(DISTINCT age)/COUNT(*) as age_sel – 0.6 中等
FROM User;
– ❌ 不适合建索引的列:
– 1. 低选择性列(如性别、状态)
CREATE INDEX idx_sex ON User(sex); – 不建议,区分度太低
– 2. 大文本字段
CREATE INDEX idx_email ON User(email(100)); – 如必须,用前缀索引
联合索引设计原则
-- 联合索引列顺序:高频查询 + 高选择性 + 等值查询 优先
-- ✅ 优秀设计:
-- 1. 等值列在前,范围列在后
CREATE INDEX idx_city_age ON User(city, age);
-- 查询:WHERE city='北京' AND age>20(city等值,age范围)
-- 2. 高频条件在前
-- 假设查询模式:80%查询用city,20%用city+status
CREATE INDEX idx_city_status ON User(city, status);
-- 3. 考虑覆盖索引
-- 常用查询:SELECT name, age FROM User WHERE city='北京'
CREATE INDEX idx_city_name_age ON User(city, name, age);
-- ❌ 错误设计:
-- 1. 范围列在前
CREATE INDEX idx_age_city ON User(age, city); -- 范围查询age会使city失效
-- 2. 冗余索引
CREATE INDEX idx_name ON User(name); -- 冗余
CREATE INDEX idx_name_sex ON User(name, sex); -- 前导列相同
索引数量控制原则
-- 黄金法则:每表不超过5-7个索引
-- 原因:
-- 1. 插入/更新/删除时需要维护所有索引
-- 2. 占用磁盘空间
-- 3. 优化器选择困难
-- 评估索引收益:
-- 读收益 vs 写成本
-- 热点查询 vs 全表扫描
索引覆盖(Covering Index)
什么是索引覆盖
-- 普通查询(需要回表):
EXPLAIN SELECT * FROM User WHERE city='北京';
-- Extra: Using where
-- 覆盖索引查询(无需回表):
EXPLAIN SELECT city, status FROM User WHERE city='北京';
-- Extra: Using index
如何实现索引覆盖
-- 方案1:创建包含所有查询列的索引
-- 查询:SELECT id, name, age FROM User WHERE city='北京' AND status=1
CREATE INDEX idx_city_status_covering ON User(city, status, id, name, age);
-- 索引包含:WHERE条件(city,status) + SELECT列(id,name,age)
-- 方案2:利用聚簇索引特性
-- InnoDB中,二级索引包含主键
-- 查询包含主键时天然覆盖
SELECT id, city FROM User WHERE city='北京'; -- 只需扫描二级索引
-- 方案3:前缀覆盖(减少索引大小)
-- 只索引必要的列部分
CREATE INDEX idx_name_part ON User(name(10)); -- 只索引前10字符
覆盖索引的优势
-- 性能对比:
-- 场景:查询北京用户数量(100万数据,10万北京用户)
-- 无覆盖索引:
SELECT COUNT(*) FROM User WHERE city='北京';
-- 1. 扫描city索引找到10万个主键
-- 2. 回表10万次读取数据
-- 总I/O:10万次随机I/O(慢!)
-- 有覆盖索引:
CREATE INDEX idx_city_covering ON User(city, id);
SELECT COUNT(*) FROM User WHERE city='北京';
-- 1. 扫描city索引统计数量
-- 2. 无需回表!
-- 总I/O:1次顺序扫描(快!)
索引下推(Index Condition Pushdown, ICP)
什么是索引下推
MySQL 5.6+引入,将WHERE条件下推到存储引擎层过滤,减少回表次数。
工作原理对比
-- 索引:idx_name_age (name, age)
-- 查询:SELECT * FROM User WHERE name LIKE '张%' AND age>25
-- ❌ 无ICP(MySQL 5.6前):
1. 存储引擎:使用索引找到所有 name LIKE '张%' 的记录
2. 返回所有找到的主键给Server层
3. Server层:根据主键回表获取完整数据
4. Server层:在内存中过滤 age>25
-- 问题:回表了所有姓张的用户,即使age不满足
-- ✅ 有ICP(MySQL 5.6+):
1. 存储引擎:使用索引找到所有 name LIKE '张%' 的记录
2. 存储引擎:在索引层直接过滤 age>25(如果age在索引中)
3. 只返回满足 age>25 的主键给Server层
4. Server层:回表获取这些数据
-- 优势:减少了回表次数
ICP生效条件
-- 条件1:二级索引(非聚簇索引)
-- 条件2:WHERE条件部分可用索引
-- 条件3:MySQL 5.6+,且默认开启
-- 查看ICP状态:
SHOW VARIABLES LIKE 'optimizer_switch';
-- index_condition_pushdown=on
-- 执行计划中识别ICP:
EXPLAIN SELECT * FROM User WHERE name LIKE '张%' AND age>25;
-- Extra: Using index condition ← ICP生效
索引失效场景
违反最左前缀原则
-- 索引:idx_name_age_sex (name, age, sex)
-- ✅ 有效使用:
WHERE name='张三' -- 使用第一列
WHERE name='张三' AND age=25 -- 使用前两列
WHERE name='张三' AND age=25 AND sex='M' -- 使用全部三列
-- ❌ 失效或部分失效:
WHERE age=25 -- 跳过第一列,全表扫描
WHERE name='张三' AND sex='M' -- 跳过age,只能用到name
WHERE age=25 AND sex='M' -- 跳过name,全表扫描
在索引列上使用函数或计算
-- ❌ 函数操作使索引失效:
WHERE YEAR(createDate) = 2024 -- 索引失效
WHERE LEFT(name, 2) = '张' -- 索引失效
WHERE age + 1 > 20 -- 索引失效
-- ✅ 优化方案:
WHERE createDate >= '2024-01-01' AND createDate < '2025-01-01'
WHERE name LIKE '张%' -- 前缀匹配,索引有效
WHERE age > 19 -- 改写计算
使用范围查询导致后续索引列失效
-- 索引:idx_city_age_status (city, age, status)
-- ✅ 范围查询在最后:
WHERE city='北京' AND age=25 AND status>0 -- status范围,不影响前两列
-- ❌ 范围查询在中间导致后续列失效:
WHERE city='北京' AND age>20 AND status=1
-- age是范围查询,status无法使用索引(只能用到city,age)
-- ❌ 多个范围查询:
WHERE age>20 AND age<30 AND createDate>'2024-01-01'
-- 通常只能用到其中一个范围索引
使用不等于(!=, <>)
-- ❌ 不等于使索引失效:
WHERE status != 1 -- 全表扫描
WHERE name <> '张三' -- 全表扫描
-- ✅ 优化方案:
-- 方案1:改为多个等值查询
WHERE status IN (0, 2, 3) -- 如果值有限
-- 方案2:使用覆盖索引
CREATE INDEX idx_status_covering ON User(status, id);
-- 扫描索引而不是全表
-- 方案3:使用位运算
-- 假设status有多个标志位
WHERE (status & 1) = 0 -- 检查特定标志位
LIKE以通配符开头
-- ❌ 前导通配符使索引失效:
WHERE name LIKE '%三' -- 全表扫描
WHERE name LIKE '%张%' -- 全表扫描
-- ✅ 后缀通配符索引有效:
WHERE name LIKE '张%' -- 使用索引
-- ✅ 优化方案:
-- 方案1:反向存储+索引
CREATE INDEX idx_name_reverse ON User(REVERSE(name));
WHERE REVERSE(name) LIKE '三张%' -- 查找'张三'
-- 方案2:使用全文索引
CREATE FULLTEXT INDEX idx_name_ft ON User(name);
WHERE MATCH(name) AGAINST('三' IN BOOLEAN MODE);
-- 方案3:使用前缀索引
CREATE INDEX idx_name_prefix ON User(name(5));
WHERE name LIKE '张%' -- 使用前缀索引
类型转换导致索引失效
-- 假设name是VARCHAR类型
-- ❌ 隐式类型转换:
WHERE name = 123 -- MySQL将数字转为字符串,索引失效
-- ❌ 字符集不匹配:
-- 表字符集utf8,连接字符集latin1
WHERE name = '张三' -- 需要转换,可能使索引失效
-- ✅ 保持类型一致:
WHERE name = '123' -- 字符串比较,索引有效
OR条件导致索引失效
-- 索引:idx_name (name), idx_age (age)
-- ❌ OR条件可能使索引失效:
WHERE name='张三' OR age=25 -- 可能全表扫描
-- ✅ 优化方案:
-- 方案1:改为UNION
SELECT * FROM User WHERE name='张三'
UNION ALL
SELECT * FROM User WHERE age=25;
-- 方案2:使用覆盖索引
CREATE INDEX idx_name_age_covering ON User(name, age, id);
SELECT * FROM User
WHERE (name='张三' OR age=25)
AND id IN (
SELECT id FROM User WHERE name='张三'
UNION
SELECT id FROM User WHERE age=25
);
IS NULL 和 IS NOT NULL
-- 索引:idx_name (name)
-- ❌ IS NOT NULL 可能使索引失效(取决于数据分布):
WHERE name IS NOT NULL -- 如果大部分name不为NULL,可能全表扫描
-- ✅ 优化方案:
-- 方案1:设置NOT NULL约束
ALTER TABLE User MODIFY name VARCHAR(50) NOT NULL;
-- 方案2:使用覆盖索引
CREATE INDEX idx_name_null_covering ON User(name, id);
SELECT id FROM User WHERE name IS NOT NULL; -- 使用索引
复合索引列顺序错误
-- 错误顺序:范围列在前
CREATE INDEX idx_age_city ON User(age, city);
WHERE age>20 AND city='北京'; -- city无法使用索引
-- 正确顺序:等值列在前
CREATE INDEX idx_city_age ON User(city, age);
WHERE city='北京' AND age>20; -- 两列都使用索引
数据量太少或全表扫描更快
-- 当数据量很小时,优化器可能选择全表扫描
-- 例如:User表只有10条记录
SELECT * FROM User WHERE name='张三';
-- 优化器认为全表扫描(读10行)比索引扫描(索引+回表)更快
高级优化技巧
索引合并(Index Merge)
-- MySQL自动将多个索引扫描结果合并
-- 索引:idx_name (name), idx_age (age)
EXPLAIN SELECT * FROM User
WHERE name='张三' OR age=25;
-- type: index_merge
-- Extra: Using union(idx_name,idx_age); Using where
-- 注意:索引合并是最后手段,最好使用复合索引
索引跳跃扫描(Index Skip Scan)
-- MySQL 8.0.13+引入
-- 索引:idx_sex_age (sex, age)
-- 即使查询没指定前导列,也可能使用索引
EXPLAIN SELECT * FROM User WHERE age>25;
-- type: range
-- Extra: Using index for skip scan
-- 原理:遍历sex的不同值,对每个值使用age索引
自适应哈希索引
-- InnoDB自动为热点数据创建哈希索引
SHOW ENGINE INNODB STATUS\G
-- 查看哈希索引使用情况
-- Buffer pool and memory
-- -------------------------------------
-- Adaptive hash index 203MB
使用虚拟列或函数索引
-- MySQL 5.7+/8.0+支持
-- 优化函数查询
-- 创建虚拟列
ALTER TABLE User
ADD COLUMN name_first_char CHAR(1)
GENERATED ALWAYS AS (LEFT(name, 1)) STORED;
-- 在虚拟列上建索引
CREATE INDEX idx_name_first ON User(name_first_char);
-- 查询优化
SELECT * FROM User WHERE LEFT(name, 1) = '张';
-- 可以改为
SELECT * FROM User WHERE name_first_char = '张';
483

被折叠的 条评论
为什么被折叠?



