1. 引言
在掌握了 MySQL 的基础增删改查之后,进阶之路才刚刚开始。事务保证数据的一致性,索引决定查询的速度,存储引擎影响数据的存储方式与可靠性,而高阶特性则让 MySQL 在复杂业务场景中游刃有余。本文作为 MySQL 进阶系列的第三篇,将系统讲解事务、索引优化、存储引擎与高阶特性四大主题,帮助你从「会用 MySQL」走向「用好 MySQL」。
2. 事务:数据一致性的基石
2.1 什么是事务
事务(Transaction)是一组不可分割的数据库操作单元,要么全部成功,要么全部失败回滚。经典的转账场景最能说明问题:A 账户扣款 100 元、B 账户入账 100 元,这两步必须作为一个整体执行,任何一步失败都要撤销全部操作。
2.2 ACID 四大特性
事务的可靠性由 ACID 四大特性保证:
- 原子性(Atomicity):事务内的操作要么全部提交,要么全部回滚,不存在中间状态。
- 一致性(Consistency):事务执行前后,数据库的完整性约束不被破坏,数据始终处于合法状态。
- 隔离性(Isolation):多个事务并发执行时,彼此互不干扰,每个事务看到的数据视图是独立的。
- 持久性(Durability):事务一旦提交,其对数据库的修改就是永久性的,即使系统崩溃也不会丢失。
2.3 隔离级别与并发问题
SQL 标准定义了四种隔离级别,从低到高依次为:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交(READ UNCOMMITTED) | 可能 | 可能 | 可能 |
| 读已提交(READ COMMITTED) | 不可能 | 可能 | 可能 |
| 可重复读(REPEATABLE READ) | 不可能 | 不可能 | 可能 |
| 串行化(SERIALIZABLE) | 不可能 | 不可能 | 不可能 |
MySQL InnoDB 默认使用可重复读隔离级别,并通过 MVCC(多版本并发控制)与间隙锁(Gap Lock)在绝大多数场景下解决了幻读问题。
下面通过两个并发会话(Session A / Session B)的完整操作序列,直观演示 InnoDB 在可重复读隔离级别下,如何借助 MVCC 避免不可重复读、借助间隙锁解决幻读。
准备:建表并插入初始数据
-- 会话 A 与 B 共用同一张表CREATETABLEaccount(idINTPRIMARYKEY,balanceDECIMAL(10,2)NOTNULL)ENGINE=InnoDB;INSERTINTOaccount(id,balance)VALUES(1,100.00),(2,200.00);场景一:MVCC 避免不可重复读
-- ============ 会话 A ============STARTTRANSACTION;SELECTbalanceFROMaccountWHEREid=1;-- 读到 100.00,生成当前读版本快照-- ============ 会话 B ============STARTTRANSACTION;UPDATEaccountSETbalance=150.00WHEREid=1;-- 修改并提交COMMIT;-- ============ 会话 A(继续)============SELECTbalanceFROMaccountWHEREid=1;-- 仍读到 100.00(MVCC 快照读,不受 B 提交影响)COMMIT;说明:可重复读下,会话 A 的普通
SELECT是快照读,基于事务开始时生成的版本链读取。即使会话 B 已提交修改,A 再次查询仍看到事务开始时的旧版本100.00,从而避免了不可重复读。
场景二:间隙锁解决幻读
-- ============ 会话 A ============STARTTRANSACTION;-- 对 id 范围 (1, 3) 加间隙锁,锁定该区间内不存在的记录SELECT*FROMaccountWHEREidBETWEEN1AND3FORUPDATE;-- ============ 会话 B ============STARTTRANSACTION;-- 尝试插入 id = 2 的新记录,会被间隙锁阻塞INSERTINTOaccount(id,balance)VALUES(2,300.00);-- 阻塞等待...-- ============ 会话 A(继续)============COMMIT;-- 释放间隙锁-- ============ 会话 B(继续)============-- 阻塞解除,插入成功COMMIT;说明:会话 A 使用
SELECT ... FOR UPDATE对id BETWEEN 1 AND 3加锁,InnoDB 会在该区间加上间隙锁,阻止其他事务向其中插入新记录。这样会话 A 在事务内多次查询同一范围时,结果集不会凭空多出记录,从而解决了幻读。
2.4 事务的使用示例
-- 开启事务STARTTRANSACTION;-- 执行操作UPDATEaccountSETbalance=balance-100WHEREid=1;UPDATEaccountSETbalance=balance+100WHEREid=2;-- 提交事务COMMIT;-- 若出错则回滚-- ROLLBACK;3. 索引优化:查询提速的关键
3.1 索引的本质与分类
索引是帮助 MySQL 高效获取数据的数据结构,本质上是空间换时间。InnoDB 使用 B+ 树作为索引结构,叶子节点存放完整数据行,非叶子节点只存放索引键,因此树的高度低、IO 次数少。
常见的索引类型包括:
- 主键索引:每张表只能有一个,数据按主键有序排列。
- 唯一索引:索引列的值不允许重复,允许 NULL。
- 普通索引:加速查询,不限制值的唯一性。
- 联合索引:多个列组合建立的索引,遵循最左前缀原则。
- 全文索引:用于全文检索,适合大文本字段。
3.2 最左前缀原则
联合索引(a, b, c)实际会建立(a)、(a, b)、(a, b, c)三个索引。查询条件必须从最左列开始连续匹配才能命中索引:
-- 命中索引WHEREa=1ANDb=2ANDc=3;WHEREa=1ANDb=2;WHEREa=1;-- 无法命中索引WHEREb=2ANDc=3;WHEREc=3;3.3 索引失效的常见场景
即使建立了索引,错误的写法也会让索引失效:
- 对索引列使用函数或计算:
WHERE YEAR(create_time) = 2024 - 隐式类型转换:
WHERE phone = 13800138000(phone 为 varchar 类型) - 前导模糊查询:
WHERE name LIKE '%张' - OR 连接非索引列:
WHERE a = 1 OR b = 2(b 无索引) - 联合索引不满足最左前缀
3.4 索引优化实战建议
3.5 索引优化实战案例
下面用一个订单表orders完整演示从建表、造数据到用EXPLAIN分析慢查询、建立联合索引并对比执行计划的优化过程。
第一步:建表
CREATETABLEorders(idBIGINTAUTO_INCREMENTPRIMARYKEY,user_idBIGINTNOTNULL,order_noVARCHAR(32)NOTNULL,statusTINYINTNOTNULLDEFAULT0,amountDECIMAL(10,2)NOTNULL,create_timeDATETIMENOTNULL,KEYidx_user_id(user_id))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;第二步:插入示例数据
-- 插入 10 万条示例数据(可用存储过程批量生成)INSERTINTOorders(user_id,order_no,status,amount,create_time)SELECTFLOOR(RAND()*10000)+1,CONCAT('NO',LPAD(n,10,'0')),FLOOR(RAND()*5),ROUND(RAND()*1000,2),NOW()-INTERVALFLOOR(RAND()*365)DAYFROM(SELECT@rownum:=@rownum+1ASnFROMinformation_schema.columnsa,information_schema.columnsb,(SELECT@rownum:=0)rLIMIT100000)t;第三步:用 EXPLAIN 分析慢查询
业务上经常需要按「用户 + 状态 + 下单时间」查询订单,先看未建联合索引时的执行计划:
EXPLAINSELECT*FROMordersWHEREuser_id=100ANDstatus=1ANDcreate_time>='2024-01-01';此时key为idx_user_id,type为ref,rows可能高达数千甚至上万——因为只用了user_id单列索引,status和create_time仍需在回表后逐行过滤,数据量大时性能堪忧。
第四步:建立联合索引
ALTERTABLEordersADDINDEXidx_user_status_time(user_id,status,create_time);第五步:对比优化后的执行计划
EXPLAINSELECT*FROMordersWHEREuser_id=100ANDstatus=1ANDcreate_time>='2024-01-01';优化后key变为idx_user_status_time,type仍为ref,但rows大幅下降(可能从数万降到几十),Extra不再出现Using where的二次过滤,查询效率显著提升。
性能提升说明
- 扫描行数骤减:联合索引让
user_id、status、create_time三个条件在索引内一次定位,回表次数从「全量候选行」降为「精准命中行」。 - 减少回表与 IO:候选行越少,回表查询完整数据行的次数越少,磁盘 IO 与内存开销同步下降。
- 遵循最左前缀:该联合索引同时覆盖了
(user_id)、(user_id, status)、(user_id, status, create_time)三种查询组合,一索引多用。
优化后的查询语句与业务写法保持一致,无需改动 SQL,仅通过合理设计联合索引即可获得数量级的性能提升。
- 为高频查询的 WHERE、ORDER BY、GROUP BY 列建立索引。
- 索引列尽量选择区分度高的列,避免重复值过多。
- 控制单表索引数量,一般不超过 5 个,过多会拖慢写入。
- 使用
EXPLAIN分析执行计划,关注type、key、rows字段。
EXPLAINSELECT*FROMordersWHEREuser_id=100ANDstatus=1;4. 存储引擎:选择合适的存储底座
4.1 InnoDB 与 MyISAM 对比
MySQL 5.5 之后默认存储引擎为 InnoDB,它与 MyISAM 的核心差异如下:
| 特性 | InnoDB | MyISAM |
|---|---|---|
| 事务支持 | 支持 | 不支持 |
| 锁粒度 | 行级锁 | 表级锁 |
| 外键 | 支持 | 不支持 |
| 崩溃恢复 | 支持 | 不支持 |
| 全文索引 | 支持(5.6+) | 支持 |
| 适用场景 | 高并发、事务型业务 | 只读、报表类业务 |
4.2 InnoDB 的存储结构
InnoDB 采用聚簇索引组织数据,主键索引的叶子节点直接存储整行数据,二级索引的叶子节点存储主键值。因此:
- 主键查询只需一次索引查找即可拿到数据。
- 二级索引查询需要先找到主键,再回表查询完整数据行(回表)。
- 覆盖索引可以避免回表,即查询的列全部包含在索引中。
4.3 如何选择存储引擎
下面通过一个决策树,帮助你在不同业务场景下快速选定合适的存储引擎:
判断依据说明:
InnoDB:需要事务保证数据一致性、外键约束或行级锁并发控制时,首选 InnoDB,它是绝大多数业务表的默认选择。
MyISAM:纯只读、大量
COUNT统计、全文检索且不关心崩溃恢复的场景,可考虑 MyISAM,其查询与压缩效率更高。MEMORY:数据仅作临时缓存、重启后允许丢失、追求极快读写速度时使用,如表结构临时表、会话级缓存。
Archive:日志类、写入极快、几乎不更新且可容忍数据丢失的归档场景,压缩比高、占用空间小。
需要事务、外键、行级锁 → 选择 InnoDB。
纯只读、大量 COUNT 统计、全文检索 → 可考虑 MyISAM。
内存临时表 → 使用 MEMORY 引擎。
日志类、写入极快且可容忍丢失 → 可考虑 Archive 引擎。
-- 查看当前支持的存储引擎SHOWENGINES;-- 查看表的存储引擎SHOWTABLESTATUSWHEREName='orders';5. 高阶特性:让 MySQL 更强大
5.1 视图
视图是虚拟表,不存储实际数据,本质是保存的 SQL 查询。它简化复杂查询、提供数据安全隔离:
CREATEVIEWv_user_ordersASSELECTu.name,o.order_no,o.amountFROMusers uJOINorders oONu.id=o.user_idWHEREo.status=1;5.2 存储过程与函数
存储过程将一组 SQL 封装在服务端,减少网络传输、复用业务逻辑:
DELIMITER//CREATEPROCEDUREsp_get_user(INuidINT)BEGINSELECT*FROMusersWHEREid=uid;END//DELIMITER;CALLsp_get_user(100);5.3 触发器
触发器在 INSERT、UPDATE、DELETE 操作前后自动执行,常用于审计日志、数据校验:
CREATETRIGGERtrg_order_auditAFTERINSERTONordersFOR EACH ROWBEGININSERTINTOorder_log(order_id,action,log_time)VALUES(NEW.id,'INSERT',NOW());END;5.4 窗口函数
MySQL 8.0 引入了窗口函数,让排名、累计、移动平均等分析场景变得简洁高效:
SELECTname,salary,RANK()OVER(ORDERBYsalaryDESC)ASrank_noFROMemployees;5.5 分区表
分区表将大表按规则拆分为多个物理分区,提升查询与维护效率:
CREATETABLEorders_part(idINT,order_dateDATE)PARTITIONBYRANGE(YEAR(order_date))(PARTITIONp2022VALUESLESS THAN(2023),PARTITIONp2023VALUESLESS THAN(2024),PARTITIONp2024VALUESLESS THAN(2025));6. 总结
本文围绕 MySQL 进阶的四大核心主题展开:事务通过 ACID 保证数据一致性,索引优化是查询提速的关键手段,存储引擎决定了数据的存储方式与适用场景,高阶特性则提供了视图、存储过程、触发器、窗口函数与分区表等强大能力。掌握这些内容,你就能在真实业务中做出更合理的设计与优化决策。
在实际项目中,建议结合EXPLAIN分析执行计划、合理设计索引、根据业务特性选择存储引擎,并善用 MySQL 8.0 的新特性,让数据库真正成为业务的坚实底座。