讲解
没有索引的查询是逐行扫描全表:一万行找一行,最坏要读一万行。索引给数据建了一棵 B+ 树——一种矮而胖的多叉搜索树,一百万行数据的树高通常只有 3 层,也就是最多 3 次磁盘读取就能定位到目标。B+ 树的叶子节点按键值有序排列且相互串联成链表,所以它既擅长精确匹配(=),也擅长范围查询(>、BETWEEN)和排序。
InnoDB 里主键索引是「聚簇索引」:叶子节点直接存整行数据,按主键查只需要一次查找。其他索引叫「二级索引」,叶子节点存的是「索引列的值 + 主键值」——用二级索引查非索引列时,要先走二级索引拿到主键,再回聚簇索引取整行,这个动作叫「回表」。理解了回表,就理解了为什么 SELECT * 比只查索引列慢,也为下一章的覆盖索引埋下伏笔。
索引不是免费的:每次 INSERT/UPDATE/DELETE 都要同步维护索引,索引还占磁盘和内存(buffer pool)。所以原则是「按查询建索引」:高频出现在 WHERE、JOIN ON、ORDER BY 里的列建,低频列不建。建完必须用 EXPLAIN 验证查询真的用上了索引——本章先用一万行数据直观感受「有索引 vs 没索引」的差距。
示例
造一张一万行的用户表(递归 CTE 是 MySQL 8.0 的特性,用来批量造数很方便):
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
city VARCHAR(20) NOT NULL,
age INT NOT NULL
);
SET SESSION cte_max_recursion_depth = 100000;
INSERT INTO users (name, city, age)
WITH RECURSIVE seq AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1 FROM seq WHERE n < 10000
)
SELECT CONCAT('user', n), ELT(1 + n MOD 5, '北京', '上海', '广州', '深圳', '杭州'), 18 + n MOD 40
FROM seq;
SELECT COUNT(*) AS user_count FROM users;
先看不建索引时的执行计划(type=ALL 全表扫描),再建索引对比(type=ref,rows 从一万降到 1):
EXPLAIN SELECT * FROM users WHERE name = 'user666'\G
CREATE INDEX idx_users_name ON users (name);
EXPLAIN SELECT * FROM users WHERE name = 'user666'\G
SHOW INDEX FROM users;
常见坑
- 把索引当银弹:索引加速读、拖慢写,还占空间。写入密集而查询稀少的表(如纯日志流水),乱建索引是净亏损。
- 给低选择度列建索引:「性别」「是否删除」这种只有两三个值的列,索引过滤掉一半数据,优化器往往直接放弃使用,建了也白建。
- 从不验证索引生效:建了索引不等于用了索引——对列套函数、隐式类型转换、前导 % 的 LIKE 都会让索引失效。每条慢查询都用 EXPLAIN 确认。
- 主键乱用无规律值:聚簇索引按主键组织数据,用 UUID 这类随机主键会让插入变成随机写、页分裂频繁,写入性能明显差于自增主键。
小结
B+ 树用 3 次左右磁盘读取定位百万行中的一行;聚簇索引存整行,二级索引回表取行;索引按查询建、用 EXPLAIN 验证。下一章讲用得最多的进阶技巧:联合索引。