MySQL索引优化与查询优化、索引失效
参考链接: www.bilibili.com/video/BV1iq…
1、MySQL优化概述
都有哪些维度可以进行数据库调优?简言之:
- 索引失效、没有充分利用到索引--索引建立
- 关联查询太多
JOIN(设计缺陷或不得已的需求) -SQL优化 - 服务器调优及各个参数设置(缓冲、线程数等) -- 调整
my.cnf - 数据过多---分库分表
虽然SQL查询优化的技术有很多,但是大方向上完全可以分成物理查询优化和逻辑查询优化两大块。
- 物理查询优化是通过索引和表连接方式等技术来进行优化,这里重点需要掌握索引的使用。
- 逻辑查询优化就是通过SQL等价变换提升查询效率,直白一点就是说,换一种查询写法执行效率可能更高。
分析慢sql的步骤:
1、慢查询的开启并捕获:开启慢查询日志,设置阈值,比如超过5秒钟的就是慢SQL,并将它抓取出来。
2、explain + 慢SQL分析。
3、show Profile查询SQL在MySQL数据库中的执行细节和生命周期情况。
4、MySQL数据库服务器的参数调优。
2、单表索引分析
2.1、排序优化
问题:在 WHERE 条件字段上加索引,但是为什么在 ORDER BY 字段上还要加索引呢?
优化建议:
-
SQL 中,可以在
WHERE子句和ORDER BY子句中使用索引,目的是在WHERE子句中 避免全表扫描 ,在ORDER BY子句 避免使用FileSort排序 。当然,某些情况下全表扫描,或者FileSort排序不一定比索引慢。但总的来说,我们还是要避免,以提高查询效率。 -
尽量使用
Index完成ORDER BY排序。如果WHERE和ORDER BY后面是相同的列就使用单索引列;如果不同就使用联合索引。 -
无法使用
Index时,需要对FileSort方式进行调优。
数据准备
DROP TABLE IF EXISTS `article`;
CREATE TABLE IF NOT EXISTS `article`(
`id` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
`author_id` INT(10) UNSIGNED NOT NULL COMMENT '作者id',
`category_id` INT(10) UNSIGNED NOT NULL COMMENT '分类id',
`views` INT(10) UNSIGNED NOT NULL COMMENT '被查看的次数',
`comments` INT(10) UNSIGNED NOT NULL COMMENT '回帖的备注',
`title` VARCHAR(255) NOT NULL COMMENT '标题',
`content` VARCHAR(255) NOT NULL COMMENT '正文内容'
) COMMENT '文章';
INSERT INTO `article`(`author_id`, `category_id`, `views`, `comments`, `title`, `content`) VALUES(1,1,1,1,'1','1');
INSERT INTO `article`(`author_id`, `category_id`, `views`, `comments`, `title`, `content`) VALUES(2,2,2,2,'2','2');
INSERT INTO `article`(`author_id`, `category_id`, `views`, `comments`, `title`, `content`) VALUES(3,3,3,3,'3','3');
INSERT INTO `article`(`author_id`, `category_id`, `views`, `comments`, `title`, `content`) VALUES(1,1,3,3,'3','3');
INSERT INTO `article`(`author_id`, `category_id`, `views`, `comments`, `title`, `content`) VALUES(1,1,4,4,'4','4');
案例:查询
category_id为1且comments大于1的情况下,views最多的author_id。
1、编写SQL语句并查看SQL执行计划。
# 1、sql语句
SELECT id,author_id FROM article WHERE category_id = 1 AND comments > 1 ORDER BY views DESC LIMIT 1;
# 2、sql执行计划
mysql> EXPLAIN SELECT id,author_id FROM article WHERE category_id = 1 AND comments > 1 ORDER BY views DESC LIMIT 1\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: article
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 5
filtered: 20.00
Extra: Using where; Using filesort # 产生了文件内排序,需要优化SQL
1 row in set, 1 warning (0.00 sec)
2、结论:type是all,即最坏的情况,extra里面还出现了Using filesort情况,也是最坏的情况。
优化是必须的,优化:创建复合索引idx_article_ccv。【ccv表示category_id,comments,views这3个字段】
CREATE INDEX idx_article_ccv ON article(category_id,comments,views);
3、查看当前索引。
4、查看建立了索引之后现在SQL语句的执行计划。
我们发现,创建复合索引idx_article_ccv之后,虽然解决了全表扫描的问题【type=range,从上至下依次按照category_id,comments,views扫描】,但是在order by排序的时候没有用到索引,MySQL居然还是用的Using filesort,为什么?
这是因为按照BTree索引的工作原理,先排序category_id,如果遇到相同的category_id 则再排序comments,如果遇到相同的comments则再排序views。当comments字段在联合索引里处于中间位置时,因comments> 1条件是一个范围值(所谓range),MySQL无法利用索引再对后面的views部分进行检索,即range类型查询字段后面的索引无效。
5、我们试试把SQL修改为SELECT id,author_id FROM article WHERE category_id = 1 AND comments = 1 ORDER BY views DESC LIMIT 1; 看看SQL的执行计划。
推论:发现range从range变成了ref,当comments > 1的时候order by排序views字段索引就用不上,但是当comments = 1的时候order by排序views字段索引就可以用上!!!所以,范围之后的索引会失效。
6、我们现在知道范围之后的索引会失效,原来的索引idx_article_ccv最后一个字段views会失效,那么我们如果删除这个索引,创建idx_article_cv索引呢????【ccv表示category_id,views这2个字段】
/* 创建索引 idx_article_cv */
CREATE INDEX idx_article_cv ON article(category_id,views);
查看当前的索引
7、当前索引是idx_article_cv,来看一下SQL执行计划。
可以看出,结果非常理想【useing where】
结论:
-
两个索引同时存在,mysql自动选择最优的方案。(对于这个例子,mysql选择 idx_age_stuno_name)。但是, 随着数据量的变化,选择的索引也会随之变化的
-
当【范围条件】和【group by 或者 order by】的字段出现二选一时,优先观察条件字段的过滤数量,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在范围字段上。反之亦然。
3、两表索引分析(关联查询)
数据准备
DROP TABLE IF EXISTS `class`;
DROP TABLE IF EXISTS `book`;
CREATE TABLE IF NOT EXISTS `class`(
`id` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
`card` INT(10) UNSIGNED NOT NULL COMMENT '分类'
) COMMENT '商品类别';
CREATE TABLE IF NOT EXISTS `book`(
`bookid` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
`card` INT(10) UNSIGNED NOT NULL COMMENT '分类'
) COMMENT '书籍';
insert into class(card) values(floor(1+(rand()*20)));
insert into class(card) values(floor(1+(rand()*20)));
insert into class(card) values(floor(1+(rand()*20)));
insert into class(card) values(floor(1+(rand()*20)));
insert into class(card) values(floor(1+(rand()*20)));
insert into class(card) values(floor(1+(rand()*20)));
insert into class(card) values(floor(1+(rand()*20)));
insert into class(card) values(floor(1+(rand()*20)));
insert into class(card) values(floor(1+(rand()*20)));
insert into class(card) values(floor(1+(rand()*20)));
insert into class(card) values(floor(1+(rand()*20)));
insert into class(card) values(floor(1+(rand()*20)));
insert into class(card) values(floor(1+(rand()*20)));
insert into class(card) values(floor(1+(rand()*20)));
insert into class(card) values(floor(1+(rand()*20)));
insert into class(card) values(floor(1+(rand()*20)));
insert into class(card) values(floor(1+(rand()*20)));
insert into class(card) values(floor(1+(rand()*20)));
insert into class(card) values(floor(1+(rand()*20)));
insert into class(card) values(floor(1+(rand()*20)));
insert into book(card) values(floor(1+(rand()*20)));
insert into book(card) values(floor(1+(rand()*20)));
insert into book(card) values(floor(1+(rand()*20)));
insert into book(card) values(floor(1+(rand()*20)));
insert into book(card) values(floor(1+(rand()*20)));
insert into book(card) values(floor(1+(rand()*20)));
insert into book(card) values(floor(1+(rand()*20)));
insert into book(card) values(floor(1+(rand()*20)));
insert into book(card) values(floor(1+(rand()*20)));
insert into book(card) values(floor(1+(rand()*20)));
insert into book(card) values(floor(1+(rand()*20)));
insert into book(card) values(floor(1+(rand()*20)));
insert into book(card) values(floor(1+(rand()*20)));
insert into book(card) values(floor(1+(rand()*20)));
insert into book(card) values(floor(1+(rand()*20)));
insert into book(card) values(floor(1+(rand()*20)));
insert into book(card) values(floor(1+(rand()*20)));
insert into book(card) values(floor(1+(rand()*20)));
insert into book(card) values(floor(1+(rand()*20)));
insert into book(card) values(floor(1+(rand()*20)));
两表连接查询的SQL执行计划
1、不创建索引的情况下,SQL的执行计划。
book和class两张表都是没有使用索引,全表扫描[type = all],那么如果进行优化,索引是创建在book表还是创建在class表呢?下面进行大胆的尝试!
2、左表(book表)创建索引。
创建索引idx_book_card
/* 在book表创建索引 */
CREATE INDEX idx_book_card ON book(card);
在book表中有idx_book_card索引的情况下,查看SQL执行计划
3、删除book表的索引,右表(class表)创建索引。
创建索引idx_class_card
/* 在class表创建索引 */
CREATE INDEX idx_class_card ON class(card);
在class表中有idx_class_card索引的情况下,查看SQL执行计划
由此可见,左连接将索引创建在右表上更合适[ref比index更好],右连接将索引创建在左表上更合适。
4、三张表索引分析
数据准备
DROP TABLE IF EXISTS `phone`;
CREATE TABLE IF NOT EXISTS `phone`(
`phone_id` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
`card` INT(10) UNSIGNED NOT NULL COMMENT '分类'
) COMMENT '手机';
insert into phone(card) values(floor(1+(rand()*20)));
insert into phone(card) values(floor(1+(rand()*20)));
insert into phone(card) values(floor(1+(rand()*20)));
insert into phone(card) values(floor(1+(rand()*20)));
insert into phone(card) values(floor(1+(rand()*20)));
insert into phone(card) values(floor(1+(rand()*20)));
insert into phone(card) values(floor(1+(rand()*20)));
insert into phone(card) values(floor(1+(rand()*20)));
insert into phone(card) values(floor(1+(rand()*20)));
insert into phone(card) values(floor(1+(rand()*20)));
insert into phone(card) values(floor(1+(rand()*20)));
insert into phone(card) values(floor(1+(rand()*20)));
insert into phone(card) values(floor(1+(rand()*20)));
insert into phone(card) values(floor(1+(rand()*20)));
insert into phone(card) values(floor(1+(rand()*20)));
insert into phone(card) values(floor(1+(rand()*20)));
insert into phone(card) values(floor(1+(rand()*20)));
insert into phone(card) values(floor(1+(rand()*20)));
insert into phone(card) values(floor(1+(rand()*20)));
insert into phone(card) values(floor(1+(rand()*20)));
三表连接查询SQL优化
1、不加任何索引,查看SQL执行计划。
2、发现出现了using join buffer【连接缓存】,并且都是全表扫描,根据两表查询优化的经验,左连接需要在右表上添加索引,所以尝试在book表和phone表上添加索引。
/* 在book表创建索引 */
CREATE INDEX idx_book_card ON book(card);
/* 在phone表上创建索引 */
CREATE INDEX idx_phone_card ON phone(card);
再次执行SQL的执行计划
后面两行的type都是ref且总rows优化很好,效果不错,因此索引最好设置在需要经常查询的字段中
5、结论
JOIN语句的优化:
-
保证被驱动表的JOIN字段已经创建了索引
-
需要JOIN 的字段,数据类型保持绝对一致。
-
LEFT JOIN 时,选择小表作为驱动表, 大表作为被驱动表 。减少外层循环的次数。
-
INNER JOIN 时,MySQL会自动将 小结果集的表选为驱动表 。选择相信MySQL优化策略。
-
能够直接多表关联的尽量直接关联,不用子查询。(减少查询的趟数)
-
不建议使用子查询,建议将子查询SQL拆开结合程序多次查询,或使用 JOIN 来代替子查询。
-
衍生表建不了索引
-
尽可能减少
JOIN语句中的NestedLoop(嵌套循环)的总次数:永远都是小的结果集驱动大的结果集。 -
优先优化
NestedLoop的内层循环。 -
保证
JOIN语句中被驱动表上JOIN条件字段已经被索引。 -
当无法保证被驱动表的
JOIN条件字段被索引且内存资源充足的前提下,不要太吝惜Join Buffer的设置。
6、子查询优化
-
MySQL从4.1版本开始支持子查询,使用子查询可以进行SELECT语句的嵌套查询,即一个SELECT查询的结 果作为另一个SELECT语句的条件。 子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作 。
-
子查询是 MySQL 的一项重要的功能,可以帮助我们通过一个 SQL 语句实现比较复杂的查询。但是,子查询的执行效率不高。
-
原因:
-
执行子查询时,MySQL需要为内层查询语句的查询结果 建立一个临时表 ,然后外层查询语句从临时表中查询记录。查询完毕后,再 撤销这些临时表。这样会消耗过多的CPU和IO资源,产生大量的慢查询。
-
子查询的结果集存储的临时表,不论是内存临时表还是磁盘临时表都 不会存在索引 ,所以查询性能会 受到一定的影响。
-
对于返回结果集比较大的子查询,其对查询性能的影响也就越大。 在MySQL中,可以使用连接(JOIN)查询来替代子查询。连接查询不需要建立临时表 ,其速度比子查询要快,如果查询中使用索引的话,性能就会更好。
-
-
结论:尽量不要使用
NOT IN或者NOT EXISTS,用LEFT JOIN xxx ON xx WHERE xx IS NULL替代
8、优先考虑覆盖索引
8.1、什么是覆盖索引?
理解方式一:索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它 不必读取整个行。毕竟索引叶子节点存储了它们索引的数据;当能通过读取索引就可以得到想要的数 据,那就不需要读取行了。一个索引包含了满足查询结果的数据就叫做覆盖索引。
理解方式二:非聚簇复合索引的一种形式,它包括在查询里的SELECT、JOIN和WHERE子句用到的所有列 (即建索引的字段正好是覆盖查询条件中所涉及的字段)。
简单说就是, 索引列+主键 包含 SELECT 到 FROM 之间查询的列
8.2、覆盖索引的利弊
好处:
- 避免Innodb表进行索引的二次查询(回表)
- 可以把随机IO变成顺序IO加快查询效率
弊端:
索引字段的维护 总是有代价的。因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了。这是业务 DBA,或者称为业务数据架构师的工作。
9、普通索引 vs 唯一索引
从性能的角度考虑,你选择唯一索引还是普通索引呢?选择的依据是什么呢? 假设,我们有一个主键列为ID的表,表中有字段k,并且在k上有索引,假设字段 k 上的值都不重复。 这个表的建表语句是:
mysql> create table test(
id int primary key,
k int not null,
name varchar(16),
index (k)
)engine=InnoDB;
表中R1~R5的(ID,k)值分别为(100,1)、(200,2)、(300,3)、(500,5)和(600,6)。
9.1、查询过程
假设,执行查询的语句是 select id from test where k=5
- 对于普通索引来说,查找到满足条件的第一个记录(5,500)后,需要查找下一个记录,直到碰到第一个不满足k=5条件的记录。
- 对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。
那么,这个不同带来的性能差距会有多少呢?答案是, 微乎其微 。
9.2、更新过程
为了说明普通索引和唯一索引对更新语句性能的影响这个问题,介绍一下change buffer
当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话, 在不影响数据一致性的前提下,InooDB会将这些更新操作缓存在hange buffer中 ,这样就不需要从磁 盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行change buffer中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。
将change buffer中的操作应用到原数据页,得到最新结果的过程称为 merge 。除了 访问这个数据页会触发merge外,系统有 后台线程会定期 merge。在 数据库正常关闭(shutdown) 的过程中,也会执行merge 操作。
如果能够将更新操作先记录在change buffer, 减少读磁盘 ,语句的执行速度会得到明显的提升。而且, 数据读入内存是需要占用 buffer pool 的,所以这种方式还能够 避免占用内存 ,提高内存利用率。
唯一索引的更新就不能使用change buffer ,实际上也只有普通索引可以使用。 如果要在这张表中插入一个新记录(4,400)的话,InnoDB的处理流程是怎样的?
9.3、change buffer的使用场景
- 普通索引和唯一索引应该怎么选择?其实,这两类索引在查询能力上是没差别的,主要考虑的是 对 更新性能 的影响。所以,建议你 尽量选择普通索引 。
- 在实际使用中会发现, 普通索引 和 change buffer 的配合使用,对于 数据量大 的表的更新优化 还是很明显的。
- 如果所有的更新后面,都马上 伴随着对这个记录的查询 ,那么你应该 关闭change buffer 。而在 其他情况下,change buffer都能提升更新性能。
- 由于唯一索引用不上change buffer的优化机制,因此如果业务可以接受,从性能角度出发建议优先考虑非唯一索引。但是如果"业务可能无法确保"的情况下,怎么处理呢?
- 首先, 业务正确性优先 。我们的前提是“业务代码已经保证不会写入重复数据”的情况下,讨论性能问题。如果业务不能保证,或者业务就是要求数据库来做约束,那么没得选,必须创建唯一索引。
- 这种情况下,本节的意义在于,如果碰上了大量插入数据慢、内存命中率低的时候,给你多提供一个排查思路。
- 然后,在一些“ 归档库 ”的场景,你是可以考虑使用唯一索引的。比如,线上数据只需要保留半年,然后历史数据保存在归档库。这时候,归档数据已经是确保没有唯一键冲突了。要提高归档效率,可以考虑把表里面的唯一索引改成普通索引。
10、其它查询优化策略
10.1、小表驱动大表
优化原则:对于MySQL数据库而言,永远都是小表驱动大表。 即小的数据集驱动大的数据集
/**
* 举个例子:可以使用嵌套的for循环来理解小表驱动大表。
* 以下两个循环结果都是一样的,但是对于MySQL来说不一样,
* 第一种可以理解为,和MySQL建立5次连接每次查询1000次。
* 第一种可以理解为,和MySQL建立1000次连接每次查询5次。
*/
for(int i = 1; i <= 5; i ++){ //小表有5条数据
for(int j = 1; j <= 1000; j++){ //大表有1000条数据
}
}
// ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
for(int i = 1; i <= 1000; i ++){
for(int j = 1; j <= 5; j++){
}
}
因此如果小的循环在外层,对于数据库连接来说就只连接5次,进行5000次操作,如果1000在外,则需要进行1000次数据库连接,从而浪费资源,增加消耗。这就是为什么要小表驱动大表的原因。
10.2、EXISTS 和 IN
提到小表驱动大表,这不可否认的又提到另外一个知识点,即IN和EXISTS
EXISTS:
- 语法:
SELECT....FROM tab WHERE EXISTS(subquery);该语法可以理解为: - 该语法可以理解为:将主查询的数据,放到子查询中做条件验证,根据验证结果(
true或是false)来决定主查询的数据结果是否得以保留。
提示:
EXISTS(subquery)子查询只返回true或者false,因此子查询中的SELECT *可以是SELECT 1 OR SELECT X,它们并没有区别。EXISTS(subquery)子查询的实际执行过程可能经过了优化而不是我们理解上的逐条对比,如果担心效率问题,可进行实际检验以确定是否有效率问题。EXISTS(subquery)子查询往往也可以用条件表达式,其他子查询或者JOIN替代,何种最优需要具体问题具体分析。
案列演示:
1、当B表的数据集小于A表数据集时,用in优于exists。
select *from tb_emp_bigdata A where A.deptno in (select B.deptno from tb_dept_bigdata B)
- B表为tb_dept_bigdata:100条数据
- A表tb_emp_bigdata:5000条数据
- 用in的查询时间为:
0.019s
将上面sql转换成exists:
SELECT * from tb_emp_bigdata A WHERE EXISTS(SELECT 1 from tb_dept_bigdata B WHERE B.deptno=A.deptno );
用exists的查询时间: 0.095s
经对比可看到,在B表数据集小于A表的时候,用in要优于exists,当前的数据集并不大,所以查询时间相差并不多。
2、当A表的数据集小于B表的数据集时,用exists优于in。
select *from tb_dept_bigdata A where A.deptno in(select B.deptno from tb_emp_bigdata B);
用in的查询时间为: 0.02 sec
将上面sql转换成exists:
select *from tb_dept_bigdata A where exists(select 1 from tb_emp_bigdata B where B.deptno=A.deptno);
用exists的查询时间:0.00 sec
由于数据量并不是很大,因此对比并不是难么的强烈。
3、结论:
下面结论都是针对
in或exists的。
in后面跟的是小表,exists后面跟的是大表。简记:
in小,exists大。对于
exists:select .....from table where exists(subquery);可以理解为:将主查询的数据放入子查询中做条件验证,根据验证结果(true或false)来决定主查询的数据是否得以保留。
10.3、ORDER BY优化
数据准备
CREATE TABLE `talA`(
`age` INT,
`birth` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO `talA`(`age`) VALUES(18);
INSERT INTO `talA`(`age`) VALUES(19);
INSERT INTO `talA`(`age`) VALUES(20);
INSERT INTO `talA`(`age`) VALUES(21);
INSERT INTO `talA`(`age`) VALUES(22);
INSERT INTO `talA`(`age`) VALUES(23);
INSERT INTO `talA`(`age`) VALUES(24);
INSERT INTO `talA`(`age`) VALUES(25);
/* 创建索引 */
CREATE INDEX idx_talA_age_birth ON `talA`(`age`, `birth`);
案例:sql分析-查看order by后面的语句是否产生filesort
/* 1.使用索引进行排序了 不会产生Using filesort */
EXPLAIN SELECT * FROM `talA` WHERE `age` > 20 ORDER BY `age`;
/* 2.使用索引进行排序了 不会产生Using filesort */
EXPLAIN SELECT * FROM `talA` WHERE `age` > 20 ORDER BY `age`,`birth`;
/* 3.没有使用索引进行排序 产生了Using filesort */
EXPLAIN SELECT * FROM `talA` WHERE `age` > 20 ORDER BY `birth`;
/* 4.没有使用索引进行排序 产生了Using filesort */
EXPLAIN SELECT * FROM `talA` WHERE `age` > 20 ORDER BY `birth`,`age`;
/* 5.没有使用索引进行排序 产生了Using filesort */
EXPLAIN SELECT * FROM `talA` ORDER BY `birth`;
/* 6.没有使用索引进行排序 产生了Using filesort */
EXPLAIN SELECT * FROM `talA` WHERE `birth` > '2020-08-04 07:42:21' ORDER BY `birth`;
/* 7.使用索引进行排序了 不会产生Using filesort */
EXPLAIN SELECT * FROM `talA` WHERE `birth` > '2020-08-04 07:42:21' ORDER BY `age`;
/* 8.没有使用索引进行排序 产生了Using filesort */
EXPLAIN SELECT * FROM `talA` ORDER BY `age` ASC, `birth` DESC;
1、ORDER BY子句,尽量使用索引index排序,避免使用Using filesort排序。
2、MySQL支持两种方式的排序,FileSort和Index,Index的效率高,它指MySQL扫描索引本身完成排序。FileSort方式效率较低。
3、ORDER BY满足两情况,会使用using Index方式排序:
ORDER BY语句使用了索引最左前列。【order by排序顺序和索引顺序一致】- 使用
WHERE子句与ORDER BY子句条件列组合满足索引最左前列。
结论:order by尽可能按照索引列顺序完成排序操作,遵照索引建的最佳左前缀原则。 【order by子句默认是升序】
4、如果order by的条件不在索引列上,就会产生Using filesort
如果不在索引列上,FileSort有两种算法:MySQL就要启动双路排序算法和单路排序算法
1、双路排序算法:MySQL4.1之前使用双路排序,字面意思就是两次扫描磁盘,最终得到数据,读取行指针和ORDER BY列,対他们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取对应的数据输出。一句话,从磁盘取排序字段,在buffer中进行排序,再从磁盘取其他字段。
取一批数据,要对磁盘进行两次扫描,众所周知,IO是很耗时的,所以在MySQL4.1之后,出现了改进的算法,就是单路排序算法。
2、单路排序算法:从磁盘读取查询需要的所有列,按照ORDER BY列在buffer対它们进行排序,然后扫描排序后的列表进行输出,它的效率更快一些,避免了第二次读取数据。并且把随机IO变成了顺序IO,但是它会使用更多的空间,因为它把每一行都保存在内存中了。
由于单路排序算法是后出的,总体而言效率好过双路排序算法。
但是单路排序算法有问题:如果SortBuffer缓冲区太小,导致从磁盘中读取所有的列不能完全保存在SortBuffer缓冲区中,这时候单路复用算法就会出现问题,反而性能不如双路复用算法。
单路复用算法的优化策略:
- 增大
sort_buffer_size参数的设置。 - 增大
max_length_for_sort_data参数的设置。
提高ORDER BY排序的速度:
-
ORDER BY时使用SELECT *是大忌,查什么字段就写什么字段,这点非常重要。在这里的影响是:- 当查询的字段大小总和小于
max_length_for_sort_data而且排序字段不是TEXT|BLOB类型时,会使用单路排序算法,否则使用多路排序算法。 - 两种排序算法的数据都有可能超出
sort_buffer缓冲区的容量,超出之后,会创建tmp临时文件进行合并排序,导致多次IO,但是单路排序算法的风险会更大一些,所以要增大sort_buffer_size参数的设置。
- 当查询的字段大小总和小于
-
尝试提高
sort_buffer_size:不管使用哪种算法,提高这个参数都会提高效率,当然,要根据系统的能力去提高,因为这个参数是针对每个进程的。
-
尝试提高
max_length_for_sort_data:提高这个参数,会增加用单路排序算法的概率。但是如果设置的太高,数据总容量
sort_buffer_size的概率就增大,明显症状是高的磁盘IO活动和低的处理器使用率。
10.4、GROUP BY优化
group by 使用索引的原则几乎跟order by一致 ,group by 即使没有过滤条件用到索引,也可以直接 使用索引。
group by 先排序再分组,遵照索引建的最佳左前缀法则 当无法使用索引列,增大 max_length_for_sort_data 和 sort_buffer_size 参数的设置 where效率高于having,能写在where限定的条件就不要写在having中了 减少使用order by,和业务沟通能不排序就不排序,或将排序放到程序端去做。
Order by、group by、distinct这些语句较为耗费CPU,数据库的CPU资源是极其宝贵的。 包含了order by、group by、distinct这些查询的语句,where条件过滤出来的结果集请保持在1000行 以内,否则SQL会很慢。
GROUP BY实质是先排序后进行分组,遵照索引建的最佳左前缀。- 当无法使用索引列时,会使用
Using filesort进行排序,增大max_length_for_sort_data参数的设置和增大sort_buffer_size参数的设置,会提高性能。 WHERE执行顺序高于HAVING,能写在WHERE限定条件里的就不要写在HAVING中了。
10.5、总结
为排序【order by】使用索引
- MySQL两种排序方式:
Using filesort[文件排序]和Index扫描有序索引排序。 - MySQL能为排序与查询使用相同的索引,创建的索引既可以用于排序也可以用于查询。
- mysql8没有这么多要求,哪怕你倒序,都不会产生文件排序Using filesort
/* 创建a b c三个字段的索引 */
idx_table_a_b_c(a, b, c)
/* 1.ORDER BY 能使用索引最左前缀 */
ORDER BY a;
ORDER BY a, b;
ORDER BY a, b, c;
ORDER BY a DESC, b DESC, c DESC;
/* 2.如果WHERE子句中使用索引的最左前缀定义为常量,则ORDER BY能使用索引 */
WHERE a = 'Ringo' ORDER BY b, c;
WHERE a = 'Ringo' AND b = 'Tangs' ORDER BY c;
WHERE a = 'Ringo' AND b > 2000 ORDER BY b, c;
/* 3.不能使用索引进行排序 */
ORDER BY a ASC, b DESC, c DESC; /* 排序不一致 */
WHERE g = const ORDER BY b, c; /* 丢失a字段索引 */
WHERE a = const ORDER BY c; /* 丢失b字段索引 */
WHERE a = const ORDER BY a, d; /* d字段不是索引的一部分 */
WHERE a IN (...) ORDER BY b, c; /* 对于排序来说,多个相等条件(a=1 or a=2)也是范围查询 */
10.6、其他
COUNT(*)与COUNT(具体字段)效率
问:在 MySQL 中统计数据表的行数,可以使用三种方式: SELECT COUNT(*) 、 SELECT COUNT(1) 和 SELECT COUNT(具体字段) ,使用这三者之间的查询效率是怎样的?
关于SELECT(*)
在表查询中,建议明确字段,不要使用 * 作为查询的字段列表,推荐使用SELECT <字段列表> 查询。原因:
- MySQL 在解析的过程中,会通过 查询数据字典 将
*按序转换成所有列名,这会大大的耗费资源和时 间。 - 无法使用 覆盖索引
LIMIT 1 对优化的影响
针对的是会扫描全表的 SQL 语句,如果你可以确定结果集只有一条,那么加上 LIMIT 1 的时候,当找 到一条结果的时候就不会继续扫描了,这样会加快查询速度。
如果数据表已经对字段建立了唯一索引,那么可以通过索引进行查询,不会全表扫描的话,就不需要加 上 LIMIT 1 了。
多使用COMMIT
只要有可能,在程序中尽量多使用 COMMIT,这样程序的性能得到提高,需求也会因为 COMMIT 所释放 的资源而减少。COMMIT 所释放的资源:
- 回滚段上用于恢复数据的信息
- 被程序语句获得的锁
redo / undo log buffer中的空间
管理上述 3 种资源中的内部花费
11、淘宝数据库,主键如何设计的?
聊一个实际问题:淘宝的数据库,主键是如何设计的?
某些错的离谱的答案还在网上年复一年的流传着,甚至还成为了所谓的MySQL军规。其中,一个最明显 的错误就是关于MySQL的主键设计。
大部分人的回答如此自信:用8字节的 BIGINT 做主键,而不要用INT。 错
这样的回答,只站在了数据库这一层,而没有 从业务的角度 思考主键。主键就是一个自增ID吗?站在 2022年的新年档口,用自增做主键,架构设计上可能 连及格都拿不到 。
11.1、自增ID的问题
自增ID做主键,简单易懂,几乎所有数据库都支持自增类型,只是实现上各自有所不同而已。自增ID除了简单,其他都是缺点,总体来看存在以下几方面的问题:
- 可靠性不高 存在自增ID回溯的问题,这个问题直到最新版本的MySQL 8.0才修复。
- 安全性不高 对外暴露的接口可以非常容易猜测对应的信息。比如:/User/1/这样的接口,可以非常容易猜测用户ID的 值为多少,总用户数量有多少,也可以非常容易地通过接口进行数据的爬取。
- 性能差 自增ID的性能较差,需要在数据库服务器端生成。
- 交互多 业务还需要额外执行一次类似 last_insert_id() 的函数才能知道刚才插入的自增值,这需要多一次的 网络交互。在海量并发的系统中,多1条SQL,就多一次性能上的开销。
- 局部唯一性 最重要的一点,自增ID是局部唯一,只在当前数据库实例中唯一,而不是全局唯一,在任意服务器间都 是唯一的。对于目前分布式系统来说,这简直就是噩梦。
11.2、业务字段做主键
为了能够唯一地标识一个会员的信息,需要为 会员信息表 设置一个主键。那么,怎么为这个表设置主 键,才能达到我们理想的目标呢? 这里我们考虑业务字段做主键。 表数据如下:
在这个表里,哪个字段比较合适呢?
选择卡号(cardno) 会员卡号(cardno)看起来比较合适,因为会员卡号不能为空,而且有唯一性,可以用来 标识一条会员 记录
mysql> CREATE TABLE demo.membermaster
-> (
-> cardno CHAR(8) PRIMARY KEY, -- 会员卡号为主键
-> membername TEXT,
-> memberphone TEXT,
-> memberpid TEXT,
-> memberaddress TEXT,
-> sex TEXT,
-> birthday DATETIME
-> );
Query OK, 0 rows affected (0.06 sec)
不同的会员卡号对应不同的会员,字段“cardno”唯一地标识某一个会员。如果都是这样,会员卡号与会 员一一对应,系统是可以正常运行的。
但实际情况是, 会员卡号可能存在重复使用 的情况。比如,张三因为工作变动搬离了原来的地址,不再 到商家的门店消费了 (退还了会员卡),于是张三就不再是这个商家门店的会员了。但是,商家不想让 这个会 员卡空着,就把卡号是“10000001”的会员卡发给了王五。
从系统设计的角度看,这个变化只是修改了会员信息表中的卡号是“10000001”这个会员 信息,并不会影 响到数据一致性。也就是说,修改会员卡号是“10000001”的会员信息, 系统的各个模块,都会获取到修 改后的会员信息,不会出现“有的模块获取到修改之前的会员信息,有的模块获取到修改后的会员信息, 而导致系统内部数据不一致”的情况。因此,从 信息系统层面 上看是没问题的。
但是从使用 系统的业务层面来看,就有很大的问题了,会对商家造成影响。
比如,我们有一个销售流水表(trans),记录了所有的销售流水明细。2020 年 12 月 01 日,张三在门店 购买了一本书,消费了 89 元。那么,系统中就有了张三买书的流水记录,如下所示:
接着,我们查询一下 2020 年 12 月 01 日的会员销售记录:
mysql> SELECT b.membername,c.goodsname,a.quantity,a.salesvalue,a.transdate
-> FROM demo.trans AS a
-> JOIN demo.membermaster AS b
-> JOIN demo.goodsmaster AS c
-> ON (a.cardno = b.cardno AND a.itemnumber=c.itemnumber);
+------------+-----------+----------+------------+---------------------+
| membername | goodsname | quantity | salesvalue | transdate |
+------------+-----------+----------+------------+---------------------+
| 张三 | 书 | 1.000 | 89.00 | 2020-12-01 00:00:00 |
+------------+-----------+----------+------------+---------------------+
1 row in set (0.00 sec)
如果会员卡“10000001”又发给了王五,我们会更改会员信息表。导致查询时:
mysql> SELECT b.membername,c.goodsname,a.quantity,a.salesvalue,a.transdate
-> FROM demo.trans AS a
-> JOIN demo.membermaster AS b
-> JOIN demo.goodsmaster AS c
-> ON (a.cardno = b.cardno AND a.itemnumber=c.itemnumber);
+------------+-----------+----------+------------+---------------------+
| membername | goodsname | quantity | salesvalue | transdate |
+------------+-----------+----------+------------+---------------------+
| 王五 | 书 | 1.000 | 89.00 | 2020-12-01 00:00:00 |
+------------+-----------+----------+------------+---------------------+
1 row in set (0.01 sec)
这次得到的结果是:王五在 2020 年 12 月 01 日,买了一本书,消费 89 元。显然是错误的!结论:千万 不能把会员卡号当做主键。
选择会员电话 或 身份证号 会员电话可以做主键吗?不行的。在实际操作中,手机号也存在 被运营商收回 ,重新发给别人用的情况。
那身份证号行不行呢?好像可以。因为身份证决不会重复,身份证号与一个人存在一一对应的关系。可 问题是,身份证号属于 个人隐私 ,顾客不一定愿意给你。要是强制要求会员必须登记身份证号,会把很 多客人赶跑的。其实,客户电话也有这个问题,这也是我们在设计会员信息表的时候,允许身份证号和 电话都为空的原因。
所以,建议尽量不要用跟业务有关的字段做主键。毕竟,作为项目设计的技术人员,我们谁也无法预测 在项目的整个生命周期中,哪个业务字段会因为项目的业务需求而有重复,或者重用之类的情况出现。
经验: 刚开始使用 MySQL 时,很多人都很容易犯的错误是喜欢用业务字段做主键,想当然地认为了解业 务需求,但实际情况往往出乎意料,而更改主键设置的成本非常高。
11.3、淘宝的主键设计
在淘宝的电商业务中,订单服务是一个核心业务。请问, 订单表的主键 淘宝是如何设计的呢?是自增ID 吗? 打开淘宝,看一下订单信息:
从上图可以发现,订单号不是自增ID!我们详细看下上述4个订单号:
1550672064762308113
1481195847180308113
1431156171142308113
1431146631521308113
订单号是19位的长度,且订单的最后5位都是一样的,都是08113。且订单号的前面14位部分是单调递增 的。
大胆猜测,淘宝的订单ID设计应该是:
订单ID = 时间 + 去重字段 + 用户ID后6位尾号
这样的设计能做到全局唯一,且对分布式系统查询及其友好。
11.4、推荐的主键设计
非核心业务 :对应表的主键自增ID,如告警、日志、监控等信息。
核心业务 :主键设计至少应该是全局唯一且是单调递增。全局唯一保证在各系统之间都是唯一的,单调 递增是希望插入时不影响数据库性能。 这里推荐最简单的一种主键设计:UUID。
UUID的特点: 全局唯一,占用36字节,数据无序,插入性能差。
认识UUID:
为什么UUID是全局唯一的? 为什么UUID占用36个字节? 为什么UUID是无序的? MySQL数据库的UUID组成如下所示:
UUID = 时间+UUID版本(16字节)- 时钟序列(4字节) - MAC地址(12字节)
我们以UUID值e0ea12d4-6473-11eb-943c-00155dbaa39d举例:
为什么UUID是全局唯一的?
在UUID中时间部分占用60位,存储的类似TIMESTAMP的时间戳,但表示的是从1582-10-15 00:00:00.00 到现在的100ns的计数。可以看到UUID存储的时间精度比TIMESTAMPE更高,时间维度发生重复的概率降 低到1/100ns。 时钟序列是为了避免时钟被回拨导致产生时间重复的可能性。MAC地址用于全局唯一。
为什么UUID占用36个字节?
UUID根据字符串进行存储,设计时还带有无用"-"字符串,因此总共需要36个字节。
为什么UUID是随机无序的呢?
因为UUID的设计中,将时间低位放在最前面,而这部分的数据是一直在变化的,并且是无序。
改造UUID
若将时间高低位互换,则时间就是单调递增的了,也就变得单调递增了。MySQL 8.0可以更换时间低位和 时间高位的存储方式,这样UUID就是有序的UUID了。
MySQL 8.0还解决了UUID存在的空间占用的问题,除去了UUID字符串中无意义的"-"字符串,并且将字符 串用二进制类型保存,这样存储空间降低为了16字节。
可以通过MySQL8.0提供的uuid_to_bin函数实现上述功能,同样的,MySQL也提供了bin_to_uuid函数进行 转化:
SET @uuid = UUID();
SELECT @uuid,uuid_to_bin(@uuid),uuid_to_bin(@uuid,TRUE);
通过函数uuid_to_bin(@uuid,true)将UUID转化为有序UUID了。全局唯一 + 单调递增,这不就是我们想要的主键!
11.5、有序UUID性能测试
16字节的有序UUID,相比之前8字节的自增ID,性能和存储空间对比究竟如何呢? 我们来做一个测试,插入1亿条数据,每条数据占用500字节,含有3个二级索引,最终的结果如下所示:
从上图可以看到插入1亿条数据有序UUID是最快的,而且在实际业务使用中有序UUID在 业务端就可以生 成 。还可以进一步减少SQL的交互次数。
另外,虽然有序UUID相比自增ID多了8个字节,但实际只增大了3G的存储空间,还可以接受。
在当今的互联网环境中,非常不推荐自增ID作为主键的数据库设计。更推荐类似有序UUID的全局 唯一的实现。
另外在真实的业务系统中,主键还可以加入业务和系统属性,如用户的尾号,机房的信息等。这样 的主键设计就更为考验架构师的水平了。
如果不是MySQL8.0 肿么办?
手动赋值字段做主键!
比如,设计各个分店的会员表的主键,因为如果每台机器各自产生的数据需要合并,就可能会出现主键 重复的问题。
可以在总部 MySQL 数据库中,有一个管理信息表,在这个表中添加一个字段,专门用来记录当前会员编 号的最大值。
门店在添加会员的时候,先到总部 MySQL 数据库中获取这个最大值,在这个基础上加 1,然后用这个值 作为新会员的“id”,同时,更新总部 MySQL 数据库管理信息表中的当 前会员编号的最大值。
这样一来,各个门店添加会员的时候,都对同一个总部 MySQL 数据库中的数据表字段进 行操作,就解 决了各门店添加会员时会员编号冲突的问题
12、索引失效
数据准备
CREATE TABLE `staffs`(
`id` INT(10) PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '姓名',
`age` INT(10) NOT NULL DEFAULT 0 COMMENT '年龄',
`pos` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '职位',
`add_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间'
)COMMENT '员工记录表';
INSERT INTO `staffs`(`name`,`age`,`pos`) VALUES('Ringo', 18, 'manager');
INSERT INTO `staffs`(`name`,`age`,`pos`) VALUES('张三', 20, 'dev');
INSERT INTO `staffs`(`name`,`age`,`pos`) VALUES('李四', 21, 'dev');
/* 创建索引 */
CREATE INDEX idx_staffs_name_age_pos ON `staffs`(`name`,`age`,`pos`);
1、索引失效的情况
MySQL中提商性能的一个最有效的方式是对数据表设计合理的索引。索引提供了高效访问数据的方法,并且加快查询的速度,因此索引对查询的速度有着至关重要的影响。
- 使用索引可以快速地定位表中的某条记录,从而提高数据库查询的速度,提高数据库的性能。
- 如果查询时没有使用索引,查询语句就会扫描表中的所有记录。在数据量大的情况下,这样查询的速度会很 慢。
大多数情况下都(默认)采用B+树来构建索引。只是空间列类型的索引使用R-树,并且MEMORY表还支持hash索引。
其实,用不用索引,最终都是优化器说了算。优化器是基于什么的优化器?基于cost开销 (CostBaseOptimizer),它不是基于规则(Rule- BasedOptimizer),也不是基于语义。怎么样开销小就怎么来。另外,SQL语句是否使用索引,跟数据库版本、数据量、数据选择度都有关系。
下面是索引失效的几种情况:
- 全值匹配我最爱。
- 最佳左前缀法则。
- 不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描。
- 索引中范围条件右边的字段会全部失效。
- 尽量使用覆盖索引(只访问索引的查询,索引列和查询列一致),减少
SELECT *。 - MySQL在使用
!=或者<>的时候无法使用索引会导致全表扫描。 is null、is not null也无法使用索引。like以通配符开头%abc索引失效会变成全表扫描(使用覆盖索引就不会全表扫描了)。- 字符串不加单引号索引失效。
- 少用
or,用它来连接时会索引失效。
2、全值匹配我最爱
/* 创建复合索引 */
CREATE INDEX idx_staffs_name_age_pos ON `staffs`(`name`,`age`,`pos`);
0、显示索引
1、用到了idx_staffs_name_age_pos索引中的name字段
EXPLAIN SELECT * FROM `staffs` WHERE `name` = 'Ringo';
2、用到了idx_staffs_name_age_pos索引中的name, age字段
EXPLAIN SELECT * FROM `staffs` WHERE `name` = 'Ringo' AND `age` = 18;
3、用到了idx_staffs_name_age_pos索引中的name,age,pos字段 这是属于全值匹配的情况!!!
EXPLAIN SELECT * FROM `staffs` WHERE `name` = 'Ringo' AND `age` = 18 AND `pos` = 'manager';
结论: 全职匹配我最爱指的是,查询的字段按照顺序在索引中都可以匹配到!【通过key_len发现精度越来越高】
结论: SQL中查询字段的顺序,跟使用索引中的字段的顺序,没有关系。优化器会在不影响SQL 执行结果的前提下,给你自动地优化。
3、最佳左前缀法则
案例
1、索引没用上,ALL全表扫描
EXPLAIN SELECT * FROM `staffs` WHERE `age` = 18 AND `pos` = 'manager';
2、索引没用上,ALL全表扫描
EXPLAIN SELECT * FROM `staffs` WHERE `pos` = 'manager';
3、用到了idx_staffs_name_age_pos索引中的name字段,pos字段索引失效(跳过了age字段)
EXPLAIN SELECT * FROM `staffs` WHERE `name` = 'Ringo' AND `pos` = 'manager';
概念
最佳左前缀法则:查询字段与索引字段顺序的不同会导致索引无法充分使用,甚至索引失效!
原因:使用复合索引,需要遵循最佳左前缀法则,即如果索引了多列,要遵守最左前缀法则。指的是查询从联合索引的最左前列开始并且不跳过索引中的列。
结论:过滤条件要使用索引必须按照索引建立时的顺序,依次满足,一旦跳过某个字段,索引后面的字段都无法被使
口诀:带头大哥不能死,中间兄弟不能断。
拓展:Alibaba《Java开发手册》索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。
4、索引列上不计算
案例
# 现在要查询`name` = 'Ringo'的记录下面有两种方式来查询!
# 1、直接使用 字段 = 值的方式来计算
mysql> SELECT * FROM `staffs` WHERE `name` = 'Ringo';
+----+-------+-----+---------+---------------------+
| id | name | age | pos | add_time |
+----+-------+-----+---------+---------------------+
| 1 | Ringo | 18 | manager | 2020-08-03 08:30:39 |
+----+-------+-----+---------+---------------------+
1 row in set (0.00 sec)
# 2、使用MySQL内置的函数
mysql> SELECT * FROM `staffs` WHERE LEFT(`name`, 5) = 'Ringo';
+----+-------+-----+---------+---------------------+
| id | name | age | pos | add_time |
+----+-------+-----+---------+---------------------+
| 1 | Ringo | 18 | manager | 2020-08-03 08:30:39 |
+----+-------+-----+---------+---------------------+
1 row in set (0.00 sec)
我们发现以上两条SQL的执行结果都是一样的,但是执行效率有没有差距呢???
通过分析两条SQL的执行计划来分析性能。
由此可见,在索引列上进行计算,会使索引失效。
口诀:索引列上不计算。
5、范围之后全失效
案例
/* 用到了idx_staffs_name_age_pos索引中的name,age,pos字段 这是属于全值匹配的情况!!!*/
EXPLAIN SELECT * FROM `staffs` WHERE `name` = 'Ringo' AND `age` = 18 AND `pos` = 'manager';
/* 用到了idx_staffs_name_age_pos索引中的name,age字段,pos字段索引失效 */
EXPLAIN SELECT * FROM `staffs` WHERE `name` = '张三' AND `age` > 18 AND `pos` = 'dev';
查看上述SQL的执行计划
由此可知,查询范围的字段使用到了索引,但是范围(比如where条件)之后的索引字段会失效。
口诀:范围之后全失效。
6、覆盖索引尽量用
在写SQL的不要使用SELECT *,用什么字段就查询什么字段。
/* 没有用到覆盖索引 */
EXPLAIN SELECT * FROM `staffs` WHERE `name` = 'Ringo' AND `age` = 18 AND `pos` = 'manager';
/* 用到了覆盖索引 */
EXPLAIN SELECT `name`, `age`, `pos` FROM `staffs` WHERE `name` = 'Ringo' AND `age` = 18 AND `pos` = 'manager';
口诀:查询一定不用*。
7、不等(!= 或者<>)有时会失效
/* 会使用到覆盖索引 */
EXPLAIN SELECT `name`, `age`, `pos` FROM `staffs` WHERE `name` != 'Ringo';
/* 索引失效 全表扫描 */
EXPLAIN SELECT * FROM `staffs` WHERE `name` != 'Ringo';
8、like百分加右边
/* 索引失效 全表扫描 */
EXPLAIN SELECT * FROM `staffs` WHERE `name` LIKE '%ing%';
/* 索引失效 全表扫描 */
EXPLAIN SELECT * FROM `staffs` WHERE `name` LIKE '%ing';
/* 使用索引范围查询 */
EXPLAIN SELECT * FROM `staffs` WHERE `name` LIKE 'Rin%';
口诀:like百分加右边。
如果一定要使用%like,而且还要保证索引不失效,那么使用覆盖索引来编写SQL。
/* 使用到了覆盖索引 */
EXPLAIN SELECT `id` FROM `staffs` WHERE `name` LIKE '%in%';
/* 使用到了覆盖索引 */
EXPLAIN SELECT `name` FROM `staffs` WHERE `name` LIKE '%in%';
/* 使用到了覆盖索引 */
EXPLAIN SELECT `age` FROM `staffs` WHERE `name` LIKE '%in%';
/* 使用到了覆盖索引 */
EXPLAIN SELECT `pos` FROM `staffs` WHERE `name` LIKE '%in%';
/* 使用到了覆盖索引 */
EXPLAIN SELECT `id`, `name` FROM `staffs` WHERE `name` LIKE '%in%';
/* 使用到了覆盖索引 */
EXPLAIN SELECT `id`, `age` FROM `staffs` WHERE `name` LIKE '%in%';
/* 使用到了覆盖索引 */
EXPLAIN SELECT `id`,`name`, `age`, `pos` FROM `staffs` WHERE `name` LIKE '%in';
/* 使用到了覆盖索引 */
EXPLAIN SELECT `id`, `name` FROM `staffs` WHERE `pos` LIKE '%na';
/* 索引失效 全表扫描 */
EXPLAIN SELECT `name`, `age`, `pos`, `add_time` FROM `staffs` WHERE `name` LIKE '%in';
口诀:覆盖索引保两边。
拓展:Alibaba《Java开发手册》 【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。
9、字符要加单引号(类型转换)
/* 使用到了覆盖索引 */
EXPLAIN SELECT `id`, `name` FROM `staffs` WHERE `name` = 'Ringo';
/* 使用到了覆盖索引 */
EXPLAIN SELECT `id`, `name` FROM `staffs` WHERE `name` = 2000;
/* 索引失效 全表扫描 */
EXPLAIN SELECT * FROM `staffs` WHERE `name` = 2000;
这里name = 2000在MySQL中会发生强制类型转换,将数字转成字符串,导致失效
口诀:字符要加单引号。
10、主键插入顺序
如果此时再插入一条主键值为 9 的记录,那它插入的位置就如下图:
可这个数据页已经满了,再插进来咋办呢?我们需要把当前 页面分裂 成两个页面,把本页中的一些记录 移动到新创建的这个页中。页面分裂和记录移位意味着什么?意味着: 性能损耗 !所以如果我们想尽量 避免这样无谓的性能损耗,最好让插入的记录的 主键值依次递增 ,这样就不会发生这样的性能损耗了。
所以我们建议:让主键具有 AUTO_INCREMENT ,让存储引擎自己为表生成主键,而不是我们手动插入 , 比如: person_info 表:
CREATE TABLE person_info(
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
birthday DATE NOT NULL,
phone_number CHAR(11) NOT NULL,
country varchar(100) NOT NULL,
PRIMARY KEY (id),
KEY idx_name_birthday_phone_number (name(10), birthday, phone_number)
);
我们自定义的主键列 id 拥有 AUTO_INCREMENT 属性,在插入记录时存储引擎会自动为我们填入自增的 主键值。这样的主键占用空间小,顺序写入,减少页分裂。
11、OR 前后存在非索引的列,索引失效
减少使用 or
使用 union all 或者 union 来替代:
12、数据库和表的字符集统一使用utf8mb4
统一使用utf8mb4( 5.5.3版本以上支持)兼容性更好,统一字符集可以避免由于字符集转换产生的乱码。不 同的 字符集 进行比较前需要进行 转换 会造成索引失效。
13、索引相关题目
假设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 | Y,使用到a,b,c |
| where b = 3 或者 where b = 3 and c = 4 或者 where c = 4 | N,没有用到a字段 |
| where a = 3 and c = 5 | 使用到a,但是没有用到c,因为b断了 |
| where a = 3 and b > 4 and c = 5 | 使用到a,b,但是没有用到c,因为c在范围之后 |
| where a = 3 and b like 'kk%' and c = 4 | Y,a,b,c都用到 |
| where a = 3 and b like '%kk' and c = 4 | 只用到a |
| where a = 3 and b like '%kk%' and c = 4 | 只用到a |
| where a = 3 and b like 'k%kk%' and c = 4 | Y,a,b,c都用到 |
14、面试题分析
数据准备
/* 创建表 */
CREATE TABLE `test03`(
`id` INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
`c1` CHAR(10),
`c2` CHAR(10),
`c3` CHAR(10),
`c4` CHAR(10),
`c5` CHAR(10)
);
/* 插入数据 */
INSERT INTO `test03`(`c1`,`c2`,`c3`,`c4`,`c5`) VALUES('a1','a2','a3','a4','a5');
INSERT INTO `test03`(`c1`,`c2`,`c3`,`c4`,`c5`) VALUES('b1','b22','b3','b4','b5');
INSERT INTO `test03`(`c1`,`c2`,`c3`,`c4`,`c5`) VALUES('c1','c2','c3','c4','c5');
INSERT INTO `test03`(`c1`,`c2`,`c3`,`c4`,`c5`) VALUES('d1','d2','d3','d4','d5');
INSERT INTO `test03`(`c1`,`c2`,`c3`,`c4`,`c5`) VALUES('e1','e2','e3','e4','e5');
/* 创建复合索引 */
CREATE INDEX idx_test03_c1234 ON `test03`(`c1`,`c2`,`c3`,`c4`);
题目
/* 最好索引怎么创建的,就怎么用,按照顺序使用,避免让MySQL再自己去翻译一次 */
/* 1.全值匹配 用到索引c1 c2 c3 c4全字段 */
EXPLAIN SELECT * FROM `test03` WHERE `c1` = 'a1' AND `c2` = 'a2' AND `c3` = 'a3' AND `c4` = 'a4';
/* 2.用到索引c1 c2 c3 c4全字段 MySQL的查询优化器会优化SQL语句的顺序*/
EXPLAIN SELECT * FROM `test03` WHERE `c1` = 'a1' AND `c2` = 'a2' AND `c4` = 'a4' AND `c3` = 'a3';
/* 3.用到索引c1 c2 c3 c4全字段 MySQL的查询优化器会优化SQL语句的顺序*/
EXPLAIN SELECT * FROM `test03` WHERE `c4` = 'a4' AND `c3` = 'a3' AND `c2` = 'a2' AND `c1` = 'a1';
/* 4.用到索引c1 c2 c3字段,c4字段失效,范围之后全失效 */
EXPLAIN SELECT * FROM `test03` WHERE `c1` = 'a1' AND `c2` = 'a2' AND `c3` > 'a3' AND `c4` = 'a4';
/* 5.用到索引c1 c2 c3 c4全字段 MySQL的查询优化器会优化SQL语句的顺序*/
EXPLAIN SELECT * FROM `test03` WHERE `c1` = 'a1' AND `c2` = 'a2' AND `c4` > 'a4' AND `c3` = 'a3';
/*
6.用到了索引c1 c2 c3三个字段, c1和c2两个字段用于查找, c3字段用于排序了但是没有统计到key_len中,c4字段失效
*/
EXPLAIN SELECT * FROM `test03` WHERE `c1` = 'a1' AND `c2` = 'a2' AND `c4` = 'a4' ORDER BY `c3`;
/* 7.用到了索引c1 c2 c3三个字段,c1和c2两个字段用于查找, c3字段用于排序了但是没有统计到key_len中*/
EXPLAIN SELECT * FROM `test03` WHERE `c1` = 'a1' AND `c2` = 'a2' ORDER BY `c3`;
/*
8.用到了索引c1 c2两个字段,c4失效,c1和c2两个字段用于查找,c4字段排序产生了Using filesort说明排序没有用到c4字段
*/
EXPLAIN SELECT * FROM `test03` WHERE `c1` = 'a1' AND `c2` = 'a2' ORDER BY `c4`;
/* 9.用到了索引c1 c2 c3三个字段,c1用于查找,c2和c3用于排序 */
EXPLAIN SELECT * FROM `test03` WHERE `c1` = 'a1' AND `c5` = 'a5' ORDER BY `c2`, `c3`;
/* 10.用到了c1一个字段,c1用于查找,c3和c2两个字段索引失效,产生了Using filesort */
EXPLAIN SELECT * FROM `test03` WHERE `c1` = 'a1' AND `c5` = 'a5' ORDER BY `c3`, `c2`;
/* 11.用到了c1 c2 c3三个字段,c1 c2用于查找,c2 c3用于排序 */
EXPLAIN SELECT * FROM `test03` WHERE `c1` = 'a1' AND `c2` = 'a2' ORDER BY c2, c3;
/* 12.用到了c1 c2 c3三个字段,c1 c2用于查找,c2 c3用于排序 */
EXPLAIN SELECT * FROM `test03` WHERE `c1` = 'a1' AND `c2` = 'a2' AND `c5` = 'a5' ORDER BY c2, c3;
/*
13.用到了c1 c2 c3三个字段,c1 c2用于查找,c2 c3用于排序 没有产生Using filesort
因为之前c2这个字段已经确定了是'a2'了,这是一个常量,再去ORDER BY c3,c2 这时候c2已经不用排序了!
所以没有产生Using filesort 和(10)进行对比学习!
*/
EXPLAIN SELECT * FROM `test03` WHERE `c1` = 'a1' AND `c2` = 'a2' AND `c5` = 'a5' ORDER BY c3, c2;
/* GROUP BY 表面上是叫做分组,但是分组之前必定排序。 */
/* 14.用到c1 c2 c3三个字段,c1用于查找,c2 c3用于排序,c4失效 */
EXPLAIN SELECT * FROM `test03` WHERE `c1` = 'a1' AND `c4` = 'a4' GROUP BY `c2`,`c3`;
/* 15.用到c1这一个字段,c4失效,c2和c3排序失效产生了Using filesort */
EXPLAIN SELECT * FROM `test03` WHERE `c1` = 'a1' AND `c4` = 'a4' GROUP BY `c3`,`c2`;
GROUP BY基本上都需要进行排序,索引优化几乎和ORDER BY一致,但是GROUP BY会有临时表的产生。
15、总结
索引优化的一般性建议:
- 对于单值索引,尽量选择针对当前
query过滤性更好的索引。 - 在选择复合索引的时候,当前
query中过滤性最好的字段在索引字段顺序中,位置越靠前越好。 - 在选择复合索引的时候,尽量选择可以能够包含当前
query中的where子句中更多字段的索引。 - 尽可能通过分析统计信息和调整
query的写法来达到选择合适索引的目的。
口诀:
- 带头大哥不能死。
- 中间兄弟不能断。
- 索引列上不计算。
- 范围之后全失效。
- 覆盖索引尽量用。
- 不等有时会失效。
- like百分加右边。
- 字符要加单引号。
- 一般SQL少用or。