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` VARBINARY(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,且comment大于 1 的情况下,views 最多的 article_id
select id,author_id from article where category_id= 1 and comments>1 order by views desc limit 1;
- explain 分析查询
explain select id,author_id from article where category_id= 1 and comments>1 order by views desc limit 1;

我们不难发现一个问题,虽然只有3条数据,但是查询类型却是全表扫描(type=ALL),并且使用的是 Using filesort文件排序显示。 那么随着表的增大到百万级的数据量时,这将会大大增加数据库的负担。
即: type为All,即最坏的情况;Extra中也出现了 Using filesort,也是最坏的情况。
- 查看索引情况
show index from article;

- 新建索引
create index idx_article_ccv on article (category_id,comments,views);

- 再次 explain 分析查询语句
explain select id,author_id from article where category_id= 1 and comments>1 order by views desc limit 1;

通过 possible_keys 和 key, 我们不难发现这已经使用了 索引 idx_article_ccv, 但是查询类型也由All 改成了 range,提高了效率。 但是Extra 依然为 Using filesort。 这里的原因是 range范围查询 之后的字段 将不会使用索引,导致索引失效的问题。
- 上述的并不是最优的建索引的做法,因此需要删除之前建的索引。
drop index idx_article_ccv on article;
再次查询,检验索引已经删除了。
explain select id,author_id from article where category_id= 1 and comments>1 order by views desc limit 1;

- 换个 建索引的做法:
create index idx_article_cv on article(category_id,views);

- 查看索引的结果
select index from article;

- explain 解析查询
explain select id,author_id from article where category_id= 1 and comments>1 order by views desc limit 1;

我们可以发现现在的结果是我们能够接受的了。查询类型为ref,不再是All全表扫描。运用了索引,也不会出现 文本内存查询 。
总结:为什么第一次会出现索引失效的情况呢?原因是我们建立索引的时候是以 category、comments、views 的顺序建的 组合索引。又因为在SQL中的索引的底层是以 BTree 的方式建立的,那就要按照 B树 索引工作原理,先排序 category_id ,如果遇到相同的 category_id,则再排序comments,否则索引结束;如果遇到了comments,再排序views。 因为此时的 comments 字段在 联合索引 里处于 中间位置, 因此 comments>1 是一个范围值(即为 range),那么MySQL的 comments字段就失效,MySQL就无法利用索引再对 views 部分进行检索,即range类型查询字段后面的索引失效。
2. (两表)索引优化案例二:
- 建表操作:
CREATE TABLE IF NOT EXISTS `class`(
`id` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`card` INT (10) UNSIGNED NOT NULL
);
CREATE TABLE IF NOT EXISTS `book`(
`bookid` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`card` INT (10) UNSIGNED NOT NULL
);
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)));
- 查询 class 和 book 存在关联的结果
select * from class left join book on class.card=book.card;
- expalin 分析 查询语句
explain select * from class left join book on class.card=book.card;

因为我们两个表的card字段都没有进行加索引,因此两张表都进行全表扫描。
- 创建为book表创建索引
create index idx_card_book on book(card);
- 查看索引,并用explain分析查询语句
explain select * from class left join book on class.card=book.card

- 当只在 left join 中的 右表建立索引, 那么执行的查询类型为 ref,此时的执行效率较高。

- 当只在left join 中的 左表建立索引,那么执行 查询类型为 index, 效率较低。但优于All;

-
总结:
左连接的特性是,左边表一定会进行全表扫描,那么不管左边是否建立索引都会全表扫描。所以关键是在右表上,确定右表是如何进行扫描,所以一定要建立索引优化。
3. (三表)索引优化案例三:
- 建表
CREATE TABLE IF NOT EXISTS `phone`(
`phoneid` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`card` INT (10) UNSIGNED NOT NULL
)ENGINE = INNODB;
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)));
-
查询表
-
查看 index 索引 情况

- 查询 三表连接
select * from class left join book on class.card = book.card left join phone on class.card = phone.card;
- explain 分析 三表连接查询
explain select * from class left join book on class.card = book.card left join phone on class.card = phone.card;

我们不难发现,此时三张表都是全表查询。Extra额外信息是有 Using join buffer, 即使用了连接缓存。
- 建索引时(驱动表class)

此时只用到了 查询类型为index,且查询的条数为20条,效率低。
- 建索引时(被驱动表 book)

此时在两个驱动表中就存在这区别了,因为book表中对card字段建立了索引,所以查询类型为 ref,额外信息Extra 中的使用的时Using index,查询行数为1,效率明显高于未建索引时。
- 建索引时(被驱动表book 和 被驱动表 phone)

此时总查询的行为 22行,相比于未建索引的效率大大提升。
4. 上述总结:
- join语句的优化时,尽可能减少join语句的 NestedLoop 的循环次数;
- ==永远使用小结果集驱动大结果集的方式进行。==left join 的左表为驱动表,右表为被驱动表。
- 保证 join 语句中的被驱动表上 join 条件字段已经被索引。
- 在无法保证被驱动表的 join 条件字段被索引,且内存资源充裕的情况下,不要吝惜 JoinBuffer 的设置。
6. 索引失效
1. 最左前缀法则
- 建表
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());
create index idx_staffs_nameAgePos on staffs(`name`,`age`,`pos`);
# ALTER TABLE staffs ADD INDEX index_staffs_nameAgePos(`name`,`age`,`pos`);
- 查看索引
show index from staffs;

- 查询语句
select * from staffs;

- explain 分析 name=‘July’;
explain select * from staffs where name = 'July';

- explain 分析 name=‘July’ and age=23 ;
explain select * from staffs where name='July' and age=23 ;

- explain 分析 name=‘July’ and age=23 and pos=‘dev’;
explain select * from staffs where name='July' and age=23 and pos='dev';

- explain 分析 name=‘July’ and pos=‘dev’ and age=23;
explain select * from staffs where name='July' and pos='dev' and age =23;

- explain 分析 name=‘July’ and age=23;
explain select * from staffs where name='July' and pos='dev' ;

- explain 分析 pos=‘dev’
explain select * from staffs where pos='dev' ;

- explain 分析 age=23;
explain select * from staffs where age =23;

分析:
- 通过示例 ①,②,③: 我们可以发现使用的索引个数为1,2,3个,且key_len 长度不断增加。
- 通过示例 ③,④:我们可以发现当查询的索引都被使用时,即为 idx_staffs_nameAgePos 中的三个索引字段都使用, 此时与where 出现的字段的先后顺序无关。
- 通过示例⑤,我们可以发现使用的字段为组合索引中的前后两个字段,没有使用中间字段时,即使用了name和Pos,此时的Pos已经失效,无法应用上。可用过示例①的key_len的长度等于示例⑤的长度论证。
- 示例⑥、⑦: 当直接使用组合索引中的后面字段时,索引将会失效。
总结:
最左前缀法则:如果索引了多列字段,要遵循最左前缀法则。指的是查询从索引的最左前列开始并且 不跳过索引中的列。
2.不在索引列上做任何操作(计算、函数、类型转换)
首先上例子,一个是使用了函数,一个没有使用函数的查询语句;
mysql> select * from staffs where name='July';
mysql> select * from staffs where left(name,4)='July';

通过结果,我们很容易的发现,这两条语句的查询结果是一样的。因为 left() 函数需要传入两个参数,第一个参数为查询的字段,第二个参数为截取长度。 left(name,4) 意思就是对name字段的结果进行截取,并返回至多4个字符。
那么接下来再用 explain 进行分析这两条语句的区别:

我们可以发现,在使用了函数之后,索引就已经失效了。
3. like 以通配符开头(%abc…)
当MySQL表中如果对某个字段建立了索引,但由于查询是使用了 like,进行匹配。如果使用通配符%开头的话,将会导致索引失效,从索引扫描变成全表扫描。

有百分百和没百分号查询的结果相同。
查看explain 分析的结果:

我们不难的发现,以 通配符%开头的话,key字段为NULL,说明没有使用上索引。
结论:只在右边使用 通配符% 的效率最高,通配符开头会导致索引失效问题
-
导致问题:如果实际开发项目中,一定要使用通配符%开头,但又会导致索引失效,该问题是如何解决的呢?
-
对于通配符导致索引失效的问题,我们通常使用覆盖索引的方式进行解决,即我们为需要查询的字段建立覆盖索引。
-
查看 表staffs 的表结构

假如我们需要对 name字段继续模糊匹配,并经常性的需要返回name,age,pos字段的话,我们可以对 name,age,pos三个字段建立一个覆盖索引
create index idx_staffs_nameAgePos on staffs(name,age,pos)
此时如果使用通配符查询时,不会导致索引失效。但这还是有前提的哦。。。。。

我们不难发现,该方式进行查询时,通配符没有导致索引失效。
-
-
但者不是意味着不会出现索引失效
-
当我们查询的是所有字段 * 或者是 不包含在覆盖索引中的字段时,依然会导致索引失效哦
-
mysql> explain select name,age,pos,add_time from staffs where name like "%J%"; mysql> explain select * from staffs where name like "%J%"; -

-
所以这两种情况最终还是导致索引失效了。
-
-
总结:在使用通配符时,我们一定要清楚什么时候会导致索引失效,哪些字段才是我们最常用的字段。
4. 字符串不加单引号,会导致索引失效
我们都知道 如果是字符串类型的话必须加引号,至于数值类型的可加可不加。 但是字符串类型是纯数字的话,不加引号也是能查询到结果的。
例1: select * from staffs

从这张表中,我们可以看到第三条记录得 name 为纯数字。
mysql> select id,name,pos,add_time from staffs where name=2000;
mysql> select id,name,pos,add_time from staffs where name='2000';

通过结果,我们可以发现 对于纯数字是可以不加单引号也能的结果的。
- 但这就代表没有问题了吗?
其实不然,我们用explain 进行分析一下:

在上图中,我们可以看出如果对字符串类型加了单引号的话,是可以使用到覆盖索引的,但是如果没加单引号,则不会使用。其实这也验证了第二条语句,自动类型转换会导致类型失效。MySQL底层自动的将整数类型转换成了字符串类型。
5. 应当少用or语句,or语句会导致索引失效
网上经常能看到一些文章总结在 mysql 中不能命中索引的各种情况,其中有一种说法就是指使用了 or 的语句都不能命中索引。
这种说法其实是不够正确的,正确的结论应该是,从 mysql5.0 后,如果在 or 连接的字段上都有独立的索引的话,是可以命中索引的,这里就是用到了 index_merge 特性。
举个栗子⚽:

在数据库表staffs中,只有 一个 name,age,pos字段的组合索引(覆盖索引)。
mysql> select * from staffs ;
mysql> select * from staffs where name='July' or age=23;
mysql> select * from staffs where name='July' or name='2000';

说明都能查询到数据,但这两种方式继续explain分析以下结果,看看是不是使用了or就不能使用索引了呢?
mysql> explain select * from staffs where name='July' or name='2000';
mysql> explain select * from staffs where name='July' or age=23;

😭很出乎意料啦,查询条件只有name字段的话,还是会使用上索引的,所以,上述说法并不完全正确。
-
那问题来了:如果我们业务需求一定要用name 或者 age进行筛选,那怎么办呢?
-
这样的话我们依然只能够使用**组合索引(覆盖索引)**的方式,将并且显示列出需要查询的字段,而不是使用 *。
-
explain select name,age from staffs where name='July' or age=23 ;
6. order by ,group by 导致索引失效(难点)
在使用索引的时候,我们理想情况下是将 Extra 信息未NULL。当然啦,如果在使用文件排序有可能会出现Using filesort,出现这个的话,会影响系统的查询效率,应该适当优化。那就来举几个栗子,看看什么时候会出现吧。
**特别强调:**索引的主要功能就是==查询和排序==
🐷栗子1: 查询的是 * 的情况
我们依然用 staffs 表进行举栗子哈
# 查询语句1
mysql> explain select * from staffs where name='July' order by name;
# 查询语句2
mysql> explain select * from staffs where name='July' order by age;
# 查询语句3
mysql> explain select * from staffs where name='July' order by pos;
# 查询语句4
mysql> explain select * from staffs where name='July' order by age,pos;

通过上面例子,我们可以发现四个查询语句都是使用了索引中的第一个字段的,也就是说查询语句只用到了一个字段进行查询;
再拿 栗2、栗3做对比的话,我们可以发现例三是出现了文件内排序(Using filesort)的,也就是说索引并没有使用上,导致索引失效了。而栗四就是用上了索引中的排序功能,虽然查询只用到了一个字段。但整体效率还是不错的。
🐷栗2:
# 查询语句5
mysql> explain select * from staffs where name='July' order by name;
# 查询语句6
mysql> explain select * from staffs order by name;
# 查询语句7
mysql> explain select name from staffs order by name;
分析语句:
没有对比就没有伤害,这一组的分析,得运用上SQL语句的执行顺序。
- 在查询5的语句中,使用了
where name=‘July’,那么就会用上索引结构,因此不会出现文件内排序; - 而查询6的语句是
没有使用where,并且是select *,经过之前的栗子,我们知道select * 是不会使用索引的,所以这次order by 会出现 文件内排序。 - 而查询7的语句
虽然没有使用where,但是select name, 因此是会使用索引的,进而使用上了索引排序,而不是文件内排序。

🐷栗3:
# 查询语句8
mysql> explain select name,age,pos from staffs ;
# 查询语句9
mysql> explain select name,age,pos from staffs order by name ;
# 查询语句10
mysql> explain select name,age,pos from staffs order by age;
# 查询语句11
mysql> explain select name,age,pos from staffs where name='July';
# 查询语句12
mysql> explain select name,age,pos from staffs where name='July' order by name;
# 查询语句13
mysql> explain select name,age,pos from staffs where name='July' order by age;
# 查询语句14
mysql> explain select name,age,pos from staffs where name='July' order by pos;
分析语句:
这再次强调了索引的作用了,索引就是优化查询和排序。在建索引时,已经订好了顺序了。
- 通过查询10 的语句,我们可以发现,如果没有name,那么就不会使用上索引的排序,那么就会将查询到的结果放入内存中,再进行排序。
- 同理啦,查询语句14,也是一样的,首先加载了name的字段,但是对于查询pos来说,是乱序的,所以只能将查询的结果也加载到内存中啦。

🐷栗4:
# 查询语句15
mysql> explain select name,age,pos from staffs where name='July' order by age;
# 查询语句16
mysql> explain select name,age,pos from staffs where name='July' order by age,name;
# 查询语句17
mysql> explain select name,age,pos from staffs order by age,name;
# 查询语句18
mysql> explain select name,age,pos from staffs;
分析语句:
- 查询语句15,16都有使用上了
where name='July',因此是先查询了所有结果为name=‘July’ 的记录项。对于查询语句16来说,此时 虽然是order by age,name.但因为name已经是一个常量了,那么这里的order by 等价于是order by age,name=const。而索引建立时就是以name,age排序的,所以最终对于age来说是有序的。 - 查询语句16对比17,区别就是 索引树中先排序的是name,再排age,但是我不使用。我要强行排age,再来排name,这就必然会文件内排序啦。

索引总结:
其实我们可以以一种很形象的形式理解什么是组合索引。
举个例子,我会把组合索引比喻成一栋酒店,那么组合索引的第一个字段,我们可以理解成一层层楼,而第二个字段比喻成一个个房间,第三个字段当然就是房间的物品了。。。。以此类推下去
那如果我们出去住酒店,拿到房间号是4-12,我们很容易就能理解,我们的房间在4楼的12号房间啦,那我们想要回到自己的房间,很自然的就直接去4楼,而不会一层一层的去看下这个是不是我们的房间。
如果一天老板说,张三,你去把房间号排下序,顺序按这个条件排下order by(字段2,字段1),张三此时心里肯定是妈卖批的啦😭。老子都拍好字段1,字段2了,你不用,却要反着来。我也不知道每一层的房间号数量是不是一致的啊。
最终张三只能很苦逼的拿着这些房卡进行再次排序。
这就是组合索引啦,很多情况以这个想法,变位想想,就很清楚啦。
1544



被折叠的 条评论
为什么被折叠?



