5、索引优化
5.1、单表索引优化案例
CREATE TABLE IF NOT EXISTS article(
id INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
author_id INT(10) UNSIGNED NOT NULL,
category_id INT(10) UNSIGNED NOT NULL,
views INT(10) UNSIGNED NOT NULL,
comments INT(10) UNSIGNED NOT NULL,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL
);
INSERT INTO article(author_id,category_id,views,comments,title,content)
VALUES
(1,1,1,1,'1','1'),
(2,2,2,2,'2','2'),
(1,1,3,3,'3','3');
SELECT * FROM article;
#查询category_id为1且comments大于1的情况下,views最多的id号和author_id
1、没有建立索引的情况
2、对category_id, comments, views都建立索引的情况
3、只对category_id, views建立索引的情况
5.2、两表索引优化案例
CREATE TABLE IF NOT EXISTS class(
id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
card INT(10) UNSIGNED NOT NULL,
PRIMARY KEY(id)
);
CREATE TABLE IF NOT EXISTS book(
bookid INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
card INT(10) UNSIGNED NOT NULL,
PRIMARY KEY(bookid)
);
INSERT INTO class(card) VALUES(FLOOR(1+(RAND()*20)));
#重复上一条共20次
INSERT INTO book(card) VALUES(FLOOR(1+(RAND()*20)));
#重复上一条共20次
1、从下面的explain开始分析
EXPLAIN SELECT * FROM class LEFT JOIN book on class.card=book.card
2、只在右表上添加索引
ALTER TABLE book add INDEX idx_book_card (card)
3、只在左表上添加索引
ALTER TABLE class add INDEX idx_class_card (card)
- type列由 ref 变为了 index;rows 的变化也比较明显
- 这是由左连接的特性决定的。left join 条件用于确定如何从右表搜索行,左表一定都有
- 所以右边是我们的关键点,一定要建立索引
- 同样,right join 的时候应该在左表上建立索引
5.3、三表索引优化案例
CREATE TABLE IF NOT EXISTS phone(
phoneid INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
card INT(10) UNSIGNED NOT NULL,
PRIMARY KEY(phoneid)
)ENGINE=INNODB;
INSERT INTO phone(card) VALUES(FLOOR(1+(RAND()*20)));
#重复上一条共20次
1、三表联合查询
EXPLAIN SELECT * FROM class LEFT JOIN book on class.card=book.card LEFT JOIN phone on book.card=phone.card
2、左连接,在两个右表中都建立索引
ALTER TABLE book add INDEX idx_book_card (card)
ALTER TABLE phone add INDEX idx_phone_card (card)
- 后两行的type都是 ref 且总的 rows 优化很好,效果不错。因此索引最好设置在经常查询的字段中
- 尽可能减少Join语句中的NestedLoop的循环总次数,永远用小结果集驱动大的结果集
- 优先优化NestedLoop的内层循环
- 保证Join语句中被驱动表上Join条件字段已经被索引
5.4、索引失效(应该避免)
CREATE TABLE staffs(
id INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(24)NOT NULL DEFAULT'' COMMENT'姓名',
`age` INT NOT NULL DEFAULT 0 COMMENT'年龄',
`pos` VARCHAR(20) NOT NULL DEFAULT'' COMMENT'职位',
`add_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT'入职时间'
)CHARSET utf8 COMMENT'员工记录表';
INSERT INTO staffs(`name`,`age`,`pos`,`add_time`) VALUES('z3',22,'manager',NOW());
INSERT INTO staffs(`name`,`age`,`pos`,`add_time`) VALUES('July',23,'dev',NOW());
INSERT INTO staffs(`name`,`age`,`pos`,`add_time`) VALUES('2000',23,'dev',NOW());
#填加索引
ALTER TABLE staffs ADD INDEX index_staffs_nameAgePos(`name`,`age`,`pos`)
5.4.1、全值匹配
- 结论:全值匹配指的是,查询的字段按照顺序在索引中都可以匹配到!
- SQL 中查询字段的顺序,跟使用索引中字段的顺序,没有关系。优化器会在不影响 SQL 执行结果的前提下,给你自动地优化,自动按照设定索引的顺序优化SQL语句。
5.4.2、最佳左前缀法则
- 查询字段与索引字段顺序的不同会导致,索引无法充分使用,甚至索引失效!
原因:使用复合索引,需要遵循最佳左前缀法则,即如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。
- 结论 : 过滤条件要使用索引必须按照索引建立时的顺序 , 依次满足 , 一旦跳过某个字段 , 索引后面的字段都无法被使用。
5.4.3、 不要在索引列上做任何计算
不在索引列上做任何操作(计算、函数、(自动 or 手动)类型转换),会导致索引失效而转向全表扫描。
在查询列上使用函数
- 结论:等号左边无计算!等号右边无转换!
5.4.4、 索引列上不能有范围查询
- 结论:之后的索引将失效,所以将可能做范围查询的字段的索引顺序放在最后
5.4.5、尽量使用覆盖索引
结论:尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),减少select *,就是直接在B+树上取值,而避免去磁盘中读取。
5.4.6、使用不等于(!= 或者<>)的时候
mysql 在使用不等于(!= 或者<>)时,有时会无法使用索引会导致全表扫描。
5.4.7、字段的 is not null 和 和 is null
is null, is not null 也无法使用索引
5.4.8、 like 的前后模糊匹配
like以通配符开头(’$abc…’)mysql索引失效会变成全表扫描操作
问题:解决like’%字符串%'索引不被使用的方法?
- 结论:使用覆盖索引解决 like’%字符串%’ 索引失效问题
5.4.9、字符串不加单引号索引失效
5.4.10、少用or,用它连接时会索引失效
解决办法:使用 union all 或者 union 来替代
5.4.11、练习
假设 index(a,b,c)
Where 语句 | 索引是否被使用 |
---|---|
where a = 3 | Y,使用到 a |
where a = 3 and b = 5 | Y,使用到 a,b |
where a = 3 and b = 5 and c = 4 | Y,使用到 a,b,c |
where b = 3 或者 where b = 3 and c = 4 或者 where c = 4 | N |
where a = 3 and c = 5 | 使用到 a, 但是 c 不可以,b 中间断了 |
where a = 3 and b > 4 and c = 5 | 使用到 a 和 b, c 不能用在范围之后,b 断了 |
where a is null and b is not null | is null 支持索引 但是 is not null 不支持,所以 a 可以使用索引,但是 b 不可以使用 |
where a <> 3 | 不能使用索引 |
where abs(a) =3 | 不能使用索引 |
where a = 3 and b like ‘kk%’ and c = 4 | Y,使用到 a,b,c |
where a = 3 and b like ‘%kk’ and c = 4 | Y,只用到 a |
where a = 3 and b like ‘%kk%’ and c = 4 | Y,只用到 a |
where a = 3 and b like ‘k%kk%’ and c = 4 | Y,使用到 a,b,c |
注意:虽然 name 字段使用了模糊匹配,但是 % 处于右边,可以使用索引(且全部都用到了,根据 key_len 判断),只是性能由 ref 变为了 index。
5.4.12、口诀
全职匹配我最爱,最左前缀要遵守;
带头大哥不能死,中间兄弟不能断;
索引列上少计算,范围之后全失效;
LIKE 百分写最右,覆盖索引不写*;
不等空值还有 OR,索引影响要注意;
VAR 引号不可丢,SQL 优化有诀窍。
6、排序分组优化
create table tblA(
#id int primary key not null auto_increment,
age int,
birth timestamp not null
);
insert into tblA(age, birth) values(22, now());
insert into tblA(age, birth) values(23, now());
insert into tblA(age, birth) values(24, now());
create index idx_A_ageBirth on tblA(age, birth);
select * from tblA;
6.1、顺序错,必排序
MySQL支持二种方式的排序,FileSort和Index,Index效率高。它指MySQL扫描索引本身完成排序。FileSort方式效率较低。
ORDER BY满足两情况,会使用Index方式排序:
- ORDER BY语句使用索引最左前列
- 使用where子句与OrderBy子句条件列组合满足索引最左前列
6.2、方向反,必排序
如果可以用上索引的字段都使用正序或者逆序,实际上是没有任何影响的,无非将结果集调换顺序;如果排序的字段,顺序有差异,就需要将差异的部分,进行一次倒置顺序,因此还是需要手动排序的!
6.3、MySQL 的排序算法
①双路排序
MySQL 4.1 之前是使用双路排序,字面意思就是两次扫描磁盘,最终得到数据,读取行指针和 orderby 列,对他们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取对应的数据输出。
从磁盘取排序字段,在 buffer 进行排序,再从磁盘取其他字段。
简单来说,取一批数据,要对磁盘进行了两次扫描,众所周知,I\O 是很耗时的,所以在 mysql4.1 之后,出现了第二种改进的算法,就是单路排序。
②单路排序
从磁盘读取查询需要的所有列,按照 order by 列在 buffer 对它们进行排序,然后扫描排序后的列表进行输出,它的效率更快一些,避免了第二次读取数据。并且把随机 IO 变成了顺序 IO,但是它会使用更多的空间,因为它把每一行都保存在内存中了。
③单路排序的问题
由于单路是后出的,总体而言好过双路。但是存在以下问题:
在 sort_buffer 中,方法 B 比方法 A 要多占用很多空间,因为方法 B 是把所有字段都取出, 所以有可能取出的数据的总大小超出了 sort_buffer 的容量,导致每次只能取 sort_buffer 容量大小的数据,进行排序(创建 tmp 文件,多路合并),排完再取取 sort_buffer 容量大小,再排……从而多次 I/O。
结论: 本来想省一次 I/O 操作,反而导致了大量的 I/O 操作,反而得不偿失。
6.4、优化策略
①增大 sort_butter_size 参数的设置
不管用哪种算法,提高这个参数都会提高效率,当然,要根据系统的能力去提高,因为这个参数是针对每个进程的 1M-8M 之间调整。
②增大 max_length_for_sort_data 参数的设置
mysql 使用单路排序的前提是排序的字段大小要小于 max_length_for_sort_data。
提高这个参数,会增加用改进算法的概率。但是如果设的太高,数据总容量超出 sort_buffer_size 的概率就增大,明显症状是高的磁盘 I/O 活动和低的处理器使用率。(1024-8192 之间调整)。
③减少 select 后面的查询的字段。
当 Query 的字段大小总和小于 max_length_for_sort_data 而且排序字段不是 TEXT|BLOB 类型时,会用改进后的算法——单路排序, 否则用老算法——多路排序。
两种算法的数据都有可能超出 sort_buffer 的容量,超出之后,会创建 tmp 文件进行合并排序,导致多次 I/O,但是用单路排序算法的风险会更大一些,所以要提高 sort_buffer_size。
6.5、Group By 优化
- groupby 实质是先排序后进行分组,遵照索引建的最佳左前缀
- 当无法使用索引列,增大max_length_for_sort_data参数的设置+增大sort_buffer_size参数的设置
- where高于having,能写在where限定的条件就不要去having限定了。
7、截取查询分析
7.1、慢查询日志
7.1.1、是什么
(1)MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中响应时间超过阀值的语句,具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。
(2)具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。long_query_time的默认值为10,意思是运行10秒以上的语句。
(3)由他来查看哪些SQL超出了我们的最大忍耐时间值,比如一条sql执行超过5秒钟,我们就算慢SQL,希望能收集超过5秒的sql,结合之前explain进行全面分析。
7.1.2、怎么用
默认情况下,MySQL 数据库没有开启慢查询日志,需要我们手动来设置这个参数。
当然,如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。慢查询日志支持将日志记录写入文件。
SQL语句 | 描述 | 备注 |
---|---|---|
SHOW VARIABLES LIKE ‘%slow_query_log%’; | 查看慢查询日志是否开启 | 默认情况下 slow_query_log 的值为 OFF,表示慢查询日志是禁用的 |
set global slow_query_log=1; | 开启慢查询日志 | |
SHOW VARIABLES LIKE ‘long_query_time%’; | 查看慢查询设定阈值 | 单位秒 |
set long_query_time=1 | 设定慢查询阈值 | 单位秒 |
7.1.3、日志分析工具 mysqldumpslow
参数 | 描述 |
---|---|
-s | 是表示按照何种方式排序 |
-c | 访问次数 |
l | 锁定时间 |
r | 返回记录 |
t | 查询时间 |
al | 平均锁定时间 |
ar | 平均返回记录数 |
at | 平均查询时间 |
-t | 即为返回前面多少条的数据 |
-g | 后边搭配一个正则匹配模式,大小写不敏感的 |
得到返回记录集最多的 10 个 SQL
mysqldumpslow -s r -t 10 /var/lib/mysql/slow.log
得到访问次数最多的 10 个 SQL
mysqldumpslow -s c -t 10 /var/lib/mysql/slow.log
得到按照时间排序的前 10 条里面含有左连接的查询语句
mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/slow.log
另外建议在使用这些命令时结合 | 和 more 使用 ,否则有可能出现爆屏情况
mysqldumpslow -s r -t 10 /var/lib/mysql/slow.log | more
7.2、Show Profile
7.2.1、开启 profile
查看 profile 是否开启:show variables like '%profiling%'
如果没有开启,可以执行 set profiling=1 开启!
7.2.2、使用 profile
执行 show prifiles 命令,可以查看最近的几次查询。
根据 Query_ID,可以进一步执行 show profile cpu,block io for query Query_Id 来查看 sql 的具体执行步骤。
需要注意的结论:
- converting HEAP to MyISAM-----查询结果太大,内存都不够用了往磁盘上搬了。
- Creating tmp table-----创建临时表,拷贝数据到临时表,用完再删除
- Copying to tmp table on disk-----把内存中临时表复制到磁盘,危险!!!
- locked