MySQL 04 - 索引、事务与优化

功能写出来只是第一步,数据量上来之后”慢查询”才是日常。本章讲解索引的原理与正确用法、EXPLAIN 执行计划分析、事务 ACID 与隔离级别,以及锁和三大日志的基本概念。


一、索引类型

类型关键字特点
主键索引PRIMARY KEY唯一 + 非空,每表一个,InnoDB 聚簇索引
唯一索引UNIQUE列值全表唯一,可为 NULL,用于业务唯一约束
普通索引INDEX / KEY最基础的加速查询结构
联合索引INDEX(a,b,c)多列组合成一个索引,遵循最左前缀原则
全文索引FULLTEXT文本关键词检索(生产多用 Elasticsearch 替代)
前缀索引INDEX(col(n))只对长字符串前 n 个字符建索引,省空间
-- 各类索引创建示例
ALTER TABLE students ADD INDEX idx_class (class_id);          -- 普通
ALTER TABLE students ADD UNIQUE INDEX uk_email (email);       -- 唯一
ALTER TABLE students ADD INDEX idx_class_score (class_id, score);  -- 联合
ALTER TABLE articles ADD FULLTEXT INDEX ft_title_body (title, body);
ALTER TABLE users ADD INDEX idx_name_prefix (name(10));       -- 前缀
 
CREATE INDEX idx_age ON students(age);    -- 另一种等价语法
SHOW INDEX FROM students;                 -- 查看已有索引
DROP INDEX idx_age ON students;           -- 删除索引

二、B+ 树原理简述

MySQL InnoDB 的索引用 B+ 树实现。为什么不用别的结构?

数据结构问题
哈希表等值查询 O(1) 很快,但不支持范围查询和排序WHERE score > 60、ORDER BY 无能为力)
二叉搜索树数据有序时退化为链表;即使用平衡二叉树,树高太深,每个节点一次磁盘 IO,代价大
B 树非叶子节点也存数据,单页能放的键少,树更高
B+ 树数据全在叶子节点且用链表串联:非叶子节点只存键,单节点容纳大量键 → 树矮胖(千万级数据仅 3~4 层),范围查询沿叶子链表顺序扫即可

一句话总结:B+ 树把树高压到 34 层,等值查询只需 34 次磁盘 IO,范围查询靠叶子节点的双向链表顺序遍历,完美契合磁盘分页读写的特性。


三、最左前缀原则

联合索引 (a, b, c) 相当于按 a 排序、a 相同再按 b、b 相同再按 c——类似字典先按首字母再按第二字母排序。因此查询条件必须从最左列开始连续命中才能用上索引:

-- 有联合索引 idx_abc (a, b, c)
WHERE a = 1                        -- 用上索引(只用 a)
WHERE a = 1 AND b = 2              -- 用上索引(用到 a,b)
WHERE a = 1 AND b = 2 AND c = 3    -- 完整命中
WHERE b = 2                        -- 失效!缺少最左列 a
WHERE b = 2 AND c = 3              -- 失效!
WHERE a = 1 AND c = 3              -- 只有 a 生效,c 断层用不上

两个实用推论:

  1. 范围查询截断WHERE a = 1 AND b > 10 AND c = 3 中 c 用不上索引(b 是范围后无法继续定位 c)
  2. 建联合索引时把等值查询多、区分度高的列放左边

四、索引失效场景清单

-- 1. 对索引列做函数或运算
WHERE YEAR(created_at) = 2026          -- 失效;改写为范围:created_at >= '2026-01-01' AND < '2027-01-01'
WHERE id + 1 = 10                      -- 失效;改为 id = 9
 
-- 2. 隐式类型转换(字符串列用数字比较)
WHERE phone = 13800138000              -- phone 是 VARCHAR 时失效;应写 '13800138000'
 
-- 3. LIKE 以 % 开头
WHERE name LIKE '%son'                 -- 失效;'son%' 可以走索引
 
-- 4. OR 连接了无索引的列
WHERE indexed_col = 1 OR no_index_col = 2   -- 整体失效
 
-- 5. 违反最左前缀(见上节)
 
-- 6. 不等于与 NOT IN(视数据分布,可能放弃索引)
WHERE status != 0

原则:让优化器”看得见”索引列本身,别包函数、别转类型、别断左列。


五、EXPLAIN 执行计划

任何慢查询先加 EXPLAIN 看执行计划:

EXPLAIN SELECT s.name, c.name
FROM students s JOIN classes c ON s.class_id = c.id
WHERE s.score > 80;

5.1 关键字段解读

字段含义
type访问类型,性能等级从优到差见下表
key实际使用的索引;NULL 表示没用索引
rows预估扫描行数,越小越好
Extra附加信息:Using index(覆盖索引,好)、Using filesort(额外排序,需优化)、Using temporary(临时表,需优化)

5.2 type 等级表

等级(优→劣)含义
system表只有一行(特例)
const主键或唯一索引等值查询,最多一行
eq_ref连接时被驱动表走主键/唯一索引,一行对应一行
ref普通索引等值查询
range索引范围扫描(BETWEEN、>、IN 等)
index扫整个索引树(比全表好一点有限)
ALL全表扫描,必须优化

经验标准:线上查询至少做到 range 及以上,出现 ALL 且 rows 巨大就要加索引或改写 SQL。


六、事务与 ACID

事务是一组”要么全部成功、要么全部失败”的操作。经典转账例子:

START TRANSACTION;
 
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 若第二条失败,整体回滚
 
COMMIT;     -- 提交生效
ROLLBACK;   -- 或回滚撤销

ACID 四特性:

特性全称含义由什么保证
原子性Atomicity事务内操作不可分割,要么全做要么全不做undo log
一致性Consistency事务前后数据满足所有完整性约束,总额不变其余三者共同作用
隔离性Isolation并发事务互不干扰锁 + MVCC
持久性Durability提交后的修改即使断电也不丢redo log

七、隔离级别

并发事务不隔离会产生三类读问题:

问题描述
脏读读到了别的事务未提交的数据,对方回滚后你的数据就是假的
不可重复读同一事务内两次读同一行,结果不同(别人 UPDATE 了并提交)
幻读同一事务内两次范围查询,行数变了(别人 INSERT 了并提交)

四种隔离级别与问题矩阵:

隔离级别脏读不可重复读幻读
READ UNCOMMITTED 读未提交可能可能可能
READ COMMITTED 读已提交避免可能可能
REPEATABLE READ 可重复读(MySQL 默认)避免避免基本避免*
SERIALIZABLE 串行化避免避免避免

*InnoDB 在可重复读级别下通过 MVCC + 间隙锁(Next-Key Lock)在很大程度上防止幻读,这是 MySQL 与标准的差异点。

查看与设置:

SELECT @@transaction_isolation;                          -- 查看当前级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;  -- 会话级修改

隔离越高越安全但并发性能越低,MySQL 默认的可重复读是绝大多数场景的最佳平衡。


八、锁提一嘴

锁名词粒度/含义
表锁锁整张表,粒度粗并发差,MDL 元数据锁保护表结构变更
行锁InnoDB 基于索引锁定行,粒度细并发好;没走索引时会升级为锁全表所有行
间隙锁(Gap Lock)锁索引记录之间的”空隙”,阻止插入,是防幻读的手段之一
Next-Key Lock行锁 + 间隙锁的组合,RR 级别的默认加锁方式
共享锁 S / 排他锁 X读锁共享、写锁独占;SELECT ... FOR UPDATE 加 X 锁

死锁排查入口:SHOW ENGINE INNODB STATUS\G 中的 LATEST DETECTED DEADLOCK 段落。


九、三大日志一句话

日志层属一句话职责
redo logInnoDB 引擎层崩溃恢复:先写日志再刷盘,保证持久性(WAL)
undo logInnoDB 引擎层回滚:记录反向操作保证原子性,同时支撑 MVCC 多版本读
binlogServer 层主从复制与时间点恢复:记录所有写操作的逻辑日志

三者的协作关系:写入时 redo log 保证崩溃不丢数据,误操作/故障切换靠 binlog 重放到从库,undo log 让任何未提交事务都能全身而退。


十、慢查询排查实战流程

把本章知识串成一条线上排查流水线:

-- 第一步:找到慢 SQL(开启慢查询日志;SET GLOBAL 重启后失效,持久化需写配置文件)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;      -- 超过 1 秒记录
 
-- 查看慢查询日志位置与已记录的慢查询数量
SHOW VARIABLES LIKE 'slow_query_log_file';
SHOW STATUS LIKE 'Slow_queries';

生产环境不要长期把 long_query_time 设得过低(如 0.1),日志量会非常大;一般先用 1~2 秒圈出明显问题,再逐步收紧。

-- 第二步:EXPLAIN 分析
EXPLAIN SELECT ... ;                 -- 看 type/key/rows/Extra
 
-- 第三步:确认表上现有索引
SHOW INDEX FROM 表名;
 
-- 第四步:补索引并验证效果
ALTER TABLE t ADD INDEX idx_xxx (col);
EXPLAIN SELECT ... ;                 -- 对比 rows 是否骤降

常见结论对照:

EXPLAIN 现象处理方向
type = ALL 且 rows 巨大加索引或改写条件让索引可用
key = NULL 但索引存在检查是否触发失效场景(函数/隐式转换/% 开头)
Extra: Using filesortORDER BY 列加入索引,或减小排序数据量
Extra: Using temporaryGROUP BY/DISTINCT 列考虑纳入索引
rows 与实际差距大ANALYZE TABLE 更新统计信息

十一、本章小结

主题要点
索引本质B+ 树,3~4 层撑起千万数据,叶子链表支持范围扫描
联合索引最左前缀原则,范围列之后的列失效
失效场景函数运算、隐式转换、% 开头 LIKE、OR 混无索引列
EXPLAINtype 达到 range 以上,警惕 ALL 与 Using filesort
事务ACID 分别由 undo log、全体、锁+MVCC、redo log 支撑
隔离级别MySQL 默认 RR,MVCC + 间隙锁基本防住幻读

下一章讲安全运维:用户权限与备份恢复;SQL 与执行计划的进阶技巧见 09 高级主题与性能诊断


返回 数据库目录