Q: 一条SQL查询语句是如何执行的?
Q:一条SQL更新语句是如何执行的?
mysql逻辑分层
SQL语句的执行流程 --- server层
建立连接
- 长连接:客户端有持续的请求使用同一个连接
- 短连接:每次连接执行完很少的查询动作后断开连接
- 建立连接的过程是比较复杂并消耗资源的,因此需要避免频繁的建立、关闭连接,尽量使用长连接,使用连接池
- 使用过多的长链接可能会导致内存涨的特别快,这是因为mysql在执行过中临时使用的内存是管理在连接对象里面,而这部分资源只有在连接断开的时候才会被释放,当内存太大,内存溢出的时候mysql就会被Kill掉,从而出现mysql异常重启的情况
查询缓存
- 由于数据库更新频繁(刚维护好的缓存还未使用就被另一个update动作清空),查询缓存的命中率往往很低、弊大于利,一般不推荐使用,静态表 可以使用,query_cache_type=demand可关闭查询缓存
- 分析器
- 词法分析,主要用来验证SQL语句的正确性
优化器
- 索引的选择,选择一种mysql“自认为”最佳的执行方案
- 执行
- 调用引擎接口读取数据,并返回给客户端
日志系统
- redo log --- 重做日志(InnoDB 特有的日志)
- redo log属于物理日志,记录的是“某个数据页做了什么修改”
- WAL Write-Ahead-Logging
- 当有更新任务需要执行的时候,Inodb引擎会把记录写到redo log里再更新内存,此时更新就算是完成了,等到“适当”得时候,Inodb再把这个操作写到磁盘
- 先写日志,再写磁盘,解决了每次更新IO成本、查询成本很高的问题
- redo log是有固定大小的(可以设置,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么这块“粉板”总共就可以记录4个GB的操作)从头开始循环写,写到末尾又从头开始,如下图所示
- write pos是记录当前的位置,一边写一边后移,到第三号文教末尾后又回到0号文件开头处,checkpoint是当前要擦除的位置,也是往后推移并且循环的,擦除前需要把数据更新到数据文件
- write pos和check point 之间还空着的部分是redo log还空着(可用)的部分,当write pos追上了check point时redo log就满了,这时候就不能再执行更新操作了,需要先停下来推进check point
- 有了redo log,InnoDB就能保证及时数据库异常重启,之前提交的记录不会丢失,这个机制就叫做crash-safe
- innodb_flush_log_at_trx_commit这个参数设置成1时表示每次事务的redo log都直接持久化到磁盘,将改参数设置成1可以保证异常重启数据不丢失
- binlog --- 归档日志(server层的日志)
- binlog是逻辑日志,记录的是语句的原始逻辑,比如“给ID=2的这一行的C字段加1”
- binglog是追加写的,文件写到一定大小后会切换到下一个文件,并不会覆盖之前的日志
- redo log 分割成prepare和commit两阶段提交的意义
- 目的:让两份日志之间的逻辑一致
- 怎样让数据恢复到半个月前任意一秒的状态?拉取备份的binlog,前提是你需要有近半个月的binlog
- 如果分开写,第一个日志写完准备写第二个日志的期间发生crash
- 先写redo log 再写binlog(期间crash)
- 崩溃恢复后redo log的crash-safe保证了动作会被恢复,但是binlog并没有记录这个动作,当我们从备份里面拉取binlog进行恢复的时候这个动作就丢失了,这和我们期望的不符
- 先写binlog再写redo log(期间crash)
- 崩溃恢复后这个动作并未被捕捉,但是binlog被写入了,当我们从备份拉取binlog进行恢复的时候,该动作也会被捕获, 这和我们期望的不符
- 先写redo log 再写binlog(期间crash)
sync_binlog这个参数设置成1表示每次事务的binlog都持久化到磁盘,设置成1可以保证异常重启不丢失binlog
事务
事务:ACID 原子性,一致性,隔离性,持久性
当前读:更新数据都是先读后写的,而这个读只能读当前值,称为:“当前读”(current read)
- 多个事务同时执行的时候就可能会出现:脏读,不可重读,幻读的问题,而隔离性正是为了解决这些问题 --- 隔离级别
- 什么是幻读?
- 幻读指的是一个事务在前后两次在同一个范围查询的时候,后一次查询看到了前一次查询没看到的行。也就是说一个事务看到了别的事务进行的动作
- “当前读”才会出现幻读
- 幻读仅专指“新插入的行”
- 幻读有什么问题?
- 一是语义问题
- 二是数据一致性问题,包含数据和日志两个维度
- 即使把所有的记录都加上锁还是阻止不了新插入的记录
- 那么如何解决幻读呢?
- 幻读的根本原因是行锁只能锁住行,但是新插入记录这个动作要更新的是记录之间的“间隙”,因此,为了解决幻读问题,Inodb只能引入了“间隙锁(Gap lock)”,间隙锁锁的就是两个值之间的空隙
- 什么是幻读?
- 事务的启动方式
- update语句本身就是一个事务,语句完成的时候会自动提交事务
- 显式启动事务语句
- begin/start transaction 并不是一个事务的起点,执行到第一个操作Inodb表语句的时候事务才真正开始
- start transaction with consistant snapshot 马上启动一个事务
- commit
- rollback
- set autocommit=0 该命令会把线程的自动提交关掉,意味着只执行一个select语句这个事务就启动了,并且不会自动提交,这个事务持续存在知道主动commit或rollback或断开连接,因此建议使用autocommit=1,通过显式语句的的方式启动事务
- 多一次交互的问题:使用commit work and chain
- 查询长事务的语法(持续时间超过60s的事务):
- select * from t where TIME_TO_SEE(timediff(NOW(),trx_started))>60
- mysql的隔离级别包括:
- 读未提交:一个事务还未提交时它所做的变更就能被别的事务看到
- 读提交:一个事务提交过后它所做的变更才会被别的事务看到
- 可重复读:一个事务在执行过程中看到的数据总是和这个事务启动的时候看到的是一致的
- 串行化:对于同一行记录,“写”会加“写锁”,“读”会加“读锁”
- 隔离级别越高执行的效率就越低
- 事务隔离的实现
- 回滚日志:undo log
- 在MYSQL中每条记录在更新的同时都会记录一个回滚操作,记录上的最新值,通过回滚操作都能得到前一个状态的值
- 什么是一致性视图?
- consistent read view 用来支持RC(read commit 读提交)和 RR(reaptable read 可重复读)隔离级别的实现
- 假设一个数从1被按顺序修改成2,3,4那么在回滚日志里面就会有如下记录:
- MVCC:不同时刻启动的事务会有不同的read-view,同一条记录在系统中可以存在多个版本,这就是数据库的多版本并发控制
- 回滚日志什么时候会被删除?
- 由系统判定,当没有事务再需要用到这些回滚记录的时候回滚日志就会被删除
- 什么时候才不需要了呢?
- 当系统里没有比这个更早的read view的时候
为什么不建议使用长事务?
- 长事务以为着系统里面会存在很老的事务视图,由于这些事务随时可能访问数据库里的其他数据,所以在这个事务提交之前,数据库里面它可能用到的回滚记录都需要保留,从而导致大量的存储空间被占用
- 长事务还占用锁资源,可能会拖垮整个库
- 回滚日志:undo log
- 快照在MVCC里是怎么工作的?
- 事务在启动的时候会基于整个库“拍照”
快照是怎么实现的?
- InnoDB利用了“所有数据都有多个版本”的特性,实现了“秒级创建快照”的能力
- InnoDB里每一个事务都有唯一的ID,叫做transaction id,在事务开启的时候向InnoDB事务系统申请,是按申请顺序严格递增的
- 每行数据都有多个版本,每次事务更新数据的时候都会生成一个新的数据版本,并且把对应的事务ID赋给这个数据版本的事务ID,记为:row trx_id,同时旧版本的数据需要保留,并且在新版本中能够有信息直接拿到它
- 按照可重复读的定义,一个事务只需要在启动的时候声明:以我启动的时刻为准,如果一个数据版本在我启动前生成,就认;如果在我启动之后生成我就不认,必须找到它的上一个版本(如果上一个版本也不可见就找上上一个版本),当然这个事务自己更新的数据它还是要认的
- InnoDB为每一个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在活跃的所有事务ID,活跃指的是那些启动了但是还没有提交的事务
- 数据版本可见性规则:
- 低水位:数据里面事务ID最小的值
- 高水位:数组里面事务ID最大值加1
- 视图数组和高水位组成了当前事务的一致性视图
这样对于当前事务的启动瞬间来说,一个数据版本的row trx_id就有可能有以下几种情况:
- 绿色部分:表示这个版本是已经提交了的或者是当前事务自己生成的,可见
- 红色部分:将来事务生成的,不可见
- 黄色部分
- 若 row trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见
- 若row trx_id不在数组中,表示这个版本是已经提交了的事务生成的,可见
- 再来看可重复读是怎么实现的?
- 核心:一致性读(consistent read),而事务更新数据的时候只能用当前读,如果当前记录的行锁被其他事务占用的话就需要进入锁等待
- 在可重复读隔离级别下,只需要在事务开始的时候创建一个一致性视图,之后事务里的其他查询都共用这个视图
- 在读提交隔离级别下,每一个语句执行前都会重新算出
mysql中的锁
锁的初衷:解决并发问题
- 锁分类:全局锁,表锁,行锁
全局锁:对整个数据库实例加锁
- Flush tables with read lock:添加全局读锁,此时整个库处于只读状态
- 全局锁的典型使用场景:做全库逻辑备份
- 但是整个库处于只读很危险,如果在主库上备份,不能进行更新等操作业务基本停摆,在备份上操作不能及时处理主库同步来的binlog导致主从延迟
- 另一个方案:在可重复读隔离级别下开启一个事务
- 官方自带的逻辑备份工具:mysqldump
- 一致性读这么好为什么还需要FTWRL?前提是引擎需要支持这个隔离级别,MyISAM这种不支持事务的引擎就需要用到FTWRL了
- 表级锁
mysql表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock MDL)
- 表锁
元数据锁
- 不需要显示使用,访问表时会自动加上,MDL锁保证读写的正确性
- 所有的增删改查操作都会获取元数据锁
- 读锁不互斥,读写锁之间、写锁之间互斥
- 为什么给小表加字段也会导致数据库挂了?
- 当前正在读,add被阻塞,add之后的读也被阻塞,如果请求很频繁,那么数据库的线程很快就会爆满
- 如何安全的给小表加字段?
- 避免长事务
- alter table语句里加上等待时间,超时放弃
- 行锁功过
- 行锁是引擎层实现的,并不是所有引擎都支持行锁,比如:MyISAM
- 两阶段锁协议:在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放
知道两阶段锁有什么意义?
- 如果事务中需要锁多个行,那应该把最有可能造成锁冲突,最有可能影响并发度的事务往后放
- 死锁以及死锁检测
出现死锁的解决策略:
- 直接进入等待,直到超时。超时时间可以通过inodb_lock_wait_timeout(默认50s) 来设置,太长无法接受,太短会有误伤,所以尽量采用第二种方案
- 发起死锁检测:发现死锁后主动回滚死锁链条中的某一事务,让其他事务得以继续进行。将inodb_deadlock_detect设置为on表示开启这个逻辑
- 怎么解决热点行更新导致的性能问题?
- 问题的症结:死锁检测耗费了大量的CPU
- 如果能确保业务不会出现死锁,那么可以把死锁检测关闭
- 控制并发度:做在数据库服务端(修改mysql源码)
索引
索引的维护是有代价的
就像书本的目录一样索引就是为了加快查询的速度
- 索引模型:
- 哈希表 范围查询需要扫描全表,适用于只有等值查询的场景
- 有序数组 等值查询和范围查询性能优异,但更新数据代价很大,适用于静态存储引擎
- 搜索树 InnoDB使用B+树
- 索引分为主键索引和非主键索引
- 主键索引(InnoDB中也称为聚簇索引)叶子节点存的是整行数据
- 非主键索引(InnoDB中也称为二级索引)叶子节点存的是主键ID(回表)
- 覆盖索引可以避免回表,减少树的搜索次数,显著提升查询性能,因此覆盖索引是一个常用的性能优化手段
- 联合索引也能有效规避回表
- 由以上两点得出结论:非主键索引需要多扫描一棵索引树,因此在实际应用中应该尽量用主键查询
- 索引维护
- 主键长度越小,普通索引的叶子节点占用的空间就会越小,普通索引占用的空间也就越小
- 普通索引和唯一索引怎么选择?
- 查询过程:
- 普通索引:需要在查到满足条件的记录后查找下一条记录,知道碰到第一个不满足条件的记录
- 唯一索引:由于索引定义了唯一性,查找到第一个满足条件的记录后就会停止往下检索
- 上面两个“不同”带来的性能差距是多少呢?答案是微乎其微
- InnoDB的数据是按数据页来读写的,也就是当需要读取一条数据的时候,并不是仅仅只从磁盘读一条数据,而是以页为单位将其整体读入内存中,数据页默认大小16KB
- 更新过程:
change buffer:
- 目的:减少随机磁盘访问(数据库里面成本最高的操作之一)
- 工作机制:更新数据页时,如果数据页在内存中则直接更新,如果不在内存中,在不影响数据一致性的前提下,InnoDB会把这些操作缓存在change buffer中,这样就不需要从磁盘读取数据页了。在下次查询需要访问这个数据页的时候再把数据页读入内存中,然后执行change buffer中和这个数据页有关的操作
- change buffer中的操作应用到原数据页的操作叫做:merge
- 访问这个数据页会触发merge
- 系统后台有线程会定期merge
- 数据库正常关闭(shutdown)的过程中也会merge
- 只适用于普通索引, 唯一索引在更新操作前都要判断唯一键是否冲突,而这必须把数据读入内存中才能判断,既然数据读进来了那直接更新即可不需要再使用cahnge buffer
- 使用的是buffer pool的空间,在内存中有拷贝,也会被写到磁盘上
change buffer使用场景:写多读少,并且页面在写完后马上被访问到的概率较小,此时change buffer效果最好,例如:账单、日志系统,如果更新后立马就会进行查询反而会增加change buffer的维护代价
- 普通索引和唯一索引在查询能力上是没什么差别的,主要考虑对更新性能的影响,因此建议选择普通索引(业务允许前提下)
- redo log主要省的是随机写磁盘的IO消耗(转成顺序写),change buffer主要省的是随机读磁盘的IO操作
- 查询过程:
mysql为什么有的时候会选错索引?
- 优化器的逻辑 判断是否需要创建临时表、是否需要排序、扫描行数
扫描行数出了问题?索引区分度
一个索引上不同的值的个数-->索引基数mysql是怎样得到索引的基数
采样统计analyze table t 重新统计索引信息
- 索引选择异常处理
- 使用force index 表结构调整时如果没有修改代码就会出问题,而且开发的时候都不会考虑选错索引的情况,用到它说明已经出问题了
- 修改SQL语句,引导优化器选适当的索引
- 新建一个合适的索引供优化器选择或删掉误用索引
为什么mysql会“抖”一下?
- 脏页:内存中的数据页和磁盘的数据不一致的时候称为脏页
干净页:内存写入到磁盘后数据一致了,叫做干净页
- 那么到底为什么会“抖”一下?
- 平时很快的操作是在更新内存和写日志,“抖”一下的瞬间可能正在刷脏页(flush)
- 什么时候会引发flush的过程?
- 第一种情况:redo log满了,mysql只能停下其他工作推进check point
- 系统内存不足:当需要新的数据页,而内存又不足的时候就需要淘汰一些数据页来空出内存给别的数据页使用,如果淘汰的是脏页,就要先将脏页写到磁盘
- 系统空闲的时候: 闲着也没事就刷一刷脏页
- mysql正常关闭:脏页都flush到磁盘上
- 出现以下两种情况都是会明显影响性能的:
- 一个查询要淘汰的脏页数量太多,导致响应时间明显变长
- 日志写满,更新全部堵住,此时 写的性能为零
- 所以要使用脏页控制策略来控制脏页比例从而避免以上两种情况发生
- InnoDB刷脏页控制策略:
- 告诉InnoDB能,让InnoDB知道全力刷脏页的时候能刷多快,使用innodb_io_capacity=IOPS(建议)参数告诉InnoDB磁盘能力
- 另一个有趣的策略:
- 当一个查询需要flush脏页的时候这个查询就会比平常慢,而mysql中的一个机制会让这个查询更慢:在准备刷一个脏页的时候,如果这个数据页旁边的数据页刚好也是脏页,就会把这个邻居一起刷掉,而这个把邻居一起“拖下水”的逻辑还可以继续蔓延
- innodb_flush_neighbors这个参数就是来控制这个行为的,1为连坐机制,0为自己刷自己的,MYSQL 8.0中这个参数已经默认为0
- 为什么逻辑相同的SQL执行效率相差距大?
- 条件字段做函数操作,对索引字段做函数操作,可能会破坏索引值的有序性,优化器会放弃走搜索树
- 隐式类型转换
- 数据类型转换的规则是什么?select "10" > 9 返回值为1 说明是将字符串转化成数字
- 为什么有数据类型转换就需要走全表扫描
- 隐式字符编码转换 utf8mb4是utf8的超集,Mysql内部会把utf8转成utf8mb4
- 为什么只查一行的语句也这么慢?
- MySQL在压力很大情况下(CPU占用率或IO利用率很高),所有语句的执行都有可能变慢
- 查询长时间不返回
- 表被锁 show processlist命令查看当前语句处于什么状态
- 等MDL锁 找到谁持有MDL锁 kill掉
- 等flush
- 等行锁
- 查询慢 坏查询不一定是慢查询
- 一致性读需要undo log ,lock in share mode 当前读
- 为什么只改一行的语句锁这么多?
- 删除数据的时候尽量加Limit,可以控制删除的条数还可以减少加锁的范围
主机内存100G,对200G的大表扫描会不会把内存打爆?
- 逻辑备份对整库进行扫描都挂不了,对大表扫描也没问题
- 流程是怎样的?
- 获取一行,写到net_buffer,这块内存的大小由net_buffer_length参数定义。默认16K
- 重复获取行,直到net_buffer写满,调用网络接口发送出去
- 如果发送成功就清空net_buffer进行下一回合
- 如果发送函数返回EAGAIN或WSAEWOULDBLOCK,就表示本地网络栈(socket send buffer)写满了,进入等待,直到网络栈可写了再继续发送
在整个过程中占用内存最大就是net_buffer_length这么大,并不会达到200G
- MySQL是“边读边发”的
- 如果客户端接收很慢,会导致服务端由于结果发不出去,事务的执行时间变得很长
全表扫描对InnoDB的影响
- InnoDB的内存管理:Leastly Recently Used,LUR算法,即淘汰最久未使用数据页
- LUR算法基本模型:
说明:使用链表实现,最新访问的放在链表头部,当内存不足时淘汰tail节点 使用该方案扫描的问题:会把当前buffer pool里面的数据全部淘汰,存入扫描过程中访问到的数据页,也就是说buffer pool里面放的主要是这个表的历史数据,那么问题来了,如果是一个线上作业的服务,Buffer pool的内存命中率会急剧下降,磁盘压力增加,SQL响应变慢
- 基于以上原因InnoDB未直接使用上面的LUR算法,而是对其做了改进
- 改进后的LUR算法:
- 基于5:3的比例把LRU链表分成了young区域和old区域,靠近链表头部的5/8是young区,靠近尾部的3/8是old区
- 执行流程:
- 状态1:访问数据页p3,由于p3在young区因此和优化前的LUR算法一样把p3放到young区头部,变成状态2
- 状态2:之后要访问一个不存在于当前链表的数据页,这时候淘汰pm页,新插入的数据页px放在LUR_old区头部
- 处于Old区的数据页每次被访问的时候作如下判断:
- 若这个数据在LUR链表中存在时间超过1秒,就把它移到链表头部
- 存在时间少于1秒则位置不变 一秒这个时间由innodb_old_blocks_time控制 默认1000ms
- 这个策略就是为处理类似全表扫描做定制的,扫描的时候对young区没有影响,从而保障了buffer pool查询命中率
误删数据除了跑路还能怎么办?
- 删数据类型:
- delete语句误删数据行
- drop/truncate语句误删表数据表
- drop database误删数据库
- rm命令删除数据库实例
- delete语句误删数据行
- 预防
- sql_safe_updates参数设置时为On,delete/update语句缺省where条件时会报错,阻止sql执行
- sql上线前进行审计
- 治疗
- delete删除数据行可以使用Flushback工具通过闪回把数据恢复回来, Flushback恢复数据的原理是修改binlog的内容,拿回原库重放。使用这个方案的前提是:binlog_format=1和binlog_row_image=FULL
注意:不要在主库上进行该操作,避免对数据造成二次破坏
- 预防
- drop/truncate语句误删表数据表
- 预防
- 账号分离
- 开发人员只分配DML权限,不分配truncate/drop权限,有DDL需求则找DBA支持
- 即使是DBA人员,日常也只是用只读账号,有特殊需求的时候再使用特殊账号
- 制定规范
- 在删除数据表之前先对表进行改名操作,观察一段时间,确保对业务没有影响再进行删除操作
- 改表名的时候给表加上固定的后缀(比如:_to_be_deleted),删除的动作必须通过管理系统执行,并且在删除的时候只能删除固定后缀的表
- 账号分离
- 治疗
- 预防
- rm命令删除数据库实例
- 一个有高可用的MYSQL集群最不怕的就是rm删除数据了,只要不是整个集群被删除,只要恢复这个节点的数据再整入集群
- 批量下线机器的操作会让mysql集群全军覆没,应对这种情况可以备份跨机房、跨城市保存
自增Id用完怎么办?
order by 是怎么工作的?
如何正确的显示随机消息?
count(*)问题
- MYISAM 把一个表的总行数存在了磁盘上,执行count(*) 时会直接返回这个记录,效率很高
- Innodb只能把数据一行一行的取出来累加
- 为什么InnoDB不能做成和MyISAM一样?MVCC导致InnoDB自己也不知道应该返回多少行
- 效率:
- count(字段)
- count(字段)
- 缓存计数?数据库计数?
为什么表数据删了一大半,表文件大小没有变?
- 为什么简单的删除数据达不到回收表空间的效果?
- 表数据既可以存在于共享表空间,也可以是独立的文件,这个行为由参数innodb_file_per_table控制,ON表示独立(推荐做法)
- 删除一行记录的时候,InnoDB只会把对应行标记删除(可复用),磁盘文件大小并不会缩小
- 我们知道InnoDB的数据是按页存储的,那么如果我们删掉一个数据页上的所有记录会怎样?
- 整个数据页就可以被复用了
- 数据页的复用和记录的复用是不同的
- 记录的复用 只限于符合范围条件的数据
- 当整个页从b+数摘掉后可以复用到任何位置
- 如果相邻的两个数据页利用率都很小,系统会把两个数据页的数据合并,把另外一个数据页标记为可复用
- 如果用delete命令删除整张表,结果就是所有数据页都能复用了,而磁盘上的文件大小不会变小,也就是说delete命令删除数据是不会回收表空间的,这些可被复用但未被使用的空间就像是“空洞”
- 删除数据会造成空洞,插入数据也会造成空洞
- 如果能够把这些空洞去掉就能达到回收表空间的目的
- 重建表 alter table A engine=Innodb mysql自动完成临时表创建、数据转储、交换表名、删除旧表,这个DDL不是online的A表有数据写入的话就会有数据丢失(5.6版本后就是online DDL)
- 由于重建会扫描原表结构和数据,并创建临时文件,对于大表来说这个操作是很费IO和CPU的,因此需要小心,可以使用Github开源的gh-ost
Mysql有哪些“饮鸩止渴”的提高性能的方案?
- 短连接风暴
- 干掉那些不干活的线程 通过show processlist踢掉sleep的线程
- 减少链接过程的消耗 比如跳过权限验证---风险极高,不建议使用
- 慢查询性能问题
- 索引没设计好
- SQL写的有问题
- MySQL选错了索引
- QPS突增问题
- 业务突然出现高峰
- 程序出现bug
读写分离有哪些坑?
主备延迟,导致“过期读”
- 强制走主库
- sleep方案
- 判断主备无延迟方案
- 判断seconds_behind_master是否等于0
- 对比位点确保主备无延迟
- 对比GTID集合确保主备无延迟