MySQL 的索引是什么?用图书馆的目录讲明白

几乎每个初学者都经历过这样的场景:一张表数据量不大时查询飞快,某天数据涨到几十万条,同一个查询突然要跑好几秒,加一个索引之后又瞬间恢复。这个 "加了一个东西就变快了" 的东西,就是索引。

网上的资料讲索引,往往一上来就是 B+ 树的图,看得人云里雾里。这篇文章换一种讲法,只回答三个实际问题:索引为什么快、什么字段该建索引、哪些写法会让建好的索引白费。理解了这三件事,你就能应付日常开发里绝大部分索引问题。示例基于 MySQL 8.0,用 SQL 原生写法。

1 索引为什么快

1.1 从图书馆找书说起

图书馆的书架上有几十万本书,你要找某一本,不会一本一本扫过去,而是先查目录系统,拿到索书号,直接去对应的书架取书。这个 "从查找依据到存放位置" 的对应关系,就是目录的价值。

数据库没有索引时的查找方式,恰恰就是 "一本一本扫过去":把整张表从头到尾读一遍,逐行判断条件是否成立,这叫全表扫描。100 万行的表,就要读 100 万行。而索引是一份独立维护的 "目录"------它把某几列的值做了排序,并记录每个值对应的行的位置。查找时先在索引里定位(索引是有序的,可以二分式地跳着找,不用逐条翻),再拿着位置直接取行,读取的量从 "整张表" 缩小到 "命中的一小段"。

先记住这个结论就够了:索引快的本质,是把大规模的逐行扫描,变成了在有序结构里的小范围定位。至于底层用的 B+ 树具体怎么组织,初学阶段不必深究,知道它是一种为磁盘查找优化的有序结构即可。

1.2 动手验证一次

用一张 100 万行的用户表做对比。表结构如下:

sql 复制代码
CREATE TABLE users (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(50) NOT NULL,
  phone CHAR(11) NOT NULL,
  created_at DATETIME NOT NULL
) ENGINE = InnoDB;

先不加索引,按手机号查一次:

sql 复制代码
SELECT * FROM users WHERE phone = '13812345678';
-- 耗时约 0.5 秒,走了全表扫描

然后给 phone 列加上索引:

sql 复制代码
CREATE INDEX idx_users_phone ON users (phone);

SELECT * FROM users WHERE phone = '13812345678';
-- 耗时约 0.001 秒

同一个查询,耗时差了几百倍。顺手可以看一眼 MySQL 自己怎么评价这两次查询:

sql 复制代码
EXPLAIN SELECT * FROM users WHERE phone = '13812345678';

加索引前,EXPLAIN 结果里 type 列显示 ALL(全表扫描),rows 列显示接近 100 万;加索引后,type 变成 ref,key 列显示用上了 idx_users_phone,rows 只有 1。EXPLAIN 的读法在第 4 节展开。

不过索引不是免费的。每建一个索引,MySQL 都要额外维护一份有序结构,写入数据时(INSERT、UPDATE、DELETE)要同步更新所有相关索引,磁盘占用也会增加。所以索引是 "读快写慢" 的权衡,只给真正需要快速查找的字段建,而不是越多越好。


2 什么字段该建索引

判断标准其实就一句话:这个字段会不会出现在查找条件里。具体展开有下面几条。

2.1 优先给查询条件里的字段建

最直接的对象是 WHERE 子句里高频出现的字段,以及 JOIN 的关联字段、ORDER BY 排序字段。拿上面的用户表举例,如果业务里既有按手机号查,也有按注册时间范围查,那就是两个字段都有资格:

sql 复制代码
CREATE INDEX idx_users_phone ON users (phone);
CREATE INDEX idx_users_created ON users (created_at);

2.2 区分度高的字段才值得建

索引的价值取决于 "查完之后能筛掉多少行"。给性别字段建索引几乎没用------全表只有两个值,即便走索引,匹配到的还是几十万行,优化器算完账干脆放弃索引,直接全表扫描。反过来,手机号、身份证号、订单号这类几乎每个值都不一样的字段,才是索引的最佳服务对象。这个特性叫区分度,建索引前可以粗略估一下:字段的可能取值越多,区分度越高。

2.3 几种索引类型,够用就行

MySQL 常用的索引类型有三种,含义和建法如下:

  • **主键索引:**建表时 PRIMARY KEY 就自带了,一张表只能有一个,上面示例的 id 列已经有了。
  • **唯一索引:**保证该列的值不重复,比如手机号,用 CREATE UNIQUE INDEX 创建,重复插入会直接报错。
  • **普通索引:**单纯加速查找,用 CREATE INDEX 创建。

还有一种组合拳叫联合索引,即把多个列合进一个索引:

sql 复制代码
CREATE INDEX idx_users_name_created ON users (name, created_at);

联合索引遵循 "最左前缀" 原则:索引按 name 排序,name 相同时再按 created_at 排序。所以 WHERE name = ? 和 WHERE name = ? AND created_at > ? 都能用上它,但单独的 WHERE created_at > ? 用不上------就像字典先按拼音首字母排序,你跳过首字母直接找第二个字母,只能整本翻。初学阶段记住这条就够:查询条件要尽量从联合索引的最左列开始。


3 哪些写法会让索引失效

建了索引不等于一定用得上。MySQL 的优化器每次查询都会评估 "走索引划不划算",而不少常见的写法会直接让索引派不上用场。下面四种是最常踩的。

3.1 在索引列上做函数或运算

sql 复制代码
-- 索引失效:对索引列做了函数计算
SELECT * FROM users WHERE YEAR(created_at) = 2024;

-- 改写成范围查询,索引生效
SELECT * FROM users
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';

索引按列的 "原始值" 排序,一旦套上函数或参与计算,MySQL 就无法在有序结构里定位,只能对全表的每一行算一遍 YEAR()。改写思路是固定的:把对列的操作转化成对常量的范围比较。同理,WHERE id + 1 = 100 应改写成 WHERE id = 99。

3.2 隐式类型转换

sql 复制代码
-- phone 是 CHAR(11),却用数字去查
SELECT * FROM users WHERE phone = 13812345678;

phone 是字符串类型,查询值却写成了数字,MySQL 会隐式地把每一行的 phone 转成数字再比较,效果等同于对索引列套了函数,索引失效。改成 WHERE phone = '13812345678' 即可。这类 bug 特别隐蔽------不报错、结果也对,只是慢,很多人线上慢查询的根源就是少了一对引号。

3.3 LIKE 的通配符打头

sql 复制代码
SELECT * FROM users WHERE name LIKE '%三';    -- 索引失效
SELECT * FROM users WHERE name LIKE '张%';    -- 索引生效

索引是按列值从头开始排序的,LIKE '张%' 表示 "以张开头的",可以在有序结构里直接定位到这一段;而 '%三' 表示 "以三结尾的",谁也不知道它藏在排序后的哪个位置,只能全表扫。确实需要任意位置匹配时,要么接受全表扫描的代价,要么引入全文索引或搜索引擎,那是另一个话题了。

3.4 OR 连接了没有索引的列

sql 复制代码
-- phone 有索引,address 没有索引
SELECT * FROM users WHERE phone = '13812345678' OR address = '北京';

OR 的语义是两边的条件满足其一即可,address 没索引就得全表扫,既然反正要扫全表,phone 的索引也失去了意义。解决办法是给 OR 涉及的每一列都配上索引,缺一个就可能整体失效。


4 学会用 EXPLAIN 检查

判断一个查询有没有真正用上索引,不要靠猜,在语句前面加上 EXPLAIN 让 MySQL 自己说:

sql 复制代码
EXPLAIN SELECT * FROM users WHERE phone = '13812345678';

结果是一张表,初学阶段重点看四列:

列名 含义 期望值
type 访问方式 最好到 ref 或 range,出现 ALL 就是全表扫描
key 实际用上的索引 应该显示你建的索引名,为 NULL 则没走索引
rows 预估扫描行数 越小越好,接近表总行数说明没筛掉什么
Extra 附加信息 出现 Using index 是好事,Using filesort 需要留意

日常开发里养成一个习惯:凡是新写的、条件里带非主键字段的查询,跑上线前用 EXPLAIN 看一眼 type 和 key,很多慢查询在测试阶段就能拦下来。

把三个问题串起来,索引这件事的完整逻辑是:它用 "读时更快、写时更贵" 的代价,把逐行扫描变成有序定位;建在哪,取决于哪些字段频繁出现在查询条件里且区分度够高;而函数运算、隐式转换、前导通配符这些写法,会悄悄把这份优化吃掉。上线前用 EXPLAIN 确认一眼,比出问题后再排查省心得多------索引的功夫不在建,而在 "每次写查询时记得它会怎么被使用"。

相关推荐
CoderYanger6 小时前
Java EE 进阶:8.MyBatis 操作数据库(入门)(第二弹)
java·数据库·spring boot·spring·面试·java-ee·mybatis
wafrdggh9 小时前
2026电商快递批量查询怎么用?固乔助手模板设置评测
大数据·数据库·小程序
一个天蝎座 白勺 程序猿9 小时前
KingbaseES数据库归档重做日志文件管理:从配置到运维的实战指南
运维·数据库·oracle·kingbasees·金仓数据库
龙腾AI白云12 小时前
仿真到现实(Sim-to-Real)迁移:跨越仿真与现实鸿沟
数据库·机器学习·plotly·知识图谱·tornado
freejackman15 小时前
【实战二】字段级血缘追踪:拆解 AST 迷局,精准穿透函数链与子查询
大数据·数据库·sql
.痞老板.16 小时前
mysql初阶--增删改查操作
数据库·mysql
lambo mercy21 小时前
AI智能租房助手
数据库
浮生若城1 天前
MySQL:库与表的操作
数据库·mysql
薛定谔的悦1 天前
光储充一体机从主控到执行单元的双层架构
数据库·mongodb·架构