MySQL面试
1 基础
MySQL执行一条Select语句期间发生了什么?
通过连接器建立连接,管理链接、校验用户身份。
查询缓存:命中直接返回。
解析SQL,解析器对SQL查询语句进行词法语法分析。
执行SQL:
预处理:检查表或者字段是否存在;
优化:选择查询成本最小的执行计划;
执行:根据查询计划执行SQL语句,从存储引擎读取数据,返回给客户端。
MySQL 的 NULL 值是怎么存放的?
MySQL 的 Compact 行格式中会用「NULL值列表」来标记值为 NULL 的列,NULL 值并不会存储在行格式中的真实数据部分。
NULL值列表会占用 1 字节空间,当表中所有字段都定义成 NOT NULL,行格式中就不会有 NULL值列表,这样可节省 1 字节的空间。
MySQL 怎么知道 varchar(n) 实际占用数据的大小?
MySQL 的 Compact 行格式中会用「变长字段长度列表」存储变长字段实际占用的数据大小。
varchar(n) 中 n 最大取值为多少?
一行记录最大能存储 65535 字节的数据,但是这个是包含「变长字段字节数列表所占用的字节数」和「NULL值列表所占用的字节数」。所以, 我们在算 varchar(n) 中 n 最大值时,需要减去这两个列表所占用的字节数。
行溢出后,MySQL 是怎么处理的?
如果一个数据页存不了一条记录,InnoDB 存储引擎会自动将溢出的数据存放到「溢出页」中。
Compact 行格式针对行溢出的处理是这样的:当发生行溢出时,在记录的真实数据处只会保存该列的一部分数据,而把剩余的数据放在「溢出页」中,然后真实数据处用 20 字节存储指向溢出页的地址,从而可以找到剩余数据所在的页。
Compressed 和 Dynamic 这两种格式采用完全的行溢出方式,记录的真实数据处不会存储该列的一部分数据,只存储 20 个字节的指针来指向溢出页。而实际的数据都存储在溢出页中。
2 索引
索引的分类
按数据结构分类:B+tree索引、Hash索引、Full-text索引
按物理存储分类:聚簇索引、非聚簇索引
按字段特性分类:主键索引、唯一索引、普通索引、前缀索引
按字段个数:单列索引、联合索引
为什么选择B+Tree作为索引的数据结构?
相比于二叉树,二叉树在相同数据量下层数更高,并且极端情况会退化成链表。
相比于Hash,hash表不适合做范围查询。
相比于Btree,B+Tree旨在叶子节点存放数据,并且用链表连接叶子节点,更适合做范围查询。
什么时候需要索引,什么时候不需要?
需要:字段有唯一性限制,常用于where查询,常用于group by和order by的字段。
不需要:不用于where、group by、order by,字段中重复数据高,表数据太少,经常更新的字段。
优化索引的方法有哪些?
前缀索引优化、覆盖索引优化、索引设置为Not Null、防止索引失效、主键最好是自增的
如何防止索引失效(索引失效有哪些情况)?
LIKE开头或者开头结尾使用模糊匹配
查询条件中对索引列进行了计算、函数、类型转换等操作
联合索引没有遵循最左匹配原则
where中使用or,并且or的条件不全是索引
什么是自适应哈希?
InnoDB存储引擎注意到某些索引值被频繁使用时,会在内存中基于B-Tree索引之上创建一个哈希索引,以加速查找。
3 事务
事务有什么特性?
原子性、一致性、隔离性、持久性
并行事务会出现什么问题?
脏读、不可重复读、幻读
事务隔离级别?
读未提交、读已提交、可重复读、串行化
ReadView在MVCC中如何工作?
ReadView有四个字段:
creator_trx_id: 创建readview的事务id
m_ids: 创建readview时,活跃的事务id集合
min_trx_id: 活跃事务中id最小的事务
max_trx_id: 活跃事务中id最大的事务+1
聚簇索引两个跟事务相关的隐藏列:
trx_id:最新对该列修改的事务id
roll_pouinter:指向上一个版本的记录
可见性:
trx_id < min_trx_id:对当前事务可见
trx_id < max_trx_id:对当前事务不可见
min_trx_id < trx_id < max_trx_id
trx_id [in] m_ids:对当前事务不可见
trx_id [not in] m_ids:对当前事务可见
可重复读是如何工作的?
先后启动的两个事务,根据自己的ReadView,沿着版本链找到自己应该读取的版本
读提交是如何工作的?
每次读取时,重新创建ReadView。
InnoDB如何解决幻读问题的?
快照读:通过MVCC解决了幻读
当前读:通过next-key lock解决了幻读,此时其他事务的更新、插入、删除会被阻塞
4 锁
锁的分类
按照加锁范围,可以分为:全局锁、表级锁、行级锁三种
表级锁中有:表锁、元数据锁、意向锁、自增锁
行级锁中有:记录锁、间隙锁、临键锁
什么SQL语句会加锁?
select … lock in share mode
select … for update
update …
delete …
update没加索引会锁全表?
Innodb 源码里面在扫描记录的时候,都是针对索引项这个单位去加锁的, update 不带索引就是全表扫扫描,也就是表里的索引项都加锁,相当于锁了整张表,所以大家误以为加了表锁。
产生死锁怎么办?
为什么会产生:InnoDB为了解决可重复读级别下的欢度问题,引入了next-key lock。
当两个事务同时要插入新数据时,都会获取到next-key lock(间隙锁之间相互兼容),后续两个事务都尝试获取插入意向锁,但是插入意向锁和next-lock不兼容,所以阻塞死锁。
间隙锁兼容情况:一个间隙锁包含的范围是另一个的子集,两个间隙锁的范围相同且没有锁住真实记录(如果是真实的记录则会阻塞)。
插入意向锁:是一种特殊的间隙锁,但是这个锁只用于并发插入,插入意向锁锁住的是一个点。
发生后打破死锁:设置事务等待锁的超时时间(超时回滚并释放锁);开启主动检测死锁(检测到自锁,主动回滚事务)
排查:
正在运行的任务:show full processlist; 找到卡主的进程
解开死锁:UNLOCK TABLES ;
查看当前运行的事务:SELECT * FROM information_schema.INNODB_TRX;
当前出现的锁:SELECT * FROM information_schema.INNODB_LOCKS;
观察错误日志
查看InnoDB锁状态:
show status like "innodb_row_lock%";
kill id 杀死进程
解决:
死锁无法避免,上线前要进行严格的压力测试
快速失败:innodb_lock_wait_timeout 行锁超时时间
拆分sql,严禁大事务
充分利用索引,优化索引,尽量把有风险的事务sql使用上覆盖索,优化where条件前缀匹配,提升查询速度,引减少表锁
无法避免时:
操作多张表时,尽量以相同的顺序来访问避免形成等待环路
单张表时先排序再操作
使用排它锁 比如 for update
5 日志
为什么需要undo log?
MySQL会隐式开启事务来执行增删改语句,为了在执行过程中异常崩溃后进行回滚,在执行事务时,需要把回滚时需要的信息都记录到undo log中。undo log保证了事务的原子性。
在事务没有提交前,MySQL会先记录更新前的数据到undo log中。
插入:undo log记录主键,回滚时删除;
删除:undo log记录全部内容,回滚时插入;
更新:undo log记录更新列的旧值,回滚时更新回旧值。
一条记录每一次更新操作产生的undo log都有一个roll_pointer和trx_id,roll_pointer用于指向前一个undo log,串成版本链,trx_id用于记录是哪一个事务进行的修改。从而与ReadView实现MVCC。
undo log如何持久化的?
undo log和数据页的刷盘策略相同,都需要通过redo log保证持久化。
Buffer pool中有undo页,对undo页的修改会记录到redo log中,redo log会每秒刷盘,提交事务也会刷盘,数据页和undo页都是靠这个机制保证持久化的。
为什么需要redo log?
由于Buffer pool是基于内存的,为了防止断电导致数据丢失的问题,当有一条记录需要更新时,InnoDB会先更新内存,然后将针对这个数据页的修改以redo log的形式记录下来,这时更新才算完成。后续InnoDB在适当的时候,通过后台线程将Buffer Pool中的脏页刷新到磁盘,这就是WAL(Write-Ahead Logging)技术,即写操作先写日志,后刷盘。
redo log是物理日志,记录的是某个数据也做了什么修改,事务提交时,先讲redo log持久化到磁盘即可,不需要等到将缓存在Buffer Pool里的脏页数据持久化到磁盘。系统崩溃时,由于redolog已经持久化,可以根据redolog将数据恢复。
redolog保证了持久性,让MySQL有crash-safe(崩溃恢复)的能力。
同时WAL技术将写操作从随机写变成了顺序写,进一步提升性能。
被修改的undo页,需要记录对应的redo log吗?
开启事务后,InnoDB更新前,先记录响应的undo log到Undo页,内存修改该Undo页后,会记录对应redolog。
redo log 和 undo log 区别在哪?
redo log 记录此次事务完成后的数据状态,记录的是更新之后的值,事务提交后崩溃,根据redolog恢复。
undo log记录此次事务开始前的数据状态,记录的是更新之前的值,事务提交前崩溃,根据undolog恢复。
redolog是直接写入磁盘的吗?
不是,redo log有自己的redo log buffer。redo log buffer 默认大小 16 MB,可以通过
innodb_log_Buffer_size
参数动态的调整大小,增大它的大小可以让 MySQL 处理「大事务」是不必写入磁盘,进而提升写 IO 性能。
redo log在以下几个时机会刷盘:
MySQL正常关闭。
redo log buffer写入量大于一半。
InnoDB每隔1秒进行一次刷盘。
每次事务提交都会进行刷盘(可以通过参数改变策略)
redo log文件写满了怎么办?
循环写策略,当写入太多导致当前写的位置追上了要擦除的位置时,MySQL将不能进行更新操作,即会被阻塞,此时MySQL会将Buffer Pool中的脏页刷新到磁盘,并标记可擦除位置,对redolog进行擦除。
为什么需要bin log?
bin log是MySQL Server层实现的日志,有STATEMENT、ROW、MIXED三种格式,并且是追加写的方式,写满一个文件会新建一个文件继续写,不会覆盖日志。
bin log主要用于备份恢复、主从复制,而 redo log主要用于掉电回复等故障。
如果不小心整个数据库的数据都被删了,只能用binlog恢复,因为binlog是全量日志。
主从复制是怎么实现的(过程)?
主库接受到事务提交请求后,写入binlog,然后提交事务,更新存储引擎中的数据,事务提交完成后,返回客户端成功响应。
从库创建专门的IO线程,连接主库的log dump线程,接受来自主库的binlog,然后把binlog写入relay log中继日志中,在返回给主库复制成功的响应。
从库创建一个用于回放binlog的线程,读取relaylog中的日志,回放更新存储引擎中的数据,实现主从的数据一致性。
主从复制有哪些模型?
同步复制:所有从库复制成功,才返回客户端响应。
异步复制(默认):主库事务提交成功后,返回响应,主库宕机数据会发生丢失。
半同步复制:只要数据成功复制到任何一个从库上,就返回响应。
bin log什么时候刷盘?
每次提交事务,仅写入内核缓冲区,后续交给操作系统决定何时刷盘。
每次提交事务,写入内核缓冲区后,立即刷盘。
每次提交事务,都写入内核缓冲区,累计够N个事务后刷盘。
事务没提交时,redo log会被持久化到磁盘吗?
会的。事务执行中间过程的 redo log 也是直接写在 redo log buffer 中的,这些缓存在 redo log buffer 里的 redo log 也会被「后台线程」每隔一秒一起持久化到磁盘。
也就是说,事务没提交的时候,redo log 也是可能被持久化到磁盘的。
两阶段提交有什么问题?
磁盘 I/O 次数高:对于“双1”配置,每个事务提交都会进行两次 fsync(刷盘),一次是 redo log 刷盘,另一次是 binlog 刷盘。
锁竞争激烈:两阶段提交虽然能够保证「单事务」两个日志的内容一致,但在「多事务」的情况下,却不能保证两者的提交顺序一致,因此,在两阶段提交的流程基础上,还需要加一个锁来保证提交的原子性,从而保证多事务的情况下,两个日志的提交顺序一致。
执行一条 update 语句,期间发生了什么?
客户端通过连接器建立连接
(update不查询缓存)清空缓存
解析器进行语法分析,判断是否符合语法
预处理器判断表和字段是否存在
优化器确定执行计划
执行器负责执行
调用存储引擎结构查询索引(如果有用到,否则查全表)
得到聚簇索引记录,查看更新前和更新后是否一样:一样则不执行后续流程
开启事务,记录相应的undo log,写入到Buffer Pool中的Undo页中,同时记录Undo log的redo log。
InnoDB开始更新记录,先更新内存,并标记为脏页,然后将记录写入到redo log,随后由后台线程刷盘。(WAL技术)
更新完成,开始记录对应的bin log,并将对应的bin log保存到binlog cache中,在事务提交时同一将该事务过程中所有的binlog刷盘。
事务提交:
prepare阶段:将redo log对应的事务状态设置为prepare,然后将redo log刷盘。
commit阶段:将binlog刷盘,并将redo log状态设置为commit。
结束。
6 内存
为什么需要Buffer pool?
为了提高读写性能,InnoDB存储引擎设计了一个缓冲池。
读取时,命中直接返回。
修改时,命中直接修改buffer pool中的页,然后标记为脏页,后续由一个后台线程写入到磁盘。
Buffer中存储数据页、索引页、插入缓存页、undo页、自适应哈希索引、锁信息。
其中undo页记录生成的undo log。
查询一条数据时,会将整个数据页放入缓存,然后通过页目录查询。
7 开发
TIMESTAMP和DATATIME的区别
TIMESTAMP插入数据库会转为UTC,查询时又转为当前时区。
DATATIME不做任何改变。
DATATIME所能存储的时间范围更大。
预编译SQL
将SQL语句中的值用占位符替代,将SQL模板化或者参数化,可以做到一次编译,多次运行,可以防止SQL注入。
子查询和Join
子查询更加灵活,但是会创建临时表,导致效率比Join低,Join最多关联61张表。
MySQL调优工具
EXPLAIN
MySQLdumpslow
show profiles 时间
optimizer_trace
何时进行分库分表?
能不分就不分
单机性能下降明显时:先尝试缓存,再尝试读写分离,最后才是分库分表
count(col) 和count(*)的区别
count(col)不会统计NULL
count(*)会统计NULL