MySQL 高频考点
索引相关
索引的底层数据结构
MySQL 索引对比:
| 特性 | MyISAM | InnoDB |
|---|---|---|
| 索引类型 | 非聚集索引(数据和索引分离) | 主键索引为聚集索引(数据与主键绑定),其他索引为非聚集索引 |
| 数据结构 | B+树 | B+树 |
| 主键索引 | 叶子节点存储数据的物理地址 | 叶子节点直接存储完整数据行 |
| 辅助索引 | 叶子节点存储数据的物理地址 | 叶子节点存储主键值(需二次查询主键索引) |
| 事务支持 | 不支持 | 支持(ACID兼容) |
| 行级锁 | 仅表级锁 | 支持行级锁(默认) |
| 外键约束 | 不支持 | 支持 |
| 全文索引 | 支持(5.6以前) | 5.6前不支持,5.6+支持 |
| 崩溃恢复 | 数据易损坏,恢复困难 | 通过Redo Log和Undo Log保障恢复 |
| 适用场景 | 读密集、无事务、静态数据 | 读写混合、事务、高并发、数据一致性要求高 |
各个索引数据结构选型对比:
| 数据结构 | 优点 | 缺点 | 为何不适合InnoDB |
|---|---|---|---|
| B+Tree | 1. 阶数高,查询路径短,效率高 2. 内部节点不存数据,减少磁盘I/O 3. 叶子节点双向链表,支持高效范围查询 | - | - |
| 二叉树 | 理想情况下(平衡时)查询效率为O(log N) | 1. 顺序插入退化为链表,查询效率O(N) 2. 数据量大时层级深,磁盘I/O次数多 | 层级深导致检索慢;无法保证平衡性,易退化 |
| 红黑树 | 自平衡,避免退化为链表 | 1. 仍为二叉树结构,层级深(数据量大时高度远高于B+Tree) 2. 插入/删除需频繁调整平衡,代价高 | 层级深导致磁盘I/O多;频繁平衡操作影响性能 |
| B-Tree | 多叉树结构,比二叉树层级更低 | 1. 内部节点存储数据,占用空间大,导致单节点键值减少(树变高) 2. 范围查询需中序遍历,效率低 | 数据存储分散增加磁盘I/O;范围查询性能差;插入/删除需频繁平衡节点 |
聚簇索引和非聚簇索引
| 对比维度 | 聚簇索引 | 非聚簇索引(二级索引) |
|---|---|---|
| 存储方式 | 数据和索引存储在一起 | 数据和索引分开存储 |
| 数量限制 | 必须有且仅有一个 | 可以存在多个 |
| 叶子节点 | 存储整行数据(数据页) | 存储对应记录的主键值(InnoDB)或数据指针(如 MyISAM) |
| 查询效率 | 主键查询极快(直接定位数据) | 需要二次查询(回表查询),效率较低 |
| 索引维护 | 数据插入/更新时可能触发页分裂和页合并 | 维护成本相对较低 |
| 应用场景 | 主键查询、范围查询(物理存储有序) | 非主键列的快速查询、覆盖索引 |
聚簇索引的选举规则:
- 存在主键:主键即聚簇索引
- 不存在主键:使用第一个唯一索引(UNIQUE)作为聚簇索引
- 都不存在:InnoDB 会自动生成一个
rowid作为隐藏的聚簇索引
回表查询:
例如 现有表 USER,其中 USER 表的主键为 id,唯一索引 name:
对该表执行语句:SELECT * FROM USER WHERE name = 'OfferDash'
- 上述语句查询时会走二级索引
name - 由于
name是非聚簇索引,叶子节点只保存了name对应的主键id的值 - 因此,需要再通过
id查询全部信息,这个过程就是回表查询
即,通过二级索引找到对应的主键值,再到聚簇索引中查找整行数据,这个过程就是回表。
覆盖索引优化:
查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到 。
例如:现有表 USER,其中属性分别为 id,name,gender,其中主键为 id,对 name 创建了唯一 索引,对于以下 sql:
SELECT * FROM USER WHERE id = 1:走 id 聚簇索引,返回全部的数据,是覆盖索引。SELECT id FROM USER WHERE name = 'OfferDash':走 name 二级索引,返回 id 和 name,是覆盖索引。SELECT id, name FROM USER WHERE name = 'OfferDash':走 name 二级索引,返回 id 和 name,是覆盖索引。SELECT * FROM USER WHERE name = 'OfferDash':走 name 二级索引,返回 id 和 name,但 gender 需要回表查询,不是覆盖索引。
索引失效的场景
| 失效场景 | 失效原因 |
|---|---|
| 违反最左前缀法则 | 1.联合索引必须从最左字段开始查询,跳过中间字段时后续字段失效 2.完全跳过最左字段则整个索引失效 |
| 范围查询后索引失效 | 范围查询(如 >、<、BETWEEN)后的字段索引失效,但范围查询前的字段仍有效。 |
| 索引列运算导致失效 | 对索引列进行运算或使用函数导致索引失效。 |
| 类型转换导致失效 | 字符串字段与数值比较时索引失效,数值字段与字符串比较时不失效。 |
| 模糊查询导致失效 | 以 % 开头的 LIKE 查询失效,尾部模糊查询有效。 |
实战:现有表格 USER,对字段 name 创建索引,对字段 age 创建索引,对 name,age,gender 创建联合索引:
SELECT * FROM USER WHERE name = 'OfferDash':索引不失效SELECT * FROM USER WHERE name = 'OfferDash' AND age = 114:索引不失效SELECT * FROM USER WHERE age = 114:索引失效,跳过了nameSELECT * FROM USER WHERE name = 'OfferDash' AND gender = 'man':索引失效,跳过了ageSELECT * FROM USER WHERE name = 'OfferDash' AND age <= 114 AND gender = 'man':索引部分失效,由于age使用了范围查询,name和age会走索引,但gender没有走索引SELECT * FROM USER WHERE SUBSTRING(name, 1, 1) = 'O':在索引列name上做运算,索引失效SELECT * FROM USER WHERE name LIKE '%Dash':头部模糊查询,索引失效SELECT * FROM USER WHERE name LIKE 'Offer%':尾部模糊查询,索引不失效
事务相关
事务的特性
事务是数据库中的一个逻辑单元,包含多个操作,这些操作要么全部成功,要么全部失败。其核心特性是 ACID:
- 原子性(Atomicity):事务中的操作要么全部完成,要么全部回滚,不可分割。
- 一致性(Consistency):事务执行前后,数据库必须保持合法状态(如约束、触发器等)。
- 隔离性(Isolation):并发事务之间互不干扰,每个事务的操作对其他未提交的事务不可见。
- 持久性(Durability):事务提交后,修改永久保存,即使系统故障也不丢失。
事务的隔离级别
并发事务带来的问题:
| 问题 | 描述 | 示例 |
|---|---|---|
| 脏读 | 读到其他事务未提交的数据。 | 事务 A 读取事务 B 未提交的数据,事务 B 回滚导致事务 A 读到无效数据。 |
| 不可重复读 | 同一事务多次读取同一数据,结果不一致(因其他事务修改数据并提交)。 | 事务 A 第一次读数据为 100,事务 B 修改为 200 并提交,事务 A 再读为 200。 |
| 幻读 | 同一事务多次查询结果集不一致(因其他事务插入或删除数据并提交)。 | 事务 A 查询无记录,事务 B 插入一条记录并提交,事务 A 再查询发现新记录。 |
由此引出事务的隔离级别:
| 隔离级别 | 解决的问题 | 存在的问题 | 备注 |
|---|---|---|---|
| 读未提交 | 无 | 脏读、不可重复读、幻读 | 最低隔离级别,性能高但数据一致性差。 |
| 读已提交 | 解决脏读 | 不可重复读、幻读 | Oracle 默认级别 |
| 可重复读 | 解决脏读、不可重复读 | 幻读(理论) | MySQL 默认级别,通过 MVCC 和锁机制避免幻读 |
| 串行化 | 解决脏读、不可重复读、幻读 | 无 | 最高隔离级别,事务串行执行,性能最低。 |
可重复读是否解决了幻读
解决了部分情况下的幻读问题,没有完全解决:
- 事务 A 开启,事务 B 插入数据并提交
- 事务 A 读记录不会发生幻读
- 事务 A 插入,事务 B 插入的数据会发生主键冲突,产生了幻读问题
可重复读解决幻读问题有两种方式:
- 针对快照读(普通select语句):
- 通过MVCC方式解决了幻读
- 因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的
- 即使中途有其他事务插入了一条数据,是查询不出来这条数据的
- 针对当前读(select…for update等语句):
- 通过next-key lock(记录锁+间隙锁)方式解决了幻读
- 因为当执行select for update语句的时候,会加上next-key lock
- 如果有其他事务在next-key lock锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入
SQL 优化
定位慢查询
| 定位方式 | MySQL 自带慢查询日志 | Arthas |
|---|---|---|
| 数据来源 | MySQL Server 记录的慢查询日志 | JVM 运行时方法调用跟踪,可定位到代码层 |
| 使用方式 | 需修改MySQL配置(如 slow_query_log) | 无需修改应用代码,动态attach到JVM |
| 性能开销 | 开启慢查询日志对MySQL有一定I/O开销 | 轻量级,对应用性能影响较小 |
| 监控方式 | mysqldumpslow 或 pt-query-digest 分析日志 | 支持实时动态监控方法调用,定位到具体方法 |
MySQL慢查询日志:
- 配置文件:/etc/my.cnf
- 查询是否生效:
SHOW VARIABLES LIKE '%slow_query%',SHOW VARIABLES LIKE '%long_query_time%' - 开启慢日志:slow_query_log = 1 或
SET GLOBAL slow_query_log = 'ON' - 设置超时记录时间:long_query_time = x 或
SET GLOBAL long_query_time = x,x 为秒,超过 x 秒记录该 sql - 日志保存位置:/var/lib/mysql/localhost-slow.log 或
SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log'
Arthas监控
- 监控方法调用耗时:
trace com.example.mapper.UserMapper selectById - 监控JDBC查询:
trace *JDBC4Connection* query - 监控Druid连接池:
watch com.alibaba.druid.pool.DruidPooledConnection * '{params, throwExp}' -x 3
分析慢查询
直接在 SELECT 语句之前加上关键字 EXPLAIN / DESC:
sql
EXPLAIN SELECT 字段列表 FROM 表名 WHERE 条件 ;| 字段/概念 | 优化建议 |
|---|---|
| id | 查询执行顺序标识。相同id按顺序执行,不同id从大到小执行(子查询可能导致不同id) |
| select_type |
|
| type(性能排序) |
|
| possible_keys | 可能使用的索引(检查是否包含预期索引,若无则需创建) |
| key | 实际使用的索引(若为NULL说明未走索引,需优化) |
| key_len | 索引使用的字节数(相同查询条件下,值越小效率通常越高) |
| Extra |
|
常见索引优化:
- 索引是否命中:
possible_keys不为空但key为NULL,需强制索引或优化SQL - 索引是否失效:
type为all或index说明索引失效 - 是否使用覆盖索引:
Extra出现Using index说明走了二级索引 - 索引长度优化:对比
key_len与字段实际长度,过大会影响内存使用