MySQL高级(索引、查询优化、主从复制)
1.索引
什么是索引?
索引是一种数据结构。索引是为了加速对表中数据行的检索而创建的一种分散的存储结构。
作用:
(1)快速取数据;
(2)保证数据记录的唯一性;
(3)实现表与表之间的参照完整性;
(4)在使用ORDER by、group by子句进行数据检索时,利用索引可以减少排序和分组的时间。
索引分类
单值索引
唯一索引(列数据唯一)
复合索引(多列索引)
哪些情况下需要建立索引?
1.主键自动建立唯一索引
2.频繁作为查询条件的字段应该创建索引
3.查询中与其它表关联的字段,外键关系建立索引
4.不频繁更新的字段适合创建索引,因为每次更新不单单是更新了记录还会更新索引
5.Where条件里用到的字段创建索引
6.单键/组合索引的选择问题,who?(在高并发下倾向创建组合索引)
7.查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度
哪些情况不需要建索引?
1.表数据太少
2.经常增删的表
提高了查询速度,但是降低表更新的速度,因为增删的时候还需要另外保存索引文件
3.列中有太多重复的字段,不用建。 一般100万条记录,distinct=2就没必要建索引了
性能优化ELPLAIN
使用EXPLAIN可以模拟优化器执行SQL查询语句,从而知道MYSQL如何处理sql
id*
- id类似优先级
- id相同时:执行顺序由上到下
- id不同时:如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行。
- id有相同也有不同的:先按照优先级执行id值大的查询,再对id相同的顺序执行
select type
SIMPLE
简单的select查询,
不包含
子查询和union
PRIMARY
查询中若包含任何复杂的
子部分
,最外层查询被标记为PRIMARY
SUBQUERY
在select或者where列表中包含了
子查询
DERIVED
FROM列表中包含的子查询为DERIVED(衍生),MYSQL会
递归
执行这些子查询,把结果放在临时表
里。UNION
若第二个SELECT出现在UNION之后,则被标记为UNION; 若UNION包含在FROM子句的子查询中,外层SELECT将被标记为:DERIVED
UNION RESULT
从UNION表中获取结果的select
type*
显示查询使用了何种类型
从好到坏:system>const>eq_ref>ref>range>index>ALL
ALL:全表扫描
一般得保证查询至少达到range
级别,最好能达到ref
system
表只有一行记录(等于系统表),这是
const
类型的特列,平时不会出现,这个也可以忽略不计const
表示通过索引一次(指定id)就找到了,
const
用于比较primary key
或者unique
索引。因为只匹配一行数据,所以很快。如将主键置于where
列表中,MySQL就能将该查询转换为一个常量
eq_ref
唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于主键或唯一索引扫描
ref
非唯一性索引扫描,返回匹配某个单独值的所有行.本质上也是一种索引访问,它返回所有匹配某个单独值的行,然而,它可能会找到多个符合条件的行,所以他应该属于查找和扫描的混合体.
跟
eq_ref
区别在于,eq_ref
只存在单独行数据,最终返回结果也只有一行。但是ref
不一定,如建立联合索引时查询单独某一列索引值,返回多行数据。eq_ref
是一对一,ref
是一对多range
只检索给定范围的行,使用一个索引来选择行。key列显示使用了哪个索引一般就是在你的where语句中出现了
between、<、>、in
等的查询,这种范围扫描索引扫描比全表扫描要好,因为它只需要开始于索引的某一点,而结束语另一点,不用扫描全部索引。index
虽然all和Index都是读全表,但index是从索引中读取的,而all是从硬盘中读的,通常比ALL快。
总结:从上到下,type本质上指定了查询的范围,越往下查询范围越大,最大的是All,全表扫描,最小的是System,只查询一行,其他的都是查询范围大小的不同。
possible_keys
显示可能应用到这张表上的索引。查询涉及到的字段如果有索引,则索引会被列出,但不一定会被实际查询使用。
key*
- 实际使用的索引,如果为null,则没有使用到索引。
- 查询中若使用了覆盖索引,则该索引仅出现在
key
列表中
keylen
- 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。在不损失精确性的情况下,长度越短越好。
- 显示的值为索引字段最大可能长度,并非实际使用长度,key_len是根据表定义计算而得的,不是通过表内检索出来的。
ref
显示索引的哪一列被使用了,如果可能的话,是一个常数。哪些列被用来查找索引列上的值。
rows*
根据表统计信息及索引选用情况,大致估算出找到所需记录所需要读取的行数。即每张表有多少行被优化器查询
extra*
包含不适合在其他列中显示但十分重要的额外信息
- Using FileSort
MySQL会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取。MySQL中无法利用索引完成的排序被称为文件排序
。最好不要出现Using
FileSort
上面截图中创建了col1、col2、col3
的联合索引,但是order by
的时候第一个直接by col3
,组合索引不能中断,不能跳过,所以就不会走索引了。
- Using temporary
使用了临时表保存中间结果,MySQL在对查询结果排序时使用临时表。常见于order by
和group by
Using FIleSort 和 Using temporary都会严重拖慢Sql语句的查询效率,应该尽量避免
- Using index
表示相应select操作使用了覆盖索引,避免访问了表的数据行,效率比较高。
如果同时出现Using where
,表明索引被用来执行索引键值的查找。
如果没有出现Using where
,表明索引用来读取数据而非执行查找动作。
覆盖索引:或称为索引覆盖。就是select的数据列只从索引中就能获得,不必读取数据行,MySQL可以利用索引返回select列表中的字段,即查询列要被所建的索引覆盖。 辅助键树找到叶子节点直接从内存读取数据,不用再走一遍主键树获取到叶子节点的整行数据,不需要回表查询。
- 如果使用覆盖索引,注意select中只取出需要的列,不可select *。因为如果将所有字段一起索引会导致索引文件过大,查询性能下降。
- 字段中的顺序和覆盖索引的顺序一致,不能跳过某一列
- Using where
- Using join buffer:使用了连接缓存
- impossible where : where的值总是false,不能用来获取任何元组。
- select tables optimized away: 在没有Group
by子句的情况下,基于索引优化MIN/
MAX操作或者对于MyISAM优化了
count(*)
操作,不必等到执行阶段再进行计算,查询执行计划生成的阶段即完成优化。- distinct:优化distinct操作,在扎到第一匹配的元组后即停止查找动作。
配置文件
log-bin:二进制文件,用于主从复制
log-err:记录严重错误信息,开启会影响性能
log:查询日志,开启会影响性能
数据文件
.frm
文件:存放表结构
mysql8.0表结构文件都以
.ibd
结尾
.myd
文件:存放表数据.myi
文件:存放表索引
MySQL逻辑架构
连接层、服务层、引擎层、存储层
show engines;
:查看所有存储引擎
show variables like '%storage_engine%'
:查看存储引擎
MyISAM于Innodb区别
具体细节可查看博客https://blog.csdn.net/qq_35642036/article/details/82820178
性能下降的原因
- 查询语句不够好
- 索引失效
- 关联查询太多join(设计缺陷或不得已的需求)
- 服务器调优及各个参数
Sql执行顺序
我们写的sql:
机器读到的sql:
Join理论
MySQL不支持全外连接,使用
union
联合left join
和right join
,union自带去重
索引优化
单表优化案例
1 |
|
查询语句:select id,author_id from article where category_id = 2 AND comments > 1 ORDER BY views DESC limit 1;
结果看似没有问题,而且很快
但是我们用Explain查看一下执行过程
发现type
是全表查询,而且最后使用了文件内排序
,两者都会非常影响性能,所以这不是一条好的查询语句。
优化过程
通过show index from arcticle
只有主键索引,所以我们尝试添加索引。
- 由于where条件后面查询有字段,首先我们尝试对where后的字段建立索引
create index idx_article_ccv on article(category_id,comments,views);
此时已经建立了三个索引。
再次使用explain 进行查看
发现type
已经变为range
,说明检索的范围已经缩小,达到了优化的效果。但是Using filesort
仍然存在,我们看看还能不能继续优化呢?
如果将
comment > 1
改为comment = 1
,那么不会出现filesort
,组合索引进行范围查找会导致索引部分失效。对于(a,b,c)组合索引,是一个以a字段排序而b与c相对有序的B+树,引擎可以通过二分查找定位到a=1的数据,b在a=1确定得情况下是有序的(所以b的有序是相对的),依然可以通过二分查找取出所有b大于2的数据,但这些数据的b字段可能有很多个不同的值,所以c字段是无序的,无法用二分查找来查询c=3的数据,故c用不到索引。
所以 a = 1用到了索引,b>2用到了索引,c=3用不到索引,因为b相对于a确定值,c相对于b确定值,但b的值不唯一。
所以我们删掉之前的索引drop index idx_article_ccv on article;
;
既然comment不能确定,那我不给他建,只给另外两个建索引不就不会失效了吗?
create index idx_article_cv on article(category_id,views);
;
然后再执行查询语句
发现type
变成了ref
,Extra
里的Using filesort
也不见了!说明这个方法很成功!
那我能不能单独对comments
字段加个索引呢?
create index idx_article_cmt on article(comments);
最后发现虽然可用的索引增加了,但是最终用到的索引仍然只有一个,所以我们对comments
字段新加的索引没起到作用。
backward index scan:倒叙索引范围查找,使用
DESC
时会出现,MySQL8.0后特有的倒叙索引。实现原理:
上图是MySQL Innodb的B+树结构,我们只关注叶子节点,发现不同于传统的B+树,叶子节点间变成了双向链表,因此范围查询时不仅能从前往后,也能从后往前。所以倒叙索引就是基于此实现的从后往前遍历。
降序索引的引入,MySQL 8.0再也不会对group by操作进行隐式排序
两表优化案例
1 |
|
查询语句:explain select * from class left join book on class.card = book.card;
分析结果:
查询type
是全表查询,需要优化
优化过程
对于此左连接查询,首先我们尝试给右表book
连接的字段加索引create index idx_book_card on book(card)
再次查询
发现右表查询范围缩小了,有优化效果。
那么我们给主表的连接字段加上索引呢?
drop index idx_book_card on book;
create index idx_class_card on class(card);
再次查询
虽然type
从all
变成了index
,但是需要扫描的行rows
仍然是20行。优化程度不如在右表的字段上加索引。
总结:当使用左右连接时,如果是左连接,则在右表加索引,如果是右连接,则在左表加索引。因为没有建立索引的表是主表,不管是否建立索引都要全表扫描。
三表优化案例
1 |
|
查询语句:select * from class LEFT JOIN book ON class.card = book.card LEFT JOIN phone ON book.card = phone.card;
查询分析:
发现全部都是全表扫描,性能非常不好。
优化过程
首先根据上一个案例,我们对右表,即book
和phone
的连接字段各自建立索引。
1 |
|
发现总查询次数减少了很多次,优化效果很好。
总结
对于Join语句的优化
- 尽可能减少Join语句中
NestedLoop
循环总次数;永远用小结果集驱动大结果集 - 优先优化
NestedLoop
中的内层循环 - 保证Join语句中被驱动表上的Join条件字段已经被索引
- 当无法保证被驱动表的Join字段被索引且内存资源充足的前提下,可以多使用
JoinBuffer
的设置
索引失效
1 |
|
非全值匹配,违背最左前缀法则
对于上面的表,我们建立了ALTER TABLE staffs ADD INDEX index_staffs_nameAgePos(
name,
age,
pos)
三列的复合索引。
但是当我们查询的时候,依次按照列名进行查询没有问题,只是key_len的长度稍微增多了一些。
但是如果不按照顺序查询
只查询age = 22
的数据,发现索引就失效了。这就违背了最左前缀匹配法则
最左前缀匹配法则:在MySQL建立
联合索引
时会遵守最左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配
。原理:
如图创建一个(a,b)的联合索引,在
节点中是以键值对存储
的,对于a来说,B+树中的排列从左到右是有序的,但是单纯看b的排列却是无序
的。但是我们发现如果a的值确定后,所对应的b的值才是有序的。就解释了如果a的值没有匹配到,那么b列索引就不能确定,只能进行全表扫描,导致索引失效。
但是注意:如果我们只查询name和pos字段,不查询age字段
,索引看起来不会失效.
但是实际用到的索引只有name
一列,后面的索引都失效了,和上面select * from staffs where name = 'z3'
效果一样。
索引列上有计算
如果对索引列进行了计算,会导致索引失效。
存储引擎不能使用索引范围条件右边的列
即如果where
条件有<、>
等范围查询,那么会导致右边的索引全部失效。
尽量使用覆盖索引,减少select *
即查询时尽量只查询想要的字段,减少回表查询次数。
使用!= 或 <>时无法使用索引导致全表扫描
is null 或 is not null 也无法使用索引
like以通配符%开头会导致索引失效
like '%z3'
会导致索引失效
like 'z%3' 和 like 'z3%'
不会导致索引失效
如何解决
'like %abc%'
索引失效的问题?避免使用select * ,对必要的字段建立覆盖索引,如
select name,age,pos from staffs where name like 'z%3'
,对name.age,pos
建立联合索引,注意如果查询的字段没有索引也会导致索引失效。
字符串不加单引号会导致索引失效
如果字符串不加单引号,会进行强制类型转换导致索引失效。
相当于在索引列上做了隐式类型转换
,导致索引失效。
or 有时会导致索引失效
OR
导致索引是在特定情况下的,并不是所有的OR都是使索引失效,如果OR连接的是同一个字段,那么索引不会失效,反之索引失效
小口诀
全值匹配我最爱,最左前缀要遵守; 带头大哥不能死,中间兄弟不能断; 索引列上少计算,范围之后全失效; Like百分写最右,覆盖索引不写星; 不等空值还有or,索引失效要少用; VAR引号不可丢,SQL高级也不难!
查询截取分析
查询优化
发现慢Sql查询的时候一般流程:
- 观察查询的执行情况,保留观察至少一天,查看产生的慢SQL情况。
- 开启慢查询日志,设置阈值,查询时间超过阈值的SQL抓取出来进行留存。
- explain + 慢SQL分析
- show profile
- 运维经理 DBA进行数据库服务器的参数调优
小表驱动大表
即小的数据集驱动大的数据集
对于数据量小的表与数据量大的表关联查询,用exits
性能优于in
。
select ... from TABLE where exists (子查询)
可以理解为将主查询的数据放到子查询中进行验证,根据验证结果来决定主查询的数据结果是否保留。
Order BY优化
Order By
在下面情况中不会Using filesort
:
Order by
语句使用索引的最前列Where
子句和Order By
子句条件组合满足索引最前列
如果不在索引列上,就会使用filesort
,MySQL会启动单路排序和双路排序。
- 双路排序:MySQL4.1之前是双路排序,即扫描两次磁盘进行排序。两次IO非常耗时
- 单路排序:从磁盘读取查询需要的所有列,按照
order by
列在buffer
对它们进行排序,然后扫描排序后的列表进行输出,它的效率更快一些,避免了第二次读取数据并且把随机IO
变成了顺序IO
,但是它会使用更多的空间,因为它把每一行都保存在内存中了。- 但是单路排序也存在问题,由于每次只去固定大小的数据进入buffer,如果数据量太大反而会导致频繁IO。
- 优化策略:增大
sort_buffer_size
、max_length_for_sort_data
Group By优化
group by
实质是先排序后再进行分组,遵循最左前缀匹配。
当无法使用索引列时,增大sort_buffer_size
、max_length_for_sort_data
where高于having,能写在where条件下的就不要写having了。
慢查询日志
慢查询日志用来记录执行时间超过指定阈值的SQL。
指标为long_query_time
,默认为10s。
默认没有开启慢查询日志,如果非调优需要,一般不建议开启该参数,因为开启会进行文件IO读写,会带来一定的性能影响。
查询是否开启:show variables like '%slow_query_log%%';
显示已经开启。
开启关闭命令:set global slow_query_log = 1/0
;只对当前数据库生效,重启后会失效。
如果需要永久生效,需要修改配置文件
添加slow_query_log=1
和slow_query_log_file = ???
;
查看记录sql的阈值:show variables like 'long_query_time%';
如果执行时间正好等于阈值,不会被记录,大于阈值时才会被记录
设置阈值:set global long_quey_time = 10
;
但是设置完后再查看依然没有改变,需要重新开启一个会话,或者使用show global variables like 'long_query_time%';
模拟检查是否生效
我们使用select sleep(4);
来模拟sql执行了4s时间,然后查看慢查询日志是否有该条记录。
结果可以看到该条sql的执行信息,包括执行用户、执行时间、是否加锁、使用的数据库,执行时间戳以及执行的sql语句。
查询当前有多少条慢查询记录:show global status like 'Slow_queries%'
查到目前有一条慢查询sql记录。可以用作系统健康度进行检查的指标。
日志分析工具mysqldumpslow
退出mysql,在控制台界面执行mysqldumpslow --help
可以查看帮助命令。
工具mysql内置安装,如果没有此命令,可以用find /MySQL安装路径 -name mysqldumpslow
查询路径,然后建立软连接ln -s 路径 /usr/bin
;
1 |
|
上面图片结果为查询10条执行时间最长的sql
批量插入数据
1 |
|
但是在这之前我们需要设置一个参数show variables like 'log_bin_trust_function_creators';
;
开启创建函数的权限。
set global log_bin_trust_function_creators = 1;
然后测试调用
可以看到插入50万条数据花费了一分多钟
然后我们查询全部。花费了0.58秒?
Show Profile
是MySQL提供可以用来分析当前会话中语句执行的资源消耗情况,可以用于SQL的调优测量。
默认关闭状态,并保存最近15次的结果。
可以用show variables like '%profiling'
查看
set profiling = on;
开启
模拟慢查询
select * from emp group by id%10 limit 150000;
然后我们通过show profiles
查看
可以看到我们能执行的每一条sql和对应的执行时间
根据每条的执行时间,我们可以通过诊断命令show profile cpu,block io for query [num]
查看
我们可以看到这条sql的完整的生命周期,和每步查询的时间。
除了以上的参数,还有如下:
1 |
|
注意status
如果出现以下的状态,说明情况急需要优化:
1 |
|
我们对4号语句进行分析,发现存在创建临时表
全局查询日志
不要在生产环境开启该功能!
配置启动
配置文件添加
1
2
3general_log=1
general_log_file = 路径
log_output=FILE编码启动
1
2
3
4set global general_log = 1;
set global out_put = 'TABLE';
#查看日志
select * from mysql.general_log;
数据库锁理论
锁的分类
按对数据的操作类型分
- 读锁(共享锁):多个读操作可以同时进行而不被影响
- 写锁(排他锁):当前写操作没有完成前,会阻断其他写操作和读操作。
按对数据操作的粒度分
- 表锁(偏读)
- 行锁(偏写)
- 页锁
读锁
- 表锁:偏向MyISAM引擎,开销小,加锁快;无死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低
表锁读锁案例
1 |
|
手动增加表锁:lock table mylock read,book write;
查看是否加锁:show open tables;
删除锁:unlock tables;
读锁是共享锁,看下面案例
我们使用
lock table mylock read
给mylock
表加一个读锁。最后发现自己和其他会话可以正常读到数据,但是不能对该表进行其他操作(增加、删除、修改)
,其他会话如果需要操作该表,会进入到阻塞
状态,等待锁被释放后再执行。
且对mylock
表加锁后,也不能去查询其他表的数据。
即加共享锁(读锁)的线程只能读自己加锁的表
总之,即读锁会阻塞写,但是不会阻塞读,写锁会把读和写都阻塞
分析表锁定:show status like 'table%'
Table_locks_immediate
:产生表级锁定的次数,表示可以立即获取锁的查询次数,每次获取值+1
Table_locks_waited
:出现表级锁定争用而发生等待的次数(不能立即获取锁,每等待一次锁值+1),越高说明表级锁竞争越严重。
MyISAM读写锁调度是锁优先,因此不适合于做写为主表的引擎。因为写锁后,其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞。
行锁
偏向Innodb引擎,开销大,加锁慢,会出现死锁。锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
与MyISAM最大的不同有两点:
- 支持事务
- 采用了行级锁
MySQL默认支持行锁,但是如果索引失效,会导致行锁膨胀为表锁,造成性能下降,甚至出现阻塞情况!
间隙锁
间隙锁(Next-Key锁):用范围条件(如>、<等)进行检索数据时,Innodb会给符合条件的已有数据记录的索引项加锁,对于范围内但是不存在的记录,叫做“间隙”。
如update [table] set [column] = 'aaa' where id > 2 and id < 6
;如果数据表中不存在2-6中的某一条数据,也不能对其进行添加,即2-6的间隙已经被锁住了。
Innodb解决幻读即采用了间隙锁和MVCC
危害:因为查询过程中进行范围查找会锁定范围内的所有键值,即使不存在。如果需要进行插入某个在范围内不存在的数据项,会导致插入失败,造成性能下降。
锁定一行:select * from table where id = 8 for update
for update
即添加行锁,可以手动锁定某些行,如果我们需要洗数据,我们可以手动先锁定某些行,结束后再去除行锁,不然不加锁洗数据可能结果还是乱的。
主从复制
MySQL的主从复制是一个异步的复制过程(虽然一般情况下感觉是实时的),数据将从一个MySQL数据库(Master)复制到另一个MySQL数据库(Slave),在Master和Slave之间实现整个主从复制的过程是由三个线程参与完成的。其中两个线程(SQL线程和IO线程)在Slave端,另一个线程(I/O线程)在Master端。 要实现MySQL的主从复制,首先必须打开Master端的binlog记录功能,否则就无法实现。binlog: binary log,是主库中保存所有更新事件日志的二进制文件。因为整个复制过程实际上就是Slave从Master端获取binlog日志,然后在Slave上以相同顺序执行获取的binlog日志中的记录的各种SQL操作。
复制的基本原则
- 每个Slave只有一个Master
- 每个Slave只能有一个唯一的服务ID
- 每个Master可以有多个Slave
使用docker配置
使用docker搭建mysql主从集群,顺便复习docker。
先docker images
查看是否有mysql镜像。
然后我们创建容器
docker run -d -p 3307:3306 --name master -e MYSQL_ROOT_PASSWORD=root mysql
主MySQL端口为3307
。
docker run -d -p 3308:3306 --name slave1 -e MYSQL_ROOT_PASSWORD=root mysql
从1端口为3308
docker exec -it master /bin/sh
进入master容器中
- 修改my.cnf
把Docker中的/etc/mysql/my.cnf
文件拷贝到Docker的/var/lib/mysql
目录,及主机的/usr/local/mysql/
目录中
cp /etc/mysql/my.cnf /var/lib/mysql
1 |
|
具体可查看博客https://blog.csdn.net/why15732625998/article/details/80463041
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!