1、多表关联查询原理与优化
1)嵌套循环关联(NLJ - Nested Loop Join)
select
tab1.col1,tab2.col2
from
tab1,tab2
where
tab1.col3 = tab2.col3
and tab1.col3 = 1
explain select
gc.*,g.title
from
t_goods g,t_goods_cover gc
where
g.goods_id = gc.goods_id
and g.category_id = 44;
从上述发现,嵌套循环的次数太多,查询效率非常低,但是如何去优化呢?
create index idx_goods_id on t_goods_cover(goods_id);
create index idx_category_id on t_goods(category_id);
explain select
gc.*,g.title
from
t_goods g,t_goods_cover gc
where
g.goods_id = gc.goods_id
and g.category_id = 44;
set global query_cache_size=0
set global query_cache_type=0
import org.junit.Test;
import java.sql.*;
public class SqlOptimize {
@Test
public void testSQL1() throws SQLException, ClassNotFoundException {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql:///babytun?useUnicode=true&characterEncoding=UTF-8&useSSL=false", "root", "root");
PreparedStatement pstmt = connection.prepareStatement("select gc.*, g.title from t_goods g , t_goods_cover gc where g.goods_id = gc.goods_id and g.category_id = 44");
long st = new java.util.Date().getTime();
for(int i = 0 ; i < 10000 ;i++) {
ResultSet rs = pstmt.executeQuery();
rs.close();
}
long et = new java.util.Date().getTime();
System.out.println("1万次执行时间:" + (et-st) + "毫秒,平均执行时间:" + (et-st)/10000f + "毫秒");
connection.close();
}
}
2)关联查询优化点
- 降低驱动表的扫描次数,从而减少驱动表对连接列的循环次数
- 驱动表循环次数较低的情况下,尽量保证连接列上存在索引
2、慢SQL日志
- MySQL是支持对数据库当中执行速度较慢sql进行记录的,但是慢sql查询默认时关闭的,需要我们手动开启,使用命令:
set GLOBAL slow_query_log=on;; - 但是执行时间超过多久才算慢sql呢?这个我们可以手动设置
set GLOBAL long_query_time=0.001;,注意它的单位是秒; - 但是我们的慢sql会保存到哪里呢?也需要我们手动设置
set GLOBAL. slow_query_log_file="slow-sql.log",该文件默认存放在mysql安装目录的data文件夹下。 - 注意:上述三项配置,使用命令在数据库会话中执行之后要新开会话才会生效;但如果数据库重启配置会丢失,如果想要持久化配置,则需要将其配置在my.cnf文件中。
1)如何定义SQL为慢SQL?
日常应用当中,一般来说SQL的执行不能超过300ms。
2)定位没有使用索引的SQL语句?
在实际开发如果项目刚上线,程序的运行速度还是比较快的,但是当数据量增大之后,程序越来越慢,这时候我们就需要查看是否有SQL语句没有使用索引?命令:set GLOBAL log_queries_not_using_indexes=on;这样一来当没有使用索引的SQL也会被记录到slow-sql.log当中,但是需要注意的是,MySQL在执行的过程中,如果使用到了系统表,他也没有索引相应执行的SQL语句也是会被记录下来的。
3)总结:
- 建议慢SQL的时间不超过300ms
- 慢SQL日志分析,Query_time的值越小查询时间越短,rows_sent的值越小查询过程中扫描的行数越少,获取结果行的速度越快
- 可以开启对没有使用索引的SQL进行慢SQL日志记录
3、Explain详解
explain select (select 1 fromactor where id =1)
from (select * from where id = 1) der;
1)id:select出现的序列号
有几个select就有几个id,也就是几行数据,并且id是根据select出现的顺序进行的顺序增长的;需要注意的是 id=1的表为驱动表。
2)select_type:查询类型
3)table:explain正在访问的表
如果是上图中单纯的表名就很好理解;
<derived3>代表,正在访问select_type为derived,id为3的select形成的结果集;
<union3,4>代表,正在访问id为3的select和id为4的select通过union所形成的结果集。
4)partitions:查询作用在的哪个分区表
分区表:把大表按条件单独存储到多个不同的“物理小表”中,再构建出的完整“逻辑表”。
查询是先查询逻辑表,快速过滤出数据在那张表,然后查询指定的物理表即可。
5)type:关联类型和访问类型
由执行效率从高到低排序中(🌟为常用):
1、system:
2、const🌟:常量引用,mysql能对查询的某部分进行优化并将其转换为一个常量,一般用于主键筛选或union key的所有列和常数做对比,所以表最多有一个匹配行,读取1次,速度快
explain select * from (select * from film whre id = 1) tmp;
3、eq_ref🌟:关联查询,primary 或 unique key 索引的所有部分被连接使用,最多返回一条符合条件的记录,简单的select查询不会出现这种type。
explain select * from film_actor left jion film on film_actor.film_id = film.film_id;
相当于就是主外键等唯一索引进行连接查询的时候。
4、ref🌟:非主键或非唯一性索引字段进行检索,要和某个值相比较,可能出现多个符合条件的行。
explain select * from film where name = “film1”;
5、fulltext:全文索引
6、ref_or_null🌟:类似于ref,但是包含null情况的索引扫描,可以搜索到值为NULL的行。
explain select * from film where name = “film1” or name is null;
7、index_merge:
8、unique_suquery:
9、index_subquery:
10、range🌟:范围扫描,通常出现在in()、between、> 、< 、>= 、<= 等等操作中,使用一个索引来检索给定范围的行。
explain select * from actor where id > 1;
11、index🌟:使用索引进行全表扫描,相比ALL来讲,只需扫描索引树查找数据,通常比ALL快一些。
explain select count(*) from film;
12、ALL🌟:全表扫描,从表的第一行数据往后查找直至到最后一行数据,通常需要使用索引进行优化。
explain select * from actor;
6)possible_key:可能使用到的索引
只是代表可能是用得到,即备选项。
7)key:使用到的索引
查询优化的时候使用到的索引名称
8)key_len:使用的索引的长度
mysql在索引里是使用的字节数,通过这个值可以算出具体使用了索引中的哪些列
9)ref:使用的列或常量
在key列记录的索引中,表查找所用到的列或常量,常见的有const(常量)、func(函数)、NULL、字段名(例如:film.id)
10)rows:查询时扫描的行数
mysql在查询时的一个估值,是需要进行读取并检测的行数
11)filtered:百分比估值
代表(rows * filtered)/ 100 得到一个数值,就是可能产生的结果行数,这个结果将于前表产生交互
12)Extra:扩展列说明
- distinct:
一旦mysql找到了与行联合匹配的行,就不再进行搜索了,也就是去重
explain select distinct name from film left join film_actor on film.id = film_actor.film_id;
- Using index:
查询的列都是在同一索引上的时候,返回索引中的信息,没有访问表中的其他列信息(这种情况是性能高的一种表现)
这种情况也称为“索引覆盖”。
explain select id from film order by id; - Using where:
mysql在存储引擎检索行数据后再进行过滤,也就是读取行数据,在按照where后的条件进行过滤(效率较差)
explain select * from film where id > 1;
- Using temporary:
使用了临时表来处理查询,通常需要优化这种情况(一般针对查询结果加索引)
explain select distinct name from actor;
- Using filesort:
使用了文件扫描对结果进行了计算排序,效率很差需要优化
expalain select * from actor order by name;
索引排序优化,select字段要覆盖到排序字段才能使用索引,索引章节有提到
如:explain select name from actor order by name;
5、关联查询慢SQL调优案例
- 数据准备(该案例为阿里云慢SQL挑战赛当中的真实案例)
- 执行结果,花费190ms
1)解读
通过a表的seller_name关联到b表的seller_name,b表的user_id关联到c表的user_id并且c表的user_id等于17,a表的gmt_create
创建时间为当前时间前后十分钟内的数据,并且将结果按照a表数据的创建时间进行正序排序,执行结果只有一条数据,并且花费190ms。
2)优化
(1) 进行explain解析SQL
每一张表都是全表扫描,并且数据量都不算小。
注意:该案例较早,explain执行计划的结果并没有filtered那一列。
(2) 确定驱动表
关联查询,那么首先就需要确定驱动表,并且确定最优的驱动表。进行分析那张表作为驱动表:
对三张表分别进行分析的时候,只要去掉表之间的关联关系,然后将单表条件跟随其后进行count(*),驱动表最优即为嵌套循环进行次数最少的外循环,所以a表和c表作为驱动表的话,外循环均只需要循环一次即可,但是SQL当中结果需要按照a表的创建时间进行排序,既然优化肯定要使用索引,并提高索引的使用率,所以将a表作为驱动表(可根据实际情况斟酌)。
(3) 创建索引
- 首先是在a表的gmt_create上创建索引,因为结果需要根据该字段进行排序,所以优化排序需要对该字段进行创建索引
- 创建联合索引意义如图所示,防止回表数据量过大,做的冗余。
回表:
先通过数据库索引扫描出数据所在的行,再通过行主键id取出索引中未提供的数据,即基于非主键索引的查询需要多扫描一棵索引树。
因此,可以通过索引先查询出id字段,再通过主键id字段,查询行中的字段数据,即通过再次查询提供MySQL查询速度。
(4) 再次执行explain
这是我们发现虽然建立了索引,但是a、b两张表还是进行了全标扫描,没有使用索引。这时候可以使用explain extended SQL对sql再次进行分析,可以得到更加详细的执行计划:
(5) 发现问题
- a表:表结构当中gmt_create为Varchar存储,但是SQL当中使用了日期函数进行计算,发生 隐式转换导致索引失效
- b表:a表的seller_name的COLLATE使用到了utf8_bin,而b表没有使用数据库默认导致COLLATE不一致,也发生 隐式转换导致索引失效(数据库5.7之后修正了,就不会发生该情况)
- c表:如图(数据库5.7之后修正了,就不会发生该情况)
(6) 解决问题