单表查询优化
案例:有一张表为article表(文章表),表中的字段有文章id、category_id、评论数comments以及被看次数views,建表语句如下所示。现有需求如下,查询 category_id 为1 且 comments 大于 1 的情况下,views 最多的 article_id与author_id。
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
);
第一次写的查询语句如下所示:
SELECT id,author_id FROM article WHERE category_id = 1 AND comments > 1 ORDER BY views DESC LIMIT 1;
通过explain分析该sql语句,结果如下图
分析:该语句有什么缺点呢?很显然,查询的类型为全表查询,并且使用了文件排序。显然性能极差。
基于索引的思想,我们在做如下的改进。
改进一:使用索引,在要进行查询的字段上面创建索引
create index idx_article_ccv on article(category_id, comments, views);
创建好索引后,再次用explain进行分析,结果如下图
分析:可以看到我们这里不再是全表扫描,而是范围,并且用到了刚创建的索引,但有一个缺点:仍然使用文件内排序,造成文件的开销。原因是范围之后索引失效,我们这里只用到了category_id和comments索引,因此做如下的改进:
改进二:删除之前建的索引,重新创建索引
drop index idx_article_ccv on article;
create index idx_article_cv on article(category_id, views);
此时,我们看到类型为ref,并且在Extra中没有了文件排序。所以此时的查询性能是满足条件的。
关联查询优化
案例:有两张表,一张book表,一张class表,book表为书籍表,class表为类别表,两表的关联字段为card,现需要查询两表对应的信息。两表的结构如下,并且分别有20条记录:
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`)
);
简单的查询语句为:
SELECT * FROM class LEFT JOIN book ON class.card = book.card;
对该语句用执行计划分析:
可见,两表的执行行数都是20,并且类型都是ALL,而且用到了连接缓存,性能不佳。
改进:因为查询使用的是左连接,左表的记录都要使用,我们在右表上创建索引
create index idx_book_card on book(card);
然后用执行计划分析:
可以看到其中的一个类型已经由ALL变为ref,并且查询的行数为1,总体而言性能有了提高。
对比:若我们删除刚创建的索引,并在class表上创建索引,然后进行分析:
drop index idx_book_card on book;
create index idx_class_card on class(card);
可见其中的一个ALL变为了index,但是rows都是20行,并且使用到了连接缓存。而且,我们知道index类型劣于ref类型。因此可以得出以下的结论。
结论:连接查询时,一般在从表中建立索引
建议:
- left join时,选择小表作为驱动表,大表作为被驱动表
- 保证被驱动表的join字段已经被索引
- inner join时,mysql会自动将小结果集的表选为驱动表
- 子查询尽量不要放在被驱动表,有可能使用不到索引
小表驱动大表
优化原则:小表驱动大表,即用小的数据集驱动大的数据集。
原理分析:
select * from A where id in (select id from B);
等价于:
for select id from B
for select * from A where A.id = B.id
即:当B表的数据集必须小于A表的数据集时,用in优于exists
select * from A where exists (select 1 from B where B.id = A.id)
等价于:
for select * from A
for select * from B where B.id = A.id
即:当A表的数据集小于B表的数据集时,用exists优于in。
注意:A表与B表的ID字段应建立索引。
OrderBy排序优化
因为我们在排序时,在没有创建索引的情况下会出现文件内排序(using filesort)这对mysql的性能是极为不利的,因此需要创建索引并进行排序优化。
案例
我们有一张员工表,其中有两个字段age和birth,我们在这两个字段上创建了一个复合索引,现需要判断通过我们指定的排序方式,是否会产生文件排序。
CREATE TABLE tblA(
id int primary key not null auto_increment,
age INT,
birth TIMESTAMP NOT NULL,
name varchar(200)
);
#创建复合索引
CREATE INDEX idx_A_ageBirth ON tblA(age,birth,name);
注意:在我们给出的案例中,我们关注的点是order by 后面的字段,以及是否产生了using filesort。
由上面的案例我们可以得到:
MySQL支持二种方式的排序,FileSort和Index,Index效率高,它指MySQL扫描索引本身完成排序。FileSort方式效率较低。
使用Index方式排序的情况:
- ORDER BY 语句使用索引最左前列
- 使用Where子句与Order BY子句条件列组合满足索引最左前列
- where子句中如果出现索引的范围查询(即explain中出现range)会导致order by 索引失效。
filesort排序方式:
filesort有双路排序和单路排序
- 双路排序:从磁盘取排序字段,在buffer进行排序,再从磁盘取其他字段。
- 单路排序:从磁盘读取查询需要的所有列,按照order by列在buffer对它们进行排序,然后扫描排序后的列表进行输出,它的效率更快一些,避免了第二次读取数据。并且把随机IO变成了顺序IO,但是它会使用更多的空间,因为它把每一行都保存在内存中了。
- MySQL4.1之前使用的是双路排序,单路排序是后面出现的,优于双路排序,但也会存在一定的问题:单路排序把所有字段都取出, 所以有可能取出的数据的总大小超出了sort_buffer的容量,导致每次只能取sort_buffer容量大小的数据,进行排序(创建tmp文件,多路合并),排完再取sort_buffer容量大小,再排……从而多次I/O。本来想省一次I/O操作,反而导致了大量的I/O操作,反而得不偿失。
- 优化策略:增大sort_buffer_size参数的设置(单路排序内存大小);增大max_length_for_sort_data参数的设置(单次排序字段大小);去掉select 后面不需要的字段
去重优化
尽量不要使用 distinct 关键字去重,可以使用group by 实现去重,可以用上索引。
举得这个例子是因为创建的索引用的是复合索引,所以加上了age,birth不然会产生索引失效的问题。