一、 搞懂 "索引是什么":数据库的 "超级目录"
【知识点提纲】
-
索引的本质与核心作用
-
索引的 "空间换时间" 代价
-
两大核心分类(功能约束 + 存储方式)
【口语化理解】
你有一本 1000 页的《百科全书》,想找 "MySQL" 相关内容:
-
无索引:从第 1 页翻到第 1000 页(全表扫描),累且慢;
-
有索引:先查目录,发现 "MySQL" 在第 356-368 页,直接定位(索引查询),秒找到。
数据库索引就是这个 "目录"------ 本质是排好序的数据结构,目的是减少磁盘 IO,加速查询。但代价很明确:
-
占空间:目录本身要占书页(磁盘存储);
-
写操作变慢:新增 / 修改 / 删除内容时,不仅要改正文,还得同步更新目录(索引维护)。
【核心分类:一眼分清】
| 分类维度 | 类型 | 核心特点 | 适用场景 |
|---|---|---|---|
| 功能约束 | 主键索引(PRIMARY) | 唯一 + 非空,一张表仅 1 个;InnoDB 中是聚簇索引 | 唯一标识数据(如用户 ID、订单 ID) |
| 唯一索引(UNIQUE) | 值唯一,允许 NULL(多个 NULL 不冲突),可建多个 | 保证字段不重复(手机号、邮箱) | |
| 普通索引(INDEX) | 无约束,仅加速查询,可建多个 | 高频查询字段(如部门 ID、商品分类) | |
| 复合索引(联合索引) | 多列组合(如 (dept_no, salary)),遵循 "最左前缀原则" | 组合查询场景(按 "部门 + 薪水" 查员工) | |
| 存储方式(InnoDB) | 聚簇索引 | 索引与数据 "长在一起",叶子节点存整行数据 | 主键索引默认是聚簇索引 |
| 非聚簇索引(二级) | 索引与数据分离,叶子节点存主键值,需 "回表" 查数据 | 普通索引、唯一索引均为非聚簇索引 |
二、 核心原理:索引怎么 "快速找到数据"
【知识点提纲】
-
B+ 树索引的结构与查询逻辑
-
复合索引的 "最左前缀匹配原则"
-
覆盖索引:性能优化的 "天花板"
【口语化理解】
1. B+ 树:MySQL 的 "导航地图"
MySQL 默认用 B+ 树做索引,你可以想象成一棵 "倒过来的矮胖树":
-
非叶子节点:只存索引值(比如 ID、手机号),不存数据,所以一层能存很多索引,树的高度只有 2-3 层;
-
叶子节点:存完整数据(聚簇索引)或主键值(非聚簇索引),且叶子节点之间是双向链表(像手拉手排队),支持范围查询(比如查薪水 5000-8000 的员工)。
查询逻辑:找 ID=1001 的数据,从树根出发 → 第一层判断 "1001 比 500 大,走右路" → 第二层判断 "1001 比 1000 大,走右路" → 第三层(叶子节点)直接找到数据,最多 3 次磁盘 IO,千万级数据也能秒查。
2. 复合索引:"排队要按顺序来"
复合索引 (a, b, c) 的排序规则是:先按 a 排,a 相同再按 b 排,b 相同再按 c 排 ------ 相当于 "先按班级分,再按学号分"。
最左前缀匹配原则:查询时必须从左到右匹配列,跳过左边列,索引直接失效:
-
有效:WHERE a=1、WHERE a=1 AND b=2、WHERE a=1 AND b=2 AND c=3;
-
无效:WHERE b=2、WHERE b=2 AND c=3(没匹配最左列 a,后续排序无意义)。
3. 覆盖索引:"目录里直接有答案"
如果索引包含了查询所需的所有字段,就不用 "翻正文"(回表),直接从索引拿结果 ------ 这是最快的查询方式。
例子:索引 (name, age),查询 SELECT name, age FROM users WHERE name='张三';
→ 索引里直接存了 name 和 age,不用回表查原数据,速度拉满!
【原理避坑】
-
为什么不用哈希索引?哈希索引只支持等值查询(=、IN),不支持范围查询(>、、BETWEEN`),而业务中范围查询很常见(比如查最近 7 天的订单);
-
为什么 B+ 树比二叉树好?二叉树是 "瘦高个",千万级数据时树高十几层,磁盘 IO 次数多;B+ 树是 "矮胖子",树高仅 2-3 层,IO 次数最少。
三、 实操:索引的创建 / 管理 / 设计
【知识点提纲】
-
索引创建 / 删除 / 查看的 SQL 语句
-
索引设计的核心原则
-
不同场景的索引设计案例
【SQL 实操:直接复制可用】
-- 1. 创建索引(推荐指定索引名,格式:idx_表名_字段名)
CREATE UNIQUE INDEX idx_users_phone ON users(phone); -- 唯一索引(手机号)
CREATE INDEX idx_employees_dept_sal ON employees(dept_no, salary); -- 复合索引(部门+薪水)
ALTER TABLE products ADD INDEX idx_products_category (category_id); -- 普通索引(商品分类)
-- 2. 查看索引(MySQL)
SHOW INDEX FROM users; -- 查看 users 表所有索引(详细)
SHOW KEYS FROM employees; -- 简洁版,查看索引名、列名
-- 3. 删除索引(没用的索引及时删,减少维护开销)
DROP INDEX idx_users_phone ON users;
ALTER TABLE products DROP INDEX idx_products_category;
【索引设计:3 个核心原则】
-
字段区分度要高:
-
- 适合:手机号(区分度 100%)、用户 ID(区分度 100%);
-
- 不适合:性别(只有男 / 女,区分度低)、状态(0/1/2,区分度低)------ 索引过滤效果差,不如全表扫描。
-
优先满足高频查询:
-
- 高频查询 WHERE dept_no=? AND salary>? → 建复合索引 (dept_no, salary);
-
- 高频排序 ORDER BY create_time → 给 create_time 建普通索引。
-
避免过度索引:
-
- 一张表索引不超过 5 个(太多会拖慢 INSERT/UPDATE/DELETE);
-
- 频繁更新的字段(如订单状态)不建索引(更新时要同步维护索引,开销大)。
【场景化设计案例】
| 业务场景 | 索引设计方案 | 理由 |
|---|---|---|
| 用户注册(手机号唯一) | CREATE UNIQUE INDEX idx_users_phone ON users(phone); | 保证手机号不重复,同时加速登录查询(按手机号查用户) |
| 电商商品列表(按分类查) | CREATE INDEX idx_products_category ON products(category_id); | 高频按分类筛选商品,加速查询 |
| 员工报表(按部门 + 薪水查) | CREATE INDEX idx_emp_dept_sal ON employees(dept_no, salary); | 复合索引匹配查询条件,遵循最左前缀 |
四、 优化避坑:索引失效 + 执行计划
【知识点提纲】
-
索引失效的 8 大场景(必记)
-
执行计划(EXPLAIN)的核心解读
-
慢查询优化实战
【索引失效:8 大死穴(附解决方案)】
| 失效场景 | 错误 SQL 示例 | 优化 SQL 示例 | 失效原因 |
|---|---|---|---|
| 索引列参与计算 / 函数 | WHERE salary + 1000 > 8000 | WHERE salary > 7000 | B+ 树存原始值,计算后的值无法匹配索引排序 |
| 左模糊查询(% 开头) | WHERE name LIKE '%张三' | WHERE name LIKE '张三%'(前缀匹配) | 索引从左往右排,开头不确定,无法定位 |
| 隐式类型转换 | WHERE id = '1001'(id 是 INT 类型) | WHERE id = 1001 | 数据库给列加 CAST() 函数,导致索引失效 |
| 复合索引不满足最左前缀 | 索引 (a,b),查询 WHERE b=2 | WHERE a=1 AND b=2 或给 b 单独建索引 | 复合索引排序依赖最左列,跳过则排序失效 |
| OR 连接非索引列 | WHERE a=1 OR c=2(c 无索引) | 给 c 建索引,或拆分为 UNION 两个查询 | OR 只要有一个列无索引,就会全表扫描 |
| 使用 NOT IN/!= | WHERE salary NOT IN (5000, 8000) | 用 IN 替代,或业务调整为范围查询 WHERE salary 0 OR salary>8000 | 否定条件无法利用索引排序,效率低 |
| SELECT * 导致回表 | SELECT * FROM employees WHERE dept_no='d001' | SELECT dept_no, salary FROM employees WHERE dept_no='d001'(覆盖索引) | 非聚簇索引需回表查数据,SELECT * 增加开销 |
| 数据量极小的表 | 给 100 行的配置表建索引 | 直接全表扫描(比索引查询快) | 索引查询需先查索引再查数据,小表没必要 |
【执行计划(EXPLAIN):索引的 "裁判"】
写好 SQL 后,加 EXPLAIN 就能判断索引是否生效,是优化慢查询的核心工具。
EXPLAIN SELECT dept_no, salary FROM employees WHERE dept_no='d001';
核心看 4 个字段:
| 字段 | 含义 | 优秀值 | 差值 |
|---|---|---|---|
| type | 连接类型(查询效率等级) | const(主键)、ref(非唯一索引)、range(范围) | ALL(全表扫描) |
| key | 实际用到的索引 | 索引名(如 idx_emp_dept_sal) | NULL(未用索引) |
| rows | 预估扫描行数 | 越小越好(如 10、100) | 接近表总数据量(如 100 万) |
| Extra | 额外信息 | Using index(覆盖索引) | Using filesort(文件排序)、Using temporary(临时表) |
【慢查询优化实战】
问题 SQL:SELECT * FROM order WHERE user_id=1001 AND create_time>'2025-01-01';(order 表 1000 万行,查询慢)
优化步骤:
-
执行 EXPLAIN,发现 type=ALL、key=NULL → 全表扫描,索引失效;
-
分析原因:user_id 和 create_time 是高频查询字段,未建索引;
-
建复合索引:CREATE INDEX idx_order_uid_ctime ON order(user_id, create_time);(遵循最左前缀,先 user_id 再 create_time);
-
优化 SQL:SELECT user_id, create_time, order_no FROM order WHERE user_id=1001 AND create_time>'2025-01-01';(覆盖索引,无需回表);
-
再次 EXPLAIN:type=range、key=idx_order_uid_ctime、rows=50 → 索引生效,查询速度提升 20 万倍。
五、 实战测试环节:用 Navicat 对比索引性能
【测试目的】
直观感受 "无索引""普通索引""覆盖索引" 的 SQL 运行时间差异,验证索引的加速效果。
【测试准备】
-
测试环境:Navicat 图形化工具(可视化操作,无需手动写复杂命令);
-
测试表:order 表(模拟电商订单数据,1000 万行,字段:id(主键)、user_id(用户 ID)、order_no(订单号)、create_time(创建时间)、amount(金额));
-
测试 SQL:固定查询 user_id=1001 且 create_time>'2025-01-01' 的数据(模拟用户查询自己的近期订单)。
【测试步骤(Navicat 操作)】
1. 测试 1:无索引(全表扫描)
-
操作:确保 order 表无 user_id 相关索引(可通过「表设计→索引」查看,无则直接测试);
-
在 Navicat 中新建查询,输入 SQL:
SELECT * FROM
orderWHERE user_id=1001 AND create_time>'2025-01-01'; -
点击「运行」(绿色三角按钮),观察 Navicat 底部状态栏的「执行时间」(如:3.28 秒);
-
记录结果:扫描行数 ≈ 1000 万行,执行时间 ≈ 3-5 秒。
2. 测试 2:普通索引(非聚簇索引,需回表)
-
操作:创建普通索引 idx_order_uid(仅包含 user_id):
CREATE INDEX idx_order_uid ON
order(user_id); -
重复测试 1 的 SQL,点击「运行」,记录执行时间(如:0.008 秒);
-
验证:在 SQL 前加 EXPLAIN,查看执行计划:
EXPLAIN SELECT * FROM
orderWHERE user_id=1001 AND create_time>'2025-01-01'; -
- 核心结果:type=ref、key=idx_order_uid、rows≈50(仅扫描用户 1001 的 50 条订单);
-
记录结果:扫描行数 ≈ 50 行,执行时间 ≈ 0.005-0.01 秒。
3. 测试 3:覆盖索引(无需回表)
-
操作:先删除普通索引,再创建复合覆盖索引(包含查询所需所有字段):
DROP INDEX idx_order_uid ON
order; -- 删除普通索引
CREATE INDEX idx_order_uid_ctime_no ONorder(user_id, create_time, order_no, amount); -- 覆盖索引 -
优化 SQL(仅查询索引包含的字段,避免回表):
SELECT user_id, create_time, order_no, amount FROM
orderWHERE user_id=1001 AND create_time>'2025-01-01'; -
点击「运行」,记录执行时间(如:0.001 秒);
-
验证:用 EXPLAIN 查看执行计划,Extra=Using index(命中覆盖索引);
-
记录结果:扫描行数 ≈ 50 行,执行时间 ≈ 0.001-0.003 秒。
【测试结果对比】
| 测试场景 | 扫描行数 | Navicat 执行时间 | 性能提升倍数 | 核心原因 |
|---|---|---|---|---|
| 无索引(全表) | 约 1000 万行 | 3.28 秒 | 1 倍(基准) | 逐行扫描所有数据,IO 开销极大 |
| 普通索引 | 约 50 行 | 0.008 秒 | 410 倍 | 索引快速定位 user_id=1001,但需回表查数据 |
| 覆盖索引 | 约 50 行 | 0.001 秒 | 3280 倍 | 索引包含所有查询字段,无需回表,IO 开销最小 |
【测试注意事项】
-
测试前确保表数据量足够大(建议 100 万行以上),否则性能差异不明显;
-
多次执行 SQL 取平均时间(避免缓存影响结果);
-
测试完成后,可删除测试用索引(避免占用存储和影响写操作)。
六、 总结:索引学习口诀 + 学习路径
【学习口诀(记死这 4 句)】
-
左前缀,要记牢:复合索引顺序不能乱;
-
算函数,别用列:索引列上不做计算 / 转换;
-
覆盖索引,速度高:查询字段尽量在索引里;
-
执行计划,常对照:EXPLAIN 验证索引是否生效。
【学习路径】
-
基础:先掌握索引创建 / 删除 / 查看 SQL,能给常见场景建索引;
-
原理:理解 B+ 树、聚簇 / 非聚簇索引、最左前缀原则,知道 "为什么索引会生效 / 失效";
-
实战:用 EXPLAIN 分析慢查询,结合 Navicat 测试性能,解决索引失效问题;
-
进阶:学习前缀索引(长字符串字段)、索引碎片整理、分区表 + 局部索引。