Skip to content

第三阶段:索引与性能优化


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 级别,避免 ALL

3.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.log

3.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 filesort

3.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 INTUUID (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+ 树原理、最左前缀、覆盖索引、索引失效场景。