mysql - 索引

1 B+树

非叶子节点只存键值(不存数据): 极大地增加了单个节点能容纳的索引数量,让树变得更“矮胖”,从而减少磁盘 I/O 次数。

数据全在叶子节点: 叶子节点之间通过双向链表连接,非常适合数据库中常见的范围查询(找到起点后,顺着链表往后遍历即可)。

示意图

表有三个字段,c1(主键)、c2(数字号码)、c3(字符)
image

1.1 对比其他数据结构

  • 二叉查找树(BST): 在极端情况下会退化成链表,查询效率从 O(logN) 变成 O(N)。

  • 平衡二叉树(AVL)/ 红黑树: 虽然解决了退化问题,但由于每个节点只能有两个子节点,树的层级会很深。数据

  • 库是存在磁盘上的,层级深意味着大量的磁盘 I/O,这是致命的性能瓶颈。

  • B 树: 变成了多叉树,层级变浅了。但是它的非叶子节点也存储了数据。这导致一个磁盘页(默认 16KB)能容纳的节点数量变少,树依然会变高。

  • Hash: 只支持等值查询(=、IN),不支持范围查询(>、<)。

2 索引分类

2.1 聚簇索引

2.2 二级索引

除了聚簇索引之外的其他索引,都叫二级索引。比如用 ALTER TABLE ADD INDEX(name) 创建的索引。

特点: 二级索引的叶子节点不存储整行数据,而是存储了索引列的值 + 对应的主键值。

示意图
image

  • c2列建索引

2.2.1 回表

  • 如果通过 name 字段查询:SELECT * FROM user WHERE name = '张三';

  • InnoDB 会先在二级索引的 B+ 树中找到 ‘张三’,拿到它的主键 ID(比如 id=5)。

  • 然后再拿着 id=5 去聚簇索引的 B+ 树中重新查找,最后拿到完整数据。

2.2.2 覆盖索引

  • 如果执行的 SQL 是:SELECT id, name FROM user WHERE name = '张三';

  • 此时,因为 id 和 name 这两个字段的值,已经全部存在于二级索引的叶子节点中了,就不需要再去聚簇索引查了。

2.2.3 联合索引

属于二级索引,多个列建索引

索引优化

索引失效

2.1示例表与数据

CREATE TABLE employees (
    id INT PRIMARY KEY AUTO_INCREMENT,
    emp_no VARCHAR(20) NOT NULL,
    name VARCHAR(50) NOT NULL,
    age INT NOT NULL,
    dept_id INT NOT NULL,
    salary DECIMAL(10,2),
    hire_date DATE NOT NULL,
    INDEX idx_emp_no (emp_no),
    INDEX idx_name (name),
    INDEX idx_age (age),
    INDEX idx_hire_date (hire_date),
    INDEX idx_dept_age (dept_id, age) -- 复合索引
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
id (主键) emp_no name age dept_id salary hire_date
1 E1001 Alice 28 1 8000.00 2020-03-15
2 E1002 Bob 32 2 9500.00 2019-07-22
3 E1003 Charlie 28 1 7800.00 2021-01-10
4 E1004 Diana 35 3 12000.00 2018-11-05
5 E1005 Eve 28 2 8200.00 2022-05-18
6 E1006 Frank 40 1 15000.00 2017-09-30
7 E1007 Grace 32 3 9800.00 2020-12-01
8 E1008 Hank 28 2 7500.00 2023-02-14
9 E1009 Ivy 35 1 11000.00 2019-04-25
10 E1010 Jack 28 3 8500.00 2021-08-08

🔍 场景一:对索引列使用函数或表达式

❌ 失效原因

B+ 树索引叶子节点存储的是列的原始值,并按原始值排序。当在索引列上套用函数或表达式时,MySQL 无法直接用索引树中的原始值进行匹配,必须对每一行数据先计算函数结果,再比较。这等价于全表扫描,优化器会直接放弃索引。

📝 结合数据的3个示例

示例SQL 匹配数据行 失效详细分析
SELECT * FROM employees WHERE YEAR(hire_date) = 2020; 行1(Alice), 行7(Grace) idx_hire_date 存储的是完整日期如 '2020-03-15'YEAR() 函数作用在列上,MySQL 必须逐行提取年份再与 2020 比较。B+ 树的日期字典序被破坏,无法定位扫描起点,走全表扫描(type=ALL)。
SELECT * FROM employees WHERE age + 2 = 30; 行1,3,5,8,10 (age=28) idx_age 叶子节点按 28,32,35,40 排序。age+2 是表达式,优化器无法将 30 反向推导为 age=28 去查索引(除非手动改写)。引擎只能逐行计算 age+2 的值,索引失效。
SELECT * FROM employees WHERE UPPER(name) = 'ALICE'; 行1(Alice) idx_name 存储的是原始大小写 'Alice'UPPER(name) 强制对每行做大小写转换,B+ 树无法直接匹配 'ALICE'。即使只返回1行,仍需扫描全表计算函数结果。

优化方案:将函数/表达式移到等号右侧,保持索引列“裸露”。
如:hire_date >= '2020-01-01' AND hire_date < '2021-01-01'age = 28name = 'Alice'


🔍 场景二:隐式类型转换(字符串列 vs 数字)

❌ 失效原因

MySQL 在比较不同类型时会自动进行隐式转换。当字符串类型的索引列与数字比较时,MySQL 会将字符串列转换为数字,等价于 CAST(column AS SIGNED) = value。这相当于在索引列上加了函数,B+ 树原始值无法直接匹配,索引失效。
⚠️ 注意:如果是 INT 列与字符串比较(如 age = '28'),MySQL 会将常量字符串转为数字,索引仍可使用。失效仅发生在字符串列被转为数字时。

📝 结合数据的3个示例

示例SQL 匹配数据行 失效详细分析
SELECT * FROM employees WHERE emp_no = 1001; 无匹配(emp_no含字母E) emp_noVARCHAR,右侧是 INT。MySQL 执行 CAST(emp_no AS SIGNED) = 1001'E1001' 转数字为 0,所有行转换后都不等于1001。因转换作用在列上,idx_emp_no 失效,全表扫描。
SELECT * FROM employees WHERE name = 0; 行1~10(全部) 字符串列 name 与数字 0 比较。MySQL 将 'Alice''Bob' 等全转为数字 0,导致 0=0 全部匹配。转换破坏索引结构,即使返回全表数据,也走 type=ALL
SELECT * FROM employees WHERE emp_no = TRUE; 无匹配 TRUE 在MySQL中等价于 1。实际执行 CAST(emp_no AS SIGNED) = 1。所有 'E10xx' 转数字均为 0,不匹配。隐式转换发生在索引列,idx_emp_no 无法使用。

优化方案:严格保持类型一致,字符串必须加引号:emp_no = 'E1001'


🔍 场景三:LIKE 左模糊或全模糊匹配

❌ 失效原因

B+ 树索引按从左到右的字符字典序构建。LIKE 'abc%' 能确定前缀起点,可直接定位索引范围。但 LIKE '%abc'LIKE '%abc%' 无法确定首字符,索引的有序性完全失效,只能逐行扫描判断。

📝 结合数据的3个示例

示例SQL 匹配数据行 失效详细分析
SELECT * FROM employees WHERE name LIKE '%lice'; 行1(Alice) idx_name 叶子节点顺序为 Alice, Bob, Charlie...%lice 首字符未知,B+ 树无法判断从哪个节点开始扫描。即使只匹配1行,也必须遍历所有叶子节点检查后缀。
SELECT * FROM employees WHERE emp_no LIKE '%100%'; 行1(E1001), 行10(E1010) %100% 属于全模糊。索引树按 E1001, E1002... 排序,中间匹配无法利用前缀跳跃。优化器判定索引扫描成本高于全表,直接 type=ALL
SELECT * FROM employees WHERE name LIKE '%ob%'; 行2(Bob) 同上。虽然 Bob 在索引中排第2位,但 %ob% 无法利用字典序定位。若表有百万行,索引树遍历+回表成本远超顺序全表扫描,优化器主动放弃索引。

优化方案:尽量使用右模糊 LIKE 'Ali%';若必须左模糊,可考虑全文索引(FULLTEXT)或 Elasticsearch。


🔍 场景四:违反复合索引最左前缀法则

❌ 失效原因

复合索引 idx_dept_age(dept_id, age) 的 B+ 树排序规则是:先按 dept_id 排序,dept_id 相同再按 age 排序。如果查询跳过 dept_id 直接用 age,或 dept_id 使用范围查询,则 age 在索引树中是全局无序的,无法利用索引快速定位。

📝 结合数据的3个示例

示例SQL 匹配数据行 失效详细分析
SELECT * FROM employees WHERE age = 28; 行1,3,5,8,10 索引树叶子节点顺序为:(1,28),(1,28),(2,28),(2,28),(3,28)...age=28 分散在不同 dept_id 下,全局无序。B+ 树无法直接跳跃到 age=28 的起始位置,idx_dept_age 完全失效。
SELECT * FROM employees WHERE dept_id > 1 AND age = 28; 行5(Eve), 行8(Hank) dept_id > 1 是范围查询。B+ 树能定位到 dept_id=2 的起点,但范围扫描后,age 的有序性仅在单个 dept_id 内有效。跨部门后 age 无序,因此只有 dept_id 走索引,age 部分失效key_len 仅包含 dept_id)。
SELECT * FROM employees WHERE age = 32 ORDER BY dept_id; 行2(Bob), 行7(Grace) 过滤条件跳过前导列 dept_id,无法使用索引查找;同时 ORDER BY dept_id 也无法利用该索引排序(因为索引是按 dept→age 排的,不是 age→dept)。最终走全表扫描 + Using filesort

优化方案:查询条件必须包含最左列;范围查询尽量放在复合索引最后;或为高频查询单独建 idx_age(age)


🔍 场景五:OR 条件中包含无索引列

❌ 失效原因

OR 本质是多个结果集的并集。如果其中一个条件没有索引,MySQL 必须全表扫描才能评估该条件。既然已经要全表扫描,优化器会认为“顺便把其他条件也一起判断”比“先走索引再合并结果”成本更低,从而整体放弃索引

📝 结合数据的3个示例

示例SQL 匹配数据行 失效详细分析
SELECT * FROM employees WHERE name = 'Alice' OR salary = 8000.00; 行1(Alice) name 有索引,但 salary 无索引。评估 salary=8000 必须全表扫描。优化器计算成本:全表扫描1次 vs 索引查name+全表查salary+合并。前者更优,故 idx_name 失效,整体 type=ALL
SELECT * FROM employees WHERE age = 35 OR emp_no LIKE '%1009'; 行4(Diana), 行9(Ivy) age 有索引,但 emp_no LIKE '%1009' 左模糊失效。两个条件都无法高效定位,优化器直接选择全表扫描。即使 age=35 能走索引,也被 OR 拖累失效。
SELECT * FROM employees WHERE dept_id = 1 OR salary > 10000; 行1,3,6,9,4 假设 dept_id 有索引。salary>10000 无索引需全表。OR 的并集特性导致优化器放弃 dept_id 索引,直接全表过滤。执行计划显示 type=ALL, Using where

优化方案:将 OR 改写为 UNION(或 UNION ALL),让每个子查询独立使用索引:

SELECT * FROM employees WHERE name = 'Alice'
UNION
SELECT * FROM employees WHERE salary = 8000.00;

posted @ 2026-04-24 20:44  cdc321  阅读(5)  评论(0)    收藏  举报