说个前提,接下来说的案例都是基于 InnoDB 存储引擎,且事务的隔离级别是可重复读。
假设有两个事务的执行顺序如下:
可以看到,事务 A 的 update 语句中 where 是等值查询,并且 id 是唯一索引,所以只会对 id = 1 这条记录加锁,因此,事务 B 的更新操作并不会阻塞。
但是,在 update 语句的 where 条件没有使用索引,就会全表扫描,于是就会对所有记录加上 next-key 锁(记录锁 + 间隙锁),相当于把整个表锁住了。
因此,当在数据量非常大的数据库表执行 update 语句时,如果没有使用索引,就会给全表的加上 next-key 锁, 那么锁就会持续很长一段时间,直到事务结束,而这期间除了
select ... from
语句,其他语句都会被锁住不能执行,业务会因此停滞,接下来等着你的,就是老板的挨骂。
并不是。
关键还得看这条语句在执行过程种,优化器最终选择的是索引扫描,还是全表扫描,如果走了全表扫描,就会对全表的记录加锁了。
force index([index_name])
可以告诉优化器使用哪个索引索引下推 (lndex Condition ushdown,简称ICP)是MySQL 5.6 版本引入的-种索引优化技术,它可以在索引遍历过程中,对索引中包含的所有字段先做判断,过滤掉不符合条件的记录之后再回表,可以有效的减少回表次数。
MySQL服务层负责SQL语法解析、生成执行计划等,并调用存储引擎层去执行数据的存储和检索。
索引下推
的下推其实就是指将部分上层(服务层)负责的事情,交给了下层(引擎层)去处理。
我们来具体看一下,在没有使用ICP的情况下,MySQL的查询:
Server
层去检测该记录是否满足WHERE
条件。使用ICP的情况下,查询过程:
联合索引:【name + age + position】
SELECT
*
FROM
employees
WHERE
name like 'wei66%'
AND
age =22
AND
position ='manager
对于联合索引(name,age,position),正常情况按照最左前缀原则,以上SQL语句只会走name字段索引。因为根据name字段过滤完,得到的索引行里的age和position是无序的,无法很好的利用索引。
当一条SQL 使用了索引下推技术后,在explain 执行计划中,Etra 列中出现 Using index condition
的信息。
Mysql 中采用 B+树来做索引结构,在介绍B树和B+树前,我们先来了解前置知识
首先系统会把数据逻辑地址传给磁盘,磁盘控制电路按照寻址逻辑把逻辑地址翻译成物理地址,也就是确定要读取的数据在哪个磁道,哪个扇区。
为了读取这个扇区的数据,需要把磁头放在这个扇区的上面,为了实现这一个点,磁盘会不断旋转,把目标扇区旋转到磁头下面,使得磁头找到对应的磁道,这里涉及到寻道事件以及旋转时间
很明显,磁盘IO这个过程的性能开销是非常大的,特别是查询的数据量比较多的情况下。
所以在InnoDB中,干脆对存储在磁盘块上的数据建立一个索引,然后把索引数据以及索引列对应的磁盘地址,以B+树的方式来存储。当我们需要查询目标数据的时候,根据索引从 B+树中查找目标数据即可
在一个多级存储结构中,数据通常存储在磁盘上,而计算机的操作是在内存中进行的。当我们需要访问存储在磁盘上的数据时,计算机必须先将所需数据从磁盘读取到内存中,然后才能进行进一步的操作。
树是一种常用的数据结构,我们可以使用树来组织存储在磁盘上的数据。树的高度表示从根节点到叶子节点的层数。当树的高度较高时,从根节点到叶子节点的路径就会变长,需要经过更多的中间节点。
当计算机查找存储在磁盘上的数据时,它需要按照树的结构从根节点开始,逐级地在各个节点中查找,直到找到目标数据所在的叶子节点。每一次从磁盘读取一个节点的数据,都需要进行一次磁盘 IO 操作,即将数据块从磁盘读取到内存中。
因此,当树的高度较高时,也就意味着从根节点到叶子节点的路径会更长,需要进行更多次的磁盘 IO 操作。相反,当树的高度较低时,路径较短,需要进行的磁盘 IO 操作次数相对较少。
所以,我们通常希望树的高度尽可能地低,以减少计算机进行磁盘 IO 操作的次数,从而提高数据的读取速度和效率。
二叉查找树,在二叉树的基础上增加了一个规则,左子树的所有节点的值都小于它的根节点,右子树的所有子节点都大于它的根节点。,二叉查找树会出现 斜树
问题,导致时间复杂度增加,因此又引入了一种平衡二叉树,它具有二叉查找树的所有特点,同时增加了一个规则:”它的左右两个子树的高度差的绝对值不超过 1“。平衡二叉树会采用左旋、右旋的方式来实现平衡。
B 树是一种多路平衡查找树,它满足平衡二叉树的规则,但是它可以有多个子树,子树的数量取决于关键字的数量(关键字数+1)
,因此从这个特征来看,在存储同样数据量的情况下,B 树的高度要小于平衡二叉树,磁盘IO的次数更少,数据的读取速度和效率更高
B+树,其实是在 B 树的基础上做的增强,二者最大的区别有两个:
数据分部的更均匀。因为B+树比跳表的检索效率更高,
跳表是通过 二路分治
的方式实现logN。
B+树是通过 多路分治
的方式实现logN。
这一块好多东西要背,专门整理出一篇文章了
举个例子,查询语句如下:
select * from user where id > 1 and name = 'wzx';
id > 1
还是 name = 'wzx'
,优化器根据自己的优化算法选择执行效率最好的方案;举个例子,更新语句如下:
update user set name = 'wzx' where id = 1;
redo log
(确保持久性) ,此时redo log
进入 prepare
状态。binlog
,然后调用引擎接口,提交redo log
为commit
状态。为什么记录完redo log
,不直接提交,而是先进入prepare
状态?
假设先写redo log
直接提交,然后写binlog
,写完redo log
后,机器挂了,binlog
日志没有被写入,那么机器重启后,这台机器会通过redo log
恢复数据,但是这个时候binlog
并没有记录该数据,后续进行机器备份的时候,就会丢失这一条数据,同时主从同步也会丢失这一条数据。
top
命令,找到 cpu 占用过高的进程是否是 mysqldshow processlist
查看当前的会话情况,确定是否有消耗资源的 SQL 正在运行explain
进行具体的分析慢查询:页面加载过慢、接口压测响应时间过长(超过1s)的SQL
慢查询日志记录了所有执行时间超过参数 long_query_time 设置值并且扫描记录数不小于 min_examined_row_limit 的所有的SQL语句的日志,默认未开启。long_query_time 默认为10 秒,最小为 0, 精度可以到微秒。
如果需要开启慢查询日志,需要在MySQL的配置文件 /etc/my.cnf 中配置如下参数:
#慢查询日志
slow_query_log=1
#执行时间参数
long_query_time=2
默认情况下,不会记录管理语句,也不会记录不使用索引进行查找的查询。可以配置如下参数进行开启记录。
#记录执行较慢的管理语句
log_slow_admin_statements =1
#记录执行较慢的未使用索引的语句
log_queries_not_using_indexes = 1
#指定的存储位置
slow_query_log_file = /var/log/mysql/mysql-slow.log
上述所有的参数配置完成之后,都需要重新启动MySQL服务器才可以生效。
可以采用 EXPLAIN
或者 DESC
命令获取 MySQL 如何执行 SELECT 语句的信息
通过key + key_len 两个字段查看是否可能会命中索引
type 这条sql的连接的类型,性能由好到差为NULL、system、const、eq_ref、ref、range、 index、all
如果一条sgl执行很慢的话,我们通常会使用mysal自动的执行计划explain来去查看这条sql的执行情况,比如在这里面可以通过key和key_len检查是否命中了索引,如果本身已经添加了索引,也可以判断索引是否有失效的情况,第二个,可以通过type字段查看sal是否有进一步的优化空间,是否存在全索引扫描或全盘扫描,第三个可以通过extra建议来判断,是否出现了回表的情况,如果出现了可以尝试添加索引或修改返回字段来修复
InnoDB解决幻读问题方案
仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView
我们看到,在RR隔离级别下,只是在事务中第一次快照读时生成ReadView,后续都是复用该ReadView,那么既然ReadView都一样, ReadView的版本链匹配规则也一样, 那么在一个事务中,最终快照读返回的结果也是一样的。
MVCC只是保证了读操作是没有问题的,但是你看到没问题不代表你就可以为所欲为,眼见不一定为实!举个栗子:在【3,7】这个区间中,我通过MVCC可以保证我返回的快照读结果都是一样的对吧,哪怕有人在5下标位置插入元素我也看不到,而如果我看快照读显示5下标是空的,那我在当前事务插入元素在5下标位置,那么不就over了嘛,所以说,我们读到的区间得加个锁,确保他不会在我这个区间中插入或修改数据,这样子实际情况就可以和我快照读结果一致,从而避免幻读问题
间隙锁只存在于RR / 序列化隔离级别下
间隙锁可以在事务读取数据时锁定间隙,从而防止其他事务在该间隙内插入新的数据,避免了幻读的问题。
注意: 间隙锁目的是防止其他事务插入间隙。间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁
串行化是最高级别的隔离级别,它将事务彼此完全隔离,每个事务按顺序逐个执行。当一个事务执行时,MVCC会对所有锁定的行进行排他锁,确保其他事务无法对这些行进行修改。因此没有了并发的概念,就没有事务的问题了
exists
用于对外表记录做筛选。exists
会遍历外表,将外查询表的每一行,代入内查询进行判断。当exists
里的条件语句能够返回记录行时,条件就为真,返回外表当前记录。反之如果exists
里的条件语句不能返回记录行,条件为假,则外表当前记录被丢弃。
select a.* from A awhere exists(select 1 from B b where a.id=b.id)
in
是先把后边的语句查出来放到临时表中,然后遍历临时表,将临时表的每一行,代入外查询去查找。
select * from Awhere id in(select id from B)
子查询的表比较大的时候,使用exists
可以有效减少总的循环次数来提升速度;当外查询的表比较大的时候,使用in
可以有效减少对外查询表循环遍历来提升速度。
相同点:
truncate
和不带where
子句的delete
、以及drop
都会删除表内所有数据。drop
、truncate
都是DDL
语句(数据定义语言),执行后会自动提交。不同点:
在数据库操作中,TRUNCATE和DELETE是可恢复的操作,而DROP是不可恢复的操作。
假设我们有一个名为
orders
的表,包含以下列:order_id
、customer_id
、order_date
和total_amount
。我们想要根据销售总额分组,并找出销售总额大于1000的顾客。
SELECT customer_id, SUM(total_amount) AS total_sales FROM orders WHERE total_amount > 1000 GROUP BY customer_id;
在这个例子中,WHERE子句用于在查询之前对行进行过滤,筛选出销售总额大于1000的订单。然后,根据顾客ID进行分组,并计算每个顾客的销售总额。
SELECT customer_id, SUM(total_amount) AS total_sales FROM orders GROUP BY customer_id HAVING total_sales > 1000;
在这个例子中,我们先将订单表按顾客ID分组,然后计算每个顾客的销售总额。然后,在HAVING子句中,我们筛选出销售总额大于1000的顾客。需要注意的是,HAVING子句在分组之后进行筛选。
分类 | 含义 | 特点 |
---|---|---|
聚集索引(Clustered Index) | 将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据 | 必须有,而且只有一个 |
二级索引(Secondary Index) | 将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键 | 可以存在多个 |
通过二级索引找到对应的主键值,到聚集索引中查找整行数据,这个过程就是回表
通常都是采用覆盖索引来避免回表查询
覆盖索引是指查询使用了索引,返回的列,必须在索引中全部能够找到,主打的就是个避免回表查询
在数据量比较大时,如果进行limit分页查询,在查询时,越往后,分页查询效率越低。我们一起来看看执行limit分页查询耗时对比:
select * from tb sku limit 0,10 (耗时0.00sec) select * from tb sku limit 9000000,10 (耗时11.05sec)
当在进行分页查询时,如果执行 limit 9000000,10 ,此时需要MySQL不通过索引来排序前9000010 记录,仅仅返回 9000000 - 9000010 的记录,其他记录丢弃,查询排序的代价非常大 。
其实以下几种优化方案的设计都是基于触发索引
的理念,最好能触发主键索引,再差也得是个覆盖索引,尽量避免回表
select * from tb_sku t, (select id from tb_sku order by id limit 9000000,10) a ——>这个子查询的结果为一张表 where t.id = a.id;
在进行分页查询时,我们可以记录上次查询的最后一个ID,然后在下次查询时,直接从这个ID开始查询,避免检索起始位置之前的数据
SELECT * FROM table WHERE id > last_id ORDER BY id LIMIT 10;
基于深分页问题的产生缘由,通过将一个表进行水平拆分为多个子表,这样子可以减少每一张表的数据量,降低过滤数据的消耗
索引需要额外占用物理空间
对创建了索引的表进行数据的增加、修改、删除时,会同步动态维护索引,这个部分会造成性能的影响
判断索引是否失效——>执行计划explain
最左前缀法则:查询从索引的最左前列开始,并且不跳过索引中的列。如果跳跃某一列,索引将部分失效(后面的字段索引失效
意思就是说,如果你绑定了ABC三项作为一个联合索引的话,那你查询的过程中。where语句后面,他第一个查询条件必须是A相关的,如果不是A,那么该索引就会失效,如果你的查询语句是以A开头,且包含了C但没有B的话,那么B和C的查询都不会调用到索引,只有A调用到了索引。
不要在索引列上进行运算操作,否则索引将失效
select * from tb user where substring(phone,10,2)= '15'; // substring()属于运算操作
字符串类型字段使用时,不加引号,索引将失效
select * from tb user where phone = 17799990015; // phone字段是varchar型
SELECT * FROM users where username = 123; -- 索引生效 SELECT * FROM users where username = '123';
如果仅仅是尾部模糊匹配,索引不会失效。如果是头部模糊匹配,索引失效。有头有尾也失效
用or分割开的条件,如果or前的条件中的列有索引,而后面的列中没有索引,那么涉及的索引都不会被用到。只有or前后的列都具备索引才能保证索引的正常使用
联合索引中,出现范围查询(>
or <
),范围查询右侧的列索引失效
解决方法很简单,尽量不要单是用一个">"或"<",而是搭配一下"=",这样子用">="或者"<="就可以解决了
第一范式(1NF):
第二范式(2NF): 确保表中的每个数据都与主键直接相关,而非间接相关。一个表可能有多个字段构成主键,当一个表的非主键字段与主键只相关于其中一部分时,就需要进行拆分。这样可以避免数据冗余和插入异常,保证数据的一致性和准确性。
第三范式(3NF): 确保表中的非主键字段之间没有传递依赖关系。当一个表中存在非主键字段之间的传递依赖时,应将其拆分成多个表。这样可以进一步避免数据冗余和更新异常,提高数据的灵活性和可维护性。
针对索引的优化详细看<索引创建原则>
批量插入
如果需要向表中插入大量数据,可以考虑使用批量插入。通过将多个insert语句合并为一个较长的SQL语句一次性执行,有效降低了网络传输和数据库访问的次数,提高了插入操作的效率。
insert into tb test values(1,'Tom'),(2,'Cat'),(3,erry')
手动提交事务
频繁的开启和提交事务会产生很大的资源开销,我们尽量在一个时间内开启事务,专门进行某些操作,最后提交,这样子就减少资源消耗
start transaction;
insert into tb test values(1,'Tom'),(2,'Cat'),(3,'erry');
insert into tb test values(4,'Tom'),(5,'Cat'),(6,'Jerry');
insert into tb test values(7,'Tom'),(8,'Cat'),(9,'Jerry');
commit;
where 子句中避免is null /is not null
SELECT语句务必指明字段名称(避免直接使用select * )
SQL语句要避免造成索引失效的写法尽量用union all代替union ,union会多一次过滤,效率低
避免在where子句中对字段进行表达式操作Join优化 能用innerjoin 就不用left join right join,如必须使用 一定要以小表为驱动
在主从复制中,有一个主数据库(Master) 和 *一个或多个从数据库(Slave) *。主数据库负责处理事务操作(INSERT、UPDATE、DELETE) ,并将这些操作的日志(binlog) 传送给从数据库。从数据库通过解析主数据库的日志并执行相同的操作,在从库上实现数据的同步。
MySQL主从复制的核心就是二进制日志
二进制日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和 DML(数据操纵语言)语句,但不包括数据查询(SELECT、SHOW)语句。
本节内容节选:MySQL 主从同步及延迟原因分析_mysql主从同步问题-CSDN博客
主从数据同步涉及到网络数据传输,由于网络通信的延迟以及从库数据处理的效率问题,就会导致主从数据同步延迟的情况。
正常情况下,在排除网络延迟或丢包的情况下,最直接的影响主从延迟就是从库上sql_thread
执行中转日志(relay log
),而造成原因一般是以下几种:
1. 从库的机器性能比主库要差
比如将20台主库放在4台机器,把从库放在一台机器。这个时候进行更新操作,由于更新时会触发大量读操作,导致从库机器上的多个从库争夺资源,导致主从延迟。
不过,目前大部分部署都是采取主从使用相同规格的机器部署。
2. 从库的压力大
按照正常的策略,读写分离,主库提供写能力,从库提供读能力。将进行大量查询放在从库上,结果导致从库上耗费了大量的CPU资源,进而影响了同步速度,造成主从延迟。
对于这种情况,可以通过一主多从,分担读压力;也可以采取binlog输出到外部系统,比如Hadoop,让外部系统提供查询能力。
3. 大事务的执行
一旦执行大事务,那么主库必须要等到事务完成之后才会写入binlog。
比如主库执行了一条insert … select非常大的插入操作,该操作产生了近几百G的binlog文件传输到只读节点,进而导致了只读节点出现应用binlog延迟。
因此,DBA经常会提醒开发,不要一次性地试用delete语句删除大量数据,尽可能控制数量,分批进行。
4. 主库的DDL(alter、drop、create)
- 只读节点与主库的DDL同步是串行进行,如果DDL操作在主库执行时间很长,那么从库也会消耗同样的时间,比如在主库对一张500W的表添加一个字段耗费了10分钟,那么从节点上也会耗费10分钟。
- 从节点上有一个执行时间非常长的的查询正在执行,那么这个查询会堵塞来自主库的DDL,表被锁,直到查询结束为止,进而导致了从节点的数据延迟。
5. 锁冲突
锁冲突问题也可能导致从节点的SQL线程执行慢,比如从机上有一些select … for update的SQL,或者使用了MyISAM引擎等。
6. 从库的复制能力
一般场景中,因偶然情况导致从库延迟了几分钟,都会在从库恢复之后追上主库。但若是从库执行速度低于主库,且主库持续具有压力,就会导致长时间主从延迟,很有可能就是从库复制能力的问题。
从库上的执行,即sql_thread
更新逻辑,在5.6版本之前,是只支持单线程,那么在主库并发高、TPS高时,就会出现较大的主从延迟
因此,MySQL自5.7版本后就已经支持并行复制了。可以在从服务上设置slave_parallel_workers
为一个大于0的数,然后把slave_parallel_type
参数设置为LOGICAL_CLOCK
主从同步问题永远都是一致性和性能的权衡,得看实际的应用场景,若想要减少主从延迟的时间,可以采取下面的办法:
并行复制
解决从库复制延迟的问题实现MySQL并行复制策略的关键在于使用多个线程同时从主库上读取不同的二进制日志,并将其应用到不同的从库中。为了实现这个目标,需要在MySQL的配置文件中添加一些参数来指定并行复制的参数和线程数。
statement
记录的是 SQL 的原文。好处是,不需要记录每一行的变化,减少了binlog 日志量,节约了 IO,提高性能。由于 sql 的执行是有上下文的,因此在保存的时候需要保存相关的信息,同时还有一些使用了函数之类的语句无法被记录复制。
这个格式的binlog有可能导致数据的不一致性,因为对于一些实时性的SQL,如带有Date()函数的SQL,当他在从库重新执行的时候,主从记录的时间数据是不同的
row【推荐】
mixed
一种折中的方案,普通操作使用 statement 记录,当无法使用 statement的时候使用 row
一样有不一致性