上次我们学了视图:一条被命名的 SELECT 语句,本质是虚拟表,能像查表一样使用。学习了 CREATE VIEW、ALTER VIEW、DROP VIEW 五项管理操作。
本次课我们换一种"加速器"——视图是为了"简化查询写法",索引则是为了"提升查询速度"。它们解决的问题不一样。
7.3 索引(Index)
先理解"索引是什么、为什么需要"。这是后面动手操作的根基。
📚 想象一下:你手上有一本 没有目录、也没有页码 的字典,要查"鼎"这个字。怎么办?只能从第一页开始一页一页翻,直到找到为止。
现在再想象一本 正常字典:前面有拼音目录"D... → 鼎 (第 358 页)"。你只需要花两步——查目录、跳到 358 页——就找到了。
第二种方式快得多,但代价是字典前面多印了几十页目录。这就是数据库 索引 的本质:用一点额外的存储空间,换取查询速度的飞跃。
① 索引的作用:以空间换时间
为什么数据库需要索引?
在大多数业务场景中,数据库 "读"操作的次数远多于"写"操作(一个网站,浏览页面可能上千次,但下单可能就几次)。所以 "如何让读得更快" 成了数据库优化的核心命题。
索引(Index)是数据库中一种 独立于数据本身的辅助数据结构,采用 键值对 的形式存储 —— "键"是要查的内容,"值"是这个内容在数据中的位置。借助索引,数据库可以快速定位到目标数据,避免逐行扫描。
字典类比图
没有索引:数据库挨个读每一行,找你要的数据 —— 叫"全表扫描"。
有了索引:数据库先查索引,再根据索引指向的位置精准取数据 —— 叫"索引扫描"。
表越大,差距越夸张:100 万行的表,全表扫可能要 1 秒,索引扫可能只要 1 毫秒。
索引也不是免费的
- 占用额外存储空间:索引本身是一份数据,要存到硬盘上。
- 降低写入性能:每次 INSERT、UPDATE、DELETE,数据库不仅要改表,还要 同步维护索引。索引越多,写越慢。
- 需要计算资源维护:MySQL 在后台自动维护索引(重新平衡 B-Tree、调整指针等),消耗 CPU 和内存。
所以索引不是"越多越好"。合适的索引才是好索引,这正是后面"设置原则"要讲的。
② 索引的类型:四个分类维度
索引可以从 4 个不同的角度 进行分类。这 4 个维度是 独立的 —— 同一个索引可以同时被打上多个标签。比如:一个索引可能既是"非聚集索引",又是"复合索引",还是"唯一索引",使用"B-Tree 存储方式"。
按物理存储关系
索引的排列顺序与数据本身的存储顺序是否一致。
按涉及的列数
索引建立在 1 列还是多列上。
按索引特征(用途)
索引在数据约束、查询场景上的特殊属性。
按存储数据结构
索引底层是用什么数据结构组织的。
重点理解 1:聚集索引 vs 非聚集索引
索引的排列顺序 和数据本身的物理顺序一致。
就像一本"按拼音排好序"的字典 —— 翻到 D 那一页,D 开头的字本身就连续排在那里。
每张表只能有一个聚集索引(因为数据物理顺序只有一种)。MySQL 中 主键自动是聚集索引。
索引和数据 分开存储,索引里只存"键 + 数据位置"。
就像一本"按出现频率排序的字"的索引 —— 索引里写"鼎 → 158 页",但鼎在正文中的位置和它在索引里的位置毫无关系。
一张表可以有多个非聚集索引。我们后面 CREATE INDEX 创建的,绝大多数都是非聚集索引。
重点理解 2:单列索引 vs 复合索引
单列索引:在一个字段上建索引,比如只对 姓名 建索引。
复合索引:在多个字段上一起建索引,比如对 (姓名, 年龄) 一起建。
适合"经常一起出现在 WHERE 条件里"的字段组合,比如查询 WHERE 姓名='张三' AND 年龄=20。
重点理解 3:按特征的 5 种索引
| 类型 | 说明 |
|---|---|
| 普通索引(INDEX) | 最基础的索引,对值没有限制 —— 允许重复、允许 NULL。 |
| 唯一索引(UNIQUE INDEX) | 索引列的值 不允许重复(但允许 NULL,且可以多个 NULL)。常用于学号、身份证等"逻辑唯一"字段。 |
| 主键索引(PRIMARY KEY) | 特殊的唯一索引,值不允许重复 也不允许 NULL。每张表只有一个,自动是聚集索引。 |
| 全文索引(FULLTEXT INDEX) | 用于文本字段(CHAR/VARCHAR/TEXT)的"模糊全文搜索",比 LIKE 高效得多。 |
| 空间索引(SPATIAL INDEX) | 用于地理位置数据(GEOMETRY 类型),比如查找"附近 1 公里内的餐厅"。 |
重点理解 4:B-Tree 索引 vs Hash 索引
底层是平衡树结构,支持范围查询(如 age > 20)和 排序。
MySQL 中 InnoDB 引擎默认使用 B+Tree(B-Tree 的变种)。绝大多数情况下都用它。
底层是哈希表,等值查询极快(O(1) 时间)。
但 不支持范围查询、不支持排序。MySQL 中 Memory 引擎默认使用,业务表中很少用。
③ 索引的设置原则:5 条铁律
索引不是越多越好,要"会用"。下面是 5 条核心原则:
-
1
严格限制同一个表上的索引数量。索引会拖慢 INSERT/UPDATE/DELETE 的执行性能。对于使用频度低或不再使用的索引,要及时删除。
-
2
对"重复值较多"的列,不建议建索引。比如"性别"字段只有"男/女"两个值 —— 索引扫一半才能锁定,效果还不如直接全表扫。
-
3
对"排序、分组、表连接"涉及的字段建索引,可显著提升检索效率。比如经常在 ORDER BY、GROUP BY、JOIN ON 中出现的字段。
-
4
对视图建立索引,将提升视图的检索效率(这是上节课视图的延伸)。
-
5
注意唯一索引和全文索引对 NULL 的处理方式。唯一索引允许多个 NULL(NULL 不参与"唯一性"判断),全文索引会忽略 NULL 值,使用前要清楚这些差异。
反例对比:什么时候 不应该 建索引?
• 学生表的 学号(值唯一,常用于查询)
• 订单表的 下单时间(常做范围查询和排序)
• 选课表的 学号 + 课程号(常用于 JOIN)
• 学生表的 性别(重复值太多)
• 备注、简介这类 大文本字段(除非用全文索引)
• 几乎从不出现在 WHERE 中的字段
📋 第一部分小结
- 索引的本质:以空间换时间。用一份独立的"键值对"结构,让查询不必扫全表。
- 4 个分类维度:物理存储(聚集/非聚集)、列数(单列/复合)、特征(普通/唯一/主键/全文/空间)、数据结构(B-Tree/Hash)。
- 4 个维度独立组合:一个索引可以同时具备多种属性。
- 5 条设置原则:限制数量、避免低区分度列、关注排序分组连接列、可对视图建、注意 NULL 处理。
- 记住代价:索引会拖慢写入并占用空间,不能滥用。
🎯 概念小测
关于索引的本质,下列说法 正确 的是?
✅ 正确答案:B
A 错:索引不是数据的压缩,而是独立的辅助结构;C 错:索引只加速读操作,对写操作反而是负担(要同步维护);D 错:索引过多会占空间、拖慢写。
下列字段中,最 不适合 建立索引的是?
✅ 正确答案:B
性别字段只有 2 个值,区分度极低 —— 即使用索引定位到"男",也要扫几十万行。重复值越多,索引价值越低。这正是设置原则第 2 条。
关于聚集索引和非聚集索引的描述,错误 的是?
✅ 正确答案:D
D 错:聚集索引数据和索引在一起,查到索引就拿到数据;非聚集索引拿到的是"位置",还要再去取数据 —— 所以一般 聚集索引更快。其他三项都是聚集/非聚集的核心特征。
7.4 MySQL 索引管理
概念熟了,开始动手。索引的"创建—查看—删除"三件套,每个都要在自己的 MySQL 上敲一遍。
📦 准备工作:复用上次课的环境
本节继续使用上次课建好的 school 数据库(s 学生、c 课程、sc 选课 三张表)。如果你已经按上次的脚本建好了,这一步可以跳过。如果忘了或者数据被改乱了,下面是简化版的重建脚本,再加一张教师表 t(本节有例子要用):
USE school;
-- 新增:教师表 t(如果不存在)
CREATE TABLE IF NOT EXISTS t (
tno VARCHAR(10) PRIMARY KEY, -- 教师工号
tn VARCHAR(20) NOT NULL, -- 教师姓名
title VARCHAR(10), -- 职称
dept VARCHAR(20) -- 所在学院
);
INSERT IGNORE INTO t VALUES
('t01', '王老师', '教授', '信息学院'),
('t02', '李老师', '副教授', '信息学院'),
('t03', '张老师', '讲师', '理学院');
本节所有索引创建例子,请直接在 school 数据库里执行。如果创建索引时报"Duplicate key name",说明同名索引已存在,先用本节"删除索引"的方法删掉再重建即可。
① 创建索引:三种方式
MySQL 中创建索引有 3 种语法路径,加上 Workbench 图形化操作共 4 种。我们重点掌握前 3 种。
语法格式
CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX 索引名称
ON 表名(字段名 [(索引字符长度) [ASC | DESC]] [, ...]);
语法关键字详解
| 关键字 | 说明 |
|---|---|
| UNIQUE / FULLTEXT / SPATIAL | 可选,指定索引类型。不写就是普通索引。三选一,不能同时多个。 |
| INDEX 索引名称 | 必填,给索引起一个名字(用于后面查看、删除时引用)。 |
| ON 表名 | 指定索引建在哪张表上。 |
| 字段名 | 建索引的字段。1 个就是单列索引,多个用逗号分隔就是复合索引。 |
| (索引字符长度) | 对字符串字段,可只取前 N 个字符建索引(节省空间)。如 sn(6) 表示只对 sn 字段的前 6 个字符建索引。 |
| ASC / DESC | 排序方向,默认 ASC。 |
例 1.1 · 创建单列索引
需求:为学生表 s 的姓名字段(sn)建立 普通索引 s_name_index,索引只取 sn 的前 6 个字节,并以 降序 方式排列。
CREATE INDEX s_name_index
ON s(sn(6) DESC);
"创建索引,名字叫 s_name_index,建在 表 s 的字段 sn 上,只取前 6 个字符,以 降序 排列。"
没写 UNIQUE/FULLTEXT/SPATIAL,所以是 普通索引。后面可以验证一下:
SHOW INDEX FROM s;
| Table | Non_unique | Key_name | Column_name | Sub_part | Collation |
|---|---|---|---|---|---|
| s | 0 | PRIMARY | sno | NULL | A |
| s | 1 | s_name_index | sn | 6 | D |
例 1.2 · 创建复合唯一索引
需求:为课程表 c 的课程名(cn)和学时(ct)字段建立 复合唯一索引 c_cn_ct_index。
CREATE UNIQUE INDEX c_cn_ct_index
ON c(cn, ct);
① UNIQUE 关键字 → 唯一索引,要求 (cn, ct) 这个 组合 不能重复(注意:不是单个 cn 唯一,也不是单个 ct 唯一,而是"两个值合起来"必须唯一)。
② 字段写了 2 个(用逗号隔开)→ 复合索引。
③ 复合索引中字段的 顺序很重要 —— 查询条件中只有用到了 cn(或 cn + ct)的查询才能用上这个索引;如果只用 ct 查,这个索引用不上(这就是数据库里著名的"最左前缀原则")。
语法格式
在 CREATE TABLE 时,在最后的列约束之后 直接附加索引定义:
CREATE TABLE 表名(
属性名1 数据类型 [列约束],
...
属性名n 数据类型 [列约束],
[表约束],
[UNIQUE | FULLTEXT | SPATIAL] INDEX | KEY 索引名1 (字段名 [(长度) [ASC|DESC]]),
...
[UNIQUE | FULLTEXT | SPATIAL] INDEX | KEY 索引名n (字段名 [(长度) [ASC|DESC]])
);
建表时,把字段定义和索引定义写在同一个括号里。索引定义和列定义之间用逗号分隔。
INDEX 和 KEY 是 同义词,写哪个都行(KEY 更短)。
例 2 · 建表附加索引
需求:创建教室表 classroom,包含自增主键 cid、教室编号 crno(非空)、教室所在教学楼 cbn(非空)。同时附加一个由 (crno, cbn) 构成的 复合唯一索引 cn_cb_index。
CREATE TABLE classroom (
cid INT AUTO_INCREMENT, -- 自增主键
crno VARCHAR(10) NOT NULL, -- 教室编号
cbn VARCHAR(10) NOT NULL, -- 教学楼
PRIMARY KEY(cid),
UNIQUE INDEX cn_cb_index(crno, cbn) -- 在这里附加索引
);
-- 验证
SHOW INDEX FROM classroom;
| Key_name | Non_unique | Column_name | Seq_in_index |
|---|---|---|---|
| PRIMARY | 0 | cid | 1 |
| cn_cb_index | 0 | crno | 1 |
| cn_cb_index | 0 | cbn | 2 |
当你 建表的同时就明确知道要建哪些索引(例如根据需求文档已经设计好),用这种方式最简洁 —— 不用建完表再单独执行 CREATE INDEX。
语法格式
ALTER TABLE 表名
ADD [UNIQUE | FULLTEXT | SPATIAL] INDEX | KEY 索引名1 (字段名 [(长度) [ASC|DESC]]),
...
ADD [UNIQUE | FULLTEXT | SPATIAL] INDEX | KEY 索引名n (字段名 [(长度) [ASC|DESC]]);
"修改表 → 添加索引"。一条语句可以 添加多个索引(每个索引前面都要 ADD)。
例 3 · 给已有表添加索引
需求:为教师表 t 的姓名字段 tn 添加索引 tn_index,索引长度为 6,使用降序排列。
ALTER TABLE t
ADD INDEX tn_index(tn(6) DESC);
-- 验证
SHOW INDEX FROM t;
| Table | Key_name | Column_name | Sub_part | Collation |
|---|---|---|---|---|
| t | PRIMARY | tno | NULL | A |
| t | tn_index | tn | 6 | D |
一次添加多个索引
ALTER TABLE 还可以"批量添加",比逐条执行效率高(只锁表一次):
ALTER TABLE t
ADD INDEX idx_title(title),
ADD INDEX idx_dept(dept);
• 建表时就需要 → 方式 2(建表附加,最简洁)
• 表已存在,想加单个索引 → 方式 1(CREATE INDEX,语法直观)
• 表已存在,想一次加多个 → 方式 3(ALTER TABLE 批量)
注意:CREATE INDEX 不能用来创建主键索引(主键只能在建表时或 ALTER TABLE ADD PRIMARY KEY 时创建)。
使用 MySQL Workbench 图形化创建索引
对不喜欢敲 SQL 的同学,Workbench 也提供了图形化方式:
- 在 Navigator 窗格的 Schemas 选项卡下,展开目标数据库下的 Tables 节点;
- 展开要建索引的表,右键点击 Indexes 下的 PRIMARY 节点,从弹出菜单中选择 Create Index...;
- 在右侧的窗口中,勾选要建索引的列;
- 点击 Create Index For Selected Columns 按钮;
- 填写索引名称、字段长度、索引类型(普通/唯一/全文)、索引算法、并发锁管理策略等;
- 点击 Apply 提交。
Workbench 图形化方式适合 偶尔操作。但学好 SQL 命令更重要 —— 因为:① 服务器没图形界面时只能敲命令;② 自动化脚本必须是命令;③ 团队协作要把建索引语句写进数据库迁移脚本里。
② 查看索引:SHOW INDEX
语法格式
SHOW INDEX FROM 表名 [FROM 数据库名];
"显示来自 某张表的索引信息"。可选的 FROM 数据库名 用于跨库查询。
例子:查看学生表 s 的索引
SHOW INDEX FROM s;
-- 等价于:SHOW INDEX FROM s FROM school;
| 列名 | 含义 |
|---|---|
| Table | 表名 |
| Non_unique | 0=唯一索引,1=非唯一索引 |
| Key_name | 索引名(PRIMARY 是主键索引) |
| Seq_in_index | 该字段在索引中的位置(复合索引看这个判断顺序) |
| Column_name | 索引涉及的字段 |
| Sub_part | 索引字符长度(NULL 表示整列) |
| Collation | 排序方式:A=升序,D=降序,NULL=无序 |
| Index_type | 索引数据结构:BTREE / HASH / FULLTEXT |
📌 用 MySQL Workbench 查看索引
在 Navigator 窗格的 Schemas 选项卡下,右键点击表名 → 选择 Alter Table,然后切换到右侧窗口的 Indexes 选项卡,就能看到该表所有索引的详细信息(也可以在这里直接修改索引)。
③ 删除索引:两种语法
MySQL 提供了两种删除索引的语法,效果一样,按习惯选用:
方式 1:ALTER TABLE ... DROP INDEX
ALTER TABLE 表名
DROP INDEX 索引名;
方式 2:DROP INDEX ... ON 表名
DROP INDEX 索引名 ON 表名;
例子:删除 s 表上的 s_name_index
DROP INDEX s_name_index;
动词是"修改表",索引是被修改的内容。
动词是"删除索引",更直接。
两种语法 底层执行的是同一个操作,性能也一样。选哪个看你的习惯:写法 B 更短更直接,写法 A 和"添加索引"的 ALTER TABLE 写法对称(如果你已经习惯了 ADD INDEX,DROP INDEX 也用 ALTER TABLE 形式更统一)。
• 删除索引是 不可逆操作,删除前最好用 SHOW INDEX FROM 表名 把索引信息记下来。
• 不能用 DROP INDEX 删除主键索引(PRIMARY KEY),删除主键要用 ALTER TABLE ... DROP PRIMARY KEY。
• 索引名要和 SHOW INDEX 中的 Key_name 一致,大小写敏感。
📋 第二部分小结
- 创建索引:
①CREATE [UNIQUE/FULLTEXT/SPATIAL] INDEX 名 ON 表(字段[(长度) [ASC/DESC]]);
② 建表时附加... INDEX 名(字段);
③ALTER TABLE 表 ADD INDEX 名(字段)。 - 查看索引:
SHOW INDEX FROM 表名; - 删除索引:
ALTER TABLE 表 DROP INDEX 名或DROP INDEX 名 ON 表 - 关键概念:复合索引 = 多个字段;唯一索引 = 加 UNIQUE;索引字符长度 = 字段名后跟
(N)。
✏️ 综合练习
以下题目综合了 索引概念 和 MySQL 操作。先想答案,再点选项验证。
下列 SQL 语句 语法正确 的是?
CREATE INDEX idx ON s WHERE sn LIKE '王%';CREATE UNIQUE INDEX idx_sn ON s(sn(10));CREATE INDEX UNIQUE idx ON s(sno);ALTER TABLE s INDEX idx_age(age);✅ 正确答案:B
A 错:CREATE INDEX 没有 WHERE 子句(索引不能"带条件");C 错:UNIQUE 必须放在 INDEX 前面;D 错:ALTER TABLE 添加索引必须用 ADD INDEX,不能省略 ADD。B 是标准的"建唯一索引、字段长度限制为 10"的写法。
执行 CREATE INDEX idx_dept_age ON s(dept, age); 后,下列哪个查询 可以利用 这个索引?
SELECT * FROM s WHERE dept='信息学院' AND age=20;SELECT * FROM s WHERE age=20;SELECT * FROM s WHERE sn='张三';SELECT * FROM s ORDER BY sno;✅ 正确答案:A
这是 最左前缀原则:复合索引 (dept, age) 中,字段顺序是 dept 在前。查询条件中 必须用到 dept(最左字段),索引才生效。
A:用了 dept + age → 完美命中; B:只用 age 没用 dept → 用不上; C:用了 sn 不在索引里 → 用不上; D:按 sno 排序,索引也无关。
想要删除表 t 上名为 tn_index 的索引,下列 都正确 的是?
DROP INDEX tn_index ON t;ALTER TABLE t DROP INDEX tn_index;DELETE INDEX tn_index FROM t;✅ 正确答案:C
MySQL 提供了 两种等价的删除索引语法:DROP INDEX 索引名 ON 表名 和 ALTER TABLE 表名 DROP INDEX 索引名,效果完全相同。D 是错的 —— SQL 中没有 DELETE INDEX 这种语法。
下列描述中 错误 的是?
✅ 正确答案:C
C 是 反向错误:索引会拖慢 INSERT/UPDATE/DELETE 性能(每次写都要维护索引)。索引"宁缺毋滥"。其他三项都正确。
想为 sc 表的 (sno, cno) 两个字段建立 复合普通索引,下列 不正确 的是?
CREATE INDEX idx_s_c ON sc(sno, cno);ALTER TABLE sc ADD INDEX idx_s_c(sno, cno);CREATE INDEX idx_s_c ON sc(sno) AND ON sc(cno);INDEX idx_s_c(sno, cno)✅ 正确答案:C
C 是错的 —— SQL 中没有"AND ON"这种语法。复合索引 所有字段写在同一个括号里,用逗号分隔(A、B、D 都是这样)。注意:A 和 B 是 建一个复合索引,不是 建两个单列索引。
🛠️ 实操任务(课后练习)
以下任务请在自己的 MySQL 环境中实际执行,并用 SHOW INDEX FROM 表名 验证。
任务 1 · 基础
为学生表 s 的年龄字段 age 建立 普通索引,名为 idx_age,使用 升序 排列。
查看参考答案
CREATE INDEX idx_age
ON s(age ASC);
-- 验证
SHOW INDEX FROM s;
ASC 是默认值,可以省略不写。
任务 2 · 中等
用 ALTER TABLE 一次性给课程表 c 添加两个索引:
① 课程名 cn 的 唯一索引,名为 uk_cn;
② 学时 ct 的 普通索引,名为 idx_ct。
查看参考答案
ALTER TABLE c
ADD UNIQUE INDEX uk_cn(cn),
ADD INDEX idx_ct(ct);
SHOW INDEX FROM c;
如果已经在前面创建过 c_cn_ct_index,cn 字段上可能已有相关索引;执行前可先 DROP。
任务 3 · 综合(思考 + 实操)
① 为 sc 表创建一个复合索引 idx_sno_cno,包含 (sno, cno);
② 然后用 SHOW INDEX FROM sc 查看;
③ 思考并回答:下列两个查询,哪一个能用上这个索引?
查询甲:SELECT * FROM sc WHERE sno='s01' AND cno='c01';
查询乙:SELECT * FROM sc WHERE cno='c01';
④ 最后用 两种语法 分别删除这个索引(先删一次,再创建一次,再用另一种方式删除)。
查看参考答案
-- ① 创建复合索引
CREATE INDEX idx_sno_cno ON sc(sno, cno);
-- ② 查看
SHOW INDEX FROM sc;
-- ④a 用 DROP INDEX 语法删除
DROP INDEX idx_sno_cno ON sc;
-- 重新创建一次
CREATE INDEX idx_sno_cno ON sc(sno, cno);
-- ④b 用 ALTER TABLE 语法删除
ALTER TABLE sc
DROP INDEX idx_sno_cno;
③ 思考题答案:
查询甲能用上索引 —— 用到了复合索引最左字段 sno。
查询乙用不上索引 —— 没用 sno,违反"最左前缀原则"。
如果业务中乙这种"只查 cno"的查询很常见,应该单独为 cno 建一个索引。