零:基础
第一讲:基础架构:一条SQL查询语句是如何执行的?
MySQL的基本架构示意图
1.MySQL基础架构
大体来说,
MySQL
可以分为
Server
层和存储引擎层两部分。
Server
层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖
MySQL
的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在
这一层实现,比如存储过程、触发器、视图等。
而存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持
InnoDB
、
MyISAM
、Memory
等多个存储引擎。现在最常用的存储引擎是
InnoDB
,它从
MySQL 5.5.5
版本开始成为了
默认存储引擎。
你也可以通过指定存储引擎的类型来选择别的引擎,比如在
create table
语句中使用
engine=memory,
来指定使用内存引擎创建表。不同存储引擎的表数据存取方式不同,支持的功
能也不同。
从图中不难看出,不同的存储引擎共用一个
Server
层
,也就是从连接器到执行器的部分。
连接器负责跟客户端建立连接、获取权限、维持和管理连接。
连接命令一般是这么写的:
mysql -h$ip -P$port -u$user -p
输完命令之后,你就需要在交互对话里面输入密码。虽然密码也可以直接跟在
-p
后面写在命令行中,但这样可能会导致你的密码泄露。
数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接。短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。
2.尽量减少长连接的原因和方案
如果长连接累积下来,可能导致内存占用太大,被系统强行杀掉(
OOM
),从现
象看就是
MySQL
异常重启了
1.
定期断开长连接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开
连接,之后要查询再重连。
2.
如果你用的是
MySQL 5.7
或更新版本,可以在每次执行一个比较大的操作后,通过执行
mysql_reset_connection
来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。
3.为什么尽量不用查询缓存
查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。MySQL也提供了这种“按需使用”的方式。你可以将参数query_cache_type设置成 DEMAND,这样对于默认的SQL语句都不使用查询缓存。而对于你确定要使用查询缓存的语句,用SQL_CACHE显式指定,像下面这个语句一样:
mysql> select SQL_CACHE * from T where ID=10;
MySQL 8.0
版本直接将查询缓存的整块功能删掉了,也就是说
8.0
开始彻底没有这个功能了。
一:索引
第四讲:深入浅出索引(上)
索引的出现其实就是为了提高数据查询的效率,就像书的目录一样。
1.数据库引擎可用的数据结构
1.哈希表只适用于等值查询,因为无序,区间查询的效率很低。哈希表是一种以键-值(key-value)存储数据的结构,我们只要输入待查找的值即key,就可以找到其对应的值即Value。不可避免地,多个key值经过哈希函数的换算,会出现同一个值的情况。处理这种情况的一种方法是,拉出一个链表。
2.有序数组的等值查询和范围查询好,但更新数据麻烦,故只适合静态存储引擎。
3.二叉树(N叉树)
表中
R1~R5
的
(ID,k)
值分别为
(100,1)
、
(200,2)
、
(300,3)
、
(500,5)
和
(600,6)
,两棵树的示例示意
图如下。
主键索引的叶子节点存的是整行数据
。在
InnoDB
里,主键索引也被称为聚簇索引。
非主键索引的叶子节点内容是主键的值
。在
InnoDB
里,非主键索引也被称为二级索引
。
第五讲:深入浅出索引(下)
回表:非主键索引树回到主键索引树的过程
1.谈谈你对覆盖索引的理解
如果一个索引包含(或覆盖)所有需要查询的字段的值,称为“覆盖所有”。优点如下:
1.索引通常远小于数据行的大小,只读取索引能大大减少数据访问量。
2.一些存储引擎(例如:MyISAM)在内存中只缓存索引,而数据依赖于操作系统来缓存。因此,只访问索引可以不使用系统调用(通常比较费时)。
3.对于 InnoDB 引擎,若辅助索引能够覆盖查询,则无需访问主索引。
4.覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。
第九讲:普通索引和唯一索引,应该怎么选择?
1.change buffer及其应用场景
当需要更新一个数据页时,如果数据页
在内存中就直接更新
,而如果这个数据页还
没有在内存中
的话,在不影响数据一致性的前提下,
InooDB
会将这些
更新操作缓存在change buffer中
,这样
就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内
存,然后执行
change buffer
中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正
确性。
将
change buffer
中的操作应用到原数据页,得到最新结果的过程称为
merge
。除了访问这个数据
页会触发
merge
外,系统有后台线程会定期
merge
。在数据库正常关闭(
shutdown
)的过程中,
也会执行
merge
操作。
唯一索引的更新就不能使用change buffer,实际上也只有普通索引可以使用。如果所有的更新后面,都马上伴随着对这个记录的查询,那么你应该关闭change buffer。而在其他情况下,change buffer都能提升更新性能。
2.普通索引和唯一索引,应该怎么选择?
两类索引在查询能力上是没差别的,主要考虑的是对更新性能的影响。
普通索引和
change buffer
的配合使用,对于数据量大的表的更新优
化还是很明显的。
所以,我建议你
尽量选择普通索引。
第十讲:MySQL为什么有时候会选错索引?
1.MySQL优化器选错索引的解决方法
1.对于由于索引统计信息不准确导致的问题,你可以用analyze table来解决。
2.对于其他优化器误判的情况,你可以在应用端用force index来强行指定索引,也可以通过修改
语句来引导优化器,还可以通过增加或者删除索引来绕过这个问题。
第十一讲:怎么给字符串字段加索引?
1.怎么给字符串字段加索引?
1.
直接创建完整索引
,这样可能比较占用空间;
2.
创建前缀索引
,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引;
3.
倒序存储
,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题;
4.
创建hash字段索引
,查询性能稳定,有额外的存储和计算消耗,跟第三种方式一样,都不支持范围扫描。
第十五讲:答疑文章(一):日志和索引相关问题
1.MySQL崩溃恢复时的判断规则
1.
如果
redo log
里面的事务是完整的,也就是已经有了
commit
标识,则直接提交;
2.
如果
redo log
里面的事务只有完整的
prepare
,则判断对应的事务
binlog
是否存在并完整:
a.
如果是,则提交事务;
b.
否则,回滚事务。
2.两阶段提交的不同时刻, 在 MySQL 异常重启会出现什么现象
1.如果在图中时刻A的地方,也就是写入redo log 处于prepare阶段之后、写binlog之前,发生了崩 溃(crash),由于此时binlog还没写,redo log也还没提交,所以崩溃恢复的时候,这个事务会回滚。这时候,binlog还没写,所以也不会传到备库。
2.在时刻B,binlog写完,redo log还没commit前发生crash,崩溃恢复过程中事务会被提交。
第十六讲:“order by”是怎么工作的?
MySQL会给每个线程分配一块内存用于 排序,称为sort_buffer。sort_buffer_size,就是MySQL为排序开辟的内存(sort_buffer)的大小。如果要排序的数据量小于sort_buffer_size,排序就在内存中完成。但如果排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。
1.全字段排序 VS rowid 排序
1.如果
MySQL
认为
内存足够大
,会优先选择
全字段排序
,把需要的字段都放到
sort_buffer
中,这
样排序后就会直接从内存里面返回查询结果了,不用再回到原表去取数据。
2.如果MySQL实在是担心排序内存太小,会影响排序效率,才会采用rowid排序算法,这样排序过 程中一次可以排序更多行,但是需要再回到原表去取数据。
第十八讲:为什么这些SQL语句逻辑相同,性能却差异巨大?
索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。
字符集
utf8mb4
是
utf8
的超集,所以当这两个类型的字
符串在做比较的时候,
MySQL
内部的操作是,先把
utf8
字符串转成
utf8mb4
字符集,再做比较。
二:事务
第三讲:事务隔离:为什么你改了我还看不见?
事务就是要保证一组数据库操作,要么全部成功,要么全部失败。在
MySQL
中,事务支持是在引擎层实现的。你现在知道,
MySQL
是一个支持多引擎的系统,但并不是所有的引
擎都支持事务。比如
MySQL
原生的
MyISAM
引擎就不支持事务,这也是
MyISAM
被
InnoDB
取代
的重要原因之一。
1. SQL标准的事务隔离级别
当数据库上有多个事务同时执行的时候,就可能出现脏读(
dirty read
)、不可重复读(
non-
repeatable read
)、幻读(
phantomread
)的问题,为了解决这些问题,就有了
“
隔离级别
”
的概 念。
1.读未提交(read uncommitted)
:一个事务还没提交时,它做的变更就能被别的事务看到。 最低的隔离级别,可能会导致脏读、幻读或不可重复读;
2.读提交(read committed)
:一个事务提交之后,它做的变更才会被其他事务看到。(Oracle默认隔离级别)
3.可重复读(repeatable read)
:
一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。(InnoDB默认隔离级别)
4.串行化(serializable )
:
对于同一行记录,“
写
”
会加
“
写锁
”
,
“
读
”
会加
“
读锁
”
。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
2.如何用正确的方式避免长事务
长事务可能由回滚段导致大量占用存储空间,
还占用锁资源,也可能拖垮整个库
。
在
autocommit
为
1
的情况下,用
begin
显式启动的事务,如果
执行commit则提交事务
。如果
执行
commit work and chain,则是提交事务并自动启动下一个事务
,这样也省去了再次执行
begin
语
句的开销。同时带来的好处是从程序开发的角度明确地知道每个语句是否处于事务中。
第八讲:事务到底是隔离的还是不隔离的?
更新数据都是先读后写的,而这个读,只能读当前的值,称为 “当前读 ”(current read )。
InnoDB
的行数据有多个版本,每个数据版本有自己的
row trx_id
,每个事务或者语句有自己的一
致性视图。普通查询语句是一致性读,一致性读会根据
row trx_id
和一致性视图确定数据版本的
可见性。
对于可重复读,查询只承认在事务启动前就已经提交完成的数据;
对于读提交,查询只承认在语句启动前就已经提交完成的数据;
而当前读,总是读取已经提交完成的最新版本。
为什么表结构不支持
“
可重复读
”
?这是因为表结构没有对应的行数据,也没有
row trx_id
,因此只能遵循当前读的逻辑。MySQL 8.0
已经可以把表结构放在
InnoDB
字典里了,也许以后会支持表结构的可重复
读。
三:锁
第六讲:全局锁和表锁 :给表加个字段怎么有这么多阻碍?
第七讲:行锁功过:怎么减少行锁对性能的影响?
1.MySQL的锁的分类
数据库锁设计的初衷是处理并发问题。作为多用户共享的资
源,当出现并发访问的时候,数据库需要合理地控制资源的访问规则。
1.全局锁:对整个数据库实例加锁。应用于全库逻辑备份。MySQL提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括 建表、修改表结构等)和更新类事务的提交语句。
2.表级锁:表锁一般是在数据库引擎不支持行锁的时候才会被用到的。MySQL里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。 表锁的语法是 lock tables …read/write。MDL不需要显式使用,在访问一个表的时候会被自动加上。
3.行锁:MySQL的行锁是在引擎层由各个引擎自己实现的。但并不是所有的引擎都支持行锁,比如 MyISAM引擎就不支持行锁。不支持行锁意味着并发控制只能使用表锁。
两阶段锁协议:在InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释 放,而是要等到事务结束时才释放。使用事务时,如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。
2.死锁和死锁解决策略
当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致
这几个线程都进入无限等待的状态,称为死锁。
1.
直接进入等待,直到超时
。这个超时时间可以通过参数
innodb_lock_wait_timeout
来设置。
2.
发起死锁检测
,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事
务得以继续执行。将参数
innodb_deadlock_detect
设置为
on
,表示开启这个逻辑。
第二十讲:幻读是什么,幻读有什么问题?
1.幻读是什么?
幻读指的是一个事务在前后两次查
询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。
2.如何解决幻读?
产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记
录之间的
“
间隙
”
。因此,为了解决幻读问题,
InnoDB
只好引入新的锁,也就是
间隙锁(Gap Lock)。
但是间隙锁的引入会影响系统的并发度,也增加了锁分析
的复杂度
第二十一讲:为什么我只改一行的语句,锁这么多?
间隙锁在可重复读隔离级别下才有效
1.加锁规则
1.
原则
1
:加锁的基本单位是
next-key lock,
next-key lock
是
前开后闭
区间。
2.
原则
2
:查找过程中访问到的对象才会加锁。
3.
优化
1
:索引上的等值查询,给唯一索引加锁的时候,
next-key lock
退化为行锁。
4.
优化
2
:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,
next-key
lock
退化为间隙锁。
5.
一个
bug
:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
四:日志与主备
第二讲:日志系统:一条SQL更新语句是如何执行的?
1.redo log(重做日志)和 binlog(归档日志)的不同
1. redo log
是
InnoDB
引擎特有的;
binlog
是
MySQL
的
Server
层实现的,所有引擎都可以使用。
2. redo log
是物理日志,记录的是
“
在某个数据页上做了什么修改
”
;
binlog
是逻辑日志,记录的
是这个语句的原始逻辑,比如
“
给
ID=2
这一行的
c
字段加
1 ”
。
3. redo log
是循环写的,空间固定会用完;
binlog
是可以追加写入的。
“
追加写
”
是指
binlog
文件
写到一定大小后会切换到下一个,并不会覆盖以前的日志。
当有一条记录需要更新的时候,
InnoDB
引擎就会先把记录写到
redo log
(粉板)里面,并更新内存,这个时候更新就算完成了。同时,
InnoDB
引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做,
有了
redo log
,
InnoDB
就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个
能力称为
c
r
a
sh
-
s
af
e
。
因为最开始
MySQL
里并没有
InnoDB
引擎。
MySQL
自带的引擎是
MyISAM
,但是
MyISAM
没有
crash-safe
的能力,
binlog
日志只能用于归档。而
InnoDB
是另一个公司以插件形式引入
MySQL的,既然只依靠
binlog
是没有
crash-safe
能力的,所以InnoDB使用redo log日志系统来实现crash-safe
能力。
2.redo log的写入采用"两阶段提交"的原因
redo log
的写入拆成了两个步骤:
prepare
和commit
,这就是
"
两阶段提交
"。
redo log
和
binlog
都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保
持数据逻辑上的一致。
第十二讲:为什么我的MySQL会“抖”一下?
当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页 ”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。
第二十三讲: MySQL是怎么保证数据不丢的?
binlog
的写入逻辑:事务执行过程中,先把日志写到
binlog cache
,事务提交的
时候,再把
binlog cache
写到
binlog
文件中。
第二十四讲: MySQL是怎么保证主备一致的?
binlog
有三种格式,一种是
statement
,一种是
row
。
第三种格式,叫作
mixed
,其实它就是前两种格式的混合。
第二十五讲:MySQL是怎么保证高可用的?
最终一致性:正常情况下,只要主库执行更新生成的所有binlog
,都可以传到备库并被正确地执行,备库就能
达到跟主库一致的状态。
MySQL
要提供高可用能力,只有最终一致性是不够的。
1.造成主备延迟的原因及解决方案
1.有些部署条件下,备库所在机器的性能要比主库所在的机器性能差。对策:主备库选用相同规格的机器,并且做对称部署。
2.备库压力大。对策:(1)一主多从。除了备库外,可以多接几个从库,让这些从库来分担读的压力。(2)通过binlog输出到外部系统,比如Hadoop这类系统,让外部系统提供统计类查询的能力。
3.大事务。对策:如不要一次性地用 delete 语句删除太多数据,大表DDL,建议使用gh-ost方案。
第二十九讲: 如何判断一个数据库是不是出问题了?
1.检测一个MySQL实例健康状态的几种方法
1、select 1判断
2、查表判断
3、更新判断
4、内部统计
第三十一讲:误删数据后除了跑路,还能怎么办?
1.怎么处理误删数据
1.
使用
delete
语句误删数据行:
用
Flashback
工具通过闪回把数据恢复回来
2.
使用
drop table
或者
truncate table
语句误删数据表;
3.
使用
drop database
语句误删数据库;
对策: 定期的全量备份,并且实时备份binlog
4.
使用
rm
命令误删整个
MySQL
实例。
2.防止误删的方法
1、账号分离。这样做的目的是,避免写错命令。(如不给truncate/drop权限,使用只读)
2、制定操作规范。这样做的目的,是避免写错要删除的表名。(如删除前改表名)
五:临时表
第十三讲:为什么表数据删掉一半,表文件大小不变?
一个
InnoDB
表包含两部分,即:表结构定义和数据。
因为表结构定义占用的空间很
小,所以我们今天主要讨论的是表数据。
1. 数据库中收缩表空间的方法
如果要收缩一个表,只是
delete
掉表里面不用的数据的话,表文件的大小是不会变的,你还要通过
alter table
命令重建表,才能达到表文件变小的目的。
第十七讲:如何正确地显示随机消息?
如果你创建的表没有主键,或者把一个表的主键删掉了,那么
InnoDB
会自己生成
一个长度为
6
字节的
rowid
来作为主键。
order by rand() 使用了内存临时表,内存临时表排序的时候使用了 rowid 排序方法。
tmp_table_size
这个配置限制了内存临时表的大小,默认值是
16M
。如果临时表大小超过了
tmp_table_size
,那么内存临时表就会转成
磁盘临时表
。
不论是使用哪种类型的临时表,
order by rand()这种写法都会让计算过程非常复杂,需要大量的扫描行数,因此排序过程的资源消耗也会很大。
如果你直接使用
order by rand()
,这个语句需要
Using temporary
和
Using filesort
,查询的执行代
价往往是比较大的。所以,在设计的时候你要量避开这种写法。
第三十四讲: 到底可不可以使用join?
1.关于join语句使用的问题
1.
如果可以使用被驱动表的索引,
join
语句还是有其优势的;
2.
不能使用被驱动表的索引,只能使用
Block Nested-Loop Join
算法,这样的语句就尽量不要
使用;
3.
在使用
join
的时候,应该让小表做驱动表。
第三十五讲:join语句怎么优化?
1. join语句怎么优化?
1. BKA
优化是
MySQL
已经内置支持的,建议你默认使用;
2. BNL
算法效率低,建议你都尽量转成
BKA
算法。优化的方向就是给被驱动表的关联字段加上
索引;
3.
基于临时表的改进方案,对于能够提前过滤出小数据的
join
语句来说,效果还是很好的;
4. MySQL
目前的版本还不支持
hash join
,但你可以配合应用端自己模拟出来,理论上效果要好
于临时表的方案。
第三十六讲:为什么临时表可以重名?
应用:临时表经常会被用在复杂查询的优化过程中。其中,分库分表系统的跨库查询就是一个典型的使用场景。
原因:
由于临时表是每个线程自己可见的,
所以不需要考虑多个线程执行同一个处理逻辑时,临时表的重名问题。在线程退出的时候,临时
表也能自动删除,省去了收尾和异常处理的工作。
六:实用性
第十四讲:count(*)这么慢,我该怎么办?
1.count(*)在不同引擎中的实现方法
1.MyISAM
引擎把一个表的总行数存在了磁盘上,因此执行
count(*)
的时候会直接返回这个数,
效率很高;
2.
InnoDB
引擎执行
count(*)
的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数。(
因为
InnoDB的事务默认的隔离级别是可重复读,在代码上就是通过多版本并发
控制,也就是
MVCC
来实现的。每一行记录都要判断自己是否对这个会话可见,因此对于
count(*)
请求说,
InnoDB
只好把数据一行一行地读出依次判断,可见的行才能够用于计算
“
基
于这个查询
”
的表的总行数。
)
2.不同count的性能问题select count(?) from t
count()
的语义:
count()
是一个聚合函数,对于返回的结果集,一行行地
判断,如果
count
函数的参数不是
NULL
,累计值就加
1
,否则不加。最后返回累计值。
count(*)
、
count(
主键
id)
和
count(1)
都表示返回满足条件的
结果集的总行数
;而
count(
字
段),则表示返回满足条件的
数据行里面,参数“字段”不为NULL的总个数
。
按照效率排序的话,
count(
字段
)<count(
主键
id)<count(1)≈count(*)
,所以我建议
你,尽量使用
count(*)
。
第三十二讲:为什么还有kill不掉的语句?
在
MySQL
中有两个
kill
命令:一个是
kill query+线程id
,表示终止这个线程中正在执行的语句;一
个是
kill connection +线程id
,这里
connection
可缺省,表示断开这个线程的连接,当然如果这个
线程有语句正在执行,也是要先停止正在执行的语句的。
第三十三讲:我查这么多数据,会不会把数据库内存打爆?
1. 我查这么多数据,会不会把数据库内存打爆?
由于
MySQL
采用的是
边读边发
的逻辑,因此对于数据量很大的查询结果来说,不会在
server
端保
存完整的结果集。所以,如果客户端读结果不及时,会堵住
MySQL
的查询过程,但是不会把内
存打爆。
而对于
InnoDB
引擎内部,由于有淘汰策略,大查询也不会导致内存暴涨。并且,由于
InnoDB
对
LRU
算法做了改进,冷数据的全表扫描,对
Buffer Pool
的影响也能做到可控。
第四十四讲: 答疑文章(三):说一说这些好问题
如果需要 left join 的语义,就不能把被驱动表的字段放在 where 条件里面做等值判断或不等值判断,必须都写在 on 里面。