
MySQL学习笔记
MySQL的执行原理
1. MySQL的基本架构
- MySQL 可以分为 Server 层和存储引擎层两部分
- 存储引擎层负责数据的存储和提取
- server层:所有跨存储引擎的功能都在此实现,比如存储过程、触发器、视图、内置函数等。
注:查询缓存板块弊大于利,因为许多是无效缓存。8.0之后已经删除
2. SQL请求流程
- 连接器:拦截器与客户端建立连接,权限验证(默认为长连接,可以接受多个请求)
- 分析器:对SQL语句词法分析,语法分析,检查SQL语句执行权限
- 优化器:优化SQL语句,分析索引使用
- 执行器:打开目标表,从存储引擎获取所有数据后响应给客户端
日志系统
- 默认引擎InnoDB有专属日志:redo log(重做日志),具备crash-safe 能力
- server层也有自己的日志:binlog(归档日志)。
- 当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log,并更新内存,这个时候更新就算完成了。之后空闲再更新数据到磁盘
- 日志进行两阶段提交
(WAL,Write-Ahead Logging):它的关键点就是先写日志,再写磁盘。
只有redolog执行完接着binlog完整写入,然后redolog再进行事务提交才算事务生效
事务
1. 事务支持是在引擎层面实现的。MySQL 是一个支持多引擎的系统,但并不是所有的引擎都支持事务。比如 MySQL 原生的 MyISAM 引擎就不支持事务,这也是 MyISAM 被 InnoDB 取代的重要原因之一。
2. ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性)
- 原子性:事务要么都成功,要么都失败(回滚到事务前状态)
- 原理:undo log,事务对DB的修改,都会生成相应的undo log,回滚则执行undo log
- 一致性:数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。
- 隔离性:事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
- 持久性:事务一旦提交,它对数据库的改变就应该是永久性的
- 原理:redo log ,具有crash-safe能力,保证提交的事务的操作一定可以正确更新到DB
3. 多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题
- 脏读:当前事务(A)中可以读到其他事务(B)未提交的数据(脏数据)
- 不可重复读:事务A中先后两次读取同一个数据,两次读取的结果不一样(因为被他人事务更改了)
- 幻读:幻读是在RR隔离级别下,且为当前读的时候才会出现,即当前读下出现“新插入的行”(因为insert的原因,行锁只能限制原有的行,insert的新行会破坏原有数据一致性问题:如进行修改操作时新插入的行却不受阻塞)
- 解决方法:引入间隙锁 (Gap Lock),间隙锁只与“往这个间隙中插入一个记录”这个操作存在冲突 (当然,如果把隔离等级改为RC,就不存在幻读问题了)
- 然而间隙锁会带来新死锁的可能:
A,B2个在同一个间隙开了间隙锁,间隙锁互相阻塞导致死锁(只是2个同样的语句并发执行就有死锁可能)
4. 事务隔离级别:
- 读未提交(read uncommitted)是指,一个事务还没提交时,它做的变更就能被别的事务看到。
- 读提交(read committed,RC)是指,一个事务提交之后,它做的变更才会被其他事务看到。
- 可重复读(repeatable read,RR)(默认隔离级别)是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的(开启事务时建了一致性视图(不是一般的view视图))。
- 串行化/序列化(serializable ),顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
5. innodb支持RC和RR隔离级别实现是用的一致性视图(consistent read view)
- InnoDB每个事务都有一个事务ID,叫做transaction id(严格递增)。每行数据的每个版本都会记录生成他的事务id(记为 row trx_id),作为隐藏列存在于聚簇索引中(InnoDB 每行有隐藏列 TransactionID(事务ID) 和 Roll Pointer(回滚指针,用于事务回滚)
- 一致性视图就是从当前版本回退到创建视图时的版本,实现一致性读。只有在一致性视图创建前的已提交事务的数据是可见的
- 而更新数据有些不同,更新都是先读后写的,而这个读,只能读当前最新版本的值,称为“当前读”(current read)。
事务内部产生新的数据版本是可见的,因此更新后的查询基于当前读更新后的新版本 - 在RR隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图。
在RC隔离级别下,每一个语句执行前都会重新算出一个新的视图。保证内部执行语句前获取所有已执行事务的数据
索引
1. 索引是在存储引擎层实现的,InnoDB 使用了 B+ 树索引模型
- 只有叶子结点存数据,叶子结点使用链指针连成链表。能够很好地配合磁盘的读写特性,减少单次查询的磁盘访问次数。(N叉树,减少树高)
- 树的每个结点存储数据是页,内部结点存取的是索引字段,所以索引字段越小,则一个内部结点可以有越多儿子,加快查询速度
- innodb的B+树分裂策略
- 每个索引页面维护了一个上次插入的位置,以及上次的插入是递增/递减的标识
- 对于新插入的数据,如果不满足递增/递减的约束,采用传统的50%分裂策略,把50%的数据移入新的一页
- 否则,采用优化策略,原本满的页数据保留。对于新插入的递增/递减数据,如果相邻页可以存放数据,并入,否则,将其单独放入新页(减少了空间利用率低的问题)
- 并入策略是为了避免下图这种BUG使得空间利用率更低
- 每个索引页面维护了一个上次插入的位置,以及上次的插入是递增/递减的标识
2. 索引类型分为主键索引和非主键索引。
- 主键索引的叶子节点存的是整行数据。主键索引也被称为聚簇索引(clustered index,索引结构和全部数据一起存放)。
主键查询只需搜索主键索引树 - 非主键索引的叶子节点内容是主键的值。非主键索引也被称为二级索引(secondary index)。
二级索引会把不包含的(联合)主键字段加入索引末尾来排序。
普通索引查询则需要先搜索其索引树,得到主键值再搜索主键索引树,这个过程称为回表。 - 主键索引树的叶子节点是数据,而普通索引树的叶子节点是主键值。所以,普通索引树比主键索引树小很多(一个叶子结点页可以存取更多数据)。
3. 覆盖索引:普通索引查询的主键字段+索引字段已经覆盖查询需求,则无需回表
- 索引覆盖也会回表的情况(不是完全避免回表)
- 使用了覆盖索引但超过了最大索引长度,或是字符串只取了前缀作为索引
- 因为事务需要获取版本号
- 只有聚簇索引有事务版本号
- 二级索引的每一页都有PAGE_MAX_TRX_ID(页内数据最后一次被修改的事务id最大值)。
- 如果当前页的PAGE_MAX_TRX_ID在Read View(事务快照)之前则无需回表,否则二级索引必须回表找到聚簇索引才能找到read view之前的版本号
4. 最左前缀原则
联合索引的排序是自左向右按字段排序。查询时无需满足索引的全部定义,只要满足最左前缀,就可以利用索引来加速检索。
这个最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。
- 通过调整字段顺序可以优化多个索引占用的空间和索引数量,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。
- 对于既有联合查询,又有各自字段的查询,需要维护(a,b),(b)2种索引以满足要求,此时a为大字段,b为小字段即可节省空间
5. 索引下推优化(index condition pushdown)
可以在索引遍历过程中,对索引中包含的所有字段先做判断,直接过滤掉不满足查询条件的记录,减少回表次数。
6. 索引选择异常和处理
- 数据发生大量变化时,导致优化器的统计信息可能不准确,从而选错索引。使用ANALYZE TABLE分析表,以保持统计信息的准确性,优化查询性能。
即使信息准确,优化器仍然可能判断出错,解决思路:- 使用force index强制使用目标索引,但是会导致后期维护不便
- 通过sql语句引导
- 增加更合适索引供其选择,或删除索引
- 如果where对字段值进行了函数等修改(或者是隐式修改,如字段类型不同进行隐式类型转换),优化器将会放弃使用字段索引(应保证等式左值纯粹为字段值)
甚至等式两边的表的字符集不同,则也会进行隐式字符集转换导致无法使用索引
7. 字符串索引创建思路
- 直接创建完整索引,这样可能比较占用空间;(若业务量数据实际不多,这种方法反而好,省去风险)
- 创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引;(查询的数据需要回表确认字符串匹配)
- 倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题,但不支持范围扫描。
- 创建 hash 字段索引,查询性能稳定,有额外的存储和计算消耗,但不支持范围扫描。
8. 索引重建的重要性
- 索引可能因为删除,或者页分裂等原因,导致数据页有空洞,浪费内存,降低查询效率,重建索引的过程会创建一个新的索引,把数据按顺序插入,这样页面的利用率最高,也就是索引更紧凑、更省空间。
- MySQL支持Online DDL操作,可以在不锁定表的情况下重建索引。ALTER TABLE table_name ENGINE=InnoDB;
MySQL锁
1. 根据加锁的范围,MySQL 里面的锁大致可以分成全局锁、表级锁和行锁三类。
- 全局锁的典型使用场景是,做全库逻辑备份。
- 表级别的锁有两种:一种是表锁(lock tables ),一种是元数据锁(meta data lock,MDL)。
- 表锁一般是在数据库引擎不支持行锁的时候才会被用到的。
- 一般的表锁,读锁(共享锁,S,LOCK TABLE t READ),写锁(排他锁,X,LOCK TABLE t WRITE)
- 特殊表锁:意向锁,意向锁只与表锁冲突,不与行锁及意向锁冲突,在添加行锁时innodb会自动添加相应的意向锁从而保证行锁与表锁兼容
意向共享锁(intention shared lock, IS),意向排他锁(intention exclusive lock, IX)
- MDL的作用是保证读写的正确性。
- 当对一个表做增删改查操作(DML,Data Manipulation Language)的时候,加 MDL 读锁;
- 当要对表做结构变更操作(DDL,Data Definition Language)的时候,加 MDL 写锁。
- MDL锁是系统默认会加的,在语句执行开始时申请,但直到事务提交才释放。
- 表锁一般是在数据库引擎不支持行锁的时候才会被用到的。
- 行锁(两阶段锁):行锁是在需要的时候自动加上的,但要等到事务结束时才释放。这个就是两阶段锁协议。
- 手动加锁方式:共享锁(S锁,lock in share mode),排他锁(X锁,for update)
事务加了锁后不再是普通的快照读(一致性读),变成了当前读 - 两阶段锁的特性所以要把最可能造成锁冲突、最可能影响并发度的锁的申请时机尽量往后放,减少锁住的时间。(如更新语句应优先放于插入后,更新涉及行锁的抢夺)
- 手动加锁方式:共享锁(S锁,lock in share mode),排他锁(X锁,for update)
2.Innodb RR隔离等级下行锁加锁原理
- 行锁加锁的基本单位是next-key lock(临键锁,间隙锁和行锁(记录锁)合称)。
- 每个 next-key lock 是前开后闭区间。间隙锁是开区间,而记录锁锁定的是明确的行。
- 锁是加在索引上的,不同索引有各自的锁,如果条件符合覆盖索引且为读锁,则只有覆盖索引会被加锁,主键索引的使用不受锁影响。
- 加next-key锁实际是间隙锁,行锁这样一个一个加,直到完成或某个具体的锁被阻塞为止,即存在next-key的间隙锁加上,而行锁阻塞停止的情况,如下图
B间隙锁锁上,但是行锁被A阻塞,A插入被B间隙锁阻塞,造成死锁,回滚B
- 只有被遍历的对象才能被加锁,且只要被遍历就一定会加锁
- 索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。 - 如果查询不到的符合条件的索引,只会对包括条件的必要区间加锁
- 如果没有符合条件的索引,则会进行全表遍历,遍历的行/间隙全部加锁(一般最后整个表都被上锁了,除非limit救场拿完足够数目停止扫描)
- 索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
- 非索引字段进行update或delete等操作(加锁,而且还是写锁),代价极高。所有记录上锁,以及所有间隔的锁。
- RC一般只有记录锁,且语句执行过程中加上的行锁,在语句执行完成后,就要把“不满足条件的行”上的行锁直接释放了,不需要等到事务提交。
也就是说,读提交隔离级别下,锁的范围更小,锁的时间更短,这也是不少业务都默认使用读提交隔离级别的原因。
数据库缓冲池
1. change buffer(插入缓冲)
- 更新数据页时,若数据页在内存中就直接更新,否则在不影响数据一致性的前提下,InnoDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。
change buffer 是可以持久化的数据,在内存中有拷贝,也会被写入到磁盘上。 - 只有普通索引可以使用chang buffer,唯一索引的更新操作都要先判断这个操作是否违反唯一性约束,这必须要将数据页读入内存才能判断。
在数据保证了唯一性下或者无要求唯一性时,优先使用普通索引提升性能
在一些“归档库”的场景,归档数据已经是确保没有唯一键冲突了。要提高归档效率,可以考虑把表里面的唯一索引改成普通索引。 - 对于写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时 change buffer 的使用效果最好。这种业务模型常见的就是账单类、日志类的系统。
若一个业务的更新模式是写入之后马上会做查询,change buffer 反而起到了副作用。
2. Innodb的flush(刷脏页)策略
- 当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。
- flush场景
- redo log写满,必须停止所有写操作清理redo log ,最为致命
- 内存页写满,淘汰的旧页为“脏页”,需要对其刷新,会占用资源,影响DB响应速度
- 空闲/关机时刷新,对性能影响不大
- innodb_io_capacity,设置磁盘的IOPS,根据磁盘性能确定刷脏页的最大速度
刷脏页速度取决一下因素- 当前脏页比列,越接近 innodb_max_dirty_pages_pct 是脏页比例上限(默认75%) 需要越快
- redo log剩余空间(分配合理的redo log内存也很重要)
3. Buffer Pool
- Buffer Pool管理缓存在内存的数据页,能加速查询速度
- 数据页淘汰算法是改进过的最近最少使用 (Least Recently Used, LRU) 算法
内存页由链表链起来,分为young和old区域,链表的LRU_OLD指针指向old区域开头 - 新插入的数据放在old区域头部,如果短时间内被再次访问,才会转到young区域开头,否则还是在old区域开头
- 这种方法保证了一定的内存命中率,防止全表扫描等大量冷数据涌入内存影响命中率
- 数据页淘汰算法是改进过的最近最少使用 (Least Recently Used, LRU) 算法
SQL语句解析
1. COUNT(*)性能
由于innodb的事务是通过多版本并发控制(MVCC,Multi-Version Concurrency Control)实现,每个事务当前的记录在并发下不一定相同,所以无法单纯记录表的行数,所以必须通过遍历整颗索引树(选择最小那颗)来计数,所以在大表中count效率不佳。
- 可以通过建立一张计数表的方式,来为每张表维护count值(利用事务特性解决该问题)
- count效率比较(低到高):count(字段)<count(主键 id)<count(1)≈count(*)
- 对于 count(主键 id) 来说,InnoDB 会遍历整张表,把每一行的 id 值都取出来,返回给 server 层。server 层拿到 id 后,判断是不可能为空的,就按行累加。
- 对于 count(1) 来说,InnoDB 遍历整张表,但不取值。server 层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。
- 对于 count(字段) 来说:取字段值,有字段索引则遍历该索引树,否则遍历最慢的主键索引树
- 对于count(*)来说,并不会把全部字段取出来,而是专门做了优化,不取值,按行累加。
2.ORDER BY 的排序方式
- 如果有符合排序条件的索引,无需排序(复合索引不能把order by、group by的列放在索引的最前面,因为查询中总是where先于order by执行)
- MySQL 会给每个线程分配一块内存用于排序,称为 sort_buffer。
所有符合条件的行都会放入buffer进行快排,如果buffer放不下,则每次快排后把数据存入磁盘临时文件,最后磁盘对所有临时文件使用归并排序合并成大文件 - rowid排序:如果全字段的总长度太大,超过设定的值:max_length_for_sort_data,使用rowid排序
该排序只选择主键和排序字段放入sort_buffer来排序,但是最后需要回表取回所有目标字段(多了磁盘读获取目标索引页内容,不一定就好于全字段排序)
3. JOIN的优化
- join的原则是小表(A,行数n)驱动大表(B,被驱动表,行数m)
A使用全表扫描,每次读入数据到join buffer供B配(显然,join buffer设置越大速度越快) - 若B有符号查询条件的索引,使用(ndex Nested-Loop Join,NLJ),复杂度k* n * m * log(m)(k为A读入join buffer 次数)
- 无符合的索引则使用hash join,把join buffer数据构建成哈希表,加速B表的匹配
4. 慢查询优化策略
- 索引优化:索引合适,对查询条件发挥作用
- 优化数据库结构:将字段很多的表分解成多个表,建立中间表(将大表间的联合查询改为中间表(临时表,小表)与大表的联合查询
5. Limit 1000000,20 的优化(深度分页问题)
分页的问题,limit是把offset+target数据全部查出来后再丢弃offset数据,查询数据每行都回表,性能降低
低效率SQL:SELECT * FROM 表 a, where 条件 LIMIT 100000,20;
- 延迟关联(本质是索引覆盖)
SQL更改为:SELECT a.* FROM 表 1 a, (select id from 表 1 where 条件 LIMIT 100000,20 ) b where a.id=b.id- 子查询在利用索引的同时,只查询符号条件的id,无需回表,效率高
- 查出来的数据是小表 join 原来的大表a,对于小表的每个id,直接从a表的主键索引拿到对应的行,只有目标的20行使用了主键索引
6. NULL字段避免使用
- 索引查询时,会降低查询效率
- 执行count,distinct,比较符等会出现数据丢失的情况(因为null不被计入)
7. explain
type(性能高到低)
- system 当表中只有一条记录并且该表使用的在储引擎的统计数据是精确的,比如MyISAM、Memory
- const 主键/唯一索引列进行等值匹配时
- eq_ref 多表 JOIN 时,被驱动表通过主键或唯一索引进行等值匹配。
- ref 通过普通索引等值查询,可能返回多行。
- fulltext 使用全文索引时的访问方式。
- ref_or_null 类似 ref,但允许值为null
- index_merge 使用Intersection交集、Union并集、sort-Union排序并集,这三种索引合并的方式来执行查询(用于and/or条件时)
- unique_subquery 子查询使用主键或唯一索引
- index_subquery 子查询使用普通索引
- range 索引范围扫描
- index 使用索引覆盖,无需回表,但遍历整个索引
- ALL 全表扫描
extra
- Using filesort:使用了外部排序
- Using temporary:使用了临时表
- Using index:使用了覆盖索引
- Using index condition:使用了索引下推优化
- Using where:server层需要过滤数据,因为存储层的索引未完全覆盖where限制的条件
- Using join buffer:使用了连接缓存
Memory引擎(内存表)
- Memory默认使用hash索引
内存表的数据部分以数组的方式单独存放,而主键 id 索引里,存的是每个数据的位置,数据按写入顺序存放。主键 id 是 hash 索引(不支持范围扫描,只能使用全表扫描),可以看到索引上的 key 并不是有序的。 - Memory也支持B-tree索引
- Memory的优缺
- 不支持事务,存在并发安全,只支持表锁,并发性能不行。数据库重启,内存表被清空,持久性不行,不适合做普通表
- 作为内存临时表的优势
- 临时表不会被其他线程访问,没有并发性的问题;
- 临时表重启后也是需要删除的,清空数据这个问题不存在;
- 对于join没有索引的字段且该表只取部分数据,先把他们存到临时内存表再join显然性能更高