一、存储引擎
一、mysql 体系结构
mysql采用分层插件式架构,核心分为4层,存储引擎是整个架构的核心,决定了数据的存储、读取、事务、锁等核心能力。
层级 | 核心组件 | 核心作用 | 和存储引擎的关系 |
连接层 | 客户端连接器、连接池 | 负责客户端连接接入、身份认证、线程复用、连接数限制、内存校验 | 所有sql请求的入口,和引擎无直接交互,只负责连接管理 |
服务层 | sql接口、解析器、查询优化器、缓存 | 核心sql处理层:负责sql语法解析、权限校验、生成执行计划、存储过程/视图/触发器等跨引擎功能 | 生成执行计划后,调用存储引擎的接口读写数据,不关心底层存储实现 |
引擎层 | 可插拔存储引擎(innodb/myisam/memory等) | 数据存储与提取的底层实现:负责数据落盘、索引管理、事务、锁、崩溃恢复等核心能力 | 是sql执行的最终执行者,不同引擎的执行逻辑、性能、特性完全不同 |
存储层 | 系统文件、数据/索引/日志文件 | 负责将数据、索引、事务日志持久化到磁盘文件系统 | 存储引擎最终将数据写入该层的磁盘文件 |
二、存储引擎简介
1. 核心定义
存储引擎是mysql中负责数据存储、索引建立、数据增删改查的底层技术实现,也被称为「表类型」。
和其他数据库(oracle、postgresql)最大的区别是:mysql的存储引擎是基于表生效,而非基于数据库,支持同一个库中不同的表使用不同的存储引擎,实现业务能力的灵活适配。
2. 基础操作示例

示例1:创建表时指定存储引擎
-- 1. 业务核心表,指定innodb引擎(mysql5.5+默认,可省略不写)
create table tb_user (
id int primary key auto_increment comment '主键id',
name varchar(50) not null comment '用户名',
age int comment '年龄',
profession varchar(50) comment '职业'
) engine = innodb default charset = utf8mb4 comment '用户核心表';
-- 2. 只读历史归档表,指定myisam引擎
create table tb_user_history_2024 (
id int primary key,
name varchar(50) not null,
create_time datetime
) engine = myisam default charset = utf8mb4 comment '2024年用户历史归档表';
-- 3. 临时缓存表,指定memory引擎
create table tb_temp_online_user (
user_id int primary key,
login_time datetime not null
) engine = memory default charset = utf8mb4 comment '用户在线状态临时表';示例2:查看存储引擎相关信息
-- 查看当前mysql支持的所有存储引擎 show engines; -- 查看当前数据库的默认存储引擎 show variables like 'default_storage_engine'; -- 查看指定表的存储引擎 show create table tb_user; -- 查看当前库所有表的引擎信息 select table_name, engine from information_schema.tables where table_schema = database();
三、核心存储引擎特点
重点讲解3个主流引擎,覆盖99%的业务场景,同时补充底层结构细节,衔接之前的索引优化知识。
1. innodb(mysql 5.5+ 默认引擎,业务首选)
核心定位
兼顾高可靠性和高性能的事务型通用引擎,是企业级开发的默认选择。
核心特点
- 事务支持:严格遵循acid模型,支持事务提交、回滚、崩溃安全恢复,保证数据不丢失。
- 锁机制:行级锁,仅锁定修改的行,不锁全表,支持高并发读写。
- 外键约束:支持foreign key外键,保证数据的参照完整性。
- 索引结构:采用聚簇索引,主键索引和数据存储在一起,之前学习的覆盖索引、联合索引优化,均基于innodb的b+树索引结构实现。
文件结构
每张innodb表对应一个 xxx.ibd 独立表空间文件,存储表结构、数据、索引,由参数 innodb_file_per_table 控制(默认开启)。
逻辑存储结构(从大到小)

表空间(tablespace) → 段(segment) → 区(extent) → 页(page) → 行(row)
- 表空间:最高层,对应磁盘上的ibd文件,分为系统表空间、独立表空间等。
- 段:分为数据段、索引段、回滚段,数据段对应聚簇索引的叶子节点,索引段对应二级索引。
- 区:固定大小1m,包含64个连续的16k页,保证磁盘io的连续性,减少随机io。
- 页:innodb磁盘io的最小单位,默认16k,b+树的一个节点就是一个页,一行行数据存储在页中。
- 行:表中的单条记录,包含自定义字段+innodb隐藏字段(事务id、回滚指针等)。
2. myisam(mysql早期默认引擎,现已基本淘汰)
核心定位
非事务型读密集引擎,仅适用于极少数特殊场景。
核心特点
- 不支持事务、不支持外键、不支持崩溃恢复,数据安全性极差。
- 锁机制:表级锁,修改任意一行都会锁定全表,并发能力几乎为0。
- 优势:占用磁盘空间小,只读场景下访问速度快(现代mysql版本中已被innodb反超)。
文件结构
每张myisam表对应3个磁盘文件:
xxx.sdi:表结构信息文件xxx.myd:数据文件(mydata)xxx.myi:索引文件(myindex)
数据和索引完全分离存储,无法实现覆盖索引免回表的优化。
3. memory(内存引擎)
核心定位
数据全量存储在内存中的临时引擎,仅适用于临时缓存场景。
核心特点
- 数据全量存放在内存中,磁盘仅存储表结构,访问速度极快,mysql重启/断电后数据完全丢失。
- 锁机制:表级锁,并发能力弱。
- 索引支持:默认使用hash索引,也支持b+树索引。
- 限制:表大小受内存限制,无法存储超大表。
文件结构
仅对应一个 xxx.sdi 表结构文件,无数据持久化文件。
三大引擎核心特性对比表
核心特性 | innodb | myisam | memory |
|---|---|---|---|
事务安全 | 支持 | 不支持 | 不支持 |
锁机制 | 行级锁 | 表级锁 | 表级锁 |
b+树索引 | 支持 | 支持 | 支持 |
hash索引 | 不支持 | 不支持 | 支持(默认) |
全文索引 | 5.6+ 支持 | 支持 | 不支持 |
外键约束 | 支持 | 不支持 | 不支持 |
数据持久化 | 磁盘持久化 | 磁盘持久化 | 不持久化,内存存储 |
崩溃数据安全 | 支持崩溃恢复 | 不支持 | 完全丢失 |
并发能力 | 极高 | 极低 | 低 |
四、存储引擎选择
核心原则:没有最好的引擎,只有最适合业务场景的引擎,无特殊需求优先默认innodb,避免不必要的兼容问题。
1. innodb 适用场景(99%业务首选)
- 业务需要事务支持(订单、支付、金融、用户核心数据等场景)
- 有高频的更新、删除操作,需要行锁保证高并发能力
- 对数据可靠性有强要求,需要崩溃恢复能力
- 需要外键约束保证数据的参照完整性
- 示例场景:电商订单表、用户信息表、支付流水表、库存表等所有核心业务表。
2. myisam 适用场景(现已极少使用)
- 业务以只读和插入为主,几乎无更新、删除操作
- 不需要事务、不需要高并发,对数据可靠性要求极低
- 示例场景:静态历史数据归档表、只读的系统配置字典表(现代场景完全可以用innodb替代)。
3. memory 适用场景
- 临时数据存储,可接受数据丢失,重启后可快速重建
- 高频访问的热点小数据缓存、报表计算的中间临时表
- 示例场景:用户在线状态临时表、活动实时排名临时表、sql查询的内部临时表。
进阶避坑提醒
- 不要在同一个事务中混用不同存储引擎的表,非事务引擎无法回滚,会直接破坏事务的acid特性。
- 不要用memory引擎存储核心业务数据,mysql重启/服务器断电会直接丢失全部数据。
- 不要轻信「myisam比innodb读得快」的老旧说法,现代mysql版本中,innodb的缓冲池可同时缓存数据和索引,读性能远超仅能缓存索引的myisam。
二、索引
一、索引概述
1.1 核心定义与本质
索引(index)是帮助mysql高效获取数据的有序数据结构。数据库系统在业务数据之外,额外维护了一套满足特定查找算法的数据结构,该结构通过指针指向真实数据,从而实现高级查找算法,大幅降低数据检索的成本。
核心本质:用空间换时间,通过额外的磁盘空间、数据维护成本,换取查询效率的指数级提升。
1.2 索引的核心作用:避免全表扫描
索引的核心价值,是把低效的全表扫描,转为高效的索引查找,对应示例sql:
select * from user where age = 45;
- 无索引场景:mysql只能执行全表扫描,从第一行开始逐行比对age字段,直到找到所有符合条件的记录。数据量越大,扫描行数越多,磁盘io成本越高,性能越差。
- 有索引场景:mysql会基于age字段的有序索引结构,通过二分查找快速定位到age=45的记录,仅需少数几次磁盘io即可完成查询,性能提升可达上千倍。
1.3 索引的优缺点
核心优势 | 核心劣势 |
|---|---|
1. 大幅提升数据检索效率,减少磁盘io次数,是索引最核心的价值 | 1. 空间成本:索引需要占用额外的磁盘空间,索引总大小甚至可能超过数据本身 |
2. 利用索引的有序性,直接避免额外排序操作,降低cpu消耗(可避免 | 2. 维护成本:insert/update/delete操作时,需要同步维护索引结构保证有序性,索引越多,写操作性能损耗越大 |
3. 将随机io转为顺序io,大幅提升磁盘读写效率 | 3. 优化成本:过多索引会增加查询优化器的选择成本,可能导致执行计划选错索引 |
1.4 索引与存储引擎的关系
mysql的索引在存储引擎层实现,而非服务层,因此不同存储引擎支持的索引类型、结构、实现逻辑完全不同,这也是索引和表类型强绑定的核心原因。
主流存储引擎的索引支持情况如下:
索引类型 | innodb(默认引擎) | myisam | memory |
b+tree索引 | 支持(默认、核心索引结构) | 支持 | 支持 |
hash索引 | 不支持(仅系统自适应hash,无法手动创建) | 不支持 | 支持(默认索引类型) |
r-tree(空间索引) | 不支持 | 支持 | 不支持 |
full-text(全文索引) | 5.6版本之后支持 | 支持 | 不支持 |
日常业务开发中,99%的场景都是基于innodb引擎的b+tree索引,这也是后续所有索引优化的核心基础。
实操示例:索引基础操作
-- 1. 创建表时同时创建索引
create table tb_user (
id int primary key auto_increment comment '主键id',
name varchar(50) not null comment '用户名',
age int comment '年龄',
profession varchar(50) comment '职业',
-- 普通单列索引
index idx_user_age (age),
-- 联合索引(对应之前学习的最左前缀原则)
index idx_user_pro_age (profession, age)
) engine = innodb default charset = utf8mb4 comment '用户表';
-- 2. 查看表的所有索引
show index from tb_user;
-- 3. 给已存在的表添加索引
create index idx_user_name on tb_user(name);
-- 4. 删除索引
drop index idx_user_name on tb_user;二、索引结构
数据库查询的最大性能瓶颈是磁盘io(磁盘io耗时是内存操作的上万倍),因此索引结构的设计核心目标是:尽量减少磁盘io次数,也就是降低树的高度。
下面我们从演进逻辑,讲清楚为什么mysql最终选择b+tree作为默认索引结构。
2.1 为什么二叉树/红黑树不适合mysql
二叉树

红黑树(平衡二叉树)
在二叉树基础上,通过自旋、变色保证树的平衡,解决了链表退化问题。
核心缺陷:
依然是二叉树结构,每个节点最多2个子节点,大数据量下树的高度依然很高,无法解决磁盘io过多的核心问题,因此不适合作为mysql的索引结构。
2.2 b-tree(多路平衡查找树):特点与局限
为了解决树的高度问题,b-tree(多路平衡查找树)被设计出来,核心是多路结构:一个节点可以存储多个key和多个子节点指针,大幅降低树的高度。

以5阶b-tree为例:每个节点最多存储4个key、5个子节点指针,所有节点都存储key、数据、指针,整棵树全局有序。
核心特点
- 多路结构大幅降低树高:100阶b-tree存储100万条数据,高度仅为3层,仅需3次磁盘io即可完成查询。
- 所有节点都存储索引key和对应的数据,整棵树有序,支持二分查找。
核心局限(不适合mysql的原因)
- 非叶子节点也存储数据:innodb中每个节点对应一个16k的页,非叶子节点存储数据会导致每个节点能存的key数量大幅减少,树的高度会增加,io次数变多。
- 范围查询效率极低:范围查询需要频繁回溯父节点,带来大量额外io。
- 排序查询需要频繁回溯,无法利用有序性做连续扫描。
2.3 b+tree:mysql默认的索引结构

b+tree是b-tree的优化版,完美解决了b-tree的缺陷,是innodb引擎默认的索引结构。
经典b+tree核心特点
- 非叶子节点仅存储key和指针,不存储数据:所有数据只存储在叶子节点。
- 核心优势:非叶子节点能存储的key数量大幅提升,千万级数据的树高仅为3-4层,查询仅需3-4次磁盘io,性能极高。
- 查询性能稳定:所有数据都在叶子节点,无论查询哪个key,都必须从根节点走到叶子节点,每次查询的io次数一致,性能稳定可控。
- 叶子节点有序串联:所有叶子节点按key从小到大排序,相邻节点通过单向链表关联,解决了b-tree范围查询需要回溯的问题。
2.4 mysql对b+tree的核心优化
mysql在经典b+tree的基础上,做了关键优化:将叶子节点的单向链表,升级为双向循环链表,在叶子节点上增加了向前、向后的双向指针。

这个优化的核心价值:
- 范围查询性能拉满:比如
age between 20 and 30,只需定位到age=20的叶子节点,即可顺着双向链表向后扫描,无需回溯父节点,无额外io。 - 完美适配排序场景:正序(asc)、倒序(desc)排序都可直接通过双向链表实现,无需额外排序,这也是联合索引能避免
using temporary的底层原因。 - 全表扫描效率高:只需遍历叶子节点的双向链表,无需遍历整棵树。
为什么b+tree是mysql最适合的索引结构?
树高低、io少、查询性能稳定,天生适配范围、排序、分组查询,是所有sql优化的底层基础。
进阶示例:验证索引的优化效果
-- 1. 无索引查询,查看执行计划(全表扫描 type: all) explain select * from tb_user where age = 25; -- 2. 创建age字段索引 create index idx_user_age on tb_user(age); -- 3. 再次查看执行计划(走索引 type: ref) explain select * from tb_user where age = 25; -- 4. 范围查询,利用b+tree双向链表优化 explain select * from tb_user where age between 20 and 30 order by age;
2.5hash

1.hash索引的底层原理
hash索引底层基于哈希表(散列表)实现,核心逻辑如下:
- 对索引列的字段值,通过固定的hash算法计算出对应的hash值(散列值);
- 将hash值映射到哈希表对应的槽位(bucket)上;
- 槽位中存储「索引字段值 + 对应行数据的指针」;
- 若多个字段值计算出相同的hash值(hash冲突/hash碰撞),则通过链表法解决,在同一个槽位上串联多个数据项。
举个例子:对name字段建立hash索引,当查询name='arm'时,mysql会对'arm'计算hash值,直接定位到对应的槽位,无需遍历树结构,一步找到数据。
2. hash索引的核心特点
核心优势 | 核心劣势 |
等值查询(=、in)性能极高,无hash冲突时仅需一次io,时间复杂度o(1),理想情况下性能优于b+tree索引 | 仅支持等值匹配,完全不支持范围查询(between、>、<、>=、<=),因为hash值是无序的,无法通过hash值判断大小范围 |
无法利用索引完成排序操作,hash值的大小和原字段值的大小无任何关联,无法利用hash索引实现order by排序 | |
不支持模糊查询(like)、最左前缀匹配原则,无法实现部分匹配查询 | |
hash冲突严重时(大量重复值),需要遍历链表比对,查询性能会大幅下降 |
3. mysql中hash索引的支持情况
- memory引擎:默认支持hash索引,也是memory引擎的首选索引类型,适用于内存临时表的等值查询场景。
- innodb引擎:不支持手动创建hash索引,但提供了**自适应hash索引(adaptive hash index)**功能。
- 原理:innodb会自动监控热点b+tree索引的查询,如果发现频繁的等值查询能通过hash索引优化,会在内存中自动为热点页构建hash索引,全程无需人工干预,是内部优化机制。
- 限制:自适应hash索引依然仅支持等值查询,不支持范围、排序等操作。
- myisam引擎:不支持hash索引。
4. 为什么innodb默认选择b+tree,而非hash索引?
- 业务场景适配性:业务中绝大多数查询都是范围查询、排序、分组查询,hash索引完全不支持这些场景,而b+tree的叶子节点双向链表天生适配范围、排序操作。
- io效率与树高:b+tree的多路结构让千万级数据的树高仅为3-4层,仅需3-4次io即可完成查询,性能稳定;而hash索引在有冲突时需要遍历链表,且无法优化范围查询的io。
- 数据存储效率:b-tree的非叶子节点存储数据,导致单页能存储的键值少,树高增加;而b+tree非叶子节点仅存键值和指针,单页能存储上千个键值,大幅降低树高,减少io次数。
三、索引分类
mysql的索引可以从功能维度和innodb存储形式维度两大维度进行完整分类,其中innodb的聚集/二级索引分类是核心中的核心,直接决定了sql的查询性能。
3.1 按功能维度分类
这是最基础的索引分类,对应创建索引时的语法关键字,共分为4大类:
分类 | 核心含义 | 核心特点 | 语法关键字 | 实操示例 |
|---|---|---|---|---|
主键索引 | 针对表的主键字段创建的索引,也叫聚簇索引 | 1. 表创建主键时,mysql会自动创建主键索引,无需手动创建; 2. 一张表有且仅有一个主键索引;3. 索引列不允许为null,不允许重复 | primary |
|
唯一索引 | 针对需要避免重复值的字段创建的索引,保证字段值在表中唯一 | 1. 一张表可以创建多个唯一索引 2. 索引列值必须唯一,但允许为null(可以有多个null); 3. 插入/更新时会校验唯一性,重复值会报错 | unique |
|
常规索引(普通索引) | 最基础的索引,仅用于快速定位特定数据,无唯一性约束 | 1. 一张表可以创建多个常规索引,是业务中最常用的索引类型; 2. 无唯一性约束,允许重复值、null值; 3. 仅用于提升查询效率,无额外约束 | index / key |
|
全文索引 | 针对长文本字段(varchar/text)创建的索引,用于关键词全文检索 | 1. 底层是倒排索引,和es、lucene核心原理一致; 2. 解决 3. 一张表可以创建多个全文索引,仅支持文本类型字段 | fulltext |
|
3.2 按innodb存储形式分类(核心重点)
在innodb存储引擎中,根据索引的存储形式和数据关联方式,索引分为聚集索引(clustered index)和二级索引(secondary index,也叫辅助索引/非聚集索引)两大类,这是innodb索引最核心的设计。
3.2.1 聚集索引
核心定义
聚集索引是将索引结构和完整的行数据存储在一起的索引,b+tree的叶子节点直接存储了整行的完整数据,一张表有且仅有一个聚集索引。
聚集索引的选取规则(优先级从高到低)
- 如果表定义了主键(primary key),主键索引就是这张表的聚集索引;
- 如果表没有定义主键,会使用第一个非空的唯一索引(unique not null)作为聚集索引;
- 如果表既没有主键,也没有合适的唯一索引,innodb会自动生成一个6字节的隐藏
rowid,作为默认的隐藏聚集索引。
核心建议:业务中所有innodb表必须显式创建主键,且优先使用自增int/bigint主键,保证聚集索引的有序性,避免页分裂带来的性能损耗。
核心特点
- 叶子节点存储完整行数据,通过主键查询时,直接在聚集索引中就能拿到整行数据,无需额外查询,性能极高;
- 数据的物理存储顺序和聚集索引的排序顺序一致,有序的主键插入会让数据顺序写入磁盘,随机io转为顺序io,性能拉满。
3.2.2 二级索引(辅助索引)
核心定义
二级索引是将索引和行数据分开存储的索引,b+tree的叶子节点不存储完整行数据,仅存储对应的主键值。一张表可以创建多个二级索引,我们手动创建的唯一索引、常规索引、联合索引,都属于二级索引。
核心特点
- 叶子节点仅存储主键值,而非完整行数据,索引文件体积远小于聚集索引,节省磁盘空间;
- 通过二级索引查询时,通常需要两步操作:先通过二级索引找到对应的主键值,再通过主键值到聚集索引中查找完整行数据,这个过程叫做回表查询。

2.2.3 回表查询详解(面试高频)
核心定义
回表查询,就是先通过二级索引定位到主键值,再拿着主键值到聚集索引中查询完整行数据的过程,需要两次b+tree查询,性能低于直接走聚集索引的查询。
执行流程:
- 先走
name字段的二级索引,在b+tree中找到name='arm'对应的叶子节点,拿到主键值id=10; - 拿着主键值
id=10,走聚集索引的b+tree,找到对应的叶子节点,拿到整行的完整数据; - 整个过程需要两次b+tree查询,这就是回表查询。
高频面试题解答
问题:以下两条sql,哪个执行效率高?为什么?
select * from user where id = 10;(id为主键)select * from user where name = 'arm';(name有二级索引)
答案:第一条sql执行效率远高于第二条。
原因:
- 第一条sql直接走聚集索引,一次b+tree查询就能直接拿到完整行数据,无需回表;
- 第二条sql先走二级索引拿到主键值,再走聚集索引回表查询完整数据,需要两次b+tree查询,额外的io操作导致性能更低。
四、索引语法
mysql索引的核心操作分为创建、查看、删除三类,所有操作均基于表级别生效,语法适配普通索引、唯一索引、全文索引、联合索引等所有索引类型,是索引优化的基础操作。
4.1 创建索引
通用语法
create [unique | fulltext] index index_name on table_name (index_col_name [长度], ...);
语法参数详解
参数 | 可选/必填 | 核心说明 |
| 可选 | 声明创建唯一索引,保证索引列的值全局唯一,允许存在多个null值 |
| 可选 | 声明创建全文索引,仅支持text/varchar长文本字段,用于关键词模糊检索 |
| 必填 | 索引名称,建议遵循命名规范: |
| 必填 | 要创建索引的目标表名 |
| 必填 | 要创建索引的字段,多个字段用逗号分隔即为联合索引,字段顺序直接决定最左前缀匹配规则;字符串字段可指定索引前缀长度,减少索引体积 |
实操场景示例
场景1:建表时同步创建索引
建表时直接定义索引,可避免数据量大后创建索引的性能损耗,对应之前的索引分类:
create table tb_user (
id bigint primary key auto_increment comment '主键id(主键索引,自动创建)',
name varchar(50) not null comment '姓名',
phone char(11) not null comment '手机号',
profession varchar(50) comment '职业',
age tinyint comment '年龄',
status tinyint default 1 comment '状态',
email varchar(100) comment '邮箱',
-- 普通索引:name字段值可重复
index idx_user_name (name),
-- 唯一索引:phone字段非空且唯一
unique index idx_user_phone (phone),
-- 联合索引:profession、age、status,遵循最左前缀原则
index idx_user_pro_age_sta (profession, age, status),
-- 普通索引:email字段提升查询效率
index idx_user_email (email)
) engine = innodb default charset = utf8mb4 comment '用户表';场景2:已存在的表创建索引
-- 需求1:name字段值可能重复,创建普通索引 create index idx_user_name on tb_user(name); -- 需求2:phone字段非空且唯一,创建唯一索引 create unique index idx_user_phone on tb_user(phone); -- 需求3:为profession、age、status创建联合索引 create index idx_user_pro_age_sta on tb_user(profession, age, status); -- 需求4:为email建立索引提升查询效率 create index idx_user_email on tb_user(email);
4.2 查看索引
用于查看表中已创建的所有索引的详细信息,包括索引类型、关联字段、是否唯一、索引长度等,是索引排查的核心命令。
语法
show index from table_name;
实操示例
-- 查看tb_user表的所有索引 show index from tb_user;
核心结果说明
执行后可获取关键信息:
key_name:索引名称,主键索引固定为primarycolumn_name:索引关联的字段名non_unique:是否允许重复值,0=唯一索引,1=普通索引seq_in_index:字段在联合索引中的顺序,从1开始,对应最左前缀原则index_type:索引类型,innodb默认为btree(b+tree)
4.3 删除索引
用于删除无用、冗余的索引,减少写操作的性能损耗和磁盘空间占用。
通用语法
drop index index_name on table_name;
实操示例
-- 删除tb_user表中的idx_user_email索引 drop index idx_user_email on tb_user; -- 特殊:删除主键索引(一张表仅有一个主键索引) alter table tb_user drop primary key;
注意事项
- 删除索引前需确认无业务sql依赖该索引,避免引发全表扫描导致线上故障
- 大表删除索引需在低峰期操作,避免影响数据库性能
- 唯一索引无法通过删除解决重复值问题,需先清理表中的重复数据
五、sql性能分析
sql优化的核心原则是先定位瓶颈,再针对性优化,mysql提供了从全局到单条sql的全链路性能分析工具,可精准定位慢sql、性能损耗点,是索引优化的前提。
5.1 sql执行频率分析
用于查看数据库全局的增删改查(insert/update/delete/select)执行频次,判断数据库的读写压力模型,是宏观性能分析的第一步。
核心语法
-- 查看全局sql执行频次(mysql启动以来的累计值) show global status like 'com_______'; -- 查看当前会话的sql执行频次 show session status like 'com_______';
核心说明
global:查看mysql服务启动以来的全局累计执行次数,用于分析整体业务模型session:查看当前数据库连接的会话执行次数,用于测试单条/一组sql的执行情况- 关键结果字段:
com_select:select语句累计执行次数,反映读压力com_insert:insert语句累计执行次数com_update:update语句累计执行次数com_delete:delete语句累计执行次数
分析逻辑
- 如果
com_select占比极高,说明数据库是读多写少模型,核心优化方向是索引优化、查询缓存、读写分离 - 如果增删改操作占比高,说明是写多读少模型,核心优化方向是索引精简、事务优化、批量写入优化
5.2 慢查询日志
慢查询日志是mysql内置的日志功能,会记录所有执行时间超过指定阈值的sql语句,是定位线上慢sql的核心工具。
核心特点
- 默认关闭,需手动修改配置文件开启
- 可自定义慢sql的时间阈值,默认阈值为10秒,生产环境通常设置为1-2秒
- 仅记录执行完成的sql,未执行完成的长sql不会记录
配置方式
需修改mysql的配置文件(linux为/etc/my.cnf,windows为my.ini),添加以下配置:
# 开启慢查询日志开关 1=开启 0=关闭 slow_query_log = 1 # 慢sql时间阈值,单位:秒,执行时间超过2秒的sql会被记录 long_query_time = 2 # 慢查询日志文件存储路径 slow_query_log_file = /var/lib/mysql/localhost-slow.log
配置完成后,重启mysql服务生效:
# linux重启命令 systemctl restart mysqld
使用方式
- 开启后,所有超过
long_query_time阈值的sql都会自动写入日志文件 - 通过查看日志文件,定位高频、长耗时的慢sql,针对性做索引优化、sql改写
- 生产环境建议长期开启,作为线上sql性能监控的核心手段
5.3 profile详情分析
profile工具可以精准查看单条sql执行时,每个阶段的耗时、cpu占用等细节,可定位sql到底慢在哪个环节(如io、排序、锁等待、优化器解析等),是微观性能分析的核心工具。
基础操作
1. 查看是否支持profile功能
select @@have_profiling;
结果为yes表示支持,no表示不支持,主流mysql版本均支持。
2. 开启profile功能
默认关闭,需在当前会话手动开启,仅对当前会话生效:
-- 开启profile,1=开启 0=关闭 set profiling = 1;
3. 查看sql执行耗时概览
执行业务sql后,通过以下命令查看所有sql的执行耗时:
-- 查看当前会话所有sql的执行耗时、query_id show profiles;
结果会返回每条sql的query_id、执行时长、sql语句,可快速定位耗时最高的sql。
4. 查看指定sql的全阶段耗时详情
通过query_id查看单条sql每个执行阶段的耗时,定位瓶颈:
-- 查看query_id为1的sql,每个执行阶段的耗时 show profile for query 1; -- 进阶:同时查看cpu占用情况,精准定位cpu瓶颈 show profile cpu for query 1;
核心分析场景
通过profile可定位常见的性能瓶颈:
sending data:数据读取、传输耗时高,通常是全表扫描、无索引导致creating sort index:排序耗时高,通常是order by字段无索引,引发using filesortcreating tmp table:创建临时表耗时高,通常是group by字段无索引,引发using temporary
5.4 explain执行计划(核心重点)
explain(或desc)是sql优化最核心的工具,可获取mysql优化器生成的sql执行计划,查看sql是否走索引、走哪个索引、是否回表、是否全表扫描、表连接顺序等核心信息,是索引优化的必备工具。
通用语法
直接在select查询语句前添加explain关键字即可:
explain select 字段列表 from 表名 where 条件 [group by 字段 order by 字段]; -- 简写方式,效果完全一致 desc select 字段列表 from 表名 where 条件;
实操示例
-- 查看主键查询的执行计划 explain select * from tb_user where id = 1; -- 查看二级索引查询的执行计划 explain select * from tb_user where name = '张三'; -- 查看联合索引查询的执行计划 explain select * from tb_user where profession = '软件工程' and age = 25;
执行计划核心字段详解
执行计划返回12个字段,核心高频字段如下,按重要优先级排序:
字段名 | 核心含义 | 重点分析规则 |
| select查询的序列号,标识sql中表的执行顺序 | 1. id相同,执行顺序从上到下; 2. id不同,id值越大,越先执行(子查询优先执行) |
| select查询的类型 | 常见取值:
|
| 索引访问类型,反映sql性能的核心指标,性能从好到坏排序: | 核心优化目标: 1. 至少达到 2. 避免出现
|
| 本次查询中,可能用到的索引(候选索引) | 仅为优化器的候选列表,不代表实际会使用 |
| 本次查询中,实际使用的索引 | 核心判断项: 1. 为 2. 需和 |
| 索引中使用的字节数,是索引字段的最大可能长度 | 1. 不损失精度的前提下,长度越短越好 2. 可通过该值判断联合索引中,实际用到了哪些字段(对应最左前缀原则) |
| mysql认为执行查询必须扫描的行数 | innodb中为估算值,值越小越好,全表扫描时会显示表的总行数 |
| 返回结果行数占扫描行数的百分比 | 值越大越好,100.00为最优;值越小说明扫描了大量无效行,需优化索引 |
| 额外信息,sql优化的核心判断项,可直接定位sql的问题 | 高频关键值:
|
核心优化判断规则
- 必须避免
type字段出现all(全表扫描) - 必须避免
extra字段出现using temporary、using filesort - 最优状态:
type为ref/const,extra为using index(覆盖索引,无回表)
5.6索引语法与性能分析实操闭环
完整的sql优化流程如下,可直接套用:
- 通过慢查询日志定位线上耗时高的慢sql
- 通过explain执行计划查看sql的索引使用情况,定位问题(是否全表扫描、是否有临时表/文件排序)
- 通过profile详情定位sql的具体耗时瓶颈
- 针对性创建/优化索引,改写sql
- 再次通过explain验证优化效果,形成闭环
六.索引使用
6.1 索引效率验证
在大表场景下,索引对查询性能的提升效果极为显著,可通过以下步骤直观验证:
-- 1. 无索引时执行查询(全表扫描) select * from tb_sku where sn = '100000003145001'; -- 2. 为sn字段创建普通索引 create index idx_sku_sn on tb_sku(sn); -- 3. 再次执行相同查询(索引扫描) select * from tb_sku where sn = '100000003145001';
现象说明:
- 无索引时,查询需遍历全表数据,耗时通常在数秒至数十秒级别;
- 创建索引后,查询通过 b+tree 直接定位数据,耗时可降至毫秒级,性能提升可达数百倍。
6.2 索引失效场景
6.2.1 最左前缀法则(联合索引核心规则)
规则定义:联合索引遵循 “最左前缀匹配” 原则,查询必须从索引的最左列开始,且不能跳过中间列;若跳过某一列,该列右侧的所有索引列将失效。
示例:假设联合索引为 idx_user_pro_age_sta(profession, age, status)
-- 场景1:完全匹配(三列都用到索引) explain select * from tb_user where profession = '软件工程' and age = 31 and status = '0'; -- 场景2:用到前两列(status列索引失效) explain select * from tb_user where profession = '软件工程' and age = 31; -- 场景3:仅用到第一列(age、status列索引失效) explain select * from tb_user where profession = '软件工程'; -- 场景4:跳过最左列(profession列未使用,索引完全失效) explain select * from tb_user where age = 31 and status = '0'; -- 场景5:完全不使用最左列(索引完全失效,全表扫描) explain select * from tb_user where status = '0';
优化建议:联合索引的字段顺序需遵循 “等值条件优先、范围条件靠后” 的原则,将高频等值查询字段放在左侧。
6.2.2 范围查询导致索引失效
规则定义:联合索引中,若某一列使用了>/</between等范围查询,该列右侧的所有索引列将失效。
示例:
-- 场景1:age使用范围查询,status列索引失效 explain select * from tb_user where profession = '软件工程' and age > 30 and status = '0'; -- 场景2:age使用>=/<=(边界范围),status列索引同样失效 explain select * from tb_user where profession = '软件工程' and age >= 30 and status = '0';
优化建议:将范围查询字段放在联合索引的最后一列,避免影响后续字段的索引使用;若业务允许,优先使用>=/<=替代>/<,减少范围扫描的行数。
6.2.3 索引列运算 / 函数操作导致失效
规则定义:对索引列进行运算(如加减乘除)或函数操作(如substring/date_format),会导致索引失效,mysql 将转为全表扫描。
示例:
-- 场景:对phone字段使用substring函数,索引失效 explain select * from tb_user where substring(phone, 10, 2) = '15';
优化建议:
- 将运算 / 函数操作移到查询条件的右侧,避免作用于索引列;
- 对于字符串截取场景,优先使用前缀索引替代函数操作;
- 高版本 mysql 支持函数索引,可直接为运算后的字段创建索引。
6.2.4 字符串字段不加引号导致索引失效
规则定义:字符串类型字段(如 varchar/char)查询时,若值未加引号,mysql 会自动进行类型转换,导致索引失效。
示例:
-- 场景1:status字段为字符串类型,不加引号,索引失效 explain select * from tb_user where profession = '软件工程' and age = 31 and status = 0; -- 场景2:phone字段为字符串类型,不加引号,索引失效 explain select * from tb_user where phone = 17799990015;
原理说明:字符串与数字比较时,mysql 会将字符串转换为数字进行比较,导致索引列的类型被隐式转换,无法匹配索引中的字符串值。优化建议:字符串字段的查询值必须加单引号,即使值是纯数字。
6.2.5 模糊查询(like)导致索引失效
规则定义:
- 尾部模糊匹配(
like '前缀%'):索引有效; - 头部模糊匹配(
like '%后缀'/like '%中间%'):索引失效,转为全表扫描。
示例:
-- 场景1:尾部模糊匹配,索引有效 explain select * from tb_user where profession like '软件%'; -- 场景2:头部模糊匹配,索引失效 explain select * from tb_user where profession like '%工程'; -- 场景3:前后都模糊匹配,索引失效 explain select * from tb_user where profession like '%工%';
优化建议:
- 优先使用前缀索引(如
create index idx_profession on tb_user(profession(4))); - 高频模糊查询场景,建议使用全文索引(
fulltext)替代普通索引; - 核心业务场景可引入 es 等搜索引擎实现全文检索。
6.2.6 数据分布影响(mysql 优化器放弃索引)
规则定义:当查询条件匹配的数据量超过表中总行数的约 20% 时,mysql 优化器会评估使用索引的随机 io 成本高于全表扫描的顺序 io 成本,因此会放弃使用索引,直接进行全表扫描。
示例:
-- 场景1:匹配数据量少,使用索引 select * from tb_user where phone >= '17799990005'; -- 场景2:匹配数据量超过20%,mysql放弃索引,全表扫描 select * from tb_user where phone >= '17799990015';
优化建议:
- 避免在低基数字段(如性别、状态)上创建索引,此类字段的查询几乎必然触发全表扫描;
- 对于大表的范围查询,可通过
force index强制使用索引,但需评估性能影响; - 定期分析表数据分布,避免索引因数据倾斜失效。
6.3 sql 索引提示(人工干预优化器)
当 mysql 优化器选择的索引不符合预期时,可通过索引提示强制指定索引,适用于复杂查询或优化器误判场景。
核心语法
-- 1. use index:建议mysql使用指定索引(优化器仍可能选择其他索引) explain select * from tb_user use index(idx_user_pro) where profession = '软件工程'; -- 2. ignore index:忽略指定索引(让优化器不使用该索引) explain select * from tb_user ignore index(idx_user_pro) where profession = '软件工程'; -- 3. force index:强制mysql使用指定索引(优先级最高) explain select * from tb_user force index(idx_user_pro) where profession = '软件工程'
使用场景:
- 优化器误判,选择了低效索引时;
- 多索引场景下,需人工干预指定最优索引;
- 临时验证不同索引的性能差异。
6.4 覆盖索引(性能天花板优化)
定义
覆盖索引是指查询所需的所有列,都能在索引中直接获取,无需回表查询聚集索引,避免了额外的 io 操作,是 innodb 中性能最优的索引使用方式。

原理示例
以联合索引 idx_user_pro_age_sta(profession, age, status) 为例:
-- 场景1:使用覆盖索引,无需回表(extra显示using index) explain select id, profession from tb_user where profession = '软件工程' and age = 31 and status = '0'; -- 场景2:索引列+主键,同样是覆盖索引(innodb二级索引默认包含主键) explain select id, profession, age, status from tb_user where profession = '软件工程' and age = 31 and status = '0'; -- 场景3:查询包含非索引列,需要回表(extra无using index) explain select id, profession, age, status, name from tb_user where profession = '软件工程' and age = 31 and status = '0'; -- 场景4:select * 查询,必然回表,无法使用覆盖索引 explain select * from tb_user where profession = '软件工程' and age = 31 and status = '0';
关键判断(extra 字段):
using index:使用了覆盖索引,无需回表,性能最优;using index condition:使用了索引,但需要回表查询完整数据;- 无上述标识:未使用索引或仅使用了部分索引,需全表扫描或大量回表。
优化建议
- 避免使用
select *,仅查询业务所需的列; - 高频查询场景,创建包含所有查询列的联合索引,实现覆盖索引;
- 二级索引默认包含主键,因此查询主键列无需额外回表。
6.5 高频真题:单条 sql 的最优索引设计
题目背景
一张用户表 tb_user,包含字段 id, username, password, status,数据量较大,需对以下 sql 进行优化:
select id, username, password from tb_user where username = 'silverkite';
最优方案设计
方案 1:普通单列索引(username)
create index idx_tb_user_username on tb_user(username);
执行逻辑:
- 通过
idx_tb_user_username索引定位到username='itcast'对应的主键id; - 再通过
id回表查询聚集索引,获取username和password字段的值; - 存在额外的回表 io 操作,性能中等。
方案 2:覆盖联合索引(最优方案)
create index idx_tb_user_uname_pwd on tb_user(username, password);
执行逻辑(无回表,性能天花板):
- 联合索引
(username, password)中,已包含查询所需的所有字段:where条件匹配username;select需要的id(二级索引默认包含主键)、username、password全部在索引中;
- mysql 可直接通过索引完成数据读取,无需回表查询聚集索引,extra 字段显示
using index; - 无额外 io 操作,性能最优,是该场景下的唯一最优解。
6.6 前缀索引优化(长字符串字段索引方案)
适用场景
当字段为varchar/text等长字符串类型时,直接创建全字段索引会导致索引体积过大,查询时 io 成本高,可通过前缀索引仅对字符串的前 n 个字符创建索引,大幅节省索引空间并提升查询效率。
核心语法
-- 为table_name表的column字段,取前n个字符创建前缀索引 create index idx_xxx on table_name(column(n));
前缀长度选择方法(核心:高选择性)
前缀长度的选择需保证索引的高选择性(即不重复值占比越高,索引效率越高),计算方式如下:
-- 1. 全字段索引的选择性(最优为1,即唯一索引) select count(distinct email) / count(*) from tb_user; -- 2. 计算取前5个字符的选择性,对比全字段选择性,越接近越好 select count(distinct substring(email, 1, 5)) / count(*) from tb_user; -- 3. 当选择性接近全字段时,即可确定前缀长度(如n=5) create index idx_tb_user_email on tb_user(email(5));
前缀索引查询流程示例
以select * from tb_user where email = 'lvbu666@163.com';为例:
- 辅助索引
email(5)存储的是字符串前 5 个字符,如lvbu6; - 通过前缀索引定位到匹配的
email前缀,获取对应的主键id; - 回表查询聚集索引,校验完整的
email值是否匹配; - 若匹配成功,则返回数据。
优缺点与适用场景
| 优点 | 缺点 | 适用场景 |
| 大幅降低索引体积,减少 io | 无法使用覆盖索引,查询时需回表校验完整值 | 长字符串字段(如邮箱、url),无法使用全字段索引的场景 |
| 提升索引创建与查询效率 | 若前缀选择性低,仍可能导致大量回表 | 高频前缀查询场景(如邮箱前缀、手机号前缀) |
6.7 单列索引 vs 联合索引:多条件查询选型
核心结论
在多条件查询场景中,优先使用联合索引,而非多个单列索引,可避免 mysql 优化器误判,同时实现覆盖索引优化。
场景对比
场景 1:多个单列索引
-- 单列索引1 create index idx_tb_user_phone on tb_user(phone); -- 单列索引2 create index idx_tb_user_name on tb_user(name); -- 多条件查询 explain select id, phone, name from tb_user where phone = '17799990010' and name = '韩信';
执行结果分析:
possible_keys中同时存在两个索引,但key仅选择了idx_tb_user_phone;- mysql 优化器会评估两个索引的效率,选择成本更低的索引(如
phone索引的基数更高); - 未被选择的索引完全失效,仅使用单个索引过滤,需在服务层额外过滤
name条件,效率低。
场景 2:联合索引(最优方案)
-- 创建phone和name的联合索引 create unique index idx_tb_user_phone_name on tb_user(phone, name); -- 相同查询 explain select id, phone, name from tb_user where phone = '17799990010' and name = '韩信';
执行结果分析:
- 直接使用联合索引
idx_tb_user_phone_name,同时匹配phone和name两个条件; - 索引中已包含
phone、name和主键id,若查询字段仅包含这三个,可实现覆盖索引,无需回表; - 避免了优化器的索引选择问题,性能稳定且高效。
选型决策树
- 多条件等值查询:优先创建联合索引,字段顺序遵循 “高频等值条件在前,范围条件在后”;
- 单条件查询:创建单列索引即可;
- 多个单条件查询:避免创建多个单列索引,优先考虑覆盖索引或联合索引,减少索引数量,降低写操作的维护成本。
索引优化核心避坑清单
| 优化场景 | 错误做法 | 正确做法 |
| 多条件查询 | 创建多个单列索引 | 创建联合索引 |
| 长字符串字段 | 创建全字段索引 | 创建前缀索引,优先保证高选择性 |
| 高频查询 | 使用select * | 仅查询业务字段,使用覆盖索引 |
| 联合索引 | 跳过最左列 | 调整查询条件,匹配最左前缀 |
| 模糊查询 | like '%前缀' | 使用like '前缀%'或前缀索引 |
七.索引设计原则
索引设计是sql性能优化的根源性工作,优秀的索引设计可从源头避免慢sql、全表扫描、回表查询等性能问题,而非事后补救。以下7条核心设计原则,覆盖业务开发全场景与面试高频考点,完全承接前文的索引原理、使用规则与优化实践。
7.1 原则一:优先为数据量大、查询频繁的表建立索引
核心逻辑
索引的核心价值是解决大数据量下的查询性能问题,需精准匹配场景,避免无效索引:
- 收益场景:表数据量≥10万行,且有高频业务查询,索引带来的查询性能提升,远大于索引维护的成本;
- 无效场景:仅几千行的小表(如系统配置表、数据字典表),全表扫描的成本极低,建索引反而会增加额外的维护开销,无实际性能收益。
实操避坑
- 不要为极少查询的冷数据归档表创建大量索引,仅需为归档筛选字段建少量索引即可;
- 不要为全量小表盲目建索引,优先通过业务逻辑优化替代索引。
7.2 原则二:优先为where、order by、group by操作的字段建立索引
索引的核心价值是过滤数据、利用有序性避免额外排序,这三类操作是索引最核心的落地场景,直接决定sql的性能上限。
分场景拆解
1.where查询条件字段
是索引最基础的使用场景,通过索引快速过滤数据,避免全表扫描,是所有索引设计的基础。
2.order by排序字段
利用b+tree索引天生的有序性,直接通过索引获取有序数据,避免mysql生成临时文件做额外排序(执行计划extra出现using filesort),大幅降低cpu消耗。
3.group by分组字段
分组操作的底层需要先对数据排序,再做聚合计算,索引可避免分组时创建临时表(执行计划extra出现using temporary),是分组查询优化的核心手段。
实操建议
多字段组合查询场景,优先创建联合索引,字段顺序遵循:where等值查询字段 → group by分组字段 → order by排序字段,可实现索引全流程覆盖,无额外排序、无临时表、无回表。
7.3 原则三:优先选择区分度(基数)高的列作为索引,优先唯一索引
核心定义
区分度(也叫选择性)= 字段不重复值的数量 / 表总行数,比值越接近1,区分度越高,索引的过滤效率越强。
- 唯一索引:区分度=1,是性能最优的索引,一次索引查找即可精准定位数据,无额外过滤开销;
- 低基数字段:如性别(男/女)、状态(0/1),区分度极低,索引过滤后仍需扫描大量数据,mysql优化器大概率会放弃索引,直接全表扫描。
实操规范
1.建索引前先计算字段区分度,优先为区分度≥0.3的字段建索引:
-- 计算字段区分度,越接近1越好 select count(distinct 字段名) / count(*) from 表名;
2.业务中保证唯一性的字段(如手机号、身份证号、订单号),必须创建唯一索引,既保证数据唯一性,又获得最优查询性能;
3.低基数字段禁止单独建索引,可通过联合索引(和高区分度字段组合)实现优化。
7.4 原则四:长字符串字段,优先建立前缀索引
核心逻辑
当字段为varchar(255)、text等长字符串类型时,直接创建全字段索引会导致索引体积过大,磁盘io成本极高;前缀索引仅对字符串的前n个字符创建索引,可大幅节省索引空间,提升索引查询效率。
实操规范
1.前缀长度选择核心:保证前缀的选择性接近全字段的选择性,平衡索引体积与过滤效率;
-- 1. 全字段选择性 select count(distinct email) / count(*) from tb_user; -- 2. 测试前10个字符的选择性,接近全字段即可确定前缀长度 select count(distinct substring(email, 1, 10)) / count(*) from tb_user; -- 3. 创建前缀索引 create index idx_user_email on tb_user(email(10));
2.适用场景:邮箱、url、长文本标题等长字符串字段的等值查询;
3.避坑提醒:前缀索引无法实现覆盖索引,因为索引中仅存储了字段前缀,无完整值,查询时必须回表校验完整数据。
7.5 原则五:优先使用联合索引,减少单列索引
核心优势
- 降低维护成本:多个单列索引在数据增删改时,需要同步维护多个b+tree结构;联合索引仅需维护一个索引结构,大幅降低写操作的性能损耗;
- 更容易实现覆盖索引:联合索引可包含查询所需的所有字段,直接实现
using index,避免回表查询,是性能优化的核心手段; - 避免优化器误判:多条件查询时,多个单列索引仅会被优化器选择一个最优的,其余索引完全失效;联合索引可同时匹配多个查询条件,过滤效率更高。
设计规范
- 联合索引字段顺序严格遵循最左前缀法则,同时满足:高频等值查询字段在前、范围查询字段在后;
- 避免冗余索引:已存在联合索引
(a,b,c),则无需再创建(a)、(a,b)这类前缀子集索引,避免冗余开销; - 单表优先通过3-5个联合索引覆盖全量业务查询,而非创建十几个单列索引。
7.6 原则六:严格控制索引数量,索引不是越多越好
核心代价
索引的本质是「空间换时间」,过量索引会带来双重成本:
- 空间成本:每个索引都需要独立的磁盘存储空间,大表的索引总体积甚至会超过业务数据本身;
- 性能成本:执行
insert/update/delete时,需要同步维护所有相关索引的b+tree结构,保证有序性,索引越多,写操作耗时越长,数据库并发性能越差; - 优化成本:过多索引会增加mysql查询优化器的选择成本,可能导致优化器选错索引,反而降低查询性能。
实操规范
- 单表索引数量严格控制在5个以内,核心业务表不超过8个;
- 定期清理无用索引:通过mysql性能_schema监控索引使用频次,删除长期未被查询使用的冗余索引;
- 禁止为每个字段单独创建单列索引,优先通过联合索引覆盖多场景查询。
7.7 原则七:索引列优先设置not null约束
核心逻辑
- 优化器判断更高效:mysql优化器处理
null值时,需要增加额外的空值判断逻辑,无法高效利用索引;明确not null的字段,优化器可更精准地生成执行计划; - 索引统计更准确:
null值会影响索引的基数统计,导致优化器对查询成本的评估出现偏差,可能选错执行计划; - 存储成本更低:innodb中,
null值需要额外的存储空间标记空值,not null字段可减少索引的存储体积,提升io效率。
实操规范
- 建表时,所有索引列必须设置
not null约束,并搭配合理的默认值:字符串类型默认空字符串'',数字类型默认0; - 禁止用
null作为业务有效值,比如用0/1表示状态,而非用null表示「未设置」; - 若业务字段确实存在空值场景,可通过特殊值(如
-1、空字符串)替代null,保证索引列的not null约束。
索引设计
- 筛选目标表:锁定数据量大、查询频繁的核心业务表,小表/冷表不做过度设计;
- 提取关键字段:梳理业务中高频使用的
where过滤、order by排序、group by分组字段; - 评估字段质量:优先选择区分度高的字段,过滤低基数字段,长字符串字段设计前缀索引;
- 设计索引结构:优先创建联合索引,字段顺序匹配最左前缀法则,尽可能实现覆盖索引;
- 控制索引规模:单表索引不超过5个,删除冗余、无用索引;
- 完善字段约束:所有索引列设置
not null约束与默认值,优化优化器执行计划。
三、sql优化
一、插入数据优化(insert 优化)
插入性能的核心瓶颈是磁盘 io 与事务提交开销,通过以下 4 种方式可大幅提升批量插入效率:
1.1 批量插入(单语句多值插入)
优化逻辑
将多条数据合并为一条insert语句执行,减少客户端与数据库的交互次数,降低网络 io 与 sql 解析开销。
示例 sql
-- 低效方式:单条插入,多次交互 insert into tb_test values(1, 'tom'); insert into tb_test values(2, 'cat'); insert into tb_test values(3, 'jerry'); -- 高效方式:批量插入,单次交互 insert into tb_test values(1, 'tom'), (2, 'cat'), (3, 'jerry');
实操规范
- 单条批量插入建议控制在100-1000 条数据,避免单条 sql 过大导致解析超时或日志膨胀;
- 批量插入的数据需提前校验合法性,避免单条失败导致整批回滚。
1.2 手动提交事务(事务包裹批量插入)
优化逻辑
innodb 默认每条 sql 都会自动提交事务(autocommit=1),频繁提交事务会产生大量 redo 日志刷盘操作,通过手动开启事务,将多条插入包裹在一个事务中,仅需一次提交刷盘,大幅减少 io 次数。
示例 sql
-- 开启事务 start transaction; -- 批量插入数据 insert into tb_test values(1, 'tom'), (2, 'cat'), (3, 'jerry'); insert into tb_test values(4, 'tom'), (5, 'cat'), (6, 'jerry'); insert into tb_test values(7, 'tom'), (8, 'cat'), (9, 'jerry'); -- 统一提交事务,仅一次刷盘 commit;
实操规范
- 事务中插入数据量建议控制在1 万条以内,避免事务过大导致锁表、日志膨胀或崩溃恢复时间过长;
- 异常场景需配合
rollback回滚,保证数据一致性。
1.3 主键顺序插入(避免页分裂)
优化逻辑
innodb 中数据按主键顺序存储在 b+tree 索引中,顺序插入时数据会追加到当前页的末尾,不会触发页分裂;乱序插入可能导致数据插入到已写满的页中,触发页分裂操作,带来额外的 io 开销。
对比示例
-- 乱序主键(低效,易触发页分裂):8, 1, 9, 21, 88, 2, 4, 15, 89, 5, 7, 3 -- 顺序主键(高效,无额外页分裂):1, 2, 3, 4, 5, 7, 8, 9, 15, 21, 88, 89
底层原理:页分裂与页合并
1.页分裂:当插入数据的主键值需要写入已写满的页时,innodb 会将当前页的数据分裂为两个页,移动数据并调整 b+tree 结构,是插入性能的主要瓶颈之一;



插入50,会落在1#page区域,但1#page空间不足,就会进行页分裂,先将1中一半数据放进3#page页中,再将50放入3页,然后调整页指针。
2.页合并:当删除数据后,页中剩余数据低于merge_threshold(默认 50%)时,innodb 会尝试将相邻的页合并,减少碎片空间,优化存储效率。
1.4 大批量数据导入(load data infile)
适用场景
一次性导入百万级以上的大批量数据,使用insert语句效率极低,推荐使用 mysql 提供的load data指令,直接从本地文件加载数据,跳过 sql 解析阶段,性能提升可达数十倍。
操作步骤
1.客户端连接时开启本地文件加载权限:
mysql --local-infile -u root -p
2.全局开启本地文件导入开关:
set global local_infile = 1;
3.执行load data指令导入数据:
load data local infile '/root/sql1.log' into table tb_user fields terminated by ',' lines terminated by '\n';
注意事项
- 导入文件需提前按主键排序,避免导入过程中触发页分裂;
- 导入过程建议关闭索引,导入完成后再重建索引,减少导入过程中的索引维护开销。
二、主键优化(核心设计原则)
主键是 innodb 数据存储与索引的核心,主键设计直接影响数据插入、查询与更新的性能,需遵循以下 4 大核心原则:
2.1 降低主键长度(短主键优先)
优化逻辑
innodb 二级索引(辅助索引)中会包含主键值,主键越短,二级索引的体积越小,占用的磁盘空间越少,查询时 io 效率越高。
实操建议
- 优先使用
int/bigint类型的自增主键,避免使用长字符串(如 uuid、身份证号)作为主键; - 业务中无需暴露的主键,可使用无业务含义的自增 id,避免主键长度过长。
2.2 优先使用自增主键(auto_increment)
优化逻辑
自增主键保证数据按顺序插入,数据会追加到当前页的末尾,不会触发页分裂操作,插入性能最优;乱序主键(如 uuid、随机 id)会频繁触发页分裂,导致插入性能下降,同时产生大量存储碎片。
底层存储原理:索引组织表(iot)
innodb 中表数据是根据主键顺序组织存放的,这种存储方式称为索引组织表(index organized table, iot),数据按主键顺序存储在 b+tree 的叶子节点中,顺序插入可保持叶子节点的有序性与连续性,避免碎片产生。
实操规范
- 业务表主键统一使用
auto_increment自增主键,数据类型优先选择bigint unsigned(范围更大,避免溢出); - 分布式场景需生成全局有序 id 时,可使用雪花算法(保证时间戳部分有序),避免纯随机 uuid。
2.3 避免使用 uuid 或自然主键
优化逻辑
- uuid:随机生成的 uuid 是乱序的,会导致频繁的页分裂,同时字符串类型的 uuid 长度较长,会增大二级索引的体积;
- 自然主键(如身份证号、手机号):存在业务变更风险,且长度较长,无法保证插入顺序,同时可能因业务需求修改主键,导致数据与索引的整体调整,成本极高。
对比示例
| 主键类型 | 优点 | 缺点 |
| 自增 int/bigint | 顺序插入,无页分裂;主键短,索引体积小;性能最优 | 需单独维护,无业务含义 |
| uuid | 全局唯一,无需提前生成 | 乱序插入,频繁页分裂;长度长,索引体积大 |
| 自然主键(身份证号) | 无需额外字段,直接复用业务字段 | 长度长,索引体积大;存在业务变更风险;无法保证顺序插入 |
2.4 避免修改主键值
优化逻辑
主键是 innodb 数据存储的核心标识,修改主键值会导致:
- 数据行需要在 b+tree 中移动位置,触发页分裂或页合并操作;
- 所有二级索引中存储的主键值都需要同步更新,维护成本极高,甚至可能导致索引失效。
实操规范
- 业务设计中,主键必须是无业务含义的字段,不参与任何业务逻辑,避免因业务变更修改主键;
- 若需修改业务标识字段(如手机号),直接更新对应字段即可,无需修改主键。
插入与主键优化总结
| 场景 | 错误做法 | 正确做法 |
| 批量插入 | 逐条插入,频繁提交事务 | 批量插入 + 事务包裹 |
| 大批量导入 | 使用 insert 循环插入 | 使用 load data infile |
| 主键设计 | 使用 uuid / 身份证号作为主键 | 使用自增 bigint 主键 |
| 主键插入 | 乱序插入 | 按主键顺序插入 |
| 主键修改 | 因业务需求修改主键值 | 主键无业务含义,永不修改 |
三、order by 优化
排序操作是sql性能的高频瓶颈,核心问题是避免using filesort(文件排序),优先通过索引实现有序数据读取。
3.1 核心概念:两种排序方式
1.using filesort:
非索引排序,通过全表扫描读取数据,在sort_buffer中完成排序操作,所有不通过索引直接返回有序结果的排序都属于文件排序,性能较差。
2.using index:
利用有序索引直接扫描返回有序数据,无需额外排序,操作效率高,是排序优化的目标。
3.2 单字段/多字段排序优化
1. 无索引时的排序(低效)
-- 无索引,触发using filesort explain select id,age,phone from tb_user order by age , phone;
2. 创建联合索引优化(高效)
-- 创建age、phone的联合索引,实现using index create index idx_user_age_phone_aa on tb_user(age,phone); -- 升序排序:索引有序,无需额外排序 explain select id,age,phone from tb_user order by age , phone; -- 同方向降序排序:索引支持,无需额外排序 explain select id,age,phone from tb_user order by age desc , phone desc;
3. 升降序混合排序优化
当排序字段为一个升序、一个降序时,需创建匹配顺序的索引:
-- 创建age升序、phone降序的索引 create index idx_user_age_phone_ad on tb_user(age asc ,phone desc); -- 匹配索引顺序,实现using index explain select id,age,phone from tb_user order by age asc , phone desc;
3.3order by优化核心原则
- 按排序字段创建合适的索引,多字段排序遵循最左前缀法则;
- 尽量使用覆盖索引,避免回表操作;
- 多字段排序方向需与索引定义的方向匹配,避免混合升降序;
- 若无法避免
using filesort,可适当增大排序缓冲区sort_buffer_size(默认256kb),提升排序性能。
四、group by优化
分组操作的核心瓶颈是临时表创建与排序,可通过索引优化避免using temporary。
4.1 索引优化原理
分组操作底层依赖数据有序性,索引的有序性可直接用于分组聚合,无需额外创建临时表。
4.2 实操示例
-- 删除旧索引 drop index idx_user_pro_age_sta on tb_user; -- 无索引分组,触发using temporary explain select profession , count(*) from tb_user group by profession ; -- 创建(profession, age, status)联合索引 create index idx_user_pro_age_sta on tb_user(profession , age , status); -- 匹配最左前缀,实现using index,无临时表 explain select profession , count(*) from tb_user group by profession ; explain select profession , count(*) from tb_user group by profession, age; -- 带过滤条件的分组:where profession='软件工程' group by age explain select age,count(*) from tb_user where profession = '软件工程' group by age;
4.3group by优化核心原则
- 分组字段遵循最左前缀法则,优先创建包含分组字段的联合索引;
- 索引中包含查询所需的所有字段,实现覆盖索引;
- 过滤条件(
where)字段需在分组字段之前,保证索引有序性; - 避免在低基数字段上进行分组,减少临时表创建的概率。
五、limit分页优化
大数据量下的分页查询(如limit 2000000,10)性能极差,需通过覆盖索引+子查询优化。
5.1 问题根源
limit m,n的执行逻辑是先读取前m+n条记录,再丢弃前m条,返回后n条,当m很大时,会产生大量无效io。
5.2 优化方案:覆盖索引+子查询
-- 低效方式:直接分页查询,全表扫描排序 select * from tb_sku limit 2000000,10; -- 高效方式:先通过覆盖索引获取id,再关联查询完整数据 explain select * from tb_sku t , (select id from tb_sku order by id limit 2000000,10) a where t.id = a.id;
5.3 优化核心原则
- 利用主键或唯一索引的有序性,通过子查询快速定位分页数据的id;
- 避免
select *,仅在子查询中获取主键id,减少数据读取量; - 对于超大分页,可通过
where id > ? limit n的方式优化,前提是主键连续有序。
六、count优化
count操作的性能差异主要由存储引擎与使用方式决定,需根据业务场景选择最优方案。
6.1 存储引擎差异
- myisam:直接将表总行数存储在磁盘中,
count(*)可直接返回结果,效率极高; - innodb:不存储总行数,执行
count(*)时需遍历数据行进行计数,性能受数据量影响较大。
6.2count用法效率对比
用法 | 执行逻辑 | 效率排序 |
|---|---|---|
| 遍历表,读取字段值并判断是否为null,不为null则计数 | 最低 |
| 遍历表,读取主键值(非null)并计数 | 较低 |
| 遍历表,不读取字段值,直接按行计数 | 高 |
| 优化处理,不读取字段值,直接按行计数 | 最高 |
结论:优先使用count(*),效率最优。
6.3 优化核心原则
- 优先使用
count(*),避免使用count(字段); - 高频统计场景可通过缓存(如redis)或统计表预存计数结果;
- 带条件的计数查询,需为过滤条件创建索引,减少扫描行数。
七、update优化
update操作的核心风险是行锁升级为表锁,导致并发性能下降,需通过索引保证行锁的有效性。
7.1 核心原理
innodb的行锁是针对索引加锁,而非针对记录加锁。若更新条件字段没有索引或索引失效,行锁会升级为表锁,严重影响并发性能。
7.2 实操示例
-- 高效更新:主键索引条件,行锁,仅锁定id=1的记录 update student set no = '2000100100' where id = 1; -- 低效更新:无索引的name字段,索引失效,行锁升级为表锁 update student set no = '2000100105' where name = '韦一笑';
7.3update优化核心原则
- 更新条件字段必须有有效索引,避免索引失效导致表锁;
- 优先使用主键或唯一索引作为更新条件,保证行锁粒度最小;
- 避免批量更新无索引的条件字段,减少表锁的概率;
- 大表更新时,分批执行,避免长时间持有锁。
补充:sql优化通用避坑清单
场景 | 错误做法 | 正确做法 |
| 混合升降序排序,无索引 | 创建匹配顺序的联合索引,使用覆盖索引 |
| 无索引分组,触发临时表 | 创建包含分组字段的联合索引,遵循最左前缀 |
| 直接使用 | 覆盖索引+子查询定位id,再关联查询 |
| 使用 | 使用 |
| 无索引条件更新,行锁升级为表锁 | 使用主键/唯一索引作为更新条件,保证索引有效 |
四、视图、存储过程、触发器
一、视图
一、视图基础概念
1. 什么是视图?
视图是虚拟存在的表,它本身不存储真实数据,只保存了查询的sql逻辑,数据在使用视图时动态从基表中生成。
- 本质:视图是
select查询的封装,相当于给复杂查询起了个“别名”。 - 特点:
- 不占用实际存储空间,仅保存sql定义;
- 视图的数据完全依赖基表,基表数据变化时,视图数据也会同步变化;
- 支持像普通表一样进行查询、修改(部分场景)操作。
二、视图的基础操作
1. 创建视图
-- 语法格式 create [or replace] view 视图名称[(列名列表)] as select语句 [with [cascaded | local] check option]; -- 示例:创建视图,查询学生表中id<=20的数据 create view v_student_20 as select id, name from student where id <= 20;
or replace:如果视图已存在,则替换原有定义;with check option:视图更新/插入数据时,必须满足视图的查询条件,否则会报错。
2. 查询视图
-- 查看视图的创建语句 show create view v_student_20; -- 查询视图数据(和普通表用法一致) select * from v_student_20 where name like '张%';
3. 修改视图
-- 方式一:create or replace view(推荐) create or replace view v_student_20 as select id, name, age from student where id <= 20; -- 方式二:alter view alter view v_student_20 as select id, name, age from student where id <= 20;
4. 删除视图
-- 语法格式 drop view [if exists] 视图名称 [,视图名称] ...; -- 示例 drop view if exists v_student_20;
三、视图的检查选项(with check option)
当使用with check option创建视图时,mysql会在视图的insert/update/delete操作中,检查数据是否符合视图的定义条件,不符合则拒绝执行。
对于多层嵌套视图,mysql提供两种检查规则:
1.cascaded(默认):级联检查
规则:检查当前视图和所有上层依赖视图的条件,只要有一层视图条件不满足,就会报错。
示例:
-- 基表student -- 视图v1:id <= 20,带cascaded检查 create view v1 as select id,name from student where id <= 20 with cascaded check option; -- 视图v2:基于v1,id >= 10,带cascaded检查 create view v2 as select id,name from v1 where id >= 10 with cascaded check option; -- 视图v3:基于v2,id <= 15,无检查 create view v3 as select id,name from v2 where id <= 15; -- 尝试向v3插入id=21的数据:会触发检查,不符合v1的id<=20条件,插入失败 insert into v3(id,name) values(21,'test');
2.local:仅检查当前视图
规则:只检查当前视图的条件,不检查上层依赖视图的条件(但上层视图带cascaded检查时,仍会触发级联检查)。
示例:
-- 基表student -- 视图v1:id <= 15,无检查 create view v1 as select id,name from student where id <= 15; -- 视图v2:基于v1,id >= 10,带local检查 create view v2 as select id,name from v1 where id >= 10 with local check option; -- 尝试向v2插入id=16的数据:满足v2的id>=10条件,但不满足v1的id<=15条件,插入失败(因为数据无法被v1查询到,视图数据不生效) insert into v2(id,name) values(16,'test');
四、视图的更新规则
视图的更新(insert/update/delete)并非都能执行,核心前提是:视图中的行与基表中的行存在一对一的映射关系。
1. 不可更新的场景
如果视图定义中包含以下任意一项,则该视图无法更新:
- 包含聚合函数或窗口函数:
sum()、min()、max()、count()、row_number()等; - 包含
distinct去重; - 包含
group by分组; - 包含
having过滤; - 包含
union或union all; - 基于多个基表的连接查询(非单表视图);
- 视图的列是基表列的计算结果(如
age+1)。
2. 可更新的场景
- 单表视图,无聚合、分组、去重等操作;
- 视图列与基表列一一对应,无计算列;
- 若使用
with check option,更新/插入的数据必须满足视图的查询条件。
五、视图的核心作用
1. 简化操作(simple)
- 封装复杂查询:将常用的多表连接、条件过滤查询定义为视图,后续直接查询视图即可,无需重复编写sql;
- 示例:把
join + where + group by的复杂报表查询封装为视图,业务人员直接查询视图即可获取数据。
2. 安全控制(security)
- 实现行级/列级权限控制:数据库无法直接对特定行/列授权,但可以通过视图实现;
- 示例:创建视图仅包含用户的非敏感字段(如隐藏手机号、身份证号),并仅展示当前用户的数据,给业务人员授予视图的查询权限,避免敏感数据泄露。
3. 数据独立(data independence)
- 屏蔽基表结构变化:基表的字段名、字段顺序调整时,只需修改视图定义,上层业务代码无需修改;
- 示例:基表
student的name字段重命名为stu_name,只需修改视图的select stu_name as name,上层查询视图的业务代码无需改动。
六、视图的优缺点与使用建议
优点
- 简化复杂查询,提升开发效率;
- 实现数据权限控制,保障数据安全;
- 解耦基表与上层业务,降低维护成本。
缺点
- 视图本身不优化查询,执行视图时仍会执行基表的查询逻辑,复杂视图可能存在性能问题;
- 多层嵌套视图会增加查询的复杂度,调试困难;
- 视图更新限制多,不适合频繁修改基表数据的场景。
使用建议
- 优先用视图封装只读查询(如报表、统计),避免用于写操作;
- 避免多层嵌套视图,建议不超过2层;
- 复杂查询优先直接编写sql,视图仅用于简化高频、固定的查询场景。
二、存储过程
一、存储过程基础概念
1. 什么是存储过程?
存储过程是一组预编译并存储在数据库中的sql语句集合,相当于数据库层面的“函数”。调用时只需传入参数即可执行封装好的逻辑,无需重复编写sql。
- 核心思想:sql语句的封装与复用;
- 优势:减少应用与数据库的网络交互、提升数据处理效率、简化开发操作。
二、存储过程基础操作
1. 创建存储过程
语法格式
-- 命令行中需先修改结束符(避免和默认;冲突)
delimiter //
create procedure 存储过程名称([参数列表])
begin
-- 存储过程内的sql逻辑
end //
delimiter ; -- 恢复默认结束符示例:无参数存储过程
-- 示例:查询用户表的总数
delimiter //
create procedure sp_get_user_count()
begin
select count(*) as user_total from tb_user;
end //
delimiter ;2. 调用存储过程
语法格式
call 存储过程名称([参数]);
示例:调用上面创建的存储过程
call sp_get_user_count();
3. 查看存储过程
-- 1. 查看指定数据库的所有存储过程信息 select * from information_schema.routines where routine_schema = 'test_db'; -- 2. 查看单个存储过程的创建语句 show create procedure sp_get_user_count;
4. 删除存储过程
语法格式
drop procedure [if exists] 存储过程名称;
示例
drop procedure if exists sp_get_user_count;
三、存储过程的参数类型
存储过程支持3种参数类型,满足输入、输出、双向交互需求:
参数类型 | 含义 | 备注 |
| 输入参数,调用时传入值 | 默认类型 |
| 输出参数,用于返回结果 | 需用变量接收 |
| 既可以作为输入,也可以作为输出 | 双向参数 |
示例1:带in输入参数的存储过程
-- 示例:根据用户id查询用户信息
delimiter //
create procedure sp_get_user_by_id(in p_user_id int)
begin
select * from tb_user where id = p_user_id;
end //
delimiter ;
-- 调用
call sp_get_user_by_id(1);示例2:带out输出参数的存储过程
-- 示例:查询用户总数,并通过输出参数返回
delimiter //
create procedure sp_get_user_count_out(out p_total int)
begin
select count(*) into p_total from tb_user;
end //
delimiter ;
-- 调用:用用户变量接收返回值
call sp_get_user_count_out(@user_count);
select @user_count as user_total;示例3:带inout双向参数的存储过程
-- 示例:传入一个数字,将其乘以2后返回
delimiter //
create procedure sp_double_num(inout p_num int)
begin
set p_num = p_num * 2;
end //
delimiter ;
-- 调用
set @num = 10;
call sp_double_num(@num);
select @num as doubled_num; -- 结果为20四、存储过程中的变量
mysql存储过程中包含三类变量:系统变量、用户定义变量、局部变量。
1. 系统变量
mysql服务器提供的内置变量,分为全局变量(global)和会话变量(session)。
查看系统变量
-- 查看所有会话变量 show session variables; -- 模糊查找变量(如查看排序缓冲区大小) show variables like 'sort_buffer_size'; -- 查看指定变量的值 select @@global.sort_buffer_size; -- 全局变量 select @@session.sort_buffer_size; -- 会话变量
设置系统变量
-- 设置会话级变量(仅当前会话生效) set session sort_buffer_size = 1024*1024; -- 1mb -- 设置全局变量(重启后失效,需修改配置文件永久生效) set global sort_buffer_size = 2*1024*1024; -- 2mb
2. 用户定义变量
用户自定义的会话级变量,无需提前声明,直接用@变量名使用,作用域为当前会话。
赋值与使用
-- 方式1:set赋值 set @user_name = 'zhangsan'; set @age := 18; -- := 也可赋值 -- 方式2:select into赋值 select name, age into @user_name, @user_age from tb_user where id = 1; -- 使用变量 select * from tb_user where name = @user_name;
3. 局部变量
存储过程内的局部变量,需用declare声明,作用域为begin...end块内。
声明与赋值
delimiter //
create procedure sp_local_var_demo()
begin
-- 声明局部变量,指定类型和默认值
declare v_total int default 0;
declare v_name varchar(20);
-- 赋值
select count(*) into v_total from tb_user;
set v_name = 'local_test';
-- 使用变量
select v_total, v_name;
end //
delimiter ;
call sp_local_var_demo();五、存储过程中的流程控制
1.if条件语句
语法格式
if 条件1 then
-- 条件1成立时执行
elseif 条件2 then
-- 条件2成立时执行(可选)
else
-- 所有条件不成立时执行(可选)
end if;示例:根据用户数量判断用户规模
delimiter //
create procedure sp_user_scale()
begin
declare v_count int default 0;
declare v_scale varchar(20);
select count(*) into v_count from tb_user;
if v_count > 1000 then
set v_scale = '大规模用户';
elseif v_count > 100 then
set v_scale = '中规模用户';
else
set v_scale = '小规模用户';
end if;
select v_count, v_scale;
end //
delimiter ;
call sp_user_scale();2.case条件语句
支持两种语法格式,适合多分支条件判断。
语法格式1:匹配固定值
case case_value
when when_value1 then statement_list1
when when_value2 then statement_list2
else statement_list
end case;语法格式2:匹配条件表达式
case
when search_condition1 then statement_list1
when search_condition2 then statement_list2
else statement_list
end case;示例:根据用户等级返回描述
delimiter //
create procedure sp_user_level_desc(in p_level int, out p_desc varchar(20))
begin
case p_level
when 1 then set p_desc = '普通用户';
when 2 then set p_desc = 'vip用户';
when 3 then set p_desc = 'svip用户';
else set p_desc = '未知等级';
end case;
end //
delimiter ;
-- 调用
call sp_user_level_desc(2, @level_desc);
select @level_desc; -- 结果为vip用户六、存储过程中的循环语句
mysql 存储过程支持 3 种循环:while、repeat、loop,适用于不同场景的批量数据处理。
1.while循环(先判断,后执行)
语法格式
while 条件 do
-- 循环体sql逻辑
end while;示例:批量插入 10 条测试用户数据
delimiter //
create procedure sp_batch_insert_user()
begin
declare i int default 1;
while i <= 10 do
insert into tb_user (name, age) values (concat('test_', i), 18 + i);
set i = i + 1; -- 必须更新计数器,否则会死循环
end while;
end //
delimiter ;
-- 调用执行
call sp_batch_insert_user();2.repeat循环(先执行,后判断)
语法格式
repeat
-- 循环体sql逻辑
until 条件 -- 条件满足时退出循环
end repeat;示例:批量插入 10 条测试用户数据(repeat 实现)
delimiter //
create procedure sp_repeat_insert_user()
begin
declare i int default 1;
repeat
insert into tb_user (name, age) values (concat('repeat_', i), 20 + i);
set i = i + 1;
until i > 10 -- i>10时退出循环
end repeat;
end //
delimiter ;
-- 调用执行
call sp_repeat_insert_user();3.loop循环(无条件循环,需手动退出)
语法格式
[begin_label:] loop
-- 循环体sql逻辑
-- 需用leave手动退出循环,否则会死循环
end loop [end_label];leave label:退出指定标签的循环;iterate label:跳过当前循环,直接进入下一次循环。
示例:loop 循环实现批量插入,含跳过逻辑
delimiter //
create procedure sp_loop_insert_user()
begin
declare i int default 1;
loop_label: loop -- 定义循环标签
if i > 10 then
leave loop_label; -- 条件满足时退出循环
end if;
-- 跳过偶数次插入,只插入奇数
if i % 2 = 0 then
set i = i + 1;
iterate loop_label; -- 跳过当前循环,直接下一次
end if;
insert into tb_user (name, age) values (concat('loop_', i), 25 + i);
set i = i + 1;
end loop loop_label;
end //
delimiter ;
-- 调用执行
call sp_loop_insert_user();七、游标(cursor):遍历结果集
游标用于存储查询结果集,可在存储过程中逐行处理数据,适合批量数据处理场景。
游标使用步骤
- 声明游标:绑定查询语句;
- 打开游标:执行查询,获取结果集;
- 获取数据:逐行读取结果集数据到变量;
- 关闭游标:释放资源。
语法格式
-- 1. 声明游标 declare 游标名称 cursor for 查询语句; -- 2. 打开游标 open 游标名称; -- 3. 获取数据 fetch 游标名称 into 变量1, 变量2...; -- 4. 关闭游标 close 游标名称;
八、条件处理程序(异常处理)
条件处理程序(handler)用于捕获存储过程执行中的异常,并定义处理逻辑,避免程序因异常中断。
语法格式
declare handler_action handler for condition_value [, condition_value] ... statement;
handler_action:continue:捕获异常后继续执行后续代码;exit:捕获异常后终止当前存储过程;
condition_value:异常条件,支持:sqlstate 'xxxx':指定 sql 状态码(如02000表示 not found);sqlwarning:捕获所有以01开头的警告;not found:捕获所有以02开头的未找到数据异常;sqlexception:捕获所有其他 sql 异常。
示例:捕获异常并记录日志
-- 先创建日志表
create table if not exists proc_log (
id int auto_increment primary key,
error_msg varchar(200),
create_time datetime default current_timestamp
);
delimiter //
create procedure sp_exception_demo()
begin
-- 声明异常处理:捕获异常后继续执行,并记录日志
declare continue handler for sqlexception
insert into proc_log (error_msg) values ('执行存储过程时发生异常');
-- 可能出错的sql:插入重复主键数据
insert into tb_user (id, name) values (1, 'test_user');
select '执行完成' as result;
end //
delimiter ;
-- 调用执行(若id=1已存在,会触发异常,但程序会继续执行并记录日志)
call sp_exception_demo();
-- 查看日志
select * from proc_log;九、存储过程完整实战示例:批量处理用户数据
综合使用变量、循环、游标、异常处理,实现一个完整的批量数据处理存储过程:
delimiter //
create procedure sp_user_batch_process(in p_start_id int, in p_end_id int)
begin
-- 1. 声明变量
declare v_id int;
declare v_age int;
declare done int default false;
declare update_count int default 0;
-- 2. 声明游标和异常处理
declare user_cursor cursor for
select id, age from tb_user where id between p_start_id and p_end_id;
declare continue handler for not found set done = true;
declare exit handler for sqlexception
insert into proc_log (error_msg) values (concat('批量处理用户数据异常,范围:', p_start_id, '-', p_end_id));
-- 3. 打开游标,遍历处理
open user_cursor;
user_loop: loop
fetch user_cursor into v_id, v_age;
if done then
leave user_loop;
end if;
-- 业务逻辑:年龄>30的用户,标记为vip
if v_age > 30 then
update tb_user set is_vip = 1 where id = v_id;
set update_count = update_count + 1;
end if;
end loop;
close user_cursor;
-- 4. 返回处理结果
select concat('批量处理完成,共更新', update_count, '条数据') as result;
end //
delimiter ;
-- 调用:处理id 1-100的用户数据
call sp_user_batch_process(1, 100);十、存储过程使用注意事项
- 循环控制:
while/repeat/loop循环中必须有计数器更新或退出条件,否则会导致死循环; - 游标资源:使用完游标后必须
close,否则会占用数据库资源; - 异常处理:批量数据处理时建议添加异常处理,避免单条数据异常导致整个批量任务中断;
- 性能问题:存储过程内避免大事务、长循环,防止锁表或影响数据库性能;
- 调试建议:mysql 存储过程调试困难,复杂逻辑建议分步测试,先验证单条 sql 再封装。
三、存储函数
1. 核心概念
存储函数是有返回值的存储过程,它的参数只能是 in 类型,且必须通过 return 语句返回一个结果。
它可以像普通内置函数一样,直接在 select 语句中调用。
2. 语法格式
create function 存储函数名称([参数列表])
returns type [characteristic ...]
begin
-- sql语句
return ...; -- 必须有return语句
end;关键字说明
returns type:指定函数的返回值类型(如int,varchar,decimal等)。characteristic:特性说明,用于优化和约束函数行为:deterministic:相同输入参数总是产生相同结果(纯函数)。no sql:函数体内不包含任何sql语句。reads sql data:函数体内只包含读数据的语句,不包含写数据的语句。
3. 基础示例
示例1:无参数的存储函数
-- 示例:获取当前系统日期
delimiter //
create function fn_get_current_date()
returns date
no sql
begin
return curdate();
end //
delimiter ;
-- 调用
select fn_get_current_date();示例2:带参数的存储函数
-- 示例:根据用户id查询用户年龄
delimiter //
create function fn_get_user_age(p_user_id int)
returns int
reads sql data
begin
declare v_age int;
select age into v_age from tb_user where id = p_user_id;
return v_age;
end //
delimiter ;
-- 调用
select fn_get_user_age(1);4. 与存储过程的核心区别
对比项 | 存储过程(procedure) | 存储函数(function) |
返回值 | 可以无返回值,也可通过 | 必须有且仅有一个返回值 |
参数类型 | 支持 | 仅支持 |
调用方式 | 使用 | 可直接在 |
适用场景 | 批量数据处理、复杂事务、多步操作 | 计算、查询单个值、可复用的业务规则 |
5. 使用注意事项
- 必须有返回值:存储函数必须包含
return语句,否则会报错。 - 参数限制:函数参数默认是
in类型,不能显式指定out或inout。 - 数据修改限制:为了保证可在
select中安全调用,存储函数内不建议执行insert/update/delete等写操作,否则可能导致数据不一致。 - 权限要求:创建存储函数需要
create routine权限,调用时需要execute权限。
四、触发器
1. 核心概念
触发器是与表关联的数据库对象,它会在 insert/update/delete 操作执行之前或之后,自动触发并执行预定义的sql语句集合。
- 作用:在数据库端确保数据完整性、记录操作日志、数据校验、级联更新等。
- 特点:
- 仅支持行级触发(for each row),每操作一行触发一次;
- 使用
old和new关键字引用数据:
触发器类型 | new(新数据) | old(旧数据) |
| 表示将要/已新增的数据 | 无 |
| 表示将要/已修改后的数据 | 表示修改前的数据 |
| 无 | 表示将要/已删除的数据 |
2. 语法格式
创建触发器
create trigger trigger_name
before/after insert/update/delete
on tbl_name for each row -- 行级触发器
begin
trigger_stmt; -- 触发时执行的sql
end;查看触发器
show triggers;
删除触发器
drop trigger [if exists] [schema_name.]trigger_name;
3. 实战示例:数据变更日志触发器
我们通过触发器实现 tb_user 表的增/改/删操作日志记录,将日志写入 user_logs 表。
步骤1:创建日志表
create table user_logs(
id int(11) not null auto_increment,
operation varchar(20) not null comment '操作类型, insert/update/delete',
operate_time datetime not null comment '操作时间',
operate_id int(11) not null comment '操作的id',
operate_params varchar(500) comment '操作参数',
primary key(`id`)
) engine=innodb default charset=utf8;步骤2:创建insert触发器(记录新增日志)
delimiter //
create trigger tb_user_insert_trigger
after insert on tb_user for each row
begin
insert into user_logs(operation, operate_time, operate_id, operate_params)
values(
'insert',
now(),
new.id,
concat('新增数据:id=', new.id, ', name=', new.name, ', phone=', new.phone)
);
end //
delimiter ;步骤3:创建update触发器(记录修改日志)
delimiter //
create trigger tb_user_update_trigger
after update on tb_user for each row
begin
insert into user_logs(operation, operate_time, operate_id, operate_params)
values(
'update',
now(),
new.id,
concat('修改前:id=', old.id, ', name=', old.name, ' | 修改后:id=', new.id, ', name=', new.name)
);
end //
delimiter ;步骤4:创建delete触发器(记录删除日志)
delimiter //
create trigger tb_user_delete_trigger
after delete on tb_user for each row
begin
insert into user_logs(operation, operate_time, operate_id, operate_params)
values(
'delete',
now(),
old.id,
concat('删除数据:id=', old.id, ', name=', old.name, ', phone=', old.phone)
);
end //
delimiter ;测试触发器
-- 1. 新增用户(触发insert触发器) insert into tb_user(id, name, phone) values(26, '张三', '18809091212'); -- 2. 修改用户(触发update触发器) update tb_user set name = '张三三' where id = 26; -- 3. 删除用户(触发delete触发器) delete from tb_user where id = 26; -- 查看日志 select * from user_logs;
4. 使用注意事项
- 触发时机:
before触发器可以修改new数据,也可以用于数据校验;after触发器不能修改数据,适合做日志记录、级联操作。 old/new关键字:insert触发器中,只有new可用;delete触发器中,只有old可用;update触发器中,old和new都可用。
- 性能影响:触发器是行级触发,批量操作时会逐行触发,可能影响性能;复杂逻辑建议放在应用层。
- 循环触发风险:避免在触发器中对同一张表执行增删改操作,否则可能导致死循环。
5. 常见应用场景
- 数据审计:记录表中数据的所有变更操作(如用户日志、订单日志);
- 数据校验:
before insert/update触发器中校验数据合法性(如年龄不能为负、手机号格式); - 级联更新:主表数据修改/删除时,自动同步更新关联表数据;
- 数据同步:实时将一张表的数据同步到另一张表(如业务表到统计表)。
存储过程、存储函数、触发器尽量不要用,阿里范式不允许
开发中为什么尽量不用存储过程、存储函数、触发器(面试必背 + 通俗易懂整理)
一、统一核心原因总览
- 业务逻辑侵入数据库,把代码写在数据库里,违背前后端/应用层控制业务的设计思想。
- 调试难、维护难、排错极麻烦。
- 可移植性极差,换数据库基本全要重写。
- 占用数据库性能,把计算、逻辑压力丢给db,容易拖垮库。
- 版本管理困难,无法像代码一样git版本控制、回滚。
- 分布式、微服务架构下完全不适用。
二、存储过程 为什么不用
1. 业务逻辑下沉到数据库
业务逻辑本该写在 java/php/go 后端,存储过程把大量逻辑写死在db里,业务分散、逻辑混乱,新人接手看不懂。
2. 调试极其困难
没有断点调试、没有日志跟踪,出问题只能靠猜、靠打印sql,复杂流程排错成本极高。
3. 版本控制难
存储过程存在数据库里,不能git管理,无法做版本迭代、快速回滚,上线、灰度都很麻烦。
4. 可移植性差
mysql、oracle、sql server 存储过程语法完全不一样,一旦换数据库,全部重写。
5. 加重数据库压力
复杂循环、计算、业务判断都在db执行,db本应只做存储和简单查询,不适合承载业务计算逻辑,容易造成cpu、连接数打满。
6. 微服务分布式不兼容
微服务提倡业务在服务层、数据只在db,存储过程无法跨服务调用,也不方便做分布式事务、限流、熔断。
7. 并发与锁风险
存储过程里多sql默认在一个事务,容易长事务、锁等待、死锁,线上隐患大。
三、存储函数 为什么不用
- 不能写复杂业务,功能有限,不如后端函数灵活。
- 无法做复杂逻辑、循环、外部调用。
- 在sql中调用容易被滥用,嵌套在查询里会隐形增加查询开销,优化器难以预估成本。
- 同样难调试、难版本管理、难迁移。
- 禁止在函数里做增删改,容易引发主从延迟、数据不一致。
一句话:能用后端代码实现的计算,绝不写存储函数。
四、触发器 为什么坚决少用/不用
1. 隐式执行,逻辑“隐身”
触发器是自动偷偷执行,开发者写 insert/update/delete 时完全感知不到还有额外逻辑在跑,出了问题根本想不到是触发器导致的。
2. 排错极难
数据莫名其妙变了、莫名多了日志、莫名被修改,排查半天最后发现是触发器偷偷触发,隐蔽性太强,坑很多。
3. 性能损耗大
触发器是行级触发,批量插入1万条,就触发1万次,严重拖慢批量操作性能。
4. 容易触发循环嵌套死循环
a表触发器改b表,b表触发器又改a表,连环触发、死循环、锁表,线上事故高危。
5. 主从同步容易出问题
触发器在主库执行,从库复制可能重复触发,导致数据重复、不一致。
6. 不利于数据迁移和分库分表
分表、分库、数据迁移时,触发器逻辑容易被遗漏,导致数据行为不一致。
五、什么时候勉强可以用(仅老旧项目)
只有以下极少数场景可容忍:
- 老旧单体项目无法改造;
- 纯数据统计、报表固化逻辑;
- 仅做简单日志记录、数据校验,逻辑极简不复杂。
新项目、微服务、分布式项目:一律禁止使用。
六、面试极简背诵版
- 存储过程:业务下沉、难调试、难版本控制、不可移植、压垮数据库、微服务不适用。
- 存储函数:功能受限、隐藏开销、无法复杂业务、难维护。
- 触发器:隐式执行逻辑隐蔽、排错难、批量性能差、易循环触发、主从数据不一致。
- 统一原则:业务逻辑放应用层,数据库只负责存数据、查数据。
五、锁
一、锁的核心概念
锁是计算机协调多个进程/线程并发访问同一资源的机制。
在数据库中,除了cpu、内存、i/o等计算资源的争用,数据本身也是一种多用户共享资源。锁的核心目标是:
- 保证并发访问下的数据一致性、有效性;
- 锁冲突是影响数据库并发性能的关键因素。
二、mysql锁的粒度分类
mysql的锁按粒度从大到小分为三类:
锁类型 | 作用范围 | 特点 |
全局锁 | 锁定整个数据库实例 | 粒度最大,影响范围最广 |
表级锁 | 锁定整张表 | 粒度中等,不区分行,影响整张表 |
行级锁 | 锁定单条数据行 | 粒度最小,仅影响被操作的行 |
三、全局锁(global lock)
1. 介绍
全局锁会对整个数据库实例加锁,加锁后实例进入只读状态,以下操作都会被阻塞:
- dml写语句(
insert/update/delete) - ddl语句(
create/alter/drop table) - 已更新事务的提交语句
2. 典型场景:全库逻辑备份

当你执行 mysqldump 全库备份时,如果不加特殊参数,就会触发全局锁。
- 目的:获取一致性视图,保证备份数据的完整性;
- 问题:备份期间如果有业务写入,会导致备份前后数据不一致(如订单表备份到一半,库存表又被更新)。
3. 核心命令
-- 加全局读锁(只读锁) flush tables with read lock; -- 释放全局锁 unlock tables;
4. 操作演示
# 备份命令(传统方式,会加全局锁) mysqldump -uroot -p1234 itcast > itcast.sql
- 加锁后:
select查询可以正常执行,insert/update/delete会被阻塞; - 解锁后:写入操作恢复执行。
5. 全局锁的问题与优化
问题

- 主库备份:备份期间无法执行更新操作,业务基本停摆;
- 从库备份:备份期间从库无法同步主库的binlog,导致主从延迟。
优化方案:innodb 不加锁备份
在 innodb 引擎中,使用 --single-transaction 参数实现不加锁的一致性备份:
mysqldump --single-transaction -uroot -p123456 itcast > itcast.sql
原理:利用 innodb 的事务隔离级别,在一个事务内完成一致性快照备份,全程不影响业务写入。
四、表级锁
一、表级锁概述
表级锁是mysql中粒度最大的锁,每次操作直接锁定整张表:
- 优点:实现简单,无死锁;
- 缺点:锁冲突概率高,并发度最低;
- 适用引擎:myisam、innodb、bdb等。
表级锁主要分为三类:
- 表锁(显式表共享/排他锁)
- 元数据锁(mdl)
- 意向锁(innodb 自动维护)
二、表锁(显式表级锁)
1. 分类与兼容性
类型 | 加锁方式 | 兼容性说明 |
表共享读锁(read lock) |
| 不阻塞其他客户端的读,但阻塞写 |
表独占写锁(write lock) |
| 既阻塞其他客户端的读,也阻塞写 |
2. 核心语法
-- 1. 加表锁 lock tables 表名 read; -- 加共享读锁 lock tables 表名 write; -- 加独占写锁 -- 2. 释放锁 unlock tables; -- 手动释放 -- 客户端断开连接时,也会自动释放锁
3. 工作机制图解
- 读锁:其他客户端可以执行
select,但insert/update/delete会被阻塞; - 写锁:其他客户端的
select和写操作都会被阻塞,只有持有锁的会话能读写。
三、元数据锁(mdl)
1. 核心概念
元数据锁(meta data lock,mdl)是mysql 5.5+自动维护的锁,无需显式使用,在访问表时自动加锁:
- 作用:维护表元数据的一致性,防止dml与ddl冲突;
- 核心规则:表上有活动事务时,不允许执行元数据写操作(如alter table)。
2. 锁类型与对应sql
对应sql | 锁类型 | 说明 |
|
| 显式表锁 |
|
| 与读/写兼容,与 |
|
| 与读/写兼容,与 |
|
| 与所有其他mdl锁互斥 |
3. 查看元数据锁
select object_type,object_schema,object_name,lock_type,lock_duration from performance_schema.metadata_locks;
四、意向锁(innodb 特有)

1. 核心作用
为了解决行锁与表锁的冲突检查效率问题,innodb引入了意向锁:
- 当事务给某行加锁时,会先在表上加对应的意向锁;
- 后续表锁请求只需检查表级意向锁,无需遍历所有行锁,大幅提升检查效率。
2. 意向锁分类与兼容性

锁类型 | 触发场景 | 兼容性说明 |
意向共享锁(is) |
| 与表共享读锁兼容,与表独占写锁互斥 |
意向排他锁(ix) |
| 与表共享读锁、写锁都互斥;意向锁之间不互斥 |
3. 查看意向锁与行锁
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from performance_schema.data_locks;
五、表级锁总结
- 表锁:手动加锁,读锁共享、写锁独占,并发度低;
- mdl锁:系统自动维护,防止dml与ddl冲突,是线上ddl阻塞的常见原因;
- 意向锁:innodb自动维护,用于优化表锁与行锁的冲突检查,不影响业务并发。
五、行级锁
1.行级锁概述
行级锁是 innodb 引擎独有的锁机制,每次操作仅锁定对应的行数据:
- 优点:粒度最小,锁冲突概率最低,并发度最高;
- 核心实现:行锁是对索引项加锁,而非直接对记录加锁;
- 三大类型:行锁(record lock)、间隙锁(gap lock)、临键锁(next-key lock)。
2.行锁(record lock)
1. 分类与兼容性
行锁分为共享锁(s锁)和排他锁(x锁),兼容性如下:
当前锁类型 | 请求s锁 | 请求x锁 |
s(共享锁) | 兼容 | 冲突 |
x(排他锁) | 冲突 | 冲突 |
2. 触发场景与sql示例
sql语句 | 锁类型 | 说明 |
| 排他锁(x) | 自动加锁 |
| 共享锁(s) | 手动加锁 |
| 排他锁(x) | 手动加锁 |
普通 | 不加锁 | mvcc快照读,无锁 |
3. 示例1:共享锁(s锁)
-- 事务a begin; select * from tb_user where id = 1 lock in share mode; -- 加s锁 -- 事务b begin; select * from tb_user where id = 1 lock in share mode; -- 可以加s锁(兼容) update tb_user set name = 'test' where id = 1; -- 加x锁,被阻塞(冲突)
4. 示例2:排他锁(x锁)
-- 事务a begin; update tb_user set name = 'test' where id = 1; -- 自动加x锁 -- 事务b begin; select * from tb_user where id = 1 for update; -- 加x锁,被阻塞(冲突) select * from tb_user where id = 1 lock in share mode; -- 加s锁,被阻塞(冲突)
5. 关键注意点
- 无索引会升级为表锁:不通过索引条件检索数据时,innodb会对表中所有记录加锁,相当于表锁;
- 唯一索引等值匹配会优化为行锁:对唯一索引的存在记录进行等值查询,仅锁定该记录。
3.间隙锁(gap lock)
1. 核心概念
间隙锁锁定索引记录的间隙(不含记录本身),目的是防止其他事务在该间隙插入数据,从而避免幻读,仅在rr隔离级别下生效。
2. 触发场景与示例
示例1:不存在记录的等值查询(唯一索引)
-- 表中id为10、20、30,无id=15的记录 begin; select * from tb_user where id = 15 for update; -- 触发间隙锁,锁定(10,20)之间的间隙,防止插入id=15的数据
示例2:普通索引等值查询的边界场景
-- 表中age为18、20、22,查询age=25(不存在) begin; select * from tb_user where age = 25 for update; -- 触发间隙锁,锁定(22, +∞)之间的间隙
3. 关键注意点
- 间隙锁的唯一目的是防止其他事务插入间隙数据;
- 间隙锁可以共存,不同事务对同一间隙加间隙锁不会互相阻塞。
4.临键锁(next-key lock)
1. 核心概念
临键锁是行锁 + 间隙锁的组合,同时锁定数据记录及其前面的间隙,是innodb在rr隔离级别下默认的锁机制,用于彻底防止幻读。
2. 触发场景与示例
示例1:范围查询(唯一索引)
-- 表中id为10、20、30、40 begin; select * from tb_user where id > 20 and id < 30 for update; -- 触发临键锁,锁定: -- 行锁:id=30(不满足条件的第一个值) -- 间隙锁:(20,30)和(30,40)之间的间隙
示例2:普通索引的范围查询
-- 表中age为18、20、22、25 begin; select * from tb_user where age >= 20 for update; -- 触发临键锁,锁定: -- 行锁:age=20、22、25 -- 间隙锁:(18,20)、(20,22)、(22,25)、(25, +∞)
3. 锁的优化规则
- 唯一索引等值匹配存在记录:优化为行锁;
- 唯一索引等值匹配不存在记录:优化为间隙锁;
- 普通索引等值查询边界不满足:临键锁退化为间隙锁;
- 范围查询(唯一/普通索引):默认使用临键锁。
5.查看锁信息
通过以下sql可以查看意向锁、行锁、间隙锁的情况:
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from performance_schema.data_locks;
6.行级锁总结
锁类型 | 作用 | 隔离级别 |
行锁(record lock) | 锁定单条记录,防止其他事务修改/删除 | rc/rr |
间隙锁(gap lock) | 锁定索引间隙,防止插入新数据 | rr |
临键锁(next-key lock) | 行锁+间隙锁组合,彻底防止幻读 | rr(默认) |
六、死锁(产生原因+解决办法)
1.死锁概念
死锁:两个或多个事务,互相持有对方需要的锁,又都不释放自己的锁,无限等待,谁也执行不下去。
innodb 会自动检测死锁,主动回滚代价更小的一个事务,让另一个正常执行。
2.死锁产生的四个必要条件(面试必背)
- 互斥条件:锁同一资源不能同时占用;
- 请求保持:事务已持有锁,还去申请新锁;
- 不可剥夺:锁不能被强行抢走,只能自己释放;
- 循环等待:事务间形成循环等待锁的环路。
四个条件同时满足,必然死锁。
3.死锁产生典型场景 + 完整示例
场景1:两个事务加锁顺序相反(最常见)
表 tb_user 有主键 id。
事务a
begin; update tb_user set name='a' where id=1; -- 持有id=1行锁 update tb_user set name='b' where id=2; -- 等待事务b的id=2锁
事务b
begin; update tb_user set name='b' where id=2; -- 持有id=2行锁 update tb_user set name='a' where id=1; -- 等待事务a的id=1锁
形成循环等待 → 直接死锁。
场景2:索引失效,行锁升级为表锁引发死锁
字段无索引,更新变成表级排他锁,互相阻塞产生死锁。
事务a
begin; update tb_user set age=20 where name='张三'; -- name无索引,锁整张表
事务b
begin; update tb_user set age=25 where name='李四'; -- 同样无索引,也锁整张表
互相等待对方表锁,形成死锁。
场景3:rr隔离级别下 间隙锁 + 行锁 互相等待
普通索引范围查询产生临键锁/间隙锁,间隙之间互相占用,引发死锁。
4.如何查看死锁日志
-- 查看最近一次死锁详情 show engine innodb status;
在输出信息中找到 latest detected deadlock,可看到:
- 哪两个事务
- 各自持有什么锁、等待什么锁
- 最终回滚了哪个事务
5.死锁解决与规避方案
1. 统一sql加锁顺序(最有效)
所有业务事务,必须按相同顺序访问表、访问行。
示例:
永远先操作 id=1,再操作 id=2,所有服务都遵守,从根源打破循环等待。
2. 避免事务过大、事务过长
- 事务里不要放无关业务逻辑;
- 尽量小事务、快提交,持有锁时间越短,死锁概率越低。
3. 确保条件字段有索引
更新/删除条件一定要走索引,避免行锁升级为表锁,大幅减少锁范围和冲突。
4. 业务层面加重试机制
捕获死锁异常后,间隔短暂时间自动重试,线上常用方案。
5. 尽量不用范围查询 for update
范围查询容易触发临键锁、间隙锁,锁范围放大,极易诱发死锁;
能用等值查询就不用范围。
6. 调低隔离级别(可选)
把隔离级别从 rr 降到 rc:
- 取消间隙锁、临键锁;
- 大幅减少死锁;
- 代价:可能出现幻读,业务能接受就可以用。
7. 避免同一事务重复加锁、交叉更新
不要在一个事务内多次更新同一张表不同行,减少锁竞争。
6.极简背诵
- 死锁四个条件:互斥、请求保持、不可剥夺、循环等待。
- 最常见原因:事务加锁顺序不一致、无索引升级表锁、间隙锁冲突。
- 解决办法:
- 统一访问顺序;
- 小事务快提交;
- 保证索引有效;
- 业务增加重试;
- 必要时降级为rc隔离级别。
六、innodb引擎
一、逻辑存储结构
1.整体层级关系
innodb 的数据是按表空间(tablespace)→ 段(segment)→ 区(extent)→ 页(page)→ 行(row)的层级组织。
2.各层级详解

3.层级关系与mvcc关联
- 数据组织:b+树索引的叶子节点就是数据段,由多个区组成,每个区包含64个页,每个页存储多行数据;
- mvcc支持:行中的
trx_id和roll_pointer,配合回滚段的 undo log,实现了事务的一致性视图和多版本读取。
4.面试速记
- 层级顺序:表空间 → 段 → 区 → 页 → 行;
- 核心参数:区1mb、页16kb,1个区=64个页;
- 关键隐藏列:
trx_id(事务id)、roll_pointer(旧版本指针),用于mvcc; - 段的分类:数据段(叶子节点)、索引段(非叶子节点)、回滚段(undo log)。
二、架构篇

1.整体架构概览
innodb 架构分为三大核心部分:
- 内存结构(in-memory structures):缓存数据、索引和日志,减少磁盘io
- 磁盘结构(on-disk structures):持久化存储数据、日志和系统信息
- 后台线程(background threads):异步处理脏页刷新、日志同步、资源回收等任务
2.内存结构详解
1. buffer pool(缓冲池)

- 核心作用:主内存中缓存磁盘数据页的区域,是 innodb 性能的核心
- 工作机制:
- 增删改查优先操作缓冲池数据,减少磁盘io
- 缓冲池无数据时,从磁盘加载并缓存
- 脏页按一定频率异步刷新到磁盘
- 页类型:
free page:空闲未使用的页clean page:已使用且数据未修改的页dirty page:已使用且数据已修改,与磁盘数据不一致的页
2. change buffer(更改缓冲区)

- 作用对象:仅针对非唯一二级索引页
- 工作机制:dml操作时,若索引页不在缓冲池中,不直接操作磁盘,而是先存到change buffer;后续数据被读取时,再合并到缓冲池并刷新磁盘
- 核心意义:减少二级索引的随机磁盘io,大幅提升写入性能
3. adaptive hash index(自适应哈希索引)

- 作用:优化缓冲池数据的查询速度
- 机制:innodb自动监控索引页查询,当哈希索引能提升性能时,自动建立哈希索引,无需人工干预
- 控制参数:
adaptive_hash_index(默认开启)
4. log buffer(日志缓冲区)

3.磁盘结构详解
1. 表空间(tablespaces)


类型 | 作用 | 关键文件/参数 |
系统表空间(system tablespace) | 存储change buffer、数据字典、undo log等 |
|
独立表空间(file-per-table) | 每个表单独存储数据和索引,默认开启 |
|
通用表空间(general tablespaces) | 自定义表空间,可指定多个表 | 需用 |
撤销表空间(undo tablespaces) | 存储undo log,支持事务回滚和mvcc | 默认两个16mb的文件 |
临时表空间(temporary tablespaces) | 存储临时表数据 | 全局 |
2. doublewrite buffer files(双写缓冲区)

3. redo log(重做日志)
- 作用:实现事务持久性,崩溃恢复时重放修改
- 机制:事务提交后将修改写入redo log,脏页异步刷盘,刷盘失败时可通过redo log恢复
- 关键文件:
ib_logfile0、ib_logfile1,以循环方式写入
4.后台线程
线程类型 | 核心职责 | 说明 |
master thread | 核心调度,异步刷新脏页、合并change buffer、回收undo页 | innodb主线程 |
io thread | 处理aio请求的回调,包括read/write/log/insert buffer线程 | 默认配置:读4个、写4个、日志1个、插入缓冲1个 |
purge thread | 回收已提交事务的undo log,释放空间 | 提升事务回滚和mvcc性能 |
page cleaner thread | 协助master thread刷新脏页,减轻主线程压力 | 减少主线程阻塞,提升并发性能 |
5.架构关键总结
- 内存优先:所有读写优先操作缓冲池,日志暂存缓冲区,再异步刷盘
- change buffer优化:非唯一二级索引写入性能的关键
- redo log保障:事务持久性的核心,崩溃恢复的基础
- 后台线程分工:脏页刷新、日志同步、资源回收全异步处理,不阻塞用户请求
三、事务原理
1.事务基础概念
事务是一组不可分割的操作集合,作为一个整体向系统提交或撤销请求:
- 要么全部操作同时成功,要么全部同时失败;
- 是数据库保证数据一致性的核心机制。
2.事务四大特性(acid)
特性 | 含义 | 实现机制 |
原子性(atomicity) | 事务是最小操作单元,不可分割,要么全成、要么全败 |
|
一致性(consistency) | 事务执行前后,数据必须保持一致状态(如转账前后总额不变) | 业务规则 + acid共同保障 |
隔离性(isolation) | 事务在不受外部并发操作影响的独立环境中运行 | 锁 + mvcc |
持久性(durability) | 事务提交后,对数据的修改永久生效,不丢失 |
|
3.redo log(重做日志):实现持久性

1. 核心作用
记录事务提交时数据页的物理修改,用于崩溃恢复,保障事务持久性。
2. 工作机制(wal 预写日志)
- wal原则:事务提交前,先写redo log,再异步刷脏页到磁盘;
- 结构分为两部分:
- 内存中:
redo log buffer(日志缓冲区) - 磁盘中:
ib_logfile0/ib_logfile1(重做日志文件,循环写入)
- 内存中:
- 流程:事务提交 → 修改写入redo log buffer → 刷盘到redo log文件 → 脏页后续异步刷入数据文件;若刷盘失败,可通过redo log恢复数据。
4.undo log(回滚日志):实现原子性
1. 核心作用
记录数据修改前的状态,提供事务回滚和mvcc多版本并发控制。
2. 关键特点
- 属于逻辑日志:不是物理页修改,而是反向操作记录(如
delete对应insert,update对应反向update); - 回滚时,可通过undo log中的反向操作恢复数据;
- 事务提交后不会立即删除undo log,因为可能还用于mvcc;
- 存储在回滚段(
rollback segment)中,每个回滚段包含1024个undo log段。
四、mvcc
1.mvcc基础概念
mvcc(multi-version concurrency control,多版本并发控制),是 innodb 实现读写不阻塞的核心机制:
- 维护数据的多个版本,使读写操作互不冲突;
- 快照读(普通select)不加锁,大幅提升并发性能;
- 核心依赖:隐藏字段 + undo log版本链 + readview。
2.两种读模式
1. 当前读
读取记录的最新版本,并对记录加锁,保证其他事务无法修改。
- 触发场景:
select ... lock in share mode(共享锁)select ... for update(排他锁)insert / update / delete(自动加排他锁)
2. 快照读
读取记录的可见版本(可能是历史数据),不加锁,是非阻塞读。
- 触发场景:普通
select(无锁); - 不同隔离级别生成快照时机不同:
read committed:每次select都生成新快照;repeatable read:事务中第一次select生成快照,后续复用;serializable:快照读退化为当前读。
3.mvcc实现三大支柱
1. 记录中的隐藏字段
每个innodb记录都包含三个隐藏字段:
字段名 | 作用 |
| 最近修改该记录的事务id |
| 回滚指针,指向该记录的上一个版本(undo log) |
| 隐藏主键,无主键时自动生成 |
2. undo log版本链

- 每次修改记录时,会生成旧版本数据,存入undo log;
- 通过
db_roll_ptr将多个版本串联成一条版本链,链表头是最新版本,尾部是最早版本; - 事务提交后,undo log不会立即删除(因为快照读可能还需要),只有当没有任何事务引用该版本时,才会被purge线程回收。
3. readview(读视图)
readview是快照读判断版本可见性的依据,记录当前系统中活跃的事务(未提交)id集合,包含四个核心字段:
字段 | 含义 |
| 当前活跃事务id集合 |
| 最小活跃事务id |
| 预分配事务id(当前最大事务id+1) |
| 创建该readview的事务id |
4.版本可见性判断规则
读取记录时,需遍历版本链,直到找到符合以下条件的版本:
trx_id == creator_trx_id:数据由当前事务修改,可见;trx_id < min_trx_id:事务已提交,可见;trx_id > max_trx_id:事务在readview生成后开启,不可见;min_trx_id <= trx_id <= max_trx_id:需判断trx_id是否在m_ids中:- 不在集合中:事务已提交,可见;
- 在集合中:事务未提交,不可见。
5.rc与rr隔离级别下的readview差异
隔离级别 | readview生成时机 | 效果 |
| 每次快照读都生成新的readview | 每次查询都能看到其他事务已提交的修改,解决不可重复读 |
| 事务中第一次快照读生成readview,后续复用 | 整个事务期间复用同一个readview,保证可重复读,同时避免幻读 |
6.mvcc总结
- 核心目的:实现读写不阻塞,提升并发性能;
- 三大支柱:隐藏字段记录事务id、undo log维护版本链、readview判断版本可见性;
- 隔离级别差异:readview生成时机不同,决定了rc和rr的可见性规则差异。
七、mysql管理
一、mysql自带系统数据库
安装mysql后,会自动创建4个系统数据库,作用如下:
数据库 | 核心作用 |
| 存储mysql服务器运行的关键信息:用户账号、权限配置、时区设置、主从复制状态等 |
| 提供访问数据库元数据的接口,包含数据库、表、字段类型、索引、权限等信息 |
| 底层性能监控数据库,收集服务器运行状态参数,用于性能分析与调优 |
| 基于 |
二、mysql常用客户端工具
1.mysql:核心客户端连接工具
用于连接mysql服务器、执行sql语句。
- 基础语法:
mysql [options] [database] - 常用选项:
-u/--user=name:指定登录用户名-p/--password[=name]:指定登录密码(交互输入更安全)-h/--host=name:指定服务器ip或域名-p/--port=port:指定连接端口(默认3306)-e/--execute=name:执行sql语句并直接退出(适合批处理脚本)
- 示例:
# 连接数据库并执行查询后退出 mysql -uroot -p123456 db01 -e "select * from stu;"
2.mysqladmin:服务器管理工具
用于执行服务器配置检查、状态监控、数据库管理等操作。
- 基础语法:
mysqladmin [options] command - 常用功能:创建/删除数据库、修改密码、刷新权限、查看服务器状态、关闭服务器等
- 示例:
# 删除test01数据库 mysqladmin -uroot -p123456 drop 'test01'; # 查看mysql版本信息 mysqladmin -uroot -p123456 version;
3.mysqlbinlog:二进制日志管理工具
用于解析二进制日志文件,查看数据修改记录,支持按时间、位置过滤日志。
- 基础语法:
mysqlbinlog [options] log-files1 log-files2 ... - 常用选项:
-d/--database=name:仅显示指定数据库的操作日志-o/--offset=#:忽略日志开头的前n条命令-r/--result-file=name:将解析后的日志输出到指定文件--start-datetime/--stop-datetime:按时间范围过滤日志--start-position/--stop-position:按日志位置过滤日志
4.mysqlshow:数据库对象查看工具
快速查询数据库、表、字段、索引等元数据信息。
- 基础语法:
mysqlshow [options] [db_name [table_name [col_name]]] - 常用选项:
--count:显示数据库/表的统计信息(表数量、记录数等)-i:显示指定数据库或表的状态信息
- 示例:
# 查询test库中每个表的字段数和行数 mysqlshow -uroot -p2143 test --count;
5.mysqldump:数据库备份与迁移工具
用于备份数据库,生成包含建表语句和数据插入语句的sql文件,支持跨数据库迁移。
基础语法:
# 备份单个数据库 mysqldump [options] db_name [tables] # 备份多个数据库 mysqldump [options] --database/-b db1 [db2 db3...] # 备份所有数据库 mysqldump [options] --all-databases/-a
常用选项:
--add-drop-database:在创建数据库前添加drop database语句--add-drop-table:在创建表前添加drop table语句(默认开启)-n/--no-create-db:不包含数据库创建语句-t/--no-create-info:不包含表创建语句-d/--no-data:仅备份表结构,不包含数据-t/--tab=name:分别生成.sql(表结构)和.txt(数据)文件
6.mysqlimport/source:数据导入工具
mysqlimport:用于导入mysqldump -t导出的文本数据文件。- 语法:
mysqlimport [options] db_name textfile1 [textfile2...] - 示例:
mysqlimport -uroot -p2143 test /tmp/city.txt
- 语法:
source:mysql客户端内的命令,用于导入sql文件。- 语法:
source /root/xxxx.sql
- 语法:
三、工具使用核心场景速记
工具 | 核心场景 |
| 连接数据库、执行sql脚本、批处理查询 |
| 服务器状态监控、权限刷新、数据库管理 |
| 日志解析、数据恢复、主从复制排查 |
| 数据库备份、跨环境数据迁移 |
| 数据批量导入、sql脚本执行 |
到此这篇关于深度解析mysql存储引擎与索引的文章就介绍到这了,更多相关mysql存储引擎与索引内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论