你是否曾遇到过这样的场景:随着业务数据量增长,曾经飞快的查询突然变得越来越慢?给字段加个索引就好了——但你知道到底该加什么类型的索引吗?

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 作为索引结构。

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 也加索引

七、结语

索引是数据库性能优化的第一杠杆,但它是一把双刃剑:加得好,查询飞起来;加得不好,写入慢成狗。

记住三个核心原则:

  1. 先看慢查询,再加索引——不要"预防性"加索引,用 EXPLAIN 验证效果
  2. 联合索引讲究顺序——等值在前,范围在后;区分度高在前
  3. 关注写入性能——每张表索引数控制在 5~8 个以内
愿你的 SQL,永远不走全表扫描。