mysql-索引详解

一、 搞懂 "索引是什么":数据库的 "超级目录"

【知识点提纲】
  • 索引的本质与核心作用

  • 索引的 "空间换时间" 代价

  • 两大核心分类(功能约束 + 存储方式)

【口语化理解】

你有一本 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 万倍。

【测试目的】

直观感受 "无索引""普通索引""覆盖索引" 的 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 order WHERE 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 order WHERE 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 ON order(user_id, create_time, order_no, amount); -- 覆盖索引

  • 优化 SQL(仅查询索引包含的字段,避免回表):

    SELECT user_id, create_time, order_no, amount FROM order WHERE 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 测试性能,解决索引失效问题;

  • 进阶:学习前缀索引(长字符串字段)、索引碎片整理、分区表 + 局部索引。

相关推荐
一个天蝎座 白勺 程序猿1 小时前
Apache IoTDB(11):分段聚合深度解析——从原理到实战的完整指南
数据库·apache·iotdb
Java Fans1 小时前
PyQt实现SQLite数据库操作案例详解
数据库·sqlite·pyqt
子夜江寒1 小时前
MySQL 学习
数据库·mysql
wepe122 小时前
FlyEnv---phpstudy平替
java·python·mysql·nginx·php
SAP小崔说事儿2 小时前
SAP B1 库龄分析报表(SQL版本&非批次管理)
数据库·sql·sap·sap b1·business one·批次管理·库龄分析
不穿格子的程序员2 小时前
Redis篇7——Redis深度剖析:主从数据同步原理与实践优化
数据库·redis·缓存·数据同步
廋到被风吹走2 小时前
【数据库】【Redis】监控与告警体系构建
数据库·redis·缓存
老华带你飞2 小时前
个人网盘管理|基于springboot + vue个人网盘管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端
代码or搬砖2 小时前
悲观锁讲解
开发语言·数据库