你是否曾遇到过这样的场景:随着业务数据量增长,曾经飞快的查询突然变得越来越慢?给字段加个索引就好了——但你知道到底该加什么类型的索引吗?
MySQL 的索引远不止 INDEX 这一种。从 B-Tree 到 Hash,从 Full-text 到 Spatial,不同的索引类型适用于不同的业务场景。选错索引,不仅无法加速查询,还可能拖慢写入性能。
MySQL 8.x 中 7 种索引类型的原理、适用场景与局限;如何在实战中根据业务特征选型;常见索引误区规避。
一、索引底层数据结构
在具体讲索引类型之前,我们先搞清楚索引为什么能加速查询。MySQL 的索引本质上是一棵有序的数据结构树,不同的存储引擎采用不同的结构。
1.1 为什么用树?不用哈希表?
哈希表(Hash Table)的查找时间复杂度是 O(1),看起来比树的 O(log n) 更快——但哈希表不支持范围查询。比如 WHERE age > 20,哈希表无法高效完成。
而树结构天生有序,既能做等值查找,也能做范围查找、排序。这就是为什么 MySQL 默认选择 B+ Tree 作为索引结构。
/* 根节点 */ [ 20 | 50 | 80 ] / | \ /* 中间节点 */ [5|15] [25|45] [55|70] [85|95] | | | | | | | | /* 叶子节点 —— 双向链表 + 完整数据 */ [1,→]←→[10,→]←→[20,→]←→[30,→]←→...←→[100,NULL]
B+ Tree 为什么比 B Tree 更适合数据库?
| 特性 | B Tree | B+ Tree |
|---|---|---|
| 数据存储位置 | 每个节点都存数据 | 只有叶子节点存数据 |
| 非叶子节点大小 | 大(存了数据) | 小(只存键值) |
| 磁盘 I/O 次数 | 多 | 少(同样树高下存更多键) |
| 范围查询 | 需中序遍历 | 叶子节点双向链表,顺序扫描即可 |
InnoDB 和 MyISAM 默认都使用 B+ Tree 作为索引结构。它们的区别在于 InnoDB 是聚簇索引(主键索引叶子节点直接存完整行数据),而 MyISAM 是非聚簇索引(索引和数据分离存储)。
二、7 种索引类型详解
搞懂了底层结构,我们来看 MySQL 提供的索引类型。不同索引适合不同场景,选错了等于没加。
2.1 普通索引 INDEX
最基础的索引类型,对字段值没有唯一性约束。适合需要加速查询但字段可能有重复值的场景。
-- 创建单列普通索引
ALTER TABLE users ADD INDEX idx_username (username);
-- 创建联合索引(多列)
ALTER TABLE orders ADD INDEX idx_user_status (user_id, status);
高频查询的筛选字段、排序字段、JOIN 关联字段。比如用户表的 username、订单表的 user_id。
2.2 唯一索引 UNIQUE
唯一索引不仅加速查询,还约束字段值不能重复(允许一个 NULL)。创建唯一索引时 MySQL 会自动检查数据唯一性,已有重复值会报错。
-- 创建唯一索引
ALTER TABLE users ADD UNIQUE INDEX uk_email (email);
-- 和主键的区别
-- UNIQUE 允许一个 NULL,PRIMARY KEY 不允许 NULL
-- 一张表可以有多个 UNIQUE,但只能有一个 PRIMARY KEY
2.3 主键索引 PRIMARY KEY
主键索引是聚簇索引的核心——在 InnoDB 中,主键索引的叶子节点就是整张表的数据行。这也是为什么InnoDB 必须有主键:如果没显式指定,MySQL 会隐式创建一个 6 字节的自增主键。
-- 建表时指定主键
CREATE TABLE users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;
优先使用自增整数作为主键。如果用 UUID 做主键,由于 UUID 值随机分布,会导致 B+ Tree 频繁分裂,写入性能下降。
2.4 全文索引 FULLTEXT
全文索引专门用于全文检索,支持中文分词(MySQL 5.7.6+ 内置 ngram 分词器)。适合博客、论坛、电商商品等需要"搜索内容"的场景。
-- 创建全文索引(支持中文 ngram)
ALTER TABLE articles ADD FULLTEXT INDEX ft_content (content)
WITH PARSER ngram;
-- 使用全文索引搜索
SELECT * FROM articles
WHERE MATCH(content) AGAINST('MySQL 索引' IN BOOLEAN MODE);
MySQL 内置全文索引不支持增量更新——每次修改数据后,索引不会立即刷新,需要等 innodb_ft_cache_size 满了才会合并。数据量大时建议用 Elasticsearch 或 Meilisearch 替代。
2.5 空间索引 SPATIAL
空间索引是为 GIS(地理信息系统)数据设计的,支持 POINT、POLYGON 等几何类型字段。底层使用 R-Tree 结构而非 B+ Tree。
-- 创建空间索引
CREATE TABLE shops (
id INT PRIMARY KEY,
name VARCHAR(100),
location POINT NOT NULL,
SPATIAL INDEX sp_location (location)
) ENGINE=InnoDB;
-- 查询 5 公里内的店铺
SELECT name FROM shops
WHERE ST_Distance(
location,
ST_GeomFromText('POINT(116.404 39.915)')
) < 5000;
2.6 Hash 索引(仅 Memory / NDB)
Hash 索引基于哈希表实现,等值查询 O(1) 极快,但不支持范围查询、排序、模糊匹配。MySQL 中只有 MEMORY 和 NDB 引擎原生支持。
-- 创建 MEMORY 表使用 Hash 索引
CREATE TABLE cache_data (
key_name VARCHAR(50) NOT NULL,
value_text TEXT,
HASH INDEX h_key (key_name)
) ENGINE=MEMORY;
小知识:InnoDB 虽然不支持显式 Hash 索引,但它在内部实现了一个自适应哈希索引(Adaptive Hash Index)——当发现某些热点查询频繁以等值方式访问时,自动为这些键值构建内存中的 Hash 索引。这个行为由参数 innodb_adaptive_hash_index 控制。
2.7 生成列索引 / 函数索引
MySQL 8.0+ 支持对表达式结果建索引,又叫"函数索引"。适合对字段的某种处理结果进行查询加速。
-- 方式一:生成列(MySQL 5.7+)
ALTER TABLE users ADD COLUMN username_lower VARCHAR(50)
GENERATED ALWAYS AS (LOWER(username)) STORED;
ALTER TABLE users ADD INDEX idx_lower (username_lower);
-- 方式二:函数索引(MySQL 8.0.13+ 原生支持)
ALTER TABLE users ADD INDEX idx_lower ((LOWER(username)));
-- 查询时 MySQL 会自动匹配函数索引
SELECT * FROM users WHERE LOWER(username) = 'admin';
三、联合索引与最左前缀原则
联合索引(复合索引)是实战中最高频的索引形式。理解它的核心——最左前缀原则——是写好 SQL 的前提。
3.1 什么是最左前缀?
假设你建了一个联合索引:INDEX idx_abc (a, b, c)。MySQL 会按 a → b → c 的顺序组织索引树。查询时,只有满足从左到右连续使用索引列的条件,才能命中索引。
| 查询条件 | 能否命中索引 | 原因 |
|---|---|---|
WHERE a = 1 |
✅ 命中 a | 满足最左前缀 |
WHERE a = 1 AND b = 2 |
✅ 命中 a, b | 连续使用 |
WHERE a = 1 AND c = 3 |
✅ 只命中 a | 跳过了 b,c 无法用索引 |
WHERE b = 2 AND c = 3 |
❌ 全表扫描 | 跳过了最左列 a |
WHERE a = 1 AND b > 2 AND c = 3 |
⚠️ 命中 a, b(部分) | b 是范围查询,后面的 c 失效 |
3.2 范围查询的坑
当联合索引中的某一列使用了范围条件(>、<、BETWEEN、LIKE 'abc%'),该列之后的所有索引列都无法继续命中。
-- 索引:INDEX idx (user_id, create_time, status)
-- ❌ status 用不到索引
SELECT * FROM orders
WHERE user_id = 100
AND create_time > '2025-01-01'
AND status = 1;
-- ✅ status 可以用索引(把等值条件放最后)
ALTER TABLE orders ADD INDEX idx_new (user_id, status, create_time);
SELECT * FROM orders
WHERE user_id = 100
AND status = 1
AND create_time > '2025-01-01';
等值条件在前,范围条件在后;区分度高的在前,区分度低的在后。
四、覆盖索引:性能核弹级优化
如果一个查询需要的所有字段都在索引里,MySQL 就不需要回表(回主键索引查完整行),直接从索引就能拿到结果——这就是覆盖索引(Covering Index)。
-- 假设有索引:INDEX idx (user_id, amount, status)
-- ✅ 覆盖索引:需要的三个字段都在索引中
SELECT amount, status FROM orders WHERE user_id = 100;
-- ❌ 非覆盖:需要回表查 create_time
SELECT amount, status, create_time FROM orders WHERE user_id = 100;
覆盖索引的威力在于:它把原本需要"索引查找 + 回表"的两步操作,压缩成了"纯索引查找"一步。对于高频查询,可以把性能提升数倍。
-- EXPLAIN 验证覆盖索引:Extra 列会出现 Using index
EXPLAIN SELECT amount, status FROM orders WHERE user_id = 100;
-- ... Extra: Using index ← 这就是覆盖索引!
五、索引设计实战 Checklist
最后给你一份可以直接套用到项目中的索引设计清单。对照检查,少走弯路。
5.1 什么时候加索引?
- ✅ WHERE 子句中频繁出现的筛选字段
- ✅ JOIN 的关联字段(通常是外键)
- ✅ ORDER BY / GROUP BY 的排序分组字段
- ✅ DISTINCT 去重字段
什么时候别加索引?
- ❌ 表数据量很小(全表扫描比走索引还快)
- ❌ 字段区分度极低(如 gender 只有 0/1 两个值)
- ❌ 频繁写入的表(每个索引都会拖慢 INSERT/UPDATE/DELETE)
- ❌ 被查询的字段频繁被函数包裹(
WHERE YEAR(create_time) = 2025)
5.2 索引条数上限
一张表的索引数建议不超过 5~8 个。索引不是越多越好——每个索引都占用磁盘空间,拖慢写入速度,并且 UPDATE 操作需要同步更新所有相关索引。
5.3 索引长度限制
-- InnoDB utf8mb4 下,单索引最大 767 字节
-- utf8mb4 一个字符占 4 字节,所以 VARCHAR(191) 是安全上限
ALTER TABLE articles ADD INDEX idx_title (title(100)); -- 前缀索引
-- 前缀索引的 trade-off:省空间 + 快,但可能影响排序和唯一性
六、索引失效避坑指南
加了索引不代表一定能用上。以下是 MySQL 索引失效的常见场景,几乎每个开发者都踩过:
| 失效场景 | 示例 | 正确做法 |
|---|---|---|
| 对索引字段用函数 | WHERE YEAR(ctime) = 2025 |
改用范围:WHERE ctime >= '2025-01-01' AND ctime < '2026-01-01' |
| 隐式类型转换 | WHERE varchar_col = 123(列是字符串) |
保持类型一致:WHERE varchar_col = '123' |
| 不等于 / NOT IN | WHERE status != 1 |
改写成:WHERE status IN (0, 2, 3) |
| LIKE 以 % 开头 | WHERE name LIKE '%张' |
全文索引或搜索引擎(ES/Meilisearch) |
| OR 连接无索引字段 | WHERE a = 1 OR b = 2(b 无索引) |
用 UNION ALL 拆分,或给 b 也加索引 |
七、结语
索引是数据库性能优化的第一杠杆,但它是一把双刃剑:加得好,查询飞起来;加得不好,写入慢成狗。
记住三个核心原则:
- 先看慢查询,再加索引——不要"预防性"加索引,用
EXPLAIN验证效果 - 联合索引讲究顺序——等值在前,范围在后;区分度高在前
- 关注写入性能——每张表索引数控制在 5~8 个以内
愿你的 SQL,永远不走全表扫描。