分为两层,Server层和存储引擎层。
server层涵盖了大部分的核心服务功能及所有的内置函数。所有跨存储引擎的功能都在这一层实现,比如存储过程,触发器,视图等。
存储引擎层就是负责对数据的存和取。其架构模式是插件式的。支持多个存储引擎。现在最常用的是InnoDB,它从5.5.5版本开始成为了默认存储引擎。
使用mysql第一步就是见到他。负责建立连接,获取权限,保持和管理连接。当用户名和密码验证通过的时候,连接器就会去权限表里查权限,之后要是会判断权限,都会以这次为准,root改了也没用。
连接肯定有个时间长短,一直连接(客户端持续有请求)的就叫长连接,查询几次就断掉的叫短连接。连接建立比较耗cpu,所以尽量长连接,但一直长连接也有问题:临时使用的内存是管理在连接对象里面的(数据放在数据库这端的内存),太大了会被系统强行杀掉(OOM)。从现象看就是Mysql异常重启了。
要不定期断开,要不在5.7之后的版本中每次会执行
mysql_reset_connection
重新初始化连接,这还不用重连和重新做权限验证。
查询语句会先到查询缓存里去找,会以key(查询语句)-value(查询结果)存在,如果命中了就直接返回,大大提高效率。
但并不建议使用,因为失效很频繁,一个字段更新会导致整个表的查询缓存都会清空。mysql8.0.0已经删掉了。
故名思意,就是来看你想做什么的。先进行词法分析"select"关键字识别出来就是查询还要识别表名,列ID等。然后进行语法分析看你句子有没有错误,有就会
“You have an error in your SQL syntax”
就是在有多个索引的时候决定使用哪个,在有多表关联(join)的时候决定各个表的连接顺序。
会做两件事:一件事是看用户是否有权限,没有会报
mysql> select * from T where ID=10;
ERROR 1142 (42000): SELECT command denied to user 'b'@'localhost' for table 'T'
如果有了就会根据你选的引擎(根据表的引擎定义,去使用这个引擎提供的接口)执行。
更新语句的流程也要先经过连接器,查询缓存,分析器,执行器。但还涉及到两个重要的日志模块redo log(重做日志)和binlog(归档日志)
如果每次更新都将数据写入硬盘里,代价太高了(IO成本,查询成本)。所以出现了WAL(Write Ahead Logging)技术:就是更新数据的时候先将要更新的写进日志,更新内存。在合适的时候再把数据写入硬盘。(先写内存,在写磁盘)但如果日志满了只能直接更新到硬盘里了。
InnoDB的redo log是固定大小。eg.配置一组四个文件,每个大小1G,从开头写,写到末尾就又从开头写循环往复。
write pos是记录当前写的位置,不停的循环往复。check point就是擦除记录,他俩之间绿色的部分就是可以写入数据的空闲区域。当wp追上cp之后就要等cp继续往前推进了。
有了redo log之后就算Inno DB异常重启之前提交的记录也不会丢失,称之为crash-safe。
redo log 是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,称为 binlog(归档日志)
redo log是物理日志,记录的是“更新了什么数据”,binlog是逻辑日志,记录的是原始语句。而且binlog是可追加的。写到一定大小会切换到下一个“追加写”
这两个日志不是竞争取代的关系,工作的层级都不同。
最后三步将redolog拆成了两个步骤,这就是两阶段提交
数据库如何实现恢复到“半个月前的任意时刻”这个功能?比如要恢复到昨天的状态。然后找到前天的一次备份,在根据binlog把数据追加写进去,就恢复了。
(为什么要两阶段提交)假设当前 ID=2 的行,字段 c 的值是 0,再假设执行 update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,会出现什么情况呢?
1.先写 redo log 后写 binlog。还没写binlog的时候突然crash,redolog可以把数据恢复但这次事务作废binlog没有写入硬盘。
2.先写 binlog 后写 redo log。还没写redo log的时候crash,本次事务无效但bin log已经记录,恢复的时候会多一次事务导致数据错误。
单独的redo log其实有crash safe能力,两阶段提交可以保证大家同生共死。最后一步事务提交的时候才会一次性写入binlog.
本质上是因为 redo log 负责事务; binlog负责归档恢复; 各司其职,相互配合,才提供(保证)了现有功能的完整性;
事务就是保证对数据库的一系列操作要么全部成功要么全部失败。是在引擎层实现的。
特性:ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性)
当数据上有多个事务在执行的时候,就会出现脏读,幻读,不可重复读等问题,为了解决这样的问题就有了事务隔离这样的概念
读未提交:一个事务没提交的数据别的也能看到
读已提交:提交的才能看到
可重复读:只能读到事务刚开启的数据
串行化:读写都会分别加锁,只有当释放了才会继续执行
在实现上,数据库会创建一个视图。读未提交没有这个概念,读已提交会在每次sql开始执行的时候创建视图,可重复读会在在每次事务开启的时候创建一个视图,能不能读到数据以视图为准。
视图就是个虚拟的表(从若干表查询出来的数据会保存为视图),数据库中只有视图的定义。视图会与相关的表进行连接,返回查询结果。
每条记录在更新的时候都会同时记录一条回滚操作。最新值可以通过回滚值回滚到之前的值
假设一个值从 1 被按顺序改成了 2、3、4,在回滚日志里面就会有类似下面的记录。
不同的视图有不同的值,多个版本可以同时存在,这就是数据库的多版本并发控制(MVCC)。
回滚日志会在系统判断其不需要的时候删除。当没有比当前回滚日志更早的read-view(视图)的时候。
为什么尽量不要使用长事务?
长事务里面会有很多的事务视图,这些视图事务会用到的任何回滚记录必须保留,就意味着回滚日志会很大,占用很多存储空间,而且长事务还会占用锁资源,拖垮整个库。
索引类似于书的目录(就是一种用来排序的数据结构),三种常见的数据结构分别是哈希表,有序数组和搜索树。
用的也是拉链法解决问题 数据是直接加的 所以做区间查找会很慢 适用于等值查询的场景
如其名,查询和区间查找会很快,但是更新就难搞了 所以只适用于静态存储引擎,
特点:所有左子树小于父子树,右子树大于父子树,为了维持O(log(N)) 的查询复杂度,所以往往是平衡二叉树。(平衡二叉树是一种二叉排序树,或者为空,或者满足以下条件: 1)左右子树高度差的绝对值不大于1; 2)左右子树都是平衡二叉树。 平衡因子:左子树的高度减去右子树的高度,显然,在平衡二叉树中,每个结点的平衡因子的值为-1,0或1。)
但是存储一般不用二叉树,因为再访问的时候就 要耗时很久,一般用n叉树,比如n为1200,树高为四就是就是1200^4,而树根的数据块一般再内存里,所以访问最多三次。而第二层一般也在内存里,所以实际上耗时更少。
在InnoDB中,表都是根据主键顺序以索引的形式存放的。(索引组织表)又因为InnoDB引擎采用了B+树索引模型,所以数据就是以B+树的形式存储。每一个索引对应着一棵B+树。
一个表的数据就放在一棵树中
假设,我们有一个主键列为 ID 的表,表中有字段 k,并且在 k 上有索引。表中 R1~R5 的 (ID,k) 值分别为 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),两棵树的示例示意图如下。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XLd7dUFJ-1686144337339)(C:\Users\44110\Pictures\八股\dcda101051f28502bd5c4402b292e38d.png)]
主键索引(聚簇索引): key:主键的值,value:整行数据。 叶子节点存的是整行数据。
普通索引(二级索引): key:索引列的值, value:主键的值。叶子节点内容是主键的值。
如果是普通索引就要多搜一棵树(回表),所以尽量使用主键查询。
b+树会在插入新值的时候维护索引的有序性。b+树中的数据以键值对的形式存在数据页中,除了根节点和叶子节点(最底层的节点),其他节点都至少可以放有两个数据页的键值对。如果更新的时候数据页满了就会创建新的数据页,但是这会降低利用率。但当利用率过低也会合并。
在典型的 KV 场景中(只有且仅有一个的索引),则最好使用业务的唯一字段作为主键。就不用考虑其他索引的叶子节点大小的问题了。但是其他场景(身份证)中用唯一字段做主键,会造成普通索引占用的空间变大。
在二级索引中,key已经覆盖了我们的查询请求,就叫覆盖索引。
减少回表的次数,提升查询性能,所以是一个常用的优化性能手段。
b+树这种索引结构,可以利用索引的“最左前缀”来定位记录
比如还是居民信息身份证这个例子,这里建立了姓名和年龄的联合索引,我要想找张三,就可以直接找到ID4然后一路往下走,“张”也是同理。
那就要考虑在建立联合索引的时候,如何考虑索引顺序?
优先考虑可以少维护一个索引的情况,但如果a,b,(a,b)都存在,那就要考虑的是如何让空间变的最小。
在mysql5.6引入的,在索引遍历的过程中,先根据索引过滤掉不合格的记录,减少回表的次数。
eg.名字第一个字是张,而且年龄是 10 岁的所有男孩
无索引下推执行流程
索引下标执行流程
因为年龄不对,直接减少了两次回表。
设计锁就是为了处理并发问题,分为行锁 表锁 全局锁。
就是让整个库处于只读状态,通常用于全逻辑备份的时候,但仅仅只有这个命令来备份的话(从主库入手导致业务直接停摆,从库入手导致无法接收主库的binlog导致数据不一致)有问题。
引入前面视图的概念,将状态设为可重复读,配合MVCC,就可以顺利备份。
但之所以要有这个锁就是因为自带的MyISAM数据库不支持事务
为什么不使用set global readonly=true 的方式呢?
一.redaonly会被用来做其他逻辑,比如判断一个库是主库还是备库。修改这个影响更大
二.异常处理上有差异。如果在执行FTWRL之后数据库异常断开,其会自动释放锁,而readonly不会,导致长期处于不可写状态。
MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。
1.表锁
//语法
lock tables … read/write
//可以用unlock tables释放锁,在客户端断开的时候也会主动释放
eg.lock tables t1 read, t2 write;
其他表 写t1读t2的句子都会被阻塞,就算拿了这个锁的线程也只能执行读t1写t2的操作,写t1都不被允许。表锁是最常用的处理并发的方式,但实际开发中用的很少,锁住整个表的影响还是太大,而且还有行级锁。
2.元数据锁MDL(metadata lock)
在 MySQL 5.5 版本中引入了 MDL,这个不需要显示的加,在增删查的时候会加个读锁,在修改表结构的时候会加个写锁。锁会在当前事务结束的时候自动释放。读锁之间不互斥,读写锁,写锁之间会排斥。
这就出现了一个问题,当多个事务开启的时候,前面几个事务正常读没事,突然来了个事务要对表的结构进行修改,但是前面的锁没有把读锁释放,写锁进入等待,申请MDL锁的操作会形成一个队列,队列中写锁获取优先级高于读锁。一旦出现写锁等待,不但当前操作会被阻塞,同时还会阻塞后续该表的所有操作。事务一旦申请到MDL锁后,直到事务执行完才会将锁释放。线程很快就会爆满,就gg.
如何安全的给表加字段呢?
首先看看有没有长事务。如果有长事务,要不等等再DDL,要不kill掉。如果是一个更新频繁的热点事务,就设置个超时时间,超时就之后人工试,当然拿到写锁是最好的。
行锁就是针对数据表中行记录的锁。这很好理解,比如事务 A 更新了一行,而这时候事务 B 也要更新同一行,则必须等事务 A 的操作完成后才能进行更新。
1.两阶段锁协议
在InnoDB事务中,行锁是在需要的时候才加上的,但也是在事务提交后才释放,这就是两阶段锁协议。
所以如果一个事务要锁住多个行,就要把可能造成锁冲突的,影响并发度的往后放。
比如顾客买票(顾客得到票,商家得到马内,记入日志)。在一个顾客买的时候另外一个顾客也买了就要等(要和商家交互)。
2.死锁及检测
解决死锁的两种策略:
设置超时时间
第一点不能设置太长,你卡住别的事务跟着一起卡住,这是不可接受的;第二点也不能设的太短,太短了可能会跟其他操作冲突(简单的锁等待)。
在 InnoDB 中,innodb_lock_wait_timeout 的默认值是 50s
发起死锁检测,主动回滚死锁链条,让其他事务得以执行。
nnodb_deadlock_detect 的默认值本身就是 on
但是也有问题,每个新来的被堵住的线程都会判断自己是否死锁了,这是一个时间复杂度是 O(n) 的操作。当高并发的时候很耗cpu,导致cpu占用率很高却没有几个事务执行。
如何解决这种热点行更新导致的性能问题呢?
1.临时把死锁检测关掉,但是风险太大。
2.控制并发度。不能在客户端控制,要在数据库服务端做。有中间件就在中间件实现,要不就是修改mysql源码。
3.还可以从设计上优化,一行改成逻辑上的多行来减少锁的冲突。比如影院一个改成十个余额,买票随机选一个加上。
在Mysql中,视图有两个概念:
view 一个用查询语句定义的虚拟表。在调用的时候执行查询语句并生成结果。
另一个是实现mvcc用到的一致性视图(consistent read view)。用于支持读已提交和可重复读隔离级别的实现。没有物理结构,用来定义“我能看到什么数据”
一个是在执行第一句查询语句时才会创建视图,一个是事务启动时就创建视图
如果是可重复读隔离级别,事务 T 启动的时候会创建一个视图 read-view,之后事务 T 执行期间,即使有其他事务修改了数据,事务 T 看到的仍然跟在启动时看到的一样。
快照就是在事务启动的时候拍了个照片,而且还是基于整个数据库的。快照配合回滚日志使用(undo log),回滚日志就是如图的虚线(u1,u2,u3),v1v2v3并不是真实存在的,还是根据日志算出来的。每一行数据都有多个版本,row trx_id来区分,这就是每个事务把自己的ID赋值给当前数据版本,用来区分版本以便判断是否可见(可重复读)。旧的版本要保留。
可重复读的事务就是“以当前为准,能看到之前事务的数据,在我执行期间其他的事务对我不可见”。
InnoDB会给每个事务生成个数组。用来保存在这个事务启动的瞬间,当前正在活跃的所有事务的ID。数组里面事务ID的最小值记为低水位,最高值为当前系统已经创建过的事务ID的最大值加1。高水位和这个数组构成了当前事务的一致性视图。
低水位之前都能看到,黄色的只有自己的(不在数组中的)更改能看到,高水位看不到。
一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:版本未提交,不可见;版本已提交,但是是在视图创建后提交的,不可见;版本已提交,而且是在视图创建前提交的,可见。
**更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。**select如果加锁的话(lock in share mode 或 for update),也是当前读。
事务的可重复读的能力是怎么实现的?
可重复读的核心是一致性读,而当update的时候只能当前读(在读到的最新值上操作)。如果行锁被拿走,只能进入锁等待。读提交差不多,但是他的视图是每次语句执行的时候创建的,总是能读到已经执行语句的最新版本。
类似可重复读,B就能读到3,而A就只能读到2。
情景:
假设你在维护一个市民系统,每个人都有一个唯一的身份证号,而且业务代码已经保证了不会写入两个重复的身份证号。要不在身份证上建立索引但是这会导致主键值大,要不就是建立普通索引。
假设,执行查询的语句是 select id from T where k=5。
先是通过B+树的树根(最上面的一层),按层搜索到叶子节点。然后通过二分法来定位记录。
如果是唯一性索引,查到满足条件的记录就会停止。普通索引会继续往下寻找,直到找到第一个不满足k=5的记录。但是这不会带来什么性能差距。InnoDB中的数据都是按数据页为单位来读写的。也就是说读取一条记录的时候是将整个页读入到内存的,很少会碰到不在同一个页的情况。
change buffer(改变缓存):当更新一个数据页时,如果这个数据页就在内存里面,就直接更新。不在的话InnoDB就会将数据写入change buffer,当下次把这个数据页读入到内存的时候在进行更改(这个过程称为merge,系统有后台线程会定期merge,在数据库正常关闭的时候也会执行merge),保证数据的一致性。但是change buffer也可以持久化,在内存中有拷贝,也会被写入到磁盘中。
什么时候才能使用change buffer?
对于唯一索引来说基本上没有必要。比如要插入一个记录,必须把数据页读入到内存区判断是否唯一,都读入了就没有必要用change buffer了,直接更新内存会更快。而普通索引直接将更新记录记入到change buffer中就结束了。
将数据从硬盘读入内存中涉及随机IO访问,是数据库里面成本最高的操作之一。
而 change buffer 的主要目的就是将记录的变更动作缓存下来,所以在一个数据页做 merge 之前,change buffer 记录的变更越多(也就是这个页面上要更新的次数越多),收益就越大。
如果一个业务在插入之后马上就查询,将更新记录在change buffer上,因为访问 所以立即触发了merge过程,随机访问IO的次数不会减少,而增加了 change buffer的维护代价。
普通索引和 change buffer 的配合使用,对于数据量大的表的更新优化还是很明显的。但是更新之后立马要查询的话就记得关闭change buffer。
其实,这两类索引在查询能力上是没差别的,主要考虑的是对更新性能的影响。所以,建议尽量选择普通索引。
例子:
buffer pool本就是一片连续的内存空间,是Mysql的一个缓存区域。Page1在内存中就直接更新,pqge2不在就记录到change buffer上。再将上述两个动作记入redo log(一个日记本,记录所有行为)中。
//ibdata1是一个changebuffer持久化表。和redo log同为系统表空间。
如果要简单地对比这两个机制在提升更新性能上的收益的话,redo log 主要节省的是随机写磁盘(将变更后的数据页写入至磁盘中。)的 IO 消耗(内存数据更新之后就可以不用立即执行写磁盘操作,可以等待一批再随机写入磁盘,所以说redo log减少了随机写磁盘的io,把减少的随机写转换成了顺序写入redo log文件中。成顺序写),而 change buffer 主要节省(不是避免)的则是随机读磁盘的 IO 消耗。
使用哪个索引是由mysql决定的,选错索引会导致执行速度变慢
选择索引是优化器的工作,如何选择索引其实就是考虑如何扫描最少的行数。MySQL并不能准确的知道要扫描的行数有多少条,主要看的是索引的区分度(称为“基数”)。
为了得到区分度 InnoDB会采样统计,选择N个页统计不同值得到一个平均值*页面数 = 基数。一般并更的数据行数超过1/M的时候就会重新做一次索引统计。
如果基数太小就会放弃使用索引,但当比较大的时候还要判断执行这个语句本身要扫描多少行(之前是使用这个索引要执行多少行?)。
1.采用force index强行选择一个索引
2.修改语句,引导Mysql选择我们期望的索引。比如order by b limit 1改成order by a,b limit 1这样的话a,b都要排序但是a扫描的行数少索引就会选择a
3.在有些场景下,我们可以新建一个更合适的索引,来提供给优化器做选择,或删掉误用的索引。 情况较少。
eg. 可以通过下面的语句创建前缀索引
mysql> alter table SUser add index index1(email);
或
mysql> alter table SUser add index index2(email(6));
前缀索引会让占用的空间变小,但是有可能会使select变繁琐(比如email同时有好几个zhangs)。所以定义好长度既可以节省空间也不用增加太多额外的成本。定义索引的时候要注意区分度。区分度越高越好(设定一个可以接收的损失比例)。
使用前缀索引就用不了索引覆盖对性能的优化了(因为前缀索引还是要回到别的索引重新去查一次)
1.倒序存储(比如身份证)
2.用hash字段。但是会增加一个字段crc32()
两种方式从占用空间来说没什么区别(hash虽然要消耗额外的存储空间,但倒叙存储不可能只存四个字节,存更多的字节的消耗就差不多),cpu消耗方面一个要调用reverse(),另外一个要额外调用cec32(),reverse()函数额外消耗的cpu资源更少一点;查询效率来看hash字段方式更稳定一点,crc32算出来的值虽然可能冲突但是概率很小,平均扫描行数为1,而倒叙存储毕竟用的还是前缀索引的方式,还会增加扫描行数。
一条sql语句正常执行的时候很快,但有时候突然变慢,就像抖了一下。
flush:把内存里的数据写入磁盘的过程
当内存数据页跟磁盘数据页内容不一致的时候,称这个内存页为“脏页”, 内存也和磁盘页的数据一致叫做干净页。
偶尔抖得那下其实就是在刷脏页。
什么情况下会flush呢?
1.对应的就是 InnoDB 的 redo log 写满了。这时候系统会停止所有更新操作,把 checkpoint 往前推进,redo log 留出空间可以继续写。
2.内存满了要淘汰一些数据页。如果发现要淘汰脏页,就先flush再放到硬盘中(如果直接覆盖得话,下次在读入内存就要判断是否有redo log,既然都要flsuh不如顺便flush了 这样还能保证从硬盘中读取得数据都是最新的)。
3.mysql认为空闲的时候。当时即使是忙碌的时候也会找时间刷脏页。
4.mysql正常关闭的时候也会把内存中的脏页都flush到磁盘上。
分析一下性能影响
第一种情况是InnoDB尽量要避免的,因为这样会导致更新堵住。
第二种情况为常态 InnoDB用缓冲池(buffer pool)管理内存,缓冲池中内存中的页有三种状态:未使用;使用且为干净页;使用了为脏页。
InnoDB的策略是尽量使用内存,当要读入的数据没有在内存的时候,就必须要到缓冲池申请一个数据页。这里就把最久不适用的数据页淘汰掉。如果是干净页就直接复用,脏页就必须刷到磁盘再复用。
第三第四种情况,人一般不会太关注性能问题。
InnoDB要有尽量控制脏页的比例的机制来避免性能的影响
控制刷脏页的速度(刷太慢会出现什么情况?):1.脏页比例(内存脏页太多 )2.redo log 写盘速度(redo log写满)
所以就要关注脏页比例,太高了影响业务。
还有一个策略:在机械硬盘时代,刷脏页的时候会看旁边是否也是脏页,如果是就一起刷了,这大大减少了随机IO的成本。但现在SSD的IOPS(随机读写速度)很高,没必要了。在mysql8.0中,innodb_flush_neighbors 参数的默认值已经是 0了。
一个InnoDB表分为两部分:表数据和结构。表数据已经在MySQL的8.0之后已经把表结构定义放系统数据表中(之前是存在以.frm为后缀的文件里)
设置为off的话就是放在系统共享表空间里,也就是跟数据字典放在一起;设置为on的话每个InnoDB表数据存储在一个以.idb为后缀的文件中。mysqll5.6.6版本开始,它的默认值就是ON,也建议为ON.一个表单独存储为一个文件更容易管理,而且在不需要这个表的时候也可以通过drop table直接删掉。而放在共享表空间中,即使删掉也不会回收。
如果要删除R4这个记录,就是把R4标记为删除,还有可能往R3和R5中间添加记录。如果整个页被删除,就是整个数据页被标记为复用。
数据页的复用和记录的复用是不同的。R4被删除后可以直接复用这个空间但不能移动。比如要插入ID为800的就不行了。但是数据页可以移动。
如果相邻的两个数据页利用率都很小,系统就会把这两个页上的数据合到其中一个页上,另外一个数据页就被标记为可复用。如果用 delete 命令把整个表的数据删除呢?结果就是,所有的数据页都会被标记为可复用。但是磁盘上文件不会变小。
delete只是把记录/数据页标记为“可复用”,但磁盘文件大小是不会变的。没有使用的空间看起来就会像空洞。
如上图,插入数据也会造成空洞。比如要插入一个ID为550的值,但是pageA已经满了,就会裂成两个表,ID为550和600的在一张表,这样就导致了pageA会有空洞。
而更新数据(增加一个数据删除一个数据)也会造成空洞。
而去除这些空洞就达到节省空间的目的,这个过程称为重建表。
说白了,就是新建一个与表A结构相同的表,按主键递增的顺序把数据从表A复制到表B过去。再用表B替换表A。
alter table A engine = InnoDB
5.5之前的重建表就是上述操作,但缺点是不能同时对表DDL。5.6开始引入online DDL对操作流程优化
图4 online DDL
1.建立一个临时表,扫描表A主键的所有数据页;
2.用数据页中表A的数据生成B+树,存储到临时文件中;
3.在2的过程中,将对A所有的DDL存放在一个日志文件(row log)中,
4.完成2之后将日志文件里的操作作用到临时文件,就是图中state3
5.用临时文件取代A
在2之前alter语句就将锁释放了,真正持有锁的时间很短,可以看成是online的。加 MDL 写锁的目的是保证在一些准备动作还未完成之前,主表不允许做任何修改或读取,之后降级是允许其他线程 DML,因为这时 log 文件已经就绪,他们的 DML 都会进入 log 文件中。
在图四中tmp-file始终都是在InnoDB中,是原地操作,称之为inplace(5.5临时表是在server层)
InnoDB 引擎就麻烦了,它执行 count(*) 的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数。而自带的MyISAM引擎把一个表的总行数存放在了磁盘上。
什么 InnoDB 不跟 MyISAM 一样,也把数字存起来呢?
因为MVCC的原因该返回多少行是不确定的。只能通过一行一行判断来计算。但也是有优化的,MySQL 优化器会找到最小的那棵B+树来遍历(小的遍历快)在保证逻辑正确的前提下,尽量减少扫描的数据量,是数据库系统设计的通用法则之一。
TABLE_ROWS 不能代替 count(*) ,因为它也是通过采样估算(索引统计)得来的,官方文档说误差可能达到 40% 到 50%。所以,show table status 命令显示的行数也不能直接使用。
不靠谱。一是因为缓存可能异常重启导致数据丢失;即使正常工作,这个值还是逻辑上不正确的。无论是先加一在计数还是先计数在加一,都不行。在并发系统里面,我们是无法精确控制不同线程的执行时刻的。
用缓存系统保存计数有丢失数据和计数不精确的问题。那么,如果直接把计数放到数据库里单独的一张计数表 C 中。既解决了崩溃丢失问题(日志)有解决了计数精确问题(利用事务的特性)。
把计数放在 Redis 里面,不能够保证计数和 MySQL 表里的数据精确一致的原因,是这两个不同的存储构成的系统,不支持分布式事务,无法拿到精确一致的视图。而把计数值也放在 MySQL 中,就解决了一致性视图的问题。
count() 是一个聚合函数,对于返回的结果集一行行地判断,如果 count 函数的参数不是 NULL,累计值就加 1,否则不加。最后返回累计值。所以,count(*)、count(主键 id) 和 count(1) 都表示返回满足条件的结果集的总行数;而 count(字段),则表示返回满足条件的数据行里面,参数“字段”不为 NULL 的总个数。
速度上count(*)=count(1)>count(主键id)>count(字段)
count(*)专门做过优化,直接取行数,不读值。
count(1)取出每行后赋值1 判断不为Null后计数。
count(主键)把每一行id拿出来给server层,server判断不为空后按行累加。
count(字段) 如果定义为not null的话,一行行地从记录里面读出这个字段判断不是空的就直接短路累加;如果定义为允许null,要判断一下,只累加不为空的。
为了避免全表扫描,需要在city字段加上索引(区分度)
MySQL 会给每个线程分配一块内存用于排序,称为 sort_buffer。
eg.假设你要查询城市是“杭州”的所有人名字,并且按照姓名排序返回前 1000 个人的姓名、年龄。
先从索引中找到“杭州”,取出整行找到要select的字段放到sort_buffer中,然后从索引city取下一个记录主键的id重复步骤直到不满足条件为止。然后对sort_buffer做快速排序取前1000行返回。
sort_buffer_size,就是 MySQL 为排序开辟的内存(sort_buffer)的大小。如果要排序的数据量小于 sort_buffer_size,排序就在内存中完成。但如果排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。
number_of_tmp_files 表示的是,排序过程中使用的临时文件数。使用外部排序的时候一般使用归并排序,依据
sort_buffer_size大小决定分成多少份临时文件,每一份单独排序后存在在这些临时文件中,然后再把这些有序文件再合并成一个有序的大文件。
刚才的算法有个问题,如果要查询的字段太多,会占用很多临时文件,排序的性能会很差。如果mysql认为要排序的单行长度太大,会采用另一种算法。
rowid 排序多访问了一次表 t 的主键索引。
最后的“结果集”是一个逻辑概念,实际上 MySQL 服务端从排序后的 sort_buffer 中依次取出 id,然后到原表查到 city、name 和 age 这三个字段的结果,不需要在服务端再耗费内存存储结果,是直接返回给客户端的。
如果mysql认为排序内存太小会影响排序效率才会用rowid排序,如果内存足够大优先选择全字段排序,把需要的字段都放在sort_buffer中。这样排序后就会直接从内存里查询结果不需要再去取一遍。
体现了mysql的设计思想:如果内存足够就尽量利用内存,尽量减少磁盘访问
如果能够保证从 city 这个索引上取出来的行,天然就是按照 name 递增排序的话,就不用在排序了。在进一步还可以使用覆盖索引(索引上的信息足够满足查询请求,不需要再回到主键索引上去取数据。)
对于内存表,回表根本不需要从磁盘读数据,优化器没了这一层顾虑,就会优先考虑用于排序的行越小越好,所以会用rowid排序。
mysql> select word from words order by rand() limit 3;//总共有10000行
这里的pos是位置信息,mysql是通过主键(如果主键被删了或者没有主键就是用每一行自己生成的rowid作为主键这也是rowid排序的名字来历)定位“一行数据的”。
这个临时表用的是memory引擎(不是索引组织表),可以认为是一个数组,这个rowid就是数组的下标。
索引组织表(Index Organized Table,IOT)是一种存储在一个索引结构(一种数据结构,用于快速查询和检索数据)中的表。IOT中存储的数据是以主键存储和排序的,而存在堆中的表时无序的。常见的索引组织表有哈希表、有序数组和搜索树。
不是所有临时表都是内存临时表,tmp_table_size这个参数控制了内存临时表的大小,默认为16M.如果超过了就会转成磁盘临时表。磁盘临时表默认使用InnoDB引擎。
使用磁盘临时表的时候,对应的就是一个没有显式索引的 InnoDB 表的排序过程。
在磁盘临时表中没有使用临时文件算法(归并排序算法),而是用了优先队列排序算法。
优先队列排序算法过程:对于准备排序的(R,rowid),先取前三行构造一个堆,取下一行(R’,rowid‘)跟当前堆里最大的R比较,如果R’小于R就换成这个R’这行。整个排序过程中,为了最快地拿到当前堆的最大值,总是保持最大值在堆顶,因此这是一个最大堆。
之前那个杭州那个为什么不用是因为limt1000产生的堆太大了,超过了sort_buffer_size大小,只能用归并算法。
分享三个例子
假设,现在已经记录了从 2016 年初到 2018 年底的所有数据,运营部门有一个需求是,要统计发生在所有年份中 7 月份的交易记录总数。这个逻辑看上去并不复杂,你的 SQL 语句可能会这么写:
mysql> select count(*) from tradelog where month(t_modified)=7;//t_modified这个字段上有索引
但是很久才出结果,因为如果对字段做了函数计算,就不用上索引了。这是mysql的规定.应为对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。但是优化器并不是要放弃使用这个索引
在第一层就不知道怎么办了
由于加了 month() 函数操作,MySQL 无法再使用索引快速定位功能,而只能使用全索引扫描。而且优化器只要发现你使用了函数,也不会考虑使用索引(之前的操作是使用全索引扫描,就是扫描了整个索引的所有值)。
eg.select * from tradelog where id + 1 = 10000 MySQL 优化器还是不能用 id 索引快速定位到 9999 这一行。要改成where id = 10000 -1才可以。
mysql> select * from tradelog where tradeid=110717;
交易编号 tradeid 这个字段上,本来就有索引,但是 explain 的结果却显示,这条语句需要走全表扫描。你可能也发现了,tradeid 的字段类型是 varchar(32),而输入的参数却是整型,所以需要做类型转换。
在Mysql中,字符串和数字作比较的话,是将字符串转换成数字。
对于优化器来说,这个语句相当于
mysql> select * from tradelog where CAST(tradid AS signed int) = 110717;
对索引字段做函数操作,优化器会放弃走树搜索功能。
字符集不同只是条件之一,连接过程中要求在被驱动表的索引字段上加函数操作,是直接导致对被驱动表做全表扫描的原因。
数据库服务器 CPU 占用率很高或 ioutil(IO 利用率)很高,这种情况下所有语句的执行都有可能变慢,不属于我们今天的讨论范围。
大概率是表被锁住了。可以执行show processlist命令,看看当前语句处于什么状态
一.等DML锁
state 为 Waiting for table metadata lock 出现这个状态表示的是,现在有一个线程正在表 t 上请求或者持有 MDL 写锁,把 select 语句堵住了。
找到谁持有DML写锁,然后把它kill掉即可:通过查询 sys.schema_table_lock_waits 这张表,我们就可以直接找出造成阻塞的 process id,把这个连接用 kill 命令断开即可。(MySQL 启动时需要设置 performance_schema=on,相比于设置为 off 会有 10% 左右的性能损失)
二.等flush
已知表t有十万行,每次刷新一秒就是十万秒,然后b表要等A表查询完才能flush。事务C要再次查询就会被b堵住了(state 为Waiting for table flush)
三.等行锁
mysql> select * from t where id=1 lock in share mode;
//in share mode表示当前读,每次读都会读最新的数据。
由于访问 id=1 这个记录时要加读锁,如果这时候已经有一个事务在这行记录上持有一个写锁,我们的 select 语句就会被堵住。可以通过sys.innodb_lock_waits表查到。
mysql> select * from t sys.innodb_lock_waits where locked_table='`test`.`t`'\G
这时候能查到是哪个线程造成堵塞了。但如果用KILL QUERY 4(停止当前正在执行的语句)是不行的,因为之前得语句已经执行完成了。只有kill有效。也就是直接断开这个连接。这里隐含的一个逻辑就是,连接被断开的时候,会自动回滚这个连接里面正在执行的线程,也就释放了 id=1 上的行锁。
mysql> select * from t where c=50000 limit 1;
由于字段c上没有索引,只能走id主键顺序扫描。要扫描五万行。
Rows_examined 显示扫描了 50000 行。11.5 毫秒就返回了,我们线上一般都配置超过 1 秒才算慢查询。但要记住:坏查询不一定是慢查询。
mysql> select * from t where id=1;
虽然扫描行数为1,但执行时间用了800毫秒。
第一个语句的查询结果里 c=1,带 lock in share mode 的语句返回的是 c=1000001。
带lock in share mode就是直接读到当前结果,而不带的话是一致性读,会经历100万次的undo log将1这个结果返回。
undo log 里记录的其实是“把 2 改成 1”,“把 3 改成 2”这样的操作逻辑,画成减 1 的目的是方便你看图。
幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。
如下图:
\1. Q1 只返回 id=5 这一行;
\2. 在 T2 时刻,session B 把 id=0 这一行的 d 值改成了 5,因此 T3 时刻 Q2 查出来的是
id=0 和 id=5 这两行;
\3. 在 T4 时刻,session C 又插入一行(1,1,5),因此 T5 时刻 Q3 查出来的是 id=0、
id=1 和 id=5 的这三行。
其中,Q3 读到 id=1 这一行的现象,被称为“幻读”。也就是说,幻读指的是一个事务在
前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。
注意:
1.在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。因此,幻读在“当前读”下才会出现。( 当前读指的是select for update或者select in share mode,指的是在更新之前必须先查寻当前的值,因此叫当前读。 快照读指的是在语句执行之前或者在事务开始的时候会创建一个视图,后面的读都是基于这个视图的,不会再去查询最新的值。)
2.幻读专指新插入的行,用update更新的语句导致数据不同的不能称作是幻读。
查询语句加了for update,都是当前读。而当前读的规则,就是要能读到所有已经提交的记录的最新值。这跟事务的可见性规则并不矛盾。
首先在语义上破坏了加锁声明
session A 在 T1 时刻就声明了,“我要把所有 d=5 的行锁住,不准别的事务进行读写操作”。而实际上,这个语义被破坏了。
我们知道,锁的设计是为了保证数据的一致性。而这个一致性,不止是数据库内部数据状态在此刻的一致性,还包含了数据和日志在逻辑上的一致性。session B 的第二条语句 update t set c=5 where id=0,语义是“我把 id=0、d=5 这一行的 c 值,改成了 5”。由于在 T1 时刻,session A 还只是给 id=5 这一行加了行锁, 并没有给 id=0 这行加上锁。因此,session B 在 T2 时刻,是可以执行这两条 update 语句的。这样,就破坏了session A 里 Q1 语句要锁住所有 d=5 的行的加锁声明。
还破坏了数据的一致性
在MySQL中,select…for update是为了锁定相关的行,保证在查询期间到释放的时候,相关的行集在这个过程中不被其他会话进行写操作果查询条件带有主键,会锁行数据,如果没有,会锁表。¹²⁴
update 的加锁语义和 select …for update 是一致的。sessionA的第二个语句就是要给d=5的语句加上锁,就是为了要更新数据,新加的这条update语句就是把它认为加上了锁的一行的d值修改成了100.
放入binlog里查看
update t set d=5 where id=0; /*(0,0,5)*/
update t set c=5 where id=0; /*(0,5,5)*/
insert into t values(1,1,5); /*(1,1,5)*/
update t set c=5 where id=1; /*(1,5,5)*/
update t set d=100 where d=5;/*所有d=5的行,d改成100*/
是我们假设“select * from t where d=5 for update 这条语句只给 d=5 这一行,也就是 id=5 的这一行加锁”导致的。
即使把扫描过程中碰到的行都加上了锁,还是会出现问题,因为在给d=5加锁的时候t4的插入还不存在,不存在也就加不上锁。
产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁 (Gap Lock)。
比如初始化六个记录就产生了7个间隙。
这样,当你执行 select * from t where d=5 for update 的时候,就不止是给数据库中已有的 6 个记录加上了行锁,还同时加了 7 个间隙锁。这样就确保了无法再插入新的记录。也就是说这时候,在一行行扫描的过程中,不仅将给行加上了行锁,还给行两边的空隙,也加上了间隙锁。
间隙锁虽然也是加锁实体(间隙),但是和其他锁又不一样。
比如行锁,跟行锁有冲突的是另一个行锁(都想写)
但是间隙锁不一样,跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。间隙锁之间都不存在冲突关系。他们俩都可以往一个间隙加锁(都是保护这个间隙,不允许插入值)
间隙锁和行锁合称 next-key lock,每个 next-key lock 是前开后闭区间。到最大的值,会和supremum(不存在的最大值)形成一个next-key lock.比如(25, +supremum]。
间隙锁是在可重复读隔离级别下才会生效的。所以,你如果把隔离级别设置为读提交的话,就没有间隙锁了。但同时,你要解决可能出现的数据和日志不一致问题,需要把 binlog 格式设置为 row。这,也是现在不少公司使用的配置组合。
正常的短连接模式就是连接到数据库后,执行很少的 SQL 语句就断开,下次需要的时候再重连。如果使用的是短连接,在业务高峰期的时候,就可能出现连接数突然暴涨的情况。
一.如果是连接数过多,你可以优先断开事务外空闲太久的连接;如果这样还不够,再考虑断开事务内空闲太久的连接。
二.是让数据库跳过权限验证阶段。但危险系数很大。
有三种可能引发慢查询(就是查询慢,超过了long_query_time参数设定的时间阈值)的问题:
1.索引没有设计好
这种场景一般就是通过紧急创建索引来解决。MySQL 5.6 版本以后,创建索引都支持 Online DDL 了,对于那种高峰期数据库已经被这个语句打挂了的情况,最高效的做法就是直接执行 alter table 语句。
2.语句没写好
比如之前的没有用上索引的问题,函数问题。
比如,语句被错误地写成了 select * from t where id + 1 = 10000,你可以通过下面的方式,增加一个语句改写规则。
mysql> insert into query_rewrite.rewrite_rules(pattern, replacement, pattern_database) values ("select * from t where id + 1 = ?", "select * from t where id = ? - 1", "db1");
call query_rewrite.flush_rewrite_rules();//这个存储过程,是让插入的新规则生效,也就是我们说的“查询重写”。
3.Mysql选错了索引
force index。同样地,使用查询重写功能,给原来的语句加上 force index,也可以解决这个问题。
1.一种是由全新业务的 bug 导致的。假设你的 DB 运维是比较规范的,也就是说白名单是一个个加的。这种情况下,如果你能够确定业务方会下掉这个功能,只是时间上没那么快,那么就可以从数据库端直接把白名单去掉。
2.如果这个新功能使用的是单独的数据库用户,可以用管理员账号把这个用户删掉,然后断开现有连接。这样,这个新功能的连接不成功,由它引发的 QPS 就会变成 0。
3.如果这个新增的功能跟主体功能是部署在一起的,那么我们只能通过处理语句来限制。这时,我们可以使用上面提到的查询重写功能,把压力最大的 SQL 语句直接重写成"select 1"返回。
binlog的写入逻辑:事务执行过程中,先把日志写到binlog cache,待事务提交的时候,再把binlog cache写入到binlog文件中。由于一个事务的binlog是不能被拆开的,无论这个事务有多大也要确保一次性写入。这就对binlog cache题除了挑战。
系统给 binlog cache 分配了一片内存,每个线程一个,参数 binlog_cache_size 用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。
事务提交的时候,执行器把 binlog cache 里的完整事务写入到 binlog 中,并清空 binlog cache。
图中的 write,指的就是指把日志写入到文件系统的 page cache,并没有把数据持久化到磁盘,所以速度比较快。
图中的 fsync(文件同步函数,用于将内存中已修改的文件数据写入储存设备。),才是将数据持久化到磁盘的操作。一般情况下,我们认为 fsync 才占磁盘的 IOPS(每秒进行读写操作的次数)。
write 和 fsync 的时机,是由参数 sync_binlog 控制的:
sync_binlog=0 的时候,表示每次提交事务都只 write,不 fsync;
sync_binlog=1 的时候,表示每次提交事务都会执行
fsync;sync_binlog=N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。
设置成较大的值对性能有提升,但是如果主机发生异常重启,会丢失这N个事务的binlog日志。
事务在执行过程中,生成的 redo log 是要先写到 redo log buffer 的。但每次redo log buffer里面的内容不是每次生成后都要持久化到硬盘的。可是部分日志有可能会被持久化到硬盘。
为了控制 redo log 的写入策略,InnoDB 提供了 innodb_flush_log_at_trx_commit 参数,它有三种可能取值:
设置为 0 的时候,表示每次事务提交时都只是把 redo log 留在 redo log buffer 中 ;
设置为 1 的时候,表示每次事务提交时都将 redo log 直接持久化到磁盘;
设置为 2 的时候,表示每次事务提交时都只是把 redo log 写到 page cache。
InnoDB 有一个后台线程,每隔 1 秒,就会把 redo log buffer 中的日志,调用 write 写到文件系统的 page cache,然后调用 fsync 持久化到磁盘。
注意,事务执行中间过程的 redo log 也是直接写在 redo log buffer 中的,这些 redo log 也会被后台线程一起持久化到磁盘。也就是说,一个没有提交的事务的 redo log,也是可能已经持久化到磁盘的。除此之外还有两种场景也会让一个没有提交的事务的redo log写入到磁盘中:
一种是,redo log buffer 占用的空间即将达到 innodb_log_buffer_size 一半的时候,后台线程会主动写盘。(但是应为没有提交,写盘动作只是write,而没有调用fsync)另一种是,并行的事务提交的时候,顺带将这个事务的 redo log buffer 持久化到磁盘。假设一个事务 A 执行到一半,已经写了一些 redo log 到 buffer 中,这时候有另外一个线程的事务 B 提交,如果 innodb_flush_log_at_trx_commit 设置的是 1,那么按照这个参数的逻辑,事务 B 要把 redo log buffer 里的日志全部持久化到磁盘。这时候,就会带上事务 A 在 redo log buffer 里的日志一起持久化到磁盘。
WAL 机制主要得益于两个方面:redo log 和 binlog 都是顺序写,磁盘的顺序写比随机写速度要快;组提交机制,可以大幅度降低磁盘的 IOPS 消耗。
随机写是指将数据写入到磁盘的任意位置,而不是按顺序写入。相比于顺序写,随机写的效率更低,因为它需要寻找可用的磁盘块并进行磁盘寻道。
就是客户端都是直接访问节点A,节点B是A的备库,只是将A的更新都同步过来到本地执行。
B最好设置成readonly:可以防止误操(运营类的查询语句会被放到备库上去查);防止切换逻辑有bug(切换过程中出现双写造成主备不一致);可以用readonly状态来判断节点的角色。而且readonly对超级权限用户是无效的,超级权限就是用于同步更新的线程。
当 binlog_format 使用 row 格式(mysql会自己翻译一遍提交的sql语句)的时候,binlog 里面记录了真实删除行的主键 id;设置为statement格式记录到binlog里的是语句原文,这就可能导致使用的索引不同出现主备不一致的情况;第三种格式,叫作 mixed,其实它就是前两种格式的混合。
因为有些 statement 格式的 binlog 可能会导致主备不一致,所以要使用 row 格式。但row格式很占空间。所以mysql就会自己判断这条sql语句是否会引起主备不一致,如果有可能就用row格式,否则就用statement格式。
现在越来越多的场景要求把 MySQL 的 binlog 格式设置成 row。这么做的理由有很多,我来给你举一个可以直接看出来的好处:恢复数据。
生产上使用比较多的是如图的双 M 结构。主从库分别有个serverid来防止binlog在两个库之间循环执行的问题。
XID是用来联系bin log和redo log的。比如redo log里面有一个事务是prepare状态,但是不知道是不是commit状态,那就可以用XID去bin log里面查询该事务到底有没有提交。有提交则是commit状态,若没有提交则回滚该事务。
在形式上,这个过程就跟我们写程序时的嵌套查询类似,并且可以用上被驱动表的索引,所以我们称之为“Index Nested-Loop Join”,简称 NLJ。
使用 join 语句,性能比强行拆成多个单表执行 SQL 语句的性能要好;如果使用 join 语句的话,需要让小表做驱动表。但前提是被驱动表要有索引,否则也是全表扫描而不是N叉树搜索。
相当于双重for循环硬查,N2。
当驱动表上没有可用的索引时:
把驱动表的数据读入线程内存join_buffer中,扫描被驱动表,把每一行都取出来和join_buffer中的数据做对比,满足条件的作为结果集的一部分返回。
从时间复杂度上来说和上面的那个算法一致,但是这个算法都是在内存中进行的,速度快好多。(simple是查被驱动表,为I/O操作,block是一次性把数据全部取出来到内存中和缓存数据比较)。
join_buffer 的大小是由参数 join_buffer_size 设定的,默认值是 256k。如果放不下驱动表的所有数据话,策略很简单,就是分段放。先放一部分比较,之后在清空join_buffer在比较一遍。
第一个问题:能不能使用 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 不够大的时候(这种情况更常见),应该选择小表做驱动表。所以,这个问题的结论就是,总是应该使用小表做驱动表。
如何定义小表?在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与 join 的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表。
多个单表执行 SQL 语句的性能要好;如果使用 join 语句的话,需要让小表做驱动表。但前提是被驱动表要有索引,否则也是全表扫描而不是N叉树搜索。
相当于双重for循环硬查,N2。
当驱动表上没有可用的索引时:
把驱动表的数据读入线程内存join_buffer中,扫描被驱动表,把每一行都取出来和join_buffer中的数据做对比,满足条件的作为结果集的一部分返回。
[外链图片转存中…(img-eWFUyuyM-1686144337351)]
从时间复杂度上来说和上面的那个算法一致,但是这个算法都是在内存中进行的,速度快好多。(simple是查被驱动表,为I/O操作,block是一次性把数据全部取出来到内存中和缓存数据比较)。
join_buffer 的大小是由参数 join_buffer_size 设定的,默认值是 256k。如果放不下驱动表的所有数据话,策略很简单,就是分段放。先放一部分比较,之后在清空join_buffer在比较一遍。
第一个问题:能不能使用 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 不够大的时候(这种情况更常见),应该选择小表做驱动表。所以,这个问题的结论就是,总是应该使用小表做驱动表。
如何定义小表?在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与 join 的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表。