MySQL整理

MySQL是后端开发很重要的一块知识体系,在这里将一些之前学习的知识,我认为比较重要的知识点,系统的整理一下:
1、一条SQL语句是如何执行的
0
2、SQL更新语句
2.1执行过程
  • 查询语句过程
    • 分析器
    • 优化器
    • 执行器
      • 调用引擎查询接口,返回数据
  • 将数据返回修改
  • 调用引擎的更新接口
2.2执行过程中的关键点
  • redolog
    • InnoDB特有的
    • WAL技术
      • 先写日志后写磁盘
    • 循环写
    • crash-safe
    • 物理日志,记录做了什么修改
  • binlog
    • MySQL的Server层
    • 追加写
    • 不具备crash-safe 的能力
      • 没有记录哪些已经刷盘,哪些未刷盘
    • 逻辑日志,记录语句的原始逻辑
  • redolog 和 binlog 的两阶段提交
    • 两阶段提交保证redolog 和binlog 的一致性
    • 两阶段提交是跨系统维持数据逻辑一致性时常用的一个方案
3、事务
3.1ACID
  • 原子性
    • 一个事务的所有操作,要么全部成功,要么全部失败
  • 一致性
    • 事务前后数据的完整性必须保持一直
  • 隔离性
    • 读未提交
      • 没有视图概念
      • 一个事务还没提交时,它做的变更就能被别的事务看到
    • 读提交
      • 事务中每句SQL执行钱获取视图
      • 一个事务提交之后,它做的变更才会被其他事务看到
    • 可重复读
      • 事务开始时获取视图
      • 一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的
    • 串行化
      • 直接用加锁的方式来避免并行访问
      • 顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”
  • 持久性
    • 数据的改变永久保存在数据库中
3.2在引擎层实现
3.3InnoDB支持事务,MyISAM不支持事务
3.4事务隔离的实现(MVCC)
  • undolog
    • 记录回滚字段,每一个回滚段其实就是一个视图
    • 当一个回滚段,也就是这个视图,以及之前的视图,没有被事务持有时,就可以删除了
    • 长事务,会导致很久之前的视图无法被删除,回滚日志变的很大
3.5事务的启动方式
  • 显式语句启动事务
    • begin
      • commit
      • rollback
    • start transaction
      • commot
      • rollback
  • 非显式
    • set autocommit=0
      • 只要执行一个select 语句,就会开启事务,且不会自动提交
    • set autocommit=1
      • 只要执行一个select 语句,就会开启事务,且自动提交
4、索引(上)
4.1索引常见的数据模型
  • 哈希表
    • KV存储结构
      • 通过索引列计算出哈希值
      • 计算的哈希值作为key值
      • 数据内容为value值
      • key相同,拉出链表
    • 特点
      • 无序
        • 插入简单
        • 区间查询复杂,需要全部扫描
          • 时间复杂度为O(N)
      • 适合等值查询
        • 时间复杂度为O(1)
  • 有序数组
    • 插入困难
      • 适合静态存储引擎
      • 插入需要做数据迁移
    • 二分查找法
      • 时间复杂度O(log(N))
  • N叉树
    • 平衡二叉树
      • 时间复杂度O(log(N))
    • B-树
      • 时间复杂度O(log(N)),趋近O(1)
    • B+树
      • 时间复杂度O(log(N)),趋近O(1)
    • 存储量
      • InnoDB一个整数字段为索引,N约等于1200,树高为4的时候,可以存1200的3次方值,就是约17亿
  • MySql中常见的索引
    • B+树(MySQL底层是经过改进的B+树)
      • 非叶子节点不存储数据,只存储键值对
      • 根节点常驻内存
      • 磁盘访问的数据也固定大小,InnoDB为16KB
    • 哈希表
  • 主键索引(聚集索引)
    • 叶子节点存储郑航数据
  • 普通索引(非聚集索引)
    • 叶子节点存主键ID
    • 查询到主键ID,再去主键索引进行检索检索的过程成为会标
  • 索引维护
    • B+树
      • 保持有序性
        • 页分裂
        • 页合并
      • 业务层的优化
        • 使用自增主键
          • 插入时追加数据,不会引起页分裂
          • 逻辑删除,不会引起页合并
        • 业务主键
          • 最好只有一个索引
            • 不需要考虑普通索引的叶子节点过大
          • 如果有多个索引
            • 不适合字段过长,不然会让普通索引占用空间过大
5、索引(下)
5.1回表
  • 回表次数计算
    • 找到一个主键ID,回表一次,计算一次回表(对于MySQL的Server层来说,是找引擎拿到的记录,得到的扫描行数,可能会比实际读取的行数要少)
  • 避免回表过程
    • 覆盖索引
    • 联合索引
    • 查询条件和查询项目都在该索引上
  • 最左前缀原则
    • 联合索引的最左N个字段
    • 字符串索引的最左M个字段
      • like 语句的使用
  • 索引下推(MySQL5.6)
    • 在索引内部进行条件的判断,不符合的数据直接跳过,减少回表的次数
6、全局锁和表锁
6.1全局锁
  • Flush tables with read lock(FTWRL)
    • 用途
      • 用于做全库逻辑备份
    • 弊端
      • 无法更新,业务停摆
    • 代替方案
      • 可重复的隔离级别下,开启一个事务
        • mysqldump single-transaction
      • set global readonloy=true(有弊端,不建议用)
6.2表锁
  • 显示表锁
    • lock table ... read/write
  • 元数据锁(MDL)
    • 对表进行增删改数据(DML)时,自动加上MDL读锁
    • 对表进行修改表结构的操作(DDL)时,会自动加上写锁
    • 读锁之间不互斥
    • 读写锁之间、写锁之间是互斥
    • MySQL5.5引入MDL
    • MDL的风险
      • MDL读锁(长事务)
      • MDL写锁(等待读锁释放)
      • MDL读锁N个,等待写锁,线程爆满,卡死
      • 解决方案
        • 尝试kill 长事务
        • 停掉DDL
        • 事前给MDL写锁设定等待时间
7、行锁
7.1相关知识点
  • 行锁时有各个引擎实现的
    • InnoDB支持
    • MyISAM不支持
  • 行锁可以有效的控制并发
7.2两阶段锁协议
  • 行锁时在需要的时候加上,事务结束时释放
  • 由于两阶段锁协议,程序设计时,对并发高的行的SQL语句,尽量放在事务的后面
7.3死锁
  • 概念及场景
    • 事务A更新语句锁住行1
    • 事务B更新语句锁住行2
    • 事务A更新语句拿到行2的写锁,卡住
    • 事务B更新语句拿到行1的写锁,卡住
  • 死锁的解决方案
    • 等待超时
      • innodb_lock_wait_timeout设置具体超时时间
      • 设置超时时间太长,高并发的业务无法接受
      • 设置时间过短,会误杀部分正常的锁等待
    • 死锁检测
      • 判断自己加入是否会产生死锁的时间复杂度为O(N)
      • 当访问同一行的线程较多时,例如1000个,死锁检测就是100万
      • 死锁检测的关闭,可能回出现大量的超时
    • 控制对行修改的并发量
      • 一行变多行,负载均衡的思路
8、事务2
8.1一致性读视图(快照)consistent view
  • RC读提交
  • RP可重复读
8.2快照在MVCC的工作方式
  • 事务ID
    • 每个事务都有事务ID,事务ID严格递增
  • 行数据版本ID, row trx_id
    • 每行数据有可能有多个版本
      • 事务在更新了该行后会将事务ID记录成改行的row trx_id
      • 每个版本的数据,并非真实的物理妇女在,而是根据undolog 推算出来的
  • 事务在启动后第一个SQL,拿到最新可见的row trx_id(可重复读)
    • 产生了row trx_id,但是事务未提交,则不可见
    • 产生了row trx_id,但是事务已经提交,则可见
  • 事务在执行update语句(或行写锁语句)(可重复读)
    • 重新拿到最可见的row trx_id(当前读)
  • 额外说明
    • 上面都是在RP可重复读的隔离级别下
    • RC的隔离级别下,没一个SQL语句,都会执行当前读
9、普通索引和唯一索引的选择
9.1从查询来看
  • 两者之间差距过小,基本一直
9.2从更新来看
  • 优先选择普通索引,因为使用了change buffer
9.3change buffer
  • 简述
    • 数据页没在内存中,在不影响数据一致性的前提下,将更新操作缓存在change buffer中
      • 减少读取磁盘的操作
      • 减少内存的使用
    • 持久化数据
  • merge
    • 当访问到这个数据页的时候
    • 定期merge
    • 数据库正常关闭
  • 唯一索引需要判断唯一性约束,所以不能使用change buffer,实际上也只能普通索引可以使用
  • 对写多读少的表,有很好的优化效果,但是对于更新后立即查询的表,反而增加了change buffer 的维护成本。
10、MySQL的索引选择
10.1 优化器的逻辑
  • 扫描行数
    • 扫描行数越少,访问磁盘数据的次数越少,消耗CPU的资源越少
    • 区分度,采用的是采用统计
  • 临时表
  • 是否排序
10.2关于选择错误索引的问题
  • 扫描行数计算错误
  • 回表次数较多,不如全表扫描,放弃回表
  • 排序和临时表的影响,选择扫描行数大的索引
10.3 关于选择错误索引的问题
  • 重新统计索引信息(analyze table t)
  • 使用强制索引(force index)
  • 调整索引,删除垃圾索引等等
10.4 使用 EXPLAIN 去分析SQL语句
11、字符串字段加索引
11.1 前缀索引
  • 前缀索引会增加扫描行数
    • 调查其区分度,决定前缀长度,控制扫描行数
  • 使用前缀索引,无法利用索引覆盖的优势
  • 索引选取的越长,占用的磁盘空间就越大,相同数据页能放下的索引值就越少,搜索的效率也就会越低
11.2 倒序存储
  • 根绝索引字段的特殊性,将字段内容倒序,然后再利用前缀索引,例如身份证
11.3 hash
  • 新增一列,用来存储hash值,然后以该列为索引
11.4 hash和倒序存储的区别
  • 从占用的额外空间来看,倒序存储方式在主键索引上,不会消耗额外的存储空间,而 hash 字段方法需要增加一个字段。当然,倒序存储方式使用 4 个字节的前缀长度应该是不够的,如果再长一点,这个消耗跟额外这个 hash 字段也差不多抵消了。
  • 在 CPU 消耗方面,倒序方式每次写和读的时候,都需要额外调用一次 reverse 函数,而 hash 字段的方式需要额外调用一次 crc32() 函数。如果只从这两个函数的计算复杂度来看的话,reverse 函数额外消耗的 CPU 资源会更小些。
  • 从查询效率上看,使用 hash 字段方式的查询性能相对更稳定一些。因为 crc32 算出来的值虽然有冲突的概率,但是概率非常小,可以认为每次查询的平均扫描行数接近 1。而倒序存储方式毕竟还是用的前缀索引的方式,也就是说还是会增加扫描行数。
12、flush
12.1 fiush触发的场景
  • redolog写满了
    • 不接受更新
  • 系统内存不足
    • 此时系统的反应,淘汰最久不适用的数据页
      • 干净页,直接淘汰
      • 脏页,flush到磁盘,再淘汰
  • 系统空闲
  • MySQL正常关闭
12.2 flush 的策略
  • 合理设置innodb_io_capacity 的值,不要让它经常接近75%
  • flush脏页的时候,如果“邻居”也是脏页,也会一起刷掉,“邻居”的“邻居”也是如此,蔓延下去
13、数据删除
13.1 参数 innodb_file_per_table
  • ON
    • 表数据存储在.ibd为后缀的文件中
    • drop table,删除文件,空间回收
    • delete,标识行记录为已删除,空间不回收,后续复用
      • 待复用的行记录变成空洞
      • 当一个数据所有记录都被删除,那么这个数据页标记成可复用
      • 页合并,两个数据页利用率都很小,就合并成一个数据页,多出来的数据页标记成可复用
      • insert 语句在产生页分裂的场景下,也会产生空洞
      • update 也会产生空洞
  • OFF
    • 表数据存放在系统共享表空间
    • drop table ,空间不会回收
13.2 解决空洞的办法
  • 重建表
    • 优势:简单
    • 劣势:需要阻止其他更新,非 online 模式
  • online DDL
    • 建立临时文件,扫描对象表主键的所有数据页,MDL写锁
    • 扫描完成后,写锁退化为MDL读锁
    • rowlog 记录所有在【临时文件生成】过程中的更新
    • 将rowlog 应用到临时文件中
    • 将临时文件替换原有的数据文件
14、count()
14.1 count(*)
  • MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高
  • 而 InnoDB 引擎就麻烦了,它执行 count(*) 的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数
  • 这里需要注意的是,讨论的是没有过滤条件的 count(*),如果加了where 条件的话,MyISAM 表也是不能返回得这么快的
  • MyISAM 表虽然 count(*) 很快,但是不支持事务
  • show table status 命令虽然返回很快,但是不准确
  • InnoDB 表直接 count(*) 会遍历全表,虽然结果准确,但会导致性能问题
  • 并不会把全部字段取出来,而是专门做了优化,不取值。count(*) 肯定不是 null,按行累加
14.2 count(主键id)
  • InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来,返回给 server 层。server 层拿到 id 后,判断是不可能为空的,就按行累加
14.3 count(1)
  • InnoDB 引擎遍历整张表,但不取值。server 层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加
14.4 count(字段)
  • 如果这个“字段”是定义为 not null 的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加
  • 如果这个“字段”定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加
14.结论
  • 按照效率排序的话,count(字段)<count(主键 id)<count(1)≈count(*),所以建议尽量使用count(*)
15、order by
15.1 MySQL会给每个线程分配一块内存用于排序,称为sort_buffer
15.2 由max_length_for_sort_data决定排序方法
  • 行长度 <= max_length_for_sort_data
    • 全字段排序
      • 排序数据量 > sort_buffer_size —》 外部排序
      • 排序数据量 <= sort_buffer_size —》内部排序
  • 行长度 > max_length_for_sort_data
    • rowid排序
      • 取出主键ID和要排序的字段 —》排序,拿着主键ID去获取其他数据
      • 排序数据量 > sort_buffer_size —》外部排序
      • 排序数量 <= sort_buffer_size —《 内部排序
15.3 全字段和rowid 排序对比
  • rowid 排序会要求回表造成磁盘IO,因此不会被优先选择
  • 如果内促够多,就要多利用内存,尽量减少磁盘访问
15.4 利用联合索引减少排序
16、临时表
16.1 什么时候用到临时表
  • union
  • group by
  • ......
16.2 临时表的种类
  • 小于 tmp_table_size
    • 内存临时表,memory 引擎
  • 大于 tmp_table_size
    • 磁盘临时表,InnoDB 引擎
16.3 临时表对排序方法的选择
  • 内存表选择rowid 排序,因为临时内存表的回表,也只是通过数据行的位置,访问内存数据而已
  • 磁盘表的排序方式,同order by 的选择
17、关于索引的案例
17.1 条件字段函数操作
  • create index xx_time ....;
  • select count(*) from T where month(xx_time) = 2;
  • 1.由于在字段上加了函数,所以不会走所以
  • 2.但是查询的是count(*),又由于主键索引比普通索引大,又走了该索引
  • 3.虽然走了该索引,但是扫描行数还是很大,说明把整个索引树都进行了扫描
  • 小结
    • 对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。需要注意的是,优化器并不是要放弃使用这个索引
17.2 隐式类型转换
  • select * from T where xxString = 1001
  • 1.xxString是一个varchar类型的字段
  • 2.那么上面的SQL就相当于 select * from T CAST(xxString as signed int) = 1001
  • 因为做了函数操作,所以也会放弃走索引
  • 需要注意的是,上面的例子中的where条件,其实是字符串和数字的对比!需要注意的是,上面的例子中的where条件,其实是字符串和数字的对比!则条件字段不会有函数操作,不会放弃走索引
18、一条SQL执行的慢
18.1 阻塞
  • 等待MDL 写锁
    • 表数据操作语句(DML)会产生MDL读锁
    • 表结构变更语句(DDL)会产生MDL写锁
    • MDL读写锁互斥
    • MDL 是表的元数据锁
  • 等待 flush
    • flush 的执行速度一般都很快,其本身有可能被其他语句堵死了
  • 等待行锁
18.2 性能慢
  • 没有走索引,扫描行数过多
  • 事务开启时,就拿到了一致性视图,而其他事务产生了过多的回滚段
19、各种读的概念
19.1 脏读
  • 主要出现在读未提交的隔离级别下,读取到了未提交的数据结果
  • 解决办法:变更事务的隔离级别
19.2 不可重复读
  • 主要出现在RU,RC两种隔离级别下
  • 解决办法:变更事务的隔离级别为可重复读
19.3 幻读
  • 读取到了其他事务插入的数据
  • 产生的问题
    • 破坏了数据的一致性,binlog的顺序发生了错乱
  • 解决方法
    • 将隔离级别调整到可重复读,间隙锁(gap lock)
    • 读提交的隔离级别,将binlog设置成row的格式
20、加锁的规则
20.1 两个“原则”、两个“优化”和一个“bug”
  • 原则 1:加锁的基本单位是 next-key lock。希望你还记得,next-key lock 是前开后闭区间
  • 原则 2:查找过程中访问到的对象才会加锁。
  • 优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
  • 优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。
  • 一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
21、主库、备库、从库
21.1 准备切换流程
  • 流程图
  • 内部流程图
  • 内部流程图说明:
    • 1.在备库 B 上通过 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量。
    • 2.在备库 B 上执行 start slave 命令,这时候备库会启动两个线程,就是图中的 io_thread 和 sql_thread。其中 io_thread 负责与主库建立连接。
    • 3.主库 A 校验完用户名、密码后,开始按照备库 B 传过来的位置,从本地读取 binlog,发给 B。
    • 4.备库 B 拿到 binlog 后,写到本地文件,称为中转日志(relay log)。
    • 5.sql_thread 读取中转日志,解析出日志里的命令,并执行。
  • 保证高可用
21.2 binlog的三种格式
  • statement (SQL的原语句)
  • row
  • mixed
21.3 主库和从库
  • 从库和备库在概念上其实差不多,在HA过程中被选成新主库的,称为备库,其他的称为从库
  • HA过程,和高可用性专一
22、数据库的误删除操作
22.1 使用delete语句误删除数据化
  • 使用Flashback工具
    • Flashback 工具原理:修改binlog 的内容
23、JOIN
  • 有两张表结构一样的表,t1,t2
23.1 Index Nested-Loop Join(NLJ)
  • select * from t1 straight_join t2 on (t1.a=t2.a)
    • 在这个语句里,t1 是驱动表,t2 是被驱动表。
    • 在这条语句里,被驱动表 t2 的字段 a 上有索引,join 过程用上了这个索引,这个语句执行流程如下
      • 1.从表 t1 中读入一行数据 R;
      • 2.从数据行 R 中,取出 a 字段到表 t2 里去查找;
      • 3.取出表 t2 中满足条件的行,跟 R 组成一行,作为结果集的一部分;
      • 4.重复执行步骤 1 到 3,直到表 t1 的末尾循环结束。
      • 小结:
        • 对驱动表 t1 做了全表扫描,而对于每一行 R,根据 a 字段去表 t2 查找,走的是树搜索过程。由于我们构造的数据都是一一对应的,因此每次的搜索过程都只扫描一行
  • 如何选择驱动表
    • 在上面 join 语句执行过程中,驱动表是走全表扫描,而被驱动表是走树搜索。
    • 假设被驱动表的行数是 M,驱动表的行数是 N
      • 被驱动表每次搜索一棵树近似复杂度是以 2 为底的 M 的对数,记为 log M,所以在被驱动表上查一行的时间复杂度是 2*log M。
      • 驱动表执行过程就要扫描驱动表 N 行,然后对于每一行,到被驱动表上匹配一次。
      • 整个执行过程,近似复杂度是 N + N*2*log M。
      • N 对扫描行数的影响更大,因此应该让小表来做驱动表
  • 小结:
    • 使用 join 语句,性能比强行拆成多个单表执行 SQL 语句的性能要好;
    • 如果使用 join 语句的话,需要让小表做驱动表。
23.2 Simple Nested-Loop Join
  • select * from t1 straight_join t2 on (t1.a=t2.b);
    • 这个Simple Nested-Loop Join 算法太“笨重了”,mysql没有使用这个算法,而是使用了另外一个叫做“Block Nested-Loop Join” 的算法,简称 BNL。
23.3 Block Nested-Loop Join(BNL)
  • 被驱动表上没有可用的索引
    • 把表 t1 的数据读入线程内存 join_buffer 中,由于我们这个语句中写的是 select *,因此是把整个表 t1 放入了内存;
    • 扫描表 t2,把表 t2 中的每一行取出来,跟 join_buffer 中的数据做对比,满足 join 条件的,作为结果集的一部分返回。
  • join_buffer :
    • 大小是由join_buffer_size 设定的,默认是256k。如果放不下表t1 的所有数据,策略很简单,就是分段放。
    • 如果join语句很慢,就把 join_buffer_size 改大
  • 小表的概念:
    • 在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与join的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表。
  • 能不能使用 join 语句?
    • 如果可以使用 Index Nested-Loop Join 算法,也就是说可以用上被驱动表上的索引,其实是没问题的;
    • 如果使用Block Nested-Loop Join,扫描行数就会过多。尤其是在大表上的join操作,这样可能要扫描被驱动表很多次,会占用大量的系统资源。所以这种 join 尽量不要用。
    • 在判断要不要使用 join 语句时,就是看 explain 结果里面,Extra 字段里面有没有出现“Block Nested Loop”字样。
  • 如果要使用 join,应该选择大表做驱动表还是选择小表做驱动表?
    • 如果是 Index Nested-Loop Join 算法,应该选择小表做驱动表;
    • 如果是 Block Nested-Loop Join 算法:
      • 在 join_buffer_size 足够大的时候,是一样的;
      • 在 join_buffer_size 不够大的时候(这种情况更常见),应该选择小表做驱动表。
      • 所以,这个问题的结论就是,总是应该使用小表做驱动表。
  • 小结:
    • 1. 如果可以使用被驱动表的索引,join 语句还是有其优势的;
    • 2. 不能使用被驱动表的索引,只能使用 Block Nested-Loop Join 算法,这样的语句就尽量不要使用;
    • 3. 在使用 join 的时候,应该让小表做驱动表。
23.4 join语句怎么优化
  • 1. BKA 优化是 MySQL 已经内置支持的,建议你默认使用;
  • 2. BNL 算法效率低,建议你都尽量转成 BKA 算法。优化的方向就是给被驱动表的关联字段加上索引;
  • 3. 基于临时表的改进方案,对于能够提前过滤出小数据的 join 语句来说,效果还是很好的;
  • 4. MySQL 目前的版本还不支持 hash join,但你可以配合应用端自己模拟出来,理论上效果要好于临时表的方案。
posted @ 2021-11-10 19:38  冰乐  阅读(174)  评论(0编辑  收藏  举报