零基础从入门到精通MySQL(下篇):精通篇——吃透索引底层、锁机制与性能优化,成为MySQL实战高手

在前两篇的学习中,我们完成了MySQL的筑基与进阶:从环境搭建、基础SQL语法,到多表关联查询、事务ACID特性、InnoDB核心架构,你已经能独立写出业务场景下的绝大多数SQL,搞定了开发中的基础需求。

但相信你一定遇到过这些瓶颈,也是区分SQL新手和MySQL高手的核心分水岭:

  • 单表百万级数据后,之前写的SQL查询从毫秒级变成了秒级甚至十秒级,完全不知道怎么优化?
  • 面试被问索引底层原理、MVCC实现、锁机制,只会背概念,被追问细节就支支吾吾?
  • 线上出现慢查询,打开EXPLAIN执行计划,一堆字段完全看不懂,不知道哪里出了问题?
  • 高并发场景下出现数据不一致、死锁问题,根本不知道怎么排查、复现和解决?
  • 业务快速增长,单库单表扛不住压力,不知道怎么做高可用架构、读写分离、分库分表?

别慌,这些问题正是本篇要彻底解决的核心。作为系列的第三篇------精通篇,我们将深入MySQL的底层核心,从索引原理、执行计划解析,到锁机制、MVCC实现,再到全场景SQL性能优化、生产级高可用架构,带你彻底打通MySQL的"任督二脉",从会用MySQL变成真正懂MySQL的实战高手,同时搞定中高级开发面试90%的MySQL核心考点。


系列整体回顾与本篇核心规划

系列进度回顾

  1. 上篇(筑基篇):MySQL核心认知、环境全场景搭建、核心数据模型、基础SQL语法、单表查询全解、权限管理
  2. 中篇(进阶篇):多表关联查询全解、子查询/CTE/窗口函数、视图/存储过程、事务ACID与隔离级别、InnoDB核心架构
  3. 下篇(精通篇,本篇):索引底层原理与设计规范、EXPLAIN执行计划全解析、InnoDB锁机制、MVCC多版本并发控制、全场景SQL性能优化、高可用架构与生产运维
  4. 附加篇(八股文全集):从基础到高阶全覆盖的MySQL面试题,附标准答案与答题思路,适配校招、社招全场景

本篇核心学习目标

学完本篇,你将达到中高级开发的MySQL水平,具备生产环境实战能力:

  • 彻底吃透InnoDB索引的底层数据结构与实现原理,能独立设计出符合业务场景的高性能索引
  • 熟练使用EXPLAIN解析SQL执行计划,精准定位慢查询的性能瓶颈,给出优化方案
  • 搞懂InnoDB锁机制的底层实现,能解决并发场景下的锁冲突、死锁问题
  • 深入理解MVCC多版本并发控制的核心原理,彻底搞懂事务隔离级别的底层实现
  • 掌握全场景SQL性能优化方案,从表结构设计、索引设计到SQL编写,搞定千万级数据下的查询优化
  • 了解MySQL生产级高可用架构、主从复制、备份恢复的核心方案,具备基础的生产运维能力

第一章 性能优化的核心:InnoDB索引全解,从底层原理到设计规范

索引是MySQL性能优化的基石,90%的慢查询问题,本质都是索引设计不合理、SQL没有命中索引导致的。想要做好SQL优化,必须先彻底搞懂索引的底层原理,而不是只会"给查询字段加索引"。

1.1 索引的核心定义与本质

索引是一种用于快速定位数据的数据结构,相当于一本书的目录。我们查询数据时,如果没有索引,MySQL会进行全表扫描,逐行匹配查询条件,数据量越大,性能越差;而有了索引,MySQL可以通过索引快速定位到符合条件的数据所在的位置,避免全表扫描,将查询性能从O(n)提升到O(logn)级别。

核心本质 :索引的核心作用,就是减少查询时需要扫描的数据量,避免全表扫描、随机磁盘IO,提升查询效率。

1.2 InnoDB索引的底层数据结构:B+树

MySQL的索引有很多类型,而InnoDB默认且最常用的,就是B+树索引。想要搞懂索引,必须先搞懂B+树的结构与特性。

1.2.1 为什么是B+树,而不是其他数据结构?

我们先对比几种常见的数据结构,就能明白为什么InnoDB选择了B+树:

数据结构 查询性能 范围查询支持 磁盘IO适配性 适用场景
哈希表 O(1) 极差,必须全表扫描 一般 仅等值查询,MySQL的自适应哈希索引
二叉搜索树 O(logn) 极差,需要中序遍历 极差,树高太高,磁盘IO多 内存数据结构,不适合磁盘存储
平衡二叉树(AVL/红黑树) O(logn) 极差 极差,树高太高,千万级数据树高20+,需要20次磁盘IO 内存数据结构,不适合数据库
B树 O(logn) 一般 好,多路平衡树,树高低 文件系统,早期数据库索引
B+树 O(logn) 极好,叶子节点形成有序链表 极好,树高极低,千万级数据树高仅3-4层 InnoDB默认索引结构

核心结论 :数据库的索引是存储在磁盘上的,每次查询都需要加载磁盘上的索引页,一次磁盘IO的耗时是内存操作的上万倍 ,因此索引设计的核心目标,就是减少磁盘IO的次数。B+树作为多路平衡搜索树,树高极低,完美适配磁盘的页式存储,同时对范围查询做了极致优化,是数据库索引的最优解。

1.2.2 B+树的核心结构与特性

InnoDB的B+树,分为叶子节点非叶子节点,核心结构如下:

  1. 非叶子节点:只存储索引键值和指向下一层节点的指针,不存储实际数据,用于快速定位到叶子节点
  2. 叶子节点:存储完整的索引数据,所有叶子节点通过双向链表连接,形成有序的序列,便于范围查询
  3. 多路平衡特性:每个节点可以存储多个键值,节点的大小等于MySQL的页大小(默认16KB),因此每个节点可以存储上千个键值,树高极低
  4. 有序性:B+树的所有节点内的键值都是有序排列的,叶子节点整体也是升序排列的

关键数据:MySQL默认页大小16KB,一个BIGINT类型的主键+指针,仅占用16字节左右,一个非叶子节点可以存储1000+个键值,那么:

  • 树高1层:根节点,能存储1000个键值
  • 树高2层:能存储 1000 × 1000 = 100万 条数据
  • 树高3层:能存储 1000 × 1000 × 1000 = 10亿 条数据

也就是说,哪怕是亿级数据的表,通过主键查询数据,最多只需要3次磁盘IO,就能定位到数据,这就是B+树索引性能强悍的核心原因。

1.3 InnoDB的两大核心索引类型:聚簇索引与二级索引

InnoDB的B+树索引,分为聚簇索引(Clustered Index)二级索引(Secondary Index,也叫辅助索引/非聚簇索引) 两大类,这是InnoDB索引最核心的概念,90%的索引优化都围绕这两个概念展开。

1.3.1 聚簇索引(主键索引)

聚簇索引,就是按照表的主键构建的B+树,叶子节点存储的是整行的完整数据。也就是说,聚簇索引的叶子节点,就是整张表的数据行,因此InnoDB的表,本质上就是按照主键排序的聚簇索引组织表,每张表有且只有一个聚簇索引。

聚簇索引的生成规则

  1. 如果你为表设置了主键,InnoDB会用主键构建聚簇索引
  2. 如果你没有设置主键,InnoDB会选择第一个唯一非空索引作为聚簇索引
  3. 如果既没有主键,也没有唯一非空索引,InnoDB会自动生成一个隐藏的6字节ROWID作为聚簇索引

核心特性

  • 聚簇索引的查询性能是最高的,通过主键查询,可以直接在叶子节点拿到整行数据,不需要额外的IO
  • 聚簇索引的排序,决定了表中数据的物理存储顺序,因此主键建议使用自增BIGINT,保证插入时是顺序写入,不会出现页分裂,提升写入性能
1.3.2 二级索引(辅助索引)

二级索引,是除了聚簇索引之外的其他索引,叶子节点存储的不是整行数据,而是索引键值 + 主键值

这是最核心的知识点:当你给username字段创建了索引,这个二级索引的B+树叶子节点,只会存储username的值和对应的主键id,不会存储其他字段的数据。

核心特性

  • 一张表可以创建多个二级索引,最多支持64个二级索引
  • 通过二级索引查询数据时,如果需要的字段不在二级索引中,需要通过叶子节点存储的主键值,回到聚簇索引中查询整行数据,这个过程叫做回表
  • 回表操作需要额外的磁盘IO,是索引优化的核心优化点,我们要尽量避免回表
1.3.3 回表操作的完整流程(必须吃透)

我们用之前的sys_user表举例子,表结构如下:

sql 复制代码
CREATE TABLE sys_user (
  id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  username VARCHAR(50) NOT NULL COMMENT '用户名',
  phone CHAR(11) NOT NULL COMMENT '手机号',
  age TINYINT NOT NULL DEFAULT 0 COMMENT '年龄',
  PRIMARY KEY (id), -- 聚簇索引
  INDEX idx_username (username) -- 二级索引
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

当我们执行这条SQL:SELECT username, age FROM sys_user WHERE username = 'zhangsan';

完整的查询流程如下:

  1. MySQL先到idx_username二级索引的B+树中,找到username='zhangsan'的叶子节点,拿到对应的主键id=1
  2. 拿着主键id=1,到聚簇索引的B+树中,找到对应的叶子节点,拿到整行数据中的age字段
  3. 把username和age字段拼接成结果,返回给客户端

这个第二步,就是回表操作,需要两次查询B+树,两次磁盘IO,性能比直接主键查询差。如果我们能避免回表,就能把性能提升一倍。

1.4 联合索引与最左匹配原则

联合索引,指的是给多个字段组合创建的索引,比如INDEX idx_name_age_phone (username, age, phone),是业务中最常用的索引类型,也是面试最高频的考点。

1.4.1 联合索引的底层结构

联合索引的B+树,是按照索引定义的字段顺序,依次排序 的。比如上面的idx_name_age_phone联合索引,会先按照username排序,username相同的情况下,再按照age排序,age相同的情况下,再按照phone排序。

非叶子节点存储的是(username, age, phone)的键值和指针,叶子节点存储的是(username, age, phone)的完整值和对应的主键id。

1.4.2 最左匹配原则(核心中的核心)

最左匹配原则:MySQL查询时,会从联合索引的最左字段开始匹配,直到遇到范围查询(>、<、BETWEEN、LIKE '%xxx')就停止匹配。只有符合最左匹配原则的查询,才能命中联合索引。

我们用idx_name_age_phone (username, age, phone)这个联合索引,举几个示例,一眼看懂最左匹配原则:

SQL查询条件 是否命中索引 命中索引的字段 原因
WHERE username = 'zhangsan' username 匹配最左第一个字段
WHERE username = 'zhangsan' AND age = 25 username、age 依次匹配前两个字段
WHERE username = 'zhangsan' AND age =25 AND phone='13800138000' 全部三个字段 完整匹配索引的所有字段,性能最优
WHERE age =25 AND username = 'zhangsan' username、age MySQL查询优化器会自动调整字段顺序,匹配最左原则
WHERE username = 'zhangsan' AND age > 20 AND phone='13800138000' username、age 遇到范围查询age>20,停止匹配,phone字段无法命中索引
WHERE age =25 AND phone='13800138000' 不匹配最左的username字段,全表扫描
WHERE username LIKE 'zhang%' AND age=25 username、age 前缀匹配LIKE 'zhang%',可以匹配最左字段,继续匹配age
WHERE username LIKE '%zhang%' AND age=25 前缀有%,无法匹配最左字段,索引失效

高频避坑点

  1. 联合索引的字段顺序至关重要,我们要把区分度高、查询频率高的字段放在最左边,区分度低的字段放在后面
  2. 范围查询会中断索引匹配,因此要把范围查询的字段放在联合索引的最后
  3. MySQL查询优化器会自动调整WHERE条件的字段顺序,只要字段都在联合索引中,就会匹配最左原则,不需要严格按照索引顺序写WHERE条件

1.5 覆盖索引:彻底避免回表,性能提升的核心

覆盖索引,指的是查询需要的所有字段,都在二级索引中,不需要回表到聚簇索引查询数据,是索引优化中最常用的优化手段。

还是用上面的例子,我们创建联合索引idx_name_age (username, age),当执行这条SQL:

sql 复制代码
SELECT username, age FROM sys_user WHERE username = 'zhangsan' AND age = 25;

查询需要的username和age字段,都在联合索引的叶子节点中,MySQL直接从二级索引中就能拿到所有需要的数据,不需要回表,性能直接翻倍。

覆盖索引的核心标志 :EXPLAIN执行计划的Extra字段中,出现Using index,说明命中了覆盖索引,不需要回表。

业务最佳实践

  1. 业务查询时,绝对禁止使用SELECT *,只查询需要的字段,更容易命中覆盖索引,避免回表
  2. 对于高频查询的SQL,可以把查询的字段加入联合索引,形成覆盖索引,避免回表
  3. 示例:高频查询SELECT phone FROM sys_user WHERE username = ?,可以创建联合索引idx_username_phone (username, phone),直接命中覆盖索引,不需要回表

1.6 索引失效的十大高频场景(避坑指南)

很多时候,你明明创建了索引,但是SQL还是全表扫描,核心原因就是你的SQL写法导致了索引失效。这里总结了生产环境中最常见的10大索引失效场景,必须牢记。

我们基于idx_username (username)idx_phone (phone)idx_age (age)idx_name_age_phone (username, age, phone)这些索引,逐个讲解失效场景:

  1. 索引字段使用函数运算/表达式计算

    sql 复制代码
    -- 失效:对索引字段使用函数
    SELECT * FROM sys_user WHERE LEFT(username, 4) = 'zhang';
    -- 失效:对索引字段进行表达式计算
    SELECT * FROM sys_user WHERE age + 1 = 26;
    -- 正确写法:把计算放在值的一侧,索引字段保持干净
    SELECT * FROM sys_user WHERE age = 25;

    原理:MySQL无法对函数计算后的结果使用索引树的有序性,只能全表扫描。

  2. 索引字段使用隐式类型转换

    sql 复制代码
    -- 失效:phone是CHAR(11)类型,传入的是数字,发生隐式类型转换
    SELECT * FROM sys_user WHERE phone = 13800138000;
    -- 正确写法:类型匹配,传入字符串
    SELECT * FROM sys_user WHERE phone = '13800138000';

    原理:隐式类型转换本质上也是对索引字段做了函数运算,导致索引失效,这是生产环境最高发的索引失效场景。

  3. 模糊查询前缀有%

    sql 复制代码
    -- 失效:前缀有%,无法匹配最左原则
    SELECT * FROM sys_user WHERE username LIKE '%san';
    -- 失效:前后都有%
    SELECT * FROM sys_user WHERE username LIKE '%san%';
    -- 有效:前缀匹配,没有前导%
    SELECT * FROM sys_user WHERE username LIKE 'zhang%';

    原理:B+树的索引是按照前缀有序排列的,前导%导致无法匹配有序的前缀,索引失效。

  4. 使用OR连接非索引字段

    sql 复制代码
    -- 失效:OR两边有一个字段没有索引,会全表扫描
    SELECT * FROM sys_user WHERE username = 'zhangsan' OR address = '北京';
    -- 有效:OR两边的字段都有索引,会走索引合并
    SELECT * FROM sys_user WHERE username = 'zhangsan' OR phone = '13800138000';

    原理:OR连接的字段中,只要有一个没有索引,MySQL为了避免漏查数据,会直接全表扫描。

  5. 违背最左匹配原则

    sql 复制代码
    -- 失效:不匹配联合索引的最左字段username
    SELECT * FROM sys_user WHERE age = 25 AND phone = '13800138000';
    -- 有效:匹配最左字段
    SELECT * FROM sys_user WHERE username = 'zhangsan' AND phone = '13800138000';
  6. 使用NOT、!=、<>、NOT IN反向查询

    sql 复制代码
    -- 大概率失效:反向查询,MySQL优化器认为全表扫描比索引查询更快
    SELECT * FROM sys_user WHERE username != 'zhangsan';
    SELECT * FROM sys_user WHERE age NOT IN (20,25,30);

    原理:反向查询需要扫描大量的索引数据,MySQL优化器会判断成本,当查询的数据量超过全表的20%-30%,就会放弃索引,走全表扫描。

  7. IS NOT NULL 大概率失效,IS NULL 可以命中索引

    sql 复制代码
    -- 大概率失效:IS NOT NULL反向查询
    SELECT * FROM sys_user WHERE email IS NOT NULL;
    -- 有效:IS NULL可以命中索引
    SELECT * FROM sys_user WHERE email IS NULL;
  8. 字符串编码不统一,关联查询发生隐式字符集转换

    sql 复制代码
    -- 失效:两个表的关联字段字符集不同,发生隐式转换,索引失效
    SELECT * FROM sys_user u
    LEFT JOIN sys_order o ON u.username = o.user_name;
    -- 假设u.username是utf8mb4,o.user_name是utf8,会发生隐式转换,索引失效
  9. MySQL优化器选错索引

    当一张表有多个索引,MySQL优化器会根据扫描行数、是否回表、排序成本等因素,选择它认为最优的索引,有时候会选错索引,导致性能变差。

    解决方案:使用FORCE INDEX强制指定索引,比如SELECT * FROM sys_user FORCE INDEX (idx_username) WHERE username = 'zhangsan';

  10. 索引区分度过低,MySQL放弃索引

    比如给gender(性别,只有男/女/未知三个值)这种区分度极低的字段创建索引,MySQL会认为走索引还不如全表扫描快,直接放弃索引。

    原理:区分度=唯一值数量/总行数,区分度越接近1,索引效率越高,区分度低于0.1的字段,不适合创建普通索引。

1.7 企业级索引设计规范(直接套用)

  1. 主键设计规范 :必须给每张表设置主键,优先使用BIGINT AUTO_INCREMENT自增主键,禁止使用UUID、雪花ID无序主键,避免页分裂,提升写入性能;禁止使用业务字段作为主键。
  2. 索引数量控制:单表索引数量控制在5个以内,禁止创建冗余索引、重复索引;联合索引的字段数量不超过5个。
  3. 联合索引设计规范:遵循"最左匹配原则",把查询频率高、区分度高的字段放在最左边;范围查询的字段放在联合索引的最后;把需要查询的字段加入索引,形成覆盖索引,避免回表。
  4. 字段选择规范:优先选择占用空间小的字段创建索引;禁止给大字段(TEXT、VARCHAR(1000+))创建普通索引,如需查询,使用全文索引;禁止给区分度极低的字段(性别、状态)创建普通索引,除非是极低基数的筛选场景。
  5. 索引更新规范:禁止在业务高峰期创建、删除索引;大表创建索引,使用MySQL8.0的Online DDL,避免锁表;定期清理无用、冗余的索引。
  6. 查询规范:禁止在索引字段上使用函数、表达式、隐式类型转换,避免索引失效;尽量使用覆盖索引,避免回表操作。

第二章 慢查询定位神器:EXPLAIN执行计划全解析

学会了索引设计,你还需要一个工具来验证你的SQL有没有命中索引,性能瓶颈在哪里,这个工具就是EXPLAIN。EXPLAIN是MySQL自带的执行计划解析工具,可以模拟MySQL优化器执行SQL语句的过程,告诉你SQL是怎么执行的,有没有命中索引,有没有全表扫描,有没有文件排序,是定位慢查询的核心神器。

2.1 EXPLAIN的基础用法

用法非常简单,只需要在你的SQL语句前面加上EXPLAIN关键字,执行即可:

sql 复制代码
-- 解析SELECT语句
EXPLAIN SELECT * FROM sys_user WHERE username = 'zhangsan';

-- 解析INSERT/UPDATE/DELETE语句(MySQL8.0+支持)
EXPLAIN UPDATE sys_user SET age = 26 WHERE id = 1;

执行后,会返回一张12个字段的结果表,我们只需要重点掌握8个核心字段,就能搞定99%的慢查询分析。

2.2 EXPLAIN核心字段全解析

我们按照SQL的执行顺序,逐个讲解核心字段的含义、判断标准和优化方向。

2.2.1 id:SQL执行的优先级

id字段表示SQL查询中操作表的顺序,核心规则:

  1. id相同:执行顺序从上到下
  2. id不同:id值越大,执行优先级越高,子查询会先执行
  3. id为NULL:表示这是一个结果集,不需要参与查询,比如UNION的结果

优化方向:尽量减少子查询的嵌套层级,id值越多,说明SQL嵌套越复杂,性能越差,优先使用JOIN、CTE替代多层子查询。

2.2.2 select_type:查询的类型

select_type表示当前查询的类型,用来区分普通查询、子查询、联合查询、派生表等,核心常见类型如下:

类型 含义 优化方向
SIMPLE 简单查询,没有子查询、UNION 最优的查询类型,优先保持
PRIMARY 复杂查询的最外层主查询 -
SUBQUERY 子查询(SELECT/WHERE中的子查询) 尽量避免,子查询会生成临时表,性能差
DERIVED 派生表,FROM子句中的子查询 尽量避免,使用CTE替代,减少临时表生成
UNION UNION中的第二个及以后的查询 优先使用UNION ALL,避免UNION的去重开销
UNION RESULT UNION的结果集 -

优化方向:业务中尽量使用SIMPLE类型的简单查询,避免DERIVED、SUBQUERY类型的复杂子查询,减少临时表的生成。

2.2.3 type:访问类型,性能的核心指标

type字段是衡量SQL性能最核心的指标,表示MySQL在表中找到所需行的方式,也叫访问类型。性能从最优到最差的排序如下:

复制代码
system > const > eq_ref > ref > range > index > ALL

业务中,我们的优化目标是至少达到range级别,最好能达到ref级别,绝对禁止出现ALL(全表扫描)。

我们逐个讲解核心类型的含义:

  1. system:表中只有一行数据(系统表),是const的特例,基本不会出现。
  2. const :常量查询,通过主键或唯一索引等值查询,最多匹配一行数据,性能极快。
    示例:EXPLAIN SELECT * FROM sys_user WHERE id = 1;
  3. eq_ref :等值关联查询,使用主键或唯一非空索引进行关联,每张表匹配一行数据,是JOIN查询中最优的类型。
    示例:EXPLAIN SELECT * FROM sys_order o LEFT JOIN sys_user u ON o.user_id = u.id;
  4. ref :非唯一索引等值查询,通过普通二级索引等值匹配,可能匹配多行数据,是业务中最常见的最优类型。
    示例:EXPLAIN SELECT * FROM sys_user WHERE username = 'zhangsan';
  5. range :范围查询,使用索引进行范围查询(>、<、BETWEEN、IN、LIKE前缀匹配),只扫描索引范围内的行。
    示例:EXPLAIN SELECT * FROM sys_user WHERE age BETWEEN 20 AND 30;
  6. index :遍历整个索引树,比ALL好一点,因为索引文件比数据文件小,但是依然是全索引扫描,性能极差。
    示例:EXPLAIN SELECT username FROM sys_user;(username有索引,遍历整个二级索引)
  7. ALL :全表扫描,遍历聚簇索引的所有叶子节点,性能最差,必须优化。
    示例:EXPLAIN SELECT * FROM sys_user WHERE address = '北京';(address没有索引)
2.2.4 possible_keys & key:索引匹配情况
  • possible_keys:表示MySQL在查询时,可能会用到的索引,只是候选,不一定会用
  • key :表示MySQL实际执行时,真正用到的索引,是核心判断指标
    • 如果key为NULL,说明没有使用索引,大概率是全表扫描
    • 如果key的值和你创建的索引一致,说明命中了索引

常见问题:possible_keys里有索引,但是key为NULL,说明索引失效了,需要排查我们上一章讲的索引失效场景。

2.2.5 key_len:使用的索引长度

key_len表示MySQL使用的索引的字节长度,通过这个值,我们可以判断命中了联合索引的哪些字段。

计算规则

  • 字符类型:VARCHAR(M),utf8mb4字符集下,每个字符占4字节,变长类型需要额外2字节存储长度,允许NULL需要额外1字节
    示例:VARCHAR(50) NOT NULL,key_len=50×4+2=202字节
  • 数值类型:TINYINT=1字节,INT=4字节,BIGINT=8字节,允许NULL额外1字节
  • 日期类型:DATE=3字节,DATETIME=5字节,允许NULL额外1字节

示例 :联合索引idx_name_age_phone (username VARCHAR(50) NOT NULL, age TINYINT NOT NULL, phone CHAR(11))

  • 如果key_len=202,说明只命中了username字段
  • 如果key_len=203,说明命中了username+age字段
  • 如果key_len=203+44=247,说明命中了全部三个字段

优化方向:在查询结果一致的情况下,key_len越小越好,说明索引的效率越高。

2.2.6 rows:预估扫描的行数

rows表示MySQL为了找到所需的行,预估需要扫描的行数,这个值越小越好

比如你查询一条数据,rows值是1,说明MySQL通过索引直接定位到了这一行,性能最优;如果rows值是100万,说明需要扫描100万行数据,性能极差,必须优化。

核心判断:rows值和实际返回的行数越接近,说明索引的效率越高。

2.2.7 Extra:额外信息,优化的核心细节

Extra字段会返回SQL执行的额外信息,是我们判断SQL是否需要优化的核心依据,这里讲解最常见的6种核心信息。

  1. Using index:命中了覆盖索引,不需要回表,性能最优,是我们优化的核心目标。
  2. Using where:使用了WHERE条件过滤数据,但是没有命中索引,需要优化。
  3. Using filesort :使用了文件排序,MySQL无法使用索引完成排序,需要在内存/磁盘中进行排序,数据量大的时候性能极差,必须优化。
    优化方案:给排序的字段创建索引,让排序操作直接使用索引的有序性,避免文件排序。
    示例:SELECT * FROM sys_user ORDER BY age; 会出现Using filesort,给age创建索引即可解决。
  4. Using temporary :使用了临时表来存储中间结果,比如GROUP BY、UNION、DISTINCT操作,MySQL会先创建临时表,再进行操作,性能极差,必须优化。
    优化方案:给GROUP BY的字段创建索引,避免临时表生成;优先使用UNION ALL替代UNION,避免去重临时表。
  5. Using index condition:索引条件下推(ICP),MySQL5.6+的优化特性,在存储引擎层就过滤掉不符合索引条件的数据,减少回表次数,是正常的优化特性,不需要优化。
  6. Impossible WHERE :WHERE条件永远为false,没有符合条件的数据,比如SELECT * FROM sys_user WHERE 1=2,不需要优化。

2.3 EXPLAIN实操示例:好SQL vs 坏SQL

我们用一个真实的业务场景,对比优化前后的执行计划,让你直观感受到EXPLAIN的用法。

业务场景:查询年龄在20-30岁之间,用户名以zhang开头的用户,返回用户名、手机号、年龄。

优化前:没有索引,坏SQL
sql 复制代码
-- 优化前SQL
EXPLAIN SELECT username, phone, age FROM sys_user WHERE username LIKE 'zhang%' AND age BETWEEN 20 AND 30;

执行计划核心结果

  • type: ALL(全表扫描)
  • key: NULL(没有命中索引)
  • rows: 100万(全表数据量)
  • Extra: Using where(全表扫描过滤)
优化后:创建联合索引,好SQL
sql 复制代码
-- 创建联合索引,形成覆盖索引
CREATE INDEX idx_name_age_phone ON sys_user (username, age, phone);

-- 优化后SQL
EXPLAIN SELECT username, phone, age FROM sys_user WHERE username LIKE 'zhang%' AND age BETWEEN 20 AND 30;

执行计划核心结果

  • type: range(范围查询,符合优化目标)
  • key: idx_name_age_phone(命中了我们创建的索引)
  • key_len: 247(命中了索引的全部三个字段)
  • rows: 1000(只需要扫描1000行,比优化前减少了1000倍)
  • Extra: Using index; Using where(命中了覆盖索引,不需要回表,性能最优)

第三章 并发控制核心:InnoDB锁机制全解

在高并发场景下,多个事务同时操作同一批数据,会出现数据不一致的问题,而InnoDB的锁机制,就是解决并发数据竞争的核心方案,也是面试100%会问到的高阶考点。

3.1 锁的核心分类

InnoDB的锁,按照不同的维度,可以分为不同的类型,我们先建立整体认知。

分类维度 锁类型 核心说明
按锁的粒度 表级锁 锁定整张表,粒度大,冲突概率高,并发性能差,MyISAM默认使用
行级锁 锁定需要操作的行,粒度小,冲突概率低,并发性能高,InnoDB默认使用
按锁的功能 共享锁(S锁,读锁) 多个事务可以同时加S锁,互不阻塞,只能读不能写
排他锁(X锁,写锁) 只有一个事务能加X锁,其他事务的S锁和X锁都会被阻塞,既能读也能写
按锁的实现方式 记录锁(Record Lock) 锁定索引中的某一行记录
间隙锁(Gap Lock) 锁定索引中的间隙,防止幻读
临键锁(Next-Key Lock) 记录锁+间隙锁,InnoDB默认的行锁算法,解决幻读问题

3.2 表级锁 vs 行级锁

3.2.1 表级锁

表级锁会锁定整张表,分为表共享读锁和表排他写锁:

  • 表共享读锁:加锁后,其他事务可以读表,但是不能写表
  • 表排他写锁:加锁后,其他事务既不能读也不能写表

InnoDB表级锁的用法

sql 复制代码
-- 加表读锁
LOCK TABLES sys_user READ;
-- 加表写锁
LOCK TABLES sys_user WRITE;
-- 释放表锁
UNLOCK TABLES;

核心提醒:InnoDB业务中,绝对禁止手动加表级锁,会导致整个表的读写都被阻塞,并发性能极差,只有在全表数据修复、批量更新等极少数场景下才会使用。

3.2.2 行级锁(核心)

InnoDB的行级锁,是通过给索引上的记录加锁 实现的,如果你的SQL没有命中索引,InnoDB会退化为表级锁,这是最高发的坑点。

行级锁分为共享锁(S锁)和排他锁(X锁):

  1. 共享锁(S锁,读锁)
    用法:在SELECT语句后面加LOCK IN SHARE MODE

    sql 复制代码
    -- 给查询到的行加共享锁
    SELECT * FROM sys_user WHERE id = 1 LOCK IN SHARE MODE;

    特性:

    • 多个事务可以同时给同一行加S锁,互不阻塞
    • 加了S锁的行,其他事务只能读,不能加X锁修改,会被阻塞
  2. 排他锁(X锁,写锁)
    用法:INSERT、UPDATE、DELETE语句会自动给对应的行加X锁,SELECT语句可以加FOR UPDATE手动加X锁

    sql 复制代码
    -- 手动给查询到的行加排他锁,也就是常说的"悲观锁"
    SELECT * FROM sys_user WHERE id = 1 FOR UPDATE;
    -- UPDATE语句自动加X锁
    UPDATE sys_user SET age = 26 WHERE id = 1;

    特性:

    • 只有一个事务能给同一行加X锁,其他事务的S锁和X锁都会被阻塞
    • 加了X锁的行,只有当前事务可以修改和读取,其他事务无法修改,普通快照读可以读取(MVCC)

锁的兼容矩阵

锁类型 共享锁(S) 排他锁(X)
共享锁(S) 兼容 冲突
排他锁(X) 冲突 冲突

3.3 InnoDB行锁的三大算法:记录锁、间隙锁、临键锁

InnoDB的行锁,有三种核心算法,用来解决不同场景下的并发问题,也是InnoDB在RR隔离级别下解决幻读的核心方案。

3.3.1 记录锁(Record Lock)

记录锁,就是锁定索引中的某一行具体的记录,只能锁定已经存在的索引记录。

比如SELECT * FROM sys_user WHERE id = 1 FOR UPDATE;,会给id=1的索引记录加记录锁,其他事务无法修改id=1的行,但是可以插入、修改其他行,不会影响并发。

核心特性:记录锁永远锁的是索引记录,不是数据行,如果没有命中索引,就会退化为表锁。

3.3.2 间隙锁(Gap Lock)

间隙锁,锁定的是索引记录之间的间隙,不锁定记录本身,核心作用是防止其他事务在间隙中插入数据,解决幻读问题。

举个例子,sys_user表的id主键有1、3、5、7、9这些记录,那么索引中的间隙就是:
(-∞,1)、(1,3)、(3,5)、(5,7)、(7,9)、(9,+∞)

当我们执行SELECT * FROM sys_user WHERE id BETWEEN 3 AND 7 FOR UPDATE;,InnoDB不仅会给3、5、7这三条记录加记录锁,还会给(1,3)、(3,5)、(5,7)、(7,9)这些间隙加间隙锁,其他事务无法在这些间隙中插入任何数据,比如插入id=2、4、6、8的行,都会被阻塞,从而避免了幻读。

核心特性

  • 间隙锁只在RR(可重复读)隔离级别下生效,RC级别下没有间隙锁
  • 间隙锁之间是兼容的,多个事务可以给同一个间隙加间隙锁,互不冲突
  • 间隙锁只会阻塞插入操作,不会阻塞其他间隙锁
3.3.3 临键锁(Next-Key Lock)

临键锁,是记录锁+间隙锁的组合,是InnoDB默认的行锁算法。临键锁会锁定一个左开右闭的区间,既锁定索引记录本身,也锁定记录前面的间隙。

还是用上面的例子,id主键的临键锁区间是:
(-∞,1]、(1,3]、(3,5]、(5,7]、(7,9]、(9,+∞]

当我们执行SELECT * FROM sys_user WHERE id = 5 FOR UPDATE;,InnoDB会给(3,5]这个临键区间加锁,既锁定id=5的记录,也锁定(3,5)的间隙,其他事务无法修改id=5的行,也无法在(3,5)的间隙中插入数据,彻底避免了幻读。

核心特性

  • 当查询的索引是唯一索引,且是等值查询,匹配到了唯一的记录,临键锁会退化为记录锁,只锁定记录本身,不锁定间隙,提升并发性能
  • 当查询的是普通二级索引,或者范围查询,临键锁会完整生效,锁定整个区间
  • 临键锁是InnoDB在RR隔离级别下解决幻读问题的核心方案

3.4 死锁的产生、排查与避免

死锁,指的是两个或多个事务,互相等待对方持有的锁,导致无限阻塞,无法继续执行,是高并发场景下的常见问题。

3.4.1 死锁产生的四个必要条件
  1. 互斥条件:一个锁只能被一个事务持有,其他事务必须等待
  2. 持有并等待:一个事务已经持有了至少一个锁,又申请其他事务持有的锁,阻塞等待
  3. 不可剥夺:锁只能被持有事务主动释放,其他事务不能强行剥夺
  4. 循环等待:多个事务之间形成循环等待锁的关系,每个事务都在等待下一个事务持有的锁
3.4.2 死锁复现示例

我们用两个事务,复现死锁的场景:

时间 事务A 事务B
T1 开启事务; UPDATE sys_user SET age=26 WHERE id=1;(持有id=1的X锁) 开启事务;
T2 UPDATE sys_user SET age=30 WHERE id=2;(持有id=2的X锁)
T3 UPDATE sys_user SET age=27 WHERE id=2;(申请id=2的X锁,被事务B阻塞)
T4 UPDATE sys_user SET age=31 WHERE id=1;(申请id=1的X锁,被事务A阻塞)
T5 两个事务互相等待,形成死锁,MySQL检测到死锁,会回滚其中一个事务
3.4.3 死锁的排查

MySQL会自动检测死锁,当检测到死锁时,会回滚代价最小的事务,同时把死锁信息记录到错误日志中。

我们可以通过以下命令查看死锁信息:

sql 复制代码
-- 查看最近一次死锁的详细信息
SHOW ENGINE INNODB STATUS;

执行后,在LATEST DETECTED DEADLOCK部分,会显示死锁发生的事务、持有的锁、等待的锁、执行的SQL,我们可以通过这些信息,定位死锁的原因。

3.4.4 死锁的避免方案
  1. 统一资源访问顺序:所有事务都按照相同的顺序操作行,比如都按照id从小到大的顺序更新,避免循环等待,这是最核心的方案。
  2. 大事务拆分为小事务:事务越小,持有锁的时间越短,锁冲突的概率越低,避免长事务持有锁长时间不释放。
  3. 避免在事务中手动加锁 :尽量使用MySQL默认的锁机制,避免手动加FOR UPDATE锁,减少锁冲突。
  4. 保证SQL命中索引:避免SQL没有命中索引,退化为表锁,大幅增加锁冲突的概率。
  5. 降低隔离级别:如果业务允许,可以使用RC隔离级别,RC级别没有间隙锁,锁的粒度更小,死锁概率更低。

3.5 锁机制的最佳实践与避坑指南

  1. 所有更新操作必须命中索引:InnoDB的行锁是加在索引上的,没有命中索引会退化为表锁,并发性能直接归零。
  2. 控制事务粒度,避免长事务:事务执行时间越短,持有锁的时间越短,锁冲突的概率越低,绝对禁止在事务中等待用户输入、调用外部接口。
  3. 禁止无范围的锁查询 :禁止使用SELECT * FROM sys_user FOR UPDATE;这种不加WHERE条件的锁查询,会锁定整张表,导致所有更新都被阻塞。
  4. 统一更新顺序:多个事务更新多张表、多行数据时,必须按照统一的顺序操作,避免循环等待导致死锁。
  5. 合理设置隔离级别:绝大多数业务场景,使用默认的RR级别即可;如果是读多写少、对一致性要求不高的场景,可以使用RC级别,减少锁冲突,提升并发性能。
  6. 避免在业务高峰期执行批量更新:批量更新会锁定大量的行,导致锁等待,业务高峰期会引发雪崩效应。

第四章 事务隔离的底层实现:MVCC多版本并发控制

在上篇中,我们学习了事务的四大隔离级别,知道了MySQL在RR隔离级别下,解决了脏读、不可重复读、幻读三大问题,而实现这一切的核心,就是MVCC(多版本并发控制)

MVCC是InnoDB实现事务隔离级别的核心机制,它让读写操作互不阻塞,读不加锁,写不加锁,大幅提升了数据库的并发性能,是面试中最高阶的核心考点之一。

4.1 MVCC的核心定义

MVCC,全称多版本并发控制,指的是在数据库中,同一条数据可以有多个版本的快照,不同的事务可以读取不同版本的数据,从而实现读写互不阻塞,不需要加锁

在没有MVCC的情况下,读操作需要加S锁,写操作需要加X锁,读写会互相阻塞,并发性能极差;而有了MVCC,读操作可以读取数据的历史版本快照,不需要加锁,写操作只需要修改最新版本的数据,读写互不阻塞,极大提升了并发性能。

核心概念

  • 当前读 :读取数据的最新版本,需要加锁,比如SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODE、INSERT、UPDATE、DELETE
  • 快照读:读取数据的历史版本快照,不需要加锁,普通的SELECT语句就是快照读,MVCC只在快照读下生效

4.2 MVCC的三大核心组件

MVCC的实现,依赖于三个核心组件:隐藏字段、undo log版本链、Read View,我们逐个讲解。

4.2.1 隐藏字段

InnoDB会给每一行数据,添加三个隐藏字段,是MVCC的基础:

  1. DB_TRX_ID(6字节):事务ID,记录最后一次插入/更新这行数据的事务ID,事务ID是严格递增的,每个事务有唯一的事务ID
  2. DB_ROLL_PTR(7字节):回滚指针,指向这行数据对应的undo log,undo log中记录了这行数据的上一个版本
  3. DB_ROW_ID(6字节):隐藏主键,如果表没有设置主键,InnoDB会自动生成这个ROWID,和MVCC无关
4.2.2 undo log 版本链

undo log我们在中篇已经学过,它记录了数据的反向操作,用来实现事务回滚,而MVCC的多版本数据,就是通过undo log实现的。

当我们修改一行数据时,InnoDB会做两件事:

  1. 把修改前的数据,写入undo log中
  2. 把当前行的DB_ROLL_PTR指针,指向这个undo log,同时更新DB_TRX_ID为当前事务的ID

如果多次修改这行数据,就会形成一条undo log的版本链,链的头部是数据的最新版本,链的尾部是数据的最早历史版本,每个版本都记录了对应的事务ID。

举个例子:

  1. 事务ID=1,插入了一行数据,DB_TRX_ID=1,DB_ROLL_PTR=NULL
  2. 事务ID=2,修改了这行数据,生成undo log,DB_TRX_ID=2,DB_ROLL_PTR指向事务1的版本
  3. 事务ID=3,再次修改这行数据,生成undo log,DB_TRX_ID=3,DB_ROLL_PTR指向事务2的版本

最终形成的版本链:最新版本(事务3)→ 事务2版本 → 事务1版本

4.2.3 Read View(读视图)

Read View是事务执行快照读时,生成的一个读视图,用来判断当前事务能看到版本链中的哪个版本的数据,是实现隔离级别的核心。

Read View包含四个核心属性:

  1. m_ids:生成Read View时,当前MySQL中正在活跃的、未提交的事务ID列表
  2. min_trx_id:m_ids中的最小事务ID,也就是当前未提交的最小事务ID
  3. max_trx_id:生成Read View时,MySQL下一个要分配的事务ID,也就是最大事务ID+1
  4. creator_trx_id:生成这个Read View的当前事务的ID

版本可见性判断规则

当事务要读取某行数据时,会拿着Read View,从版本链的头部开始,逐个判断版本是否可见,找到第一个可见的版本,就是当前事务能读取到的数据。

判断规则如下(按优先级排序):

  1. 如果版本的DB_TRX_ID == creator_trx_id:可见,当前事务自己修改的数据,肯定能看到
  2. 如果版本的DB_TRX_ID < min_trx_id:可见,这个版本的事务在Read View生成前就已经提交了
  3. 如果版本的DB_TRX_ID >= max_trx_id:不可见,这个版本的事务是在Read View生成之后才开启的
  4. 如果版本的DB_TRX_ID 在 min_trx_id 和 max_trx_id 之间:
    • 如果DB_TRX_ID 在 m_ids 中:不可见,这个版本的事务还未提交
    • 如果DB_TRX_ID 不在 m_ids 中:可见,这个版本的事务已经提交

如果当前版本不可见,就顺着DB_ROLL_PTR指针,找到下一个版本,继续按照上面的规则判断,直到找到可见的版本,或者遍历完整个版本链。

4.3 不同隔离级别下的Read View生成规则

MySQL的RC和RR隔离级别,核心区别就是Read View的生成时机不同,这也是为什么RC会出现不可重复读,而RR能实现可重复读的核心原因。

4.3.1 读已提交(RC)级别

RC级别下,事务中每次执行快照读,都会生成一个新的Read View

这就意味着,事务中两次执行相同的SELECT语句,会生成两个不同的Read View,如果两次查询之间,有其他事务提交了修改,那么第二次查询的Read View中,这个事务的ID就不在m_ids中了,就能看到最新的修改,因此会出现不可重复读的问题。

4.3.2 可重复读(RR)级别

RR级别下,事务中第一次执行快照读时,生成一个Read View,整个事务生命周期内,都复用这个Read View

这就意味着,事务中所有的快照读,都使用同一个Read View,后续其他事务提交的修改,都不会出现在这个Read View中,因此整个事务内,每次读取到的数据都是一致的,完美实现了可重复读,同时也解决了大部分幻读问题。

4.4 MVCC解决幻读的原理

很多人会问,MVCC是怎么解决幻读的?

在RR隔离级别下,事务第一次快照读生成Read View后,后续的快照读都复用这个Read View,即使其他事务插入了新的数据,新数据的DB_TRX_ID >= max_trx_id,在Read View中是不可见的,因此两次快照读的结果完全一致,不会出现幻读。

注意 :MVCC解决的是快照读 的幻读问题,而当前读 的幻读问题,是通过我们上一章讲的临键锁解决的,两者配合,让InnoDB在RR隔离级别下,彻底解决了幻读问题。


第五章 全场景SQL性能优化实战

前面我们学习了索引、执行计划、锁机制、MVCC,这些都是性能优化的理论基础,这一章我们把这些理论落地,讲解生产环境中全场景的SQL性能优化方案,从表结构设计、索引设计、SQL编写、业务优化四个维度,搞定千万级数据下的SQL性能优化。

5.1 表结构设计优化

表结构设计是性能优化的根基,不合理的表结构,后续再怎么优化SQL和索引,都无法从根本上解决性能问题。

  1. 数据类型优化

    • 遵循"最小够用"原则,能用TINYINT就不用INT,能用INT就不用BIGINT,越小的数据类型,占用的磁盘、内存、CPU缓存越少,性能越高
    • 金额必须用DECIMAL,禁止用FLOAT/DOUBLE;日期必须用DATETIME/DATE,禁止用字符串存储
    • 字符串类型,固定长度用CHAR,变长用VARCHAR,禁止用VARCHAR(255)定义所有字段,大文本用TEXT,且尽量放在单独的表中
    • 所有字段尽量设置NOT NULL + DEFAULT,避免NULL值导致的索引失效、查询异常
  2. 表结构拆分优化

    • 垂直拆分:把大表中不常用的大字段、低频字段,拆分到单独的扩展表中,比如用户表的用户详情、头像、富文本描述,拆分到user_extend表中,主表只保留高频查询的字段,减少主表的数据量,提升查询性能
    • 水平拆分:当单表数据量超过千万级,读写性能下降时,进行分库分表,按照用户ID、时间等维度,把数据拆分到多个表/库中,避免单表数据量过大
  3. 避免过度设计

    • 禁止过度范式化,业务中适当反范式,增加少量冗余字段,减少多表关联查询,提升查询性能
    • 禁止在一张表中创建太多字段,单表字段数控制在30个以内,字段越多,每行数据占用的空间越大,Buffer Pool能缓存的行数越少,磁盘IO越多

5.2 索引优化

索引优化是性能优化最核心、性价比最高的手段,正确的索引能让SQL性能提升上千倍。

  1. 索引设计原则

    • 优先设计联合索引,避免单值索引,联合索引能覆盖更多的查询场景,更容易形成覆盖索引
    • 联合索引遵循"最左匹配原则",高频查询、高区分度的字段放在最左边,范围查询字段放在最后
    • 优先设计覆盖索引,把查询需要的字段加入索引,避免回表操作,这是最常用的优化手段
    • 控制索引数量,单表索引不超过5个,禁止创建冗余、重复的索引,索引越多,写入性能越差
    • 禁止给大字段、低区分度字段创建普通索引,大字段可以使用前缀索引,比如INDEX idx_username_prefix (username(10))
  2. 索引失效优化

    • 禁止在索引字段上使用函数、表达式、隐式类型转换,保证索引字段的"干净"
    • 模糊查询避免前导%,如果必须全模糊查询,使用全文索引或者Elasticsearch
    • 避免使用OR连接非索引字段,OR两边的字段都必须有索引
    • 避免使用NOT、!=、NOT IN等反向查询,尽量使用范围查询替代
  3. 高频场景索引优化示例

    • 分页查询优化:给排序和查询条件的字段创建联合索引,避免文件排序和全表扫描
    • 关联查询优化:JOIN的关联字段必须创建索引,且类型、字符集完全一致
    • 分组统计优化:给GROUP BY的字段创建索引,避免临时表和文件排序

5.3 SQL语句编写优化

很多时候,慢查询不是因为没有索引,而是因为SQL写法不合理,导致索引失效、全表扫描、临时表、文件排序。

5.3.1 分页查询优化(最高发的慢查询场景)

当分页offset很大时,比如SELECT * FROM sys_user ORDER BY id LIMIT 100000, 10,MySQL需要扫描100010行数据,然后丢弃前100000行,性能极差,这就是深分页问题。

优化方案

  1. 主键覆盖优化法(推荐) :先通过覆盖索引找到需要的主键ID,再通过主键ID关联查询数据,避免全表扫描

    sql 复制代码
    -- 优化前
    SELECT * FROM sys_user ORDER BY id LIMIT 100000, 10;
    
    -- 优化后
    SELECT u.* FROM sys_user u
    INNER JOIN (
      SELECT id FROM sys_user ORDER BY id LIMIT 100000, 10
    ) AS t ON u.id = t.id;
  2. 主键过滤法(推荐,适用于连续主键) :通过WHERE条件过滤掉前面的offset数据,直接从目标位置开始查询

    sql 复制代码
    SELECT * FROM sys_user WHERE id > 100000 ORDER BY id LIMIT 10;
  3. 游标分页法(推荐,适用于APP/小程序分页) :前端记录上一页的最大ID,下一页通过ID过滤,避免offset

    sql 复制代码
    -- 第一页
    SELECT * FROM sys_user ORDER BY id LIMIT 10;
    -- 第二页,前端记录上一页最大id=10
    SELECT * FROM sys_user WHERE id > 10 ORDER BY id LIMIT 10;
5.3.2 关联查询优化
  1. 小表驱动大表:INNER JOIN中,MySQL会自动选择小表驱动大表;LEFT JOIN中,左表尽量用小表,右表的关联字段必须加索引
  2. 避免大表JOIN大表:两张千万级的大表JOIN,性能会极差,尽量通过业务冗余字段,减少大表关联
  3. 禁止超过7张表的关联:关联表越多,MySQL查询优化器的选择成本越高,越容易生成错误的执行计划,性能越差
  4. **避免使用SELECT ***:只查询需要的字段,更容易命中覆盖索引,减少数据传输和内存占用
5.3.3 子查询优化
  1. 优先使用JOIN、CTE替代多层嵌套子查询,避免派生表和临时表

  2. 避免使用相关子查询(子查询依赖主查询的字段),相关子查询会导致主查询每一行都执行一次子查询,性能极差

  3. IN子查询,当子查询结果集很大时,用EXISTS替代;子查询结果集很小时,用IN更合适

    sql 复制代码
    -- 子查询结果集大,用EXISTS
    SELECT * FROM sys_user u WHERE EXISTS (
      SELECT 1 FROM sys_order o WHERE o.user_id = u.id AND o.order_status = 3
    );
    
    -- 子查询结果集小,用IN
    SELECT * FROM sys_order WHERE user_id IN (1,2,3,4,5);
5.3.4 排序与分组优化
  1. 避免Using filesort:给排序的字段创建索引,让排序使用索引的有序性,避免文件排序
  2. 避免Using temporary:给GROUP BY的字段创建索引,让分组操作使用索引,避免临时表
  3. 排序和分组的字段尽量使用同一个索引,比如SELECT * FROM sys_user WHERE age = 25 ORDER BY username,创建联合索引idx_age_username (age, username),既能命中查询条件,也能避免文件排序
  4. 禁止使用ORDER BY RAND()随机排序,会导致全表扫描+文件排序,性能极差,用业务代码实现随机排序
5.3.5 批量操作优化
  1. 批量插入 :使用INSERT ... VALUES (...), (...)批量插入,替代多次单行插入,减少事务提交和网络IO次数,性能提升10倍以上

    sql 复制代码
    -- 优化前,1000次插入,1000次网络IO
    INSERT INTO sys_user (username, phone) VALUES ('user1', '13800000001');
    INSERT INTO sys_user (username, phone) VALUES ('user2', '13800000002');
    
    -- 优化后,1次插入,1次网络IO
    INSERT INTO sys_user (username, phone) VALUES 
    ('user1', '13800000001'),
    ('user2', '13800000002');
  2. 批量更新 :使用CASE WHEN批量更新,替代多次单行更新,减少锁持有时间和网络IO

    sql 复制代码
    -- 优化前,多次更新
    UPDATE sys_user SET age = 26 WHERE id = 1;
    UPDATE sys_user SET age = 30 WHERE id = 2;
    
    -- 优化后,一次更新
    UPDATE sys_user 
    SET age = CASE id
      WHEN 1 THEN 26
      WHEN 2 THEN 30
      ELSE age
    END
    WHERE id IN (1,2);
  3. 避免循环内提交事务:批量操作尽量在一个事务内提交,减少事务提交的IO开销

5.4 业务层优化

很多时候,SQL性能问题,本质是业务设计的问题,从业务层优化,往往能获得最大的性能提升。

  1. 冷热数据分离:把历史数据、低频访问的冷数据,迁移到历史表中,主表只保留高频访问的热数据,比如订单表,只保留近1年的订单,超过1年的订单迁移到order_history表中,大幅减少主表的数据量
  2. 避免数据库做业务逻辑计算:数据库的核心能力是存储数据,不是计算,复杂的业务逻辑、数值计算、格式转换,尽量放在业务代码中处理,减少数据库的CPU压力
  3. 缓存热点数据:对于高频访问、很少修改的热点数据,比如商品分类、配置信息、用户基础信息,使用Redis等缓存,减少数据库的查询压力,避免重复查询数据库
  4. 避免大事务:把大事务拆分为小事务,减少锁持有时间,避免长事务导致的锁冲突、undo log膨胀、MVCC视图过期
  5. 避免高并发下的数据库写放大:比如秒杀场景,不要直接更新数据库扣减库存,先通过Redis限流、预扣库存,再异步更新数据库,避免高并发写入打满数据库

第六章 MySQL生产级高可用架构与运维核心

当业务规模增长到一定程度,单库单表已经无法支撑业务的读写压力,同时需要保证数据库的高可用,避免单点故障导致业务中断,这就需要我们掌握MySQL的高可用架构、主从复制、备份恢复等生产运维核心能力。

6.1 MySQL主从复制

主从复制是MySQL高可用架构的基础,指的是把一台MySQL服务器(主库Master)的数据,同步到一台或多台MySQL服务器(从库Slave),主库负责写操作,从库负责读操作,实现读写分离,提升数据库的并发能力,同时实现数据备份、故障转移。

6.1.1 主从复制的核心原理

MySQL主从复制,基于binlog实现,分为三个核心步骤:

  1. Binlog写入:主库执行完事务提交后,把数据修改记录写入binlog二进制日志中
  2. Binlog同步:从库的IO线程,连接主库,读取主库的binlog,写入到本地的relay log中继日志中
  3. 中继日志重放:从库的SQL线程,读取relay log中的日志,解析成SQL语句,在从库中重放,保证主从数据一致
6.1.2 主从复制的三种模式
  1. STATEMENT模式:基于SQL语句的复制,主库把执行的SQL语句写入binlog,从库重放相同的SQL语句。优点是binlog体积小,缺点是有很多函数会导致主从数据不一致,比如NOW()、UUID(),MySQL5.7之前的默认模式。
  2. ROW模式:基于行的复制,主库把每行数据的修改写入binlog,从库重放行的修改。优点是不会出现数据不一致,是MySQL8.0的默认模式,缺点是binlog体积大,尤其是批量更新时。
  3. MIXED模式:混合模式,MySQL自动判断,对于不会导致数据不一致的SQL,用STATEMENT模式,对于会导致不一致的SQL,用ROW模式。
6.1.3 主从复制的搭建步骤(极简版)
  1. 主库配置:修改my.cnf配置文件,开启binlog,设置server-id

    ini 复制代码
    [mysqld]
    # 唯一server-id,主从不能重复
    server-id=1
    # 开启binlog
    log-bin=mysql-bin
    # binlog模式,使用ROW模式
    binlog_format=ROW
    # 同步的数据库,不设置默认同步所有库
    # binlog-do-db=user_db
    # 不同步的数据库
    binlog-ignore-db=mysql
    binlog-ignore-db=information_schema
    binlog-ignore-db=performance_schema

    重启主库,创建主从复制专用用户:

    sql 复制代码
    CREATE USER 'repl'@'%' IDENTIFIED BY 'Repl@123456';
    GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl'@'%';
    FLUSH PRIVILEGES;
    -- 查看主库状态,记录File和Position,从库配置需要
    SHOW MASTER STATUS;
  2. 从库配置:修改my.cnf配置文件,设置server-id

    ini 复制代码
    [mysqld]
    # 唯一server-id,不能和主库重复
    server-id=2
    # 开启中继日志
    relay-log=mysql-relay-bin
    # 只读,超级用户除外
    read_only=1

    重启从库,配置主从复制:

    sql 复制代码
    CHANGE MASTER TO
    MASTER_HOST='主库IP地址',
    MASTER_PORT=3306,
    MASTER_USER='repl',
    MASTER_PASSWORD='Repl@123456',
    MASTER_LOG_FILE='主库的File名称',
    MASTER_LOG_POS=主库的Position值;
    
    -- 启动主从复制
    START SLAVE;
    
    -- 查看主从复制状态,Slave_IO_Running和Slave_SQL_Running都为Yes,说明复制正常
    SHOW SLAVE STATUS\G;
6.1.4 主从复制的常见问题
  1. 主从数据不一致:最常见的问题,原因包括从库写入了数据、binlog模式错误、主库和从库的配置不一致、SQL重放失败。解决方案:重新做主从同步,使用pt-table-checksum工具校验数据一致性,pt-table-sync工具修复不一致数据。
  2. 主从延迟:从库同步主库的数据有延迟,原因包括从库性能差、大事务、批量更新、从库SQL线程单线程重放。解决方案:MySQL8.0开启并行复制,拆分大事务,优化批量操作,提升从库硬件性能。

6.2 读写分离

基于主从复制,我们可以实现读写分离:写操作全部走主库,读操作全部走从库,把读压力分散到多个从库,大幅提升数据库的读并发能力。

读写分离的实现方式

  1. 代码层实现:业务代码中,根据SQL类型,选择不同的数据源,写操作使用主库数据源,读操作使用从库数据源,优点是简单可控,缺点是代码耦合度高。
  2. 中间件实现:使用数据库中间件,比如Sharding-JDBC、MyCat、ProxySQL,中间件自动解析SQL,路由到对应的主库/从库,业务代码无感知,是企业级的主流方案。

读写分离的注意事项

  • 必须处理主从延迟问题,对于实时性要求高的读操作,必须走主库,比如下单后查询订单详情
  • 从库可以做垂直拆分,不同的从库负责不同的业务读请求,比如报表从库、用户中心从库
  • 多个从库可以做负载均衡,把读压力均匀分散到各个从库

6.3 分库分表

当单表数据量超过千万级,甚至亿级,单库的读写压力达到瓶颈,主从复制、读写分离已经无法解决问题时,就需要进行分库分表,把数据拆分到多个库、多个表中,降低单库单表的数据量,提升读写性能。

6.3.1 分库分表的两种方式
  1. 垂直拆分
    • 垂直分库:按照业务模块,把不同的业务表拆分到不同的数据库中,比如用户库、订单库、商品库,实现业务隔离,分散数据库压力
    • 垂直分表:把一张大表,按照字段的访问频率,拆分成多张表,比如主表存储高频字段,扩展表存储低频大字段,减少主表的数据量
  2. 水平拆分
    • 水平分库:把同一个表的数据,按照分片规则,拆分到多个结构相同的数据库中,比如按照用户ID取模,把用户数据拆分到user_db_0、user_db_1、user_db_2三个库中
    • 水平分表:把同一个表的数据,按照分片规则,拆分到同一个库的多个结构相同的表中,比如订单表按照订单ID取模,拆分到order_0、order_1...order_7共8张表中
6.3.2 常用分片规则
  1. 哈希取模:按照分片键(比如用户ID、订单ID)哈希后取模,拆分到对应的库/表中,优点是数据分布均匀,缺点是扩容麻烦,需要重新分片
  2. 范围分片:按照分片键的范围拆分,比如按照时间范围、ID范围拆分,优点是扩容方便,缺点是容易出现数据热点,比如最新的数据访问频率最高
  3. 一致性哈希:解决哈希取模扩容难的问题,优点是扩容时只需要迁移少量数据,缺点是数据分布不均匀
  4. 枚举分片:按照固定的枚举值拆分,比如按照地区、业务类型拆分,适用于分片键枚举值固定的场景
6.3.3 分库分表的中间件

分库分表的实现,主要依赖中间件,分为两类:

  1. 客户端中间件:以Sharding-JDBC为代表,嵌入到业务代码中,在JDBC层解析SQL,路由到对应的库/表,优点是无额外部署,性能高,缺点是和代码耦合
  2. 服务端中间件:以MyCat、ProxySQL为代表,独立部署的代理服务,业务代码连接代理服务,代理服务解析SQL,路由到对应的库/表,优点是业务代码无感知,缺点是有额外的性能损耗,需要保证代理服务的高可用
6.3.4 分库分表的注意事项
  • 能不分就不分:分库分表会大幅提升业务的复杂度,只有当单表数据量超过5000万,且读写性能出现瓶颈时,才考虑分库分表
  • 分片键选择至关重要:分片键必须是高频查询的字段,比如用户表用用户ID,订单表用订单ID/用户ID,尽量避免跨库跨表查询
  • 避免跨库事务:分库后,分布式事务的处理非常复杂,尽量通过业务设计,避免跨库事务
  • 避免跨库JOIN、跨库分页、跨库排序:这些操作的性能极差,尽量通过数据冗余、业务代码聚合实现

6.4 MySQL备份与恢复

数据备份是数据库安全的最后一道防线,无论高可用架构做的多好,都必须有完善的备份策略,避免误操作、硬件故障、数据损坏导致的数据丢失。

6.4.1 备份的分类
  1. 按照备份方式
    • 逻辑备份:备份SQL语句,恢复时执行SQL语句重建数据,比如mysqldump,优点是简单、灵活,缺点是备份恢复速度慢
    • 物理备份:直接备份数据库的物理文件,比如ibd文件、redo log、binlog,优点是备份恢复速度快,适合大库,缺点是跨平台兼容性差,比如Percona XtraBackup
  2. 按照备份是否停机
    • 冷备:停机备份,关闭MySQL服务,直接复制物理文件,优点是简单、无数据不一致问题,缺点是业务需要停机,生产环境基本不用
    • 热备:在线备份,MySQL服务正常运行,业务无感知,是生产环境的主流备份方式
  3. 按照备份粒度
    • 全量备份:备份整个数据库的所有数据
    • 增量备份:只备份上一次全量/增量备份之后变化的数据
    • 差异备份:只备份上一次全量备份之后变化的数据
6.4.2 常用备份工具与用法
  1. mysqldump:MySQL自带的逻辑备份工具,适合中小库备份,用法简单

    bash 复制代码
    # 全量备份整个实例
    mysqldump -u root -p --all-databases --single-transaction --master-data=2 > all_db_backup.sql
    
    # 备份指定数据库
    mysqldump -u root -p --databases user_db order_db --single-transaction > user_order_backup.sql
    
    # 备份指定表
    mysqldump -u root -p user_db sys_user sys_order > user_table_backup.sql
    
    # 恢复备份
    mysql -u root -p < all_db_backup.sql

    核心参数:--single-transaction:InnoDB热备,不会锁表;--master-data=2:记录主库的binlog位置,用于主从复制、时间点恢复。

  2. Percona XtraBackup:开源的物理热备工具,适合大库备份,备份恢复速度快,支持全量、增量备份,是生产环境的主流备份工具。

6.4.3 企业级备份策略
  • 全量备份:每天凌晨业务低峰期,执行一次全量备份
  • 增量备份:每6小时执行一次增量备份,减少数据丢失的风险
  • binlog备份:实时备份binlog,binlog是数据恢复的核心,通过全量备份+binlog,可以恢复到任意时间点
  • 备份验证:定期恢复备份,验证备份的有效性,避免备份文件损坏,需要恢复时无法使用
  • 备份存储:备份文件必须异地存储,不能和数据库放在同一台服务器,避免服务器故障导致备份丢失
6.4.4 数据恢复方案
  1. 误删表/数据恢复:通过最近的全量备份,恢复到临时库,然后通过binlog,恢复误操作之前的数据,再把数据导回生产库
  2. 时间点恢复:通过全量备份,恢复到备份时间点,然后通过binlog,重放到指定的时间点,实现任意时间点恢复
  3. drop database/table 恢复:通过物理备份恢复整个库/表,或者通过binlog闪回工具,恢复误删的库/表

第七章 本篇最佳实践与避坑指南

7.1 索引优化最佳实践

  1. 优先设计联合索引,遵循最左匹配原则,高频高区分度字段放最左,范围查询字段放最后
  2. 尽量设计覆盖索引,避免回表操作,Extra字段出现Using index是优化的核心目标
  3. 禁止在索引字段上使用函数、表达式、隐式类型转换,避免索引失效
  4. 控制索引数量,单表不超过5个,定期清理无用、冗余索引,避免写入性能下降
  5. 大表创建索引使用Online DDL,避免锁表,禁止在业务高峰期创建/删除索引

7.2 SQL编写最佳实践

  1. 绝对禁止使用SELECT *,只查询业务需要的字段,减少数据传输和回表概率
  2. UPDATE、DELETE必须加WHERE条件,执行前先EXPLAIN分析执行计划
  3. 深分页查询使用主键覆盖、游标分页优化,避免大offset导致的全表扫描
  4. 批量操作使用批量插入、批量更新,减少网络IO和事务提交次数,禁止循环内提交事务
  5. 避免使用ORDER BY RAND()、UNION、多层嵌套子查询,减少临时表和文件排序
  6. 所有上线的SQL,必须先通过EXPLAIN分析执行计划,type至少达到range级别,避免ALL全表扫描

7.3 事务与锁最佳实践

  1. 严格控制事务粒度,大事务拆分为小事务,禁止在事务中调用外部接口、等待用户输入
  2. 所有更新操作必须命中索引,避免行锁退化为表锁,导致并发性能归零
  3. 多个事务更新数据,必须按照统一的顺序操作,避免循环等待导致死锁
  4. 禁止无WHERE条件的锁查询,避免锁定整张表,导致所有更新阻塞
  5. 合理设置隔离级别,绝大多数场景使用默认的RR级别,读多写少场景可使用RC级别提升并发

7.4 生产运维最佳实践

  1. 必须搭建主从复制,实现读写分离和故障转移,避免单点故障
  2. 制定完善的备份策略,全量+增量+binlog实时备份,定期验证备份的有效性
  3. 开启慢查询日志,定期分析慢查询,优化性能瓶颈,避免慢查询堆积导致数据库雪崩
  4. 合理设置MySQL参数,Buffer Pool设置为物理内存的50%-70%,优化连接数、redo log大小等核心参数
  5. 禁止业务代码使用root用户连接数据库,遵循最小权限原则,限制用户访问IP
  6. 禁止在业务高峰期执行批量更新、大表DDL、全表备份等重IO操作

本篇总结与附加篇预告

本篇总结

学完本篇,你已经完成了MySQL从入门到精通的核心进阶,达到了中高级开发的MySQL水平:

  • 彻底吃透了InnoDB索引的底层B+树结构、聚簇索引与二级索引、联合索引最左匹配原则,能独立设计高性能索引
  • 熟练使用EXPLAIN解析SQL执行计划,能精准定位慢查询的性能瓶颈,给出优化方案
  • 搞懂了InnoDB锁机制的底层实现,能解决并发场景下的锁冲突、死锁问题
  • 深入理解了MVCC多版本并发控制的核心原理,彻底搞懂了事务隔离级别的底层实现
  • 掌握了全场景SQL性能优化方案,从表结构设计、索引设计到SQL编写,能搞定千万级数据下的查询优化
  • 了解了MySQL生产级高可用架构、主从复制、分库分表、备份恢复的核心方案,具备了生产运维的基础能力

附加篇预告

《零基础从入门到精通MySQL(附加篇):面试八股文全集》

在附加篇中,我们会把整个系列的核心知识点,整理成从基础到高阶全覆盖的MySQL面试八股文,包含:

  • 基础篇:数据类型、建表规范、基础SQL语法、权限管理
  • 进阶篇:多表关联查询、事务ACID、隔离级别、InnoDB核心架构
  • 精通篇:索引底层原理、执行计划解析、锁机制、MVCC、性能优化
  • 架构篇:主从复制、读写分离、分库分表、高可用架构

每道题都附带标准答案、答题思路和面试加分项,帮你搞定校招、社招中99%的MySQL面试题,助力你拿到心仪的offer。


互动环节

如果你在SQL性能优化、索引设计、慢查询排查、生产环境运维中遇到了任何问题,都可以在评论区留言,我会一一回复解答。

如果本篇内容对你有帮助,欢迎点赞、收藏、转发,关注我,后续的八股文附加篇会第一时间更新,带你彻底吃透MySQL,完成从零基础到精通的蜕变!

相关推荐
DevOpenClub2 小时前
全国三甲医院主体信息 API 接口
java·大数据·数据库
一勺菠萝丶2 小时前
管理后台使用手册在线预览与首次登录引导弹窗实现
java·前端·数据库
无忧智库2 小时前
某大型银行“十五五”金融大模型风控与智能投顾平台建设方案深度解读(WORD)
数据库·金融
爱码小白2 小时前
数据库多表命名的通用规范
数据库·python·mysql
wang09072 小时前
Linux性能优化之中断
linux·运维·性能优化
huohuopro2 小时前
Hbase伪分布式远程访问配置
数据库·分布式·hbase
XDHCOM2 小时前
ORA-12169: TNS连接标识符过长,Oracle报错故障修复与远程处理
数据库·oracle
爬山算法3 小时前
MongoDB(86)如何使用MongoDB存储大文件?
数据库·mongodb
xcLeigh3 小时前
KES数据库表空间目录自动创建特性详解与存储运维最佳实践
大数据·运维·服务器·数据库·表空间·存储