主题
第三阶段:索引与性能优化
3.1 索引基础
3.1.1 什么是索引?
索引是一种数据结构,帮助数据库快速定位数据,类似于书的目录。
没有索引的查询(全表扫描):
┌──────────────────────────────────────────┐
│ SELECT * FROM users WHERE name = 'Bob' │
│ │
│ 逐行扫描: │
│ Row 1: Alice ← 不匹配,继续 │
│ Row 2: Bob ← 匹配!但还得继续扫 │
│ Row 3: Charlie ← 不匹配 │
│ Row 4: Diana ← 不匹配 │
│ ... │
│ Row 1000000: Zara ← 扫完 100 万行 │
│ │
│ 时间复杂度:O(N) → 100 万行扫 100 万次 │
└──────────────────────────────────────────┘
有索引的查询(B+ 树搜索):
┌──────────────────────────────────────────┐
│ SELECT * FROM users WHERE name = 'Bob' │
│ │
│ B+ 树查找: │
│ 根节点 → [M] │
│ ↙ B < M │
│ 内部节点 → [D, H] │
│ ↘ B < D │
│ 叶子节点 → [Alice, Bob, Charlie] │
│ → 找到 Bob! │
│ │
│ 时间复杂度:O(log N) → 100 万行只需 20 次 │
└──────────────────────────────────────────┘3.1.2 B+ 树索引结构
MySQL InnoDB 默认使用 B+ 树作为索引数据结构。
B+ 树结构示意(假设每个节点最多 3 个 key):
[30, 60] ← 根节点(非叶子)
/ | \
/ | \
[10, 20] [40, 50] [70, 80] ← 内部节点(非叶子)
/ | \ / | \ / | \
/ | \ / | \ / | \
[5] [15] [25] [35] [45] [55] [65] [75] [85] ← 叶子节点(存数据)
↔ ↔ ↔ ↔ ↔ ↔ ↔ ↔ ↔
双向链表连接所有叶子节点
B+ 树特点:
1. 所有数据都存储在叶子节点
2. 叶子节点通过双向链表连接 → 范围查询高效
3. 非叶子节点只存索引 key → 一个节点能存更多 key
4. 树高度通常 2-4 层 → 查找次数极少
5. 每个节点大小 = 一个磁盘页(16KB)为什么用 B+ 树而不是其他结构?
| 数据结构 | 查找效率 | 范围查询 | 磁盘 I/O | 适合索引? |
|---|---|---|---|---|
| Hash 表 | O(1) 等值查询极快 | ❌ 不支持范围 | 差(随机 IO) | 仅等值查询 |
| 二叉搜索树 | O(log N) | ✅ | 差(树高,IO 多) | ❌ 树太高 |
| B 树 | O(log N) | 一般 | 好 | 一般(数据分散在所有节点) |
| B+ 树 | O(log N) | ✅ 高效 | 最优 | ✅ 最适合 |
3.1.3 索引类型
sql
-- ==================== 1. 主键索引(PRIMARY KEY)====================
-- 每张表只能有一个,值唯一且不为 NULL
-- InnoDB 中主键索引 = 聚簇索引(数据按主键顺序存储)
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT, -- 主键索引
name VARCHAR(50)
);
-- ==================== 2. 唯一索引(UNIQUE)====================
-- 值唯一,但允许 NULL(多个 NULL 不冲突)
CREATE UNIQUE INDEX uk_email ON users(email);
-- 或建表时
ALTER TABLE users ADD UNIQUE INDEX uk_email (email);
-- ==================== 3. 普通索引(INDEX / KEY)====================
-- 最基本的索引,无唯一性约束
CREATE INDEX idx_name ON users(name);
-- ==================== 4. 复合索引(联合索引)====================
-- 多个列组合成一个索引
CREATE INDEX idx_name_age ON users(name, age);
-- 遵循最左前缀原则(重点!后面详讲)
-- ==================== 5. 前缀索引 ====================
-- 对字符串列的前 N 个字符建索引(节省空间)
CREATE INDEX idx_email_prefix ON users(email(10));
-- ==================== 6. 全文索引(FULLTEXT)====================
-- 用于全文搜索(中文需要配合 ngram 解析器)
CREATE FULLTEXT INDEX ft_content ON articles(content) WITH PARSER ngram;
-- 使用全文索引
SELECT * FROM articles WHERE MATCH(content) AGAINST('MySQL 优化' IN NATURAL LANGUAGE MODE);
-- ==================== 查看表索引 ====================
SHOW INDEX FROM users;
SHOW CREATE TABLE users;3.1.4 聚簇索引 vs 非聚簇索引
InnoDB 的索引组织表(Index Organized Table):
聚簇索引(Clustered Index)= 主键索引
┌──────────────────────────────────────────┐
│ B+ 树叶子节点直接存储整行数据 │
│ │
│ 叶子节点: │
│ [id=1 | name=Alice | age=25 | ...] │
│ [id=2 | name=Bob | age=30 | ...] │
│ [id=3 | name=Charlie| age=28 | ...] │
│ │
│ → 数据按主键顺序物理存储 │
│ → 每张表只能有一个聚簇索引 │
└──────────────────────────────────────────┘
非聚簇索引(Secondary Index)= 二级索引
┌──────────────────────────────────────────┐
│ B+ 树叶子节点存储的是主键值 │
│ │
│ idx_name 索引的叶子节点: │
│ [name=Alice → id=1] │
│ [name=Bob → id=2] │
│ [name=Charlie→ id=3] │
│ │
│ 查找过程(回表): │
│ Step 1: 在 idx_name 中找到 name='Bob' │
│ Step 2: 得到 id=2 │
│ Step 3: 用 id=2 去聚簇索引查找完整数据 │
│ │
│ → 比聚簇索引多一次查找(回表) │
└──────────────────────────────────────────┘3.1.5 复合索引与最左前缀原则
sql
-- 复合索引 (a, b, c) 相当于创建了以下索引:
-- (a) ✅ 可使用
-- (a, b) ✅ 可使用
-- (a, b, c) ✅ 可使用
-- (b) ❌ 不可使用
-- (b, c) ❌ 不可使用
-- (a, c) ⚠️ 只用到 a
CREATE INDEX idx_abc ON users(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 b > 5 -- 使用 (a, b)
WHERE a = 1 AND b = 2 AND c > 3 -- 使用 (a, b, c)
-- ❌ 不命中索引
WHERE b = 2 -- 缺少最左列 a
WHERE c = 3 -- 缺少最左列 a
WHERE b = 2 AND c = 3 -- 缺少最左列 a
-- ⚠️ 部分命中
WHERE a = 1 AND c = 3 -- 只用到 a(跳过了 b)
WHERE a > 1 AND b = 2 -- 只用到 a(a 是范围查询,b 无法使用索引)
-- 设计复合索引的原则:
-- 1. 等值查询的列放前面,范围查询的列放后面
-- 2. 选择性高(区分度大)的列放前面
-- 3. 查询频率高的组合优先3.1.6 覆盖索引与索引下推
sql
-- ==================== 覆盖索引(Covering Index)====================
-- 查询的所有列都在索引中,不需要回表
CREATE INDEX idx_name_age ON users(name, age);
-- ✅ 覆盖索引(EXPLAIN 显示 Using index)
SELECT name, age FROM users WHERE name = 'Alice';
-- 只需要 name 和 age,都在 idx_name_age 中 → 不回表
-- ❌ 需要回表
SELECT name, age, email FROM users WHERE name = 'Alice';
-- email 不在索引中 → 需要回表到聚簇索引查完整数据
-- ==================== 索引下推(ICP, Index Condition Pushdown)====================
-- MySQL 5.6+ 特性,在索引扫描阶段就过滤数据,减少回表次数
-- 有复合索引 idx_name_age(name, age)
SELECT * FROM users WHERE name LIKE 'A%' AND age > 25;
-- 没有 ICP:
-- Step 1: 用索引找到所有 name LIKE 'A%' 的记录
-- Step 2: 逐行回表
-- Step 3: 在 Server 层过滤 age > 25
-- 有 ICP(EXPLAIN 显示 Using index condition):
-- Step 1: 用索引找到所有 name LIKE 'A%' 的记录
-- Step 2: 在索引层直接过滤 age > 25 ← 减少回表!
-- Step 3: 只对符合条件的行回表3.2 EXPLAIN 执行计划
3.2.1 EXPLAIN 基础使用
sql
-- 在 SQL 语句前加 EXPLAIN 即可查看执行计划
EXPLAIN SELECT * FROM books WHERE title = '三体';
-- 更详细的格式
EXPLAIN FORMAT=JSON SELECT * FROM books WHERE title = '三体';
EXPLAIN ANALYZE SELECT * FROM books WHERE title = '三体'; -- 8.0.18+,显示实际执行时间3.2.2 EXPLAIN 各字段详解
EXPLAIN 输出示例:
+----+-------------+-------+-------+---------------+-----------+---------+------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+-----------+---------+------+------+-------+
| 1 | SIMPLE | books | ref | idx_title | idx_title | 802 | const| 1 | NULL |
+----+-------------+-------+-------+---------------+-----------+---------+------+------+-------+| 字段 | 说明 | 重点关注 |
|---|---|---|
| id | 查询序号,id 相同按顺序执行,id 不同大的先执行 | — |
| select_type | 查询类型:SIMPLE、PRIMARY、SUBQUERY、DERIVED 等 | — |
| table | 当前访问的表 | — |
| type ⭐ | 访问类型,性能从优到差 | 最重要 |
| possible_keys | 可能使用的索引 | — |
| key ⭐ | 实际使用的索引 | 关注是否为 NULL |
| key_len | 使用索引的长度(字节) | 判断复合索引使用了几列 |
| ref | 索引关联的列或常量 | — |
| rows ⭐ | 预估扫描行数 | 越小越好 |
| Extra ⭐ | 额外信息 | 关注关键词 |
3.2.3 type 字段详解(性能等级)
type 性能从优到差排序:
system > const > eq_ref > ref > range > index > ALL
┌──────────┬──────────────────────────┬──────────────────────────┐
│ type │ 说明 │ 示例 │
├──────────┼──────────────────────────┼──────────────────────────┤
│ system │ 表只有一行(系统表) │ 极少见 │
│ const │ 主键/唯一索引等值查询 │ WHERE id = 1 │
│ eq_ref │ 关联查询中,主键/唯一索引 │ JOIN ... ON a.id = b.id │
│ ref │ 非唯一索引等值查询 │ WHERE name = 'Alice' │
│ range │ 索引范围扫描 │ WHERE age > 25 │
│ │ │ WHERE id IN (1, 2, 3) │
│ │ │ WHERE age BETWEEN 20 AND 30│
│ index │ 全索引扫描(遍历索引树) │ 覆盖索引但没条件 │
│ ALL ⚠️ │ 全表扫描(最差) │ 没有索引的列查询 │
└──────────┴──────────────────────────┴──────────────────────────┘
优化目标:至少达到 range 级别,避免 ALL3.2.4 Extra 字段关键词
| Extra 值 | 含义 | 是否需要优化 |
|---|---|---|
Using index | 覆盖索引,不需要回表 | ✅ 很好 |
Using index condition | 使用了索引下推(ICP) | ✅ 好 |
Using where | 在 Server 层进行过滤 | 一般 |
Using temporary | 使用了临时表 | ⚠️ 需要优化 |
Using filesort | 使用了文件排序(非索引排序) | ⚠️ 需要优化 |
Using join buffer | 关联查询未命中索引 | ⚠️ 需要优化 |
sql
-- ✅ 好的执行计划
EXPLAIN SELECT name, age FROM users WHERE name = 'Alice';
-- type: ref, key: idx_name_age, Extra: Using index
-- ⚠️ 需要优化
EXPLAIN SELECT * FROM users ORDER BY name;
-- Extra: Using filesort → 考虑添加索引
EXPLAIN SELECT DISTINCT category_id FROM books;
-- Extra: Using temporary → 考虑添加索引3.2.5 慢查询日志
sql
-- 查看慢查询配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 超过 1 秒记录
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
-- 永久配置(my.cnf)
-- [mysqld]
-- slow_query_log = ON
-- long_query_time = 1
-- slow_query_log_file = /var/log/mysql/slow.log
-- log_queries_not_using_indexes = ON # 记录未使用索引的查询bash
# 分析慢查询日志
# 使用 mysqldumpslow 工具(MySQL 自带)
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
# -s t: 按查询时间排序
# -t 10: 显示前 10 条
# 使用 pt-query-digest(Percona Toolkit,更强大)
pt-query-digest /var/log/mysql/slow.log3.3 SQL 优化技巧
3.3.1 索引失效的常见场景
sql
-- 假设有索引 idx_name(name), idx_age(age), idx_email(email)
-- ❌ 1. 对索引列使用函数
WHERE YEAR(created_at) = 2026 -- 索引失效!
WHERE created_at >= '2026-01-01' -- ✅ 改为范围查询
WHERE UPPER(name) = 'ALICE' -- 索引失效!
WHERE name = 'ALICE' -- ✅ 利用 ci 排序规则(大小写不敏感)
-- ❌ 2. 隐式类型转换
-- age 是 INT 类型
WHERE age = '25' -- MySQL 自动转换,可能失效
WHERE age = 25 -- ✅
-- phone 是 VARCHAR 类型
WHERE phone = 13800138000 -- 索引失效!(字符串列用数字匹配)
WHERE phone = '13800138000' -- ✅
-- ❌ 3. LIKE 以通配符开头
WHERE name LIKE '%Alice%' -- 索引失效(全表扫描)
WHERE name LIKE 'Alice%' -- ✅ 可以使用索引
-- ❌ 4. OR 条件中有未索引的列
WHERE name = 'Alice' OR age = 25 -- 如果 age 没索引,整个条件不走索引
-- ✅ 方案一:给 age 也加索引
-- ✅ 方案二:改用 UNION
SELECT * FROM users WHERE name = 'Alice'
UNION
SELECT * FROM users WHERE age = 25;
-- ❌ 5. != / NOT IN / NOT EXISTS
WHERE status != 'active' -- 通常不走索引
WHERE status = 'active' -- ✅ 等值查询走索引
-- ❌ 6. IS NULL / IS NOT NULL(取决于数据分布)
-- NULL 值比例很小时可能走索引,比例大时不走
-- ❌ 7. 复合索引不遵循最左前缀
-- idx_abc(a, b, c)
WHERE b = 1 AND c = 2 -- 不走索引
WHERE a = 1 AND c = 2 -- 只走 a 部分3.3.2 大表分页优化
sql
-- ❌ 深分页性能问题
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;
-- MySQL 实际上扫描了 1000010 行,丢弃前 100 万行,只返回 10 行!
-- ✅ 方案一:延迟关联(先查主键,再关联数据)
SELECT o.*
FROM orders o
INNER JOIN (
SELECT id FROM orders ORDER BY id LIMIT 1000000, 10
) AS tmp ON o.id = tmp.id;
-- 子查询只扫描索引(覆盖索引),不回表
-- ✅ 方案二:游标分页(记住上次的位置)
-- 第一页
SELECT * FROM orders WHERE id > 0 ORDER BY id LIMIT 10;
-- 假设返回的最后一条 id = 10
-- 第二页(用上次的最后 id)
SELECT * FROM orders WHERE id > 10 ORDER BY id LIMIT 10;
-- 假设返回的最后一条 id = 20
-- 第三页
SELECT * FROM orders WHERE id > 20 ORDER BY id LIMIT 10;
-- 每次查询都是 const/range 级别,极快!
-- 游标分页的限制:只能"上一页/下一页",不能直接跳转到第 N 页3.3.3 COUNT 性能分析
sql
-- COUNT 的几种写法性能对比
SELECT COUNT(*) FROM users; -- ✅ 推荐,优化器会选最小的索引
SELECT COUNT(1) FROM users; -- ✅ 等价于 COUNT(*)
SELECT COUNT(id) FROM users; -- ✅ 差异极小
SELECT COUNT(name) FROM users; -- ⚠️ 会跳过 name IS NULL 的行
-- InnoDB 中 COUNT(*) 为什么慢?
-- InnoDB 不像 MyISAM 维护精确行数
-- COUNT(*) 需要遍历索引来计数(虽然用最小的索引)
-- 优化方案:
-- 1. 使用 EXPLAIN 估算(不精确但极快)
EXPLAIN SELECT COUNT(*) FROM users; -- 看 rows 字段
-- 2. 维护计数表
CREATE TABLE table_counts (
table_name VARCHAR(50) PRIMARY KEY,
row_count BIGINT
);
-- 3. 使用缓存(Redis)
-- 4. 使用 information_schema(不精确)
SELECT TABLE_ROWS FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = 'mydb' AND TABLE_NAME = 'users';3.3.4 ORDER BY 与 GROUP BY 优化
sql
-- ORDER BY 优化:利用索引避免 filesort
-- 有索引 idx_age(age)
SELECT * FROM users ORDER BY age; -- ✅ 使用索引排序
SELECT * FROM users ORDER BY age DESC; -- ✅ 反向扫描索引
SELECT * FROM users ORDER BY name; -- ❌ name 没索引 → filesort
-- 复合索引 idx_a_b(a, b)
SELECT * FROM t WHERE a = 1 ORDER BY b; -- ✅ a 等值 + b 排序
SELECT * FROM t ORDER BY a, b; -- ✅ 索引顺序排序
SELECT * FROM t ORDER BY a ASC, b DESC; -- ❌ 方向不一致(8.0.1+ 支持降序索引)
SELECT * FROM t ORDER BY b, a; -- ❌ 顺序不一致
-- GROUP BY 优化:同 ORDER BY,利用索引避免临时表
-- 有索引 idx_category(category_id)
SELECT category_id, COUNT(*) FROM books GROUP BY category_id; -- ✅ 使用索引3.3.5 批量操作优化
sql
-- ❌ 逐条插入(每条都有网络开销和事务开销)
INSERT INTO users (name, age) VALUES ('Alice', 25);
INSERT INTO users (name, age) VALUES ('Bob', 30);
-- ... 重复 10000 次
-- ✅ 批量插入(一条语句插入多行)
INSERT INTO users (name, age) VALUES
('Alice', 25), ('Bob', 30), ('Charlie', 28), ... ;
-- 一次网络往返,一个事务
-- ✅ 使用事务包裹(减少 fsync 次数)
START TRANSACTION;
INSERT INTO users (name, age) VALUES ('Alice', 25);
INSERT INTO users (name, age) VALUES ('Bob', 30);
-- ... 更多 INSERT
COMMIT;
-- ✅ LOAD DATA INFILE(最快的批量导入方式)
LOAD DATA INFILE '/tmp/users.csv'
INTO TABLE users
FIELDS TERMINATED BY ','
LINES TERMINATED BY '\n'
(name, age, email);
-- 批量更新优化
-- ❌ 逐条更新
UPDATE products SET price = 10 WHERE id = 1;
UPDATE products SET price = 20 WHERE id = 2;
-- ✅ CASE WHEN 批量更新
UPDATE products SET price = CASE id
WHEN 1 THEN 10
WHEN 2 THEN 20
WHEN 3 THEN 30
END
WHERE id IN (1, 2, 3);3.4 实践 Demo
Demo 1:索引效果对比实验
sql
-- ===== Step 1: 创建百万级测试数据 =====
CREATE TABLE test_index (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
age INT,
email VARCHAR(100),
city VARCHAR(50),
created_at DATETIME
);
-- 使用存储过程插入 100 万条数据
DELIMITER //
CREATE PROCEDURE generate_test_data()
BEGIN
DECLARE i INT DEFAULT 0;
WHILE i < 1000000 DO
INSERT INTO test_index (name, age, email, city, created_at) VALUES
(CONCAT('user_', LPAD(i, 7, '0')),
FLOOR(18 + RAND() * 60),
CONCAT('user', i, '@example.com'),
ELT(FLOOR(1 + RAND() * 5), '北京', '上海', '广州', '深圳', '杭州'),
DATE_ADD('2020-01-01', INTERVAL FLOOR(RAND() * 2000) DAY));
SET i = i + 1;
END WHILE;
END //
DELIMITER ;
CALL generate_test_data();
-- 可能需要几分钟
-- ===== Step 2: 无索引查询 =====
SELECT * FROM test_index WHERE name = 'user_0500000';
-- 执行时间:~0.5s(全表扫描)
EXPLAIN SELECT * FROM test_index WHERE name = 'user_0500000';
-- type: ALL, rows: 1000000
-- ===== Step 3: 添加索引 =====
CREATE INDEX idx_name ON test_index(name);
-- ===== Step 4: 有索引查询 =====
SELECT * FROM test_index WHERE name = 'user_0500000';
-- 执行时间:~0.001s(快了 500 倍!)
EXPLAIN SELECT * FROM test_index WHERE name = 'user_0500000';
-- type: ref, rows: 1
-- ===== Step 5: 复合索引实验 =====
CREATE INDEX idx_city_age ON test_index(city, age);
-- 使用最左前缀
EXPLAIN SELECT * FROM test_index WHERE city = '北京' AND age = 25;
-- type: ref, key: idx_city_age ✅
-- 不使用最左前缀
EXPLAIN SELECT * FROM test_index WHERE age = 25;
-- type: ALL ❌(跳过了 city)
-- 覆盖索引
EXPLAIN SELECT city, age FROM test_index WHERE city = '北京';
-- Extra: Using index ✅(不回表)
-- ===== Step 6: 清理 =====
DROP TABLE test_index;
DROP PROCEDURE generate_test_data;Demo 2:EXPLAIN 分析与优化实战
sql
-- 使用 bookstore 数据库中的表
-- ===== 分析 1: 未优化的查询 =====
EXPLAIN SELECT b.title, a.name, c.name
FROM books b
LEFT JOIN authors a ON b.author_id = a.id
LEFT JOIN categories c ON b.category_id = c.id
WHERE a.country = '中国'
ORDER BY b.price DESC;
-- 观察:type、key、rows、Extra
-- ===== 分析 2: 添加索引优化 =====
CREATE INDEX idx_country ON authors(country);
EXPLAIN SELECT b.title, a.name, c.name
FROM books b
LEFT JOIN authors a ON b.author_id = a.id
LEFT JOIN categories c ON b.category_id = c.id
WHERE a.country = '中国'
ORDER BY b.price DESC;
-- 观察变化:authors 表的 type 从 ALL 变为 ref
-- ===== 分析 3: 优化排序 =====
CREATE INDEX idx_price ON books(price);
-- 再次 EXPLAIN 观察 Extra 中是否还有 Using filesort3.5 常见问题 QA
Q1: 索引越多越好吗?
索引的代价:
1. 空间开销:每个索引都是一棵 B+ 树,占用磁盘空间
2. 写入开销:INSERT/UPDATE/DELETE 时需要维护所有相关索引
3. 优化器开销:索引太多,优化器选择执行计划的时间也增加
建议:
- 单表索引不超过 5-6 个
- 每个索引的列数不超过 5 个
- 删除不使用的索引:
SELECT * FROM sys.schema_unused_indexes;Q2: 为什么我的索引没有被使用?
sql
-- 排查步骤:
-- 1. 用 EXPLAIN 确认
EXPLAIN SELECT * FROM users WHERE name = 'Alice';
-- 2. 常见原因:
-- a) 索引选择性太低(如 status 只有 3 种值)→ 优化器选择全表扫描更快
-- b) 数据量太小 → 全表扫描比走索引更快
-- c) 索引列参与了函数运算
-- d) 隐式类型转换
-- e) 使用了 != / NOT IN
-- f) OR 条件中有非索引列
-- 3. 强制使用索引(仅调试用)
SELECT * FROM users FORCE INDEX(idx_name) WHERE name = 'Alice';Q3: 主键用自增 INT 还是 UUID?
| 对比 | AUTO_INCREMENT INT | UUID (CHAR(36)) |
|---|---|---|
| 大小 | 4/8 字节 | 36 字节 |
| 顺序性 | ✅ 严格递增 | ❌ 随机 |
| 插入性能 | ✅ 顺序写入,高效 | ❌ 随机写入,页分裂 |
| 分布式 | ❌ 单点自增,不适合 | ✅ 全局唯一 |
| 安全性 | ❌ 可预测(爬虫易猜) | ✅ 不可预测 |
sql
-- 建议:
-- 1. 单机应用 → AUTO_INCREMENT(性能最优)
-- 2. 分布式 → 雪花 ID (BIGINT) 或 有序 UUID
-- 3. 需要对外暴露 ID → UUID 或雪花 ID
-- 4. 不要用 UUID 作为 InnoDB 主键(随机插入导致页分裂,性能差)
-- 有序 UUID(MySQL 8.0)
SELECT UUID_TO_BIN(UUID(), 1); -- 将 UUID 转为有序二进制,适合作为主键3.6 索引设计原则总结
索引设计黄金法则:
1. 为 WHERE、JOIN、ORDER BY、GROUP BY 中频繁出现的列建索引
2. 选择性高的列优先(如 email > status)
3. 复合索引:等值列在前,范围列在后
4. 利用覆盖索引避免回表(SELECT 只查索引中有的列)
5. 不要过度索引:写多读少的表少建索引
6. 长字符串用前缀索引
7. 主键尽量使用自增 INT/BIGINT📝 学习建议:本阶段是 MySQL 学习的核心重点,面试必考。建议:1) 创建百万级测试表亲自体验索引效果;2) 对每条 SQL 都习惯性地使用 EXPLAIN 分析;3) 重点掌握 B+ 树原理、最左前缀、覆盖索引、索引失效场景。