准备
mysql8(Innodb)
测试表
一共两个测试表一个用户表user
, 一个订单表order
,order
表有个user_id
关联user
user
表字段
字段 | 注释 |
---|---|
id | |
name | 姓名 |
order
表字段
字段 | 注释 |
---|---|
id | |
user_id | 用户ID |
product_name | 产品名称 |
两个表分别存储了一些数据
explain
使用explain
检查索引使用情况
trace
为了跟踪mysql的执行计划,使用trace
命令,使用方法如下
set session optimizer_trace="enabled=on",end_markers_in_json=on;
--要执行的sql
SELECT * FROM information_schema.OPTIMIZER_TRACE;
分别从explain
和trace
两个角度分析sql的执行
测试工作准备完毕,开始测试,场景就是查询订单同时携带用户姓名
一.sql1
SELECT * FROM `order` INNER JOIN `user` ON `order`.user_id = `user`.id;
疑问
mysql 是否遵循小表驱动大表,先查user再查order
情况
用户总数 | 订单总数 | user_id索引 |
---|---|---|
15 | 36 | 无 |
用户表
explain
可以看出先全表扫描order表,再根据user_id去查询user表并使用主键索引(eq_ref),使用
NLJ
算法
trace
var trace = {
"steps": [
{
"join_preparation": { // join前准备
"select#": 1,
"steps": [
{
"expanded_query": "/* select#1 */ select `order`.`user_id` AS `user_id`,`order`.`product_name` AS `product_name`,`user`.`id` AS `id`,`user`.`name` AS `name` from (`order` join `user` on((`order`.`user_id` = `user`.`id`)))"
}, // 规范sql(缺省的AS填充,括号补充等等)
{
"transformations_to_nested_joins": {// 转换到嵌套联接
"transformations": [ // 转换方式
"JOIN_condition_to_WHERE", // join条件转where
"parenthesis_removal" // 多余括号删除
] /* transformations */,
"expanded_query": "/* select#1 */ select `order`.`user_id` AS `user_id`,`order`.`product_name` AS `product_name`,`user`.`id` AS `id`,`user`.`name` AS `name` from `order` join `user` where (`order`.`user_id` = `user`.`id`)"
} /* transformations_to_nested_joins */
}
] /* steps */
} /* join_preparation */
},
{
"join_optimization": { // join优化
"select#": 1,
"steps": [
{
"condition_processing": { // 条件处理
"condition": "WHERE",
"original_condition": "(`order`.`user_id` = `user`.`id`)",
"steps": [
{
"transformation": "equality_propagation",// 1.条件优化-等值优化
"resulting_condition": "multiple equal(`order`.`user_id`, `user`.`id`)"
},
{
"transformation": "constant_propagation", // 2.条件优化-常量优化
"resulting_condition": "multiple equal(`order`.`user_id`, `user`.`id`)"
},
{
"transformation": "trivial_condition_removal",// 3.条件优化-无用条件删除
"resulting_condition": "multiple equal(`order`.`user_id`, `user`.`id`)"
}
] /* steps */
} /* condition_processing */
},
{
"substitute_generated_columns": {
} /* substitute_generated_columns */
},
{
"table_dependencies": [// 查询的表
{ // 表1
"table": "`order`",
"row_may_be_null": false,
"map_bit": 0,
"depends_on_map_bits": [
] /* depends_on_map_bits */
},
{ // 表2
"table": "`user`",
"row_may_be_null": false,
"map_bit": 1,
"depends_on_map_bits": [
] /* depends_on_map_bits */
}
] /* table_dependencies */
},
{
"ref_optimizer_key_uses": [// 可用于优化的索引 这里是user表的id
{
"table": "`user`",
"field": "id",
"equals": "`order`.`user_id`",
"null_rejecting": true
}
] /* ref_optimizer_key_uses */
},
{
"rows_estimation": [ // 根据行数评估全表扫描时间
{
"table": "`order`",
"table_scan": {
"rows": 36,
"cost": 0.25
} /* table_scan */
},
{
"table": "`user`",
"table_scan": {
"rows": 15,
"cost": 0.25
} /* table_scan */
}
] /* rows_estimation */
},
{
"considered_execution_plans": [// 可选执行计划,列出所有可能并比较
{ // 计划一
"plan_prefix": [
] /* plan_prefix */,
"table": "`order`",// 首表(即先查表)
"best_access_path": {
"considered_access_paths": [ // 可选扫描方式
{
"rows_to_scan": 36, // 要扫描的行数
"filtering_effect": [
] /* filtering_effect */,
"final_filtering_effect": 1,
"access_type": "scan", // 扫描方式:全表扫描
"resulting_rows": 36,
"cost": 3.85, // 预计耗时
"chosen": true // 是否使用
}
] /* considered_access_paths */
} /* best_access_path */,
"condition_filtering_pct": 100,
"rows_for_plan": 36, // 首表扫描行数
"cost_for_plan": 3.85, // 首表预计耗时
"rest_of_plan": [ // 关联的部分(join的计划)
{
"plan_prefix": [
"`order`"
] /* plan_prefix */,
"table": "`user`", // 关联的表
"best_access_path": {
"considered_access_paths": [ // 可选扫描方式
{
"access_type": "eq_ref",// 1.使用索引
"index": "PRIMARY",
"rows": 1,
"cost": 12.6, // 预计耗时
"chosen": true, // 使用
"cause": "clustered_pk_chosen_by_heuristics"
},
{
"rows_to_scan": 15,
"filtering_effect": [
] /* filtering_effect */,
"final_filtering_effect": 1,
"access_type": "scan", // 2.全表扫描
"using_join_cache": true,
"buffers_needed": 1,
"resulting_rows": 15,
"cost": 54.2545, // 预计耗时
"chosen": false // 不使用
}
] /* considered_access_paths */
} /* best_access_path */,
"condition_filtering_pct": 100,
"rows_for_plan": 36, // 计划一扫描行数:36*1
"cost_for_plan": 16.45, // 预计耗时 = 12.6+3.85
"chosen": true // 使用
}
] /* rest_of_plan */
},
{ // 计划二
"plan_prefix": [
] /* plan_prefix */,
"table": "`user`",
"best_access_path": { // 可选扫描方式
"considered_access_paths": [
{
"access_type": "ref", // 1、使用索引
"index": "PRIMARY",
"usable": false,
"chosen": false // 不使用
},
{
"rows_to_scan": 15,
"filtering_effect": [
] /* filtering_effect */,
"final_filtering_effect": 1,
"access_type": "scan", // 2、全表扫
"resulting_rows": 15,
"cost": 1.75,
"chosen": true // 使用
}
] /* considered_access_paths */
} /* best_access_path */,
"condition_filtering_pct": 100,
"rows_for_plan": 15, // 扫描行数
"cost_for_plan": 1.75, // 预计耗时
"rest_of_plan": [ // 关联计划
{
"plan_prefix": [
"`user`"
] /* plan_prefix */,
"table": "`order`", // 关联表
"best_access_path": {
"considered_access_paths": [ // 可选扫描方式
{
"rows_to_scan": 36,
"filtering_effect": [
] /* filtering_effect */,
"final_filtering_effect": 1,
"access_type": "scan", //1.全表扫描
"using_join_cache": true,
"buffers_needed": 1,
"resulting_rows": 36,
"cost": 54.2611,
"chosen": true // 使用
}
] /* considered_access_paths */
} /* best_access_path */,
"condition_filtering_pct": 100,
"rows_for_plan": 540, // 计划2 预计扫描行数 36*15
"cost_for_plan": 56.0111, // 计划2 预计耗时 = 1.75+54.2611
"pruned_by_cost": true // 因为cost太大被弃用
}
] /* rest_of_plan */
}
] /* considered_execution_plans */
},
{
"attaching_conditions_to_tables": {// 把条件分配给表,这里先扫描order,所以分配条件给user
"original_condition": "(`user`.`id` = `order`.`user_id`)",
"attached_conditions_computation": [
] /* attached_conditions_computation */,
"attached_conditions_summary": [
{
"table": "`order`",
"attached": null
},
{
"table": "`user`",
"attached": "(`user`.`id` = `order`.`user_id`)"
}
] /* attached_conditions_summary */
} /* attaching_conditions_to_tables */
},
{
"finalizing_table_conditions": [// 最终分配条件
{
"table": "`user`",
"original_table_condition": "(`user`.`id` = `order`.`user_id`)",
"final_table_condition ": null
}
] /* finalizing_table_conditions */
},
{
"refine_plan": [ // 提炼出来的计划。即先order再user
{
"table": "`order`"
},
{
"table": "`user`"
}
] /* refine_plan */
}
] /* steps */
} /* join_optimization */
},
{
"join_execution": {
"select#": 1,
"steps": [
] /* steps */
} /* join_execution */
}
] /* steps */
}
因为第一次trace,详细的注释了整个trace的输出,可以看出这是一个列出可执行计划、预估时间、最终确定执行计划的过程
它的步骤大概如下
- join前准备(规范sql语句,join转where,删除无用括号等)
- join优化(包含等值优化,常量优化,无用条件删除)
- 列举查询的表
- 列举可用于优化的索引
- 预估每个表全表扫描时间
- 列出所有方案,预估时间,选择最优方案
这一步最重要,比如AB两个表,有先A后B,和先B后A两种方案,每个表又分为全表扫描或使用索引等方案,最终计算时间,选出耗时最少方案 - 把关联条件分配某一个表
上面我们的trace,可以看出最终mysql选择了先全表扫描order(大表),再用order的user_id通过主键索引查找user(小表)的方式,因为这种方式耗时最少
结论
mysql在优化时,不一定小表驱动大表,可能会根据索引情况优化
二.sql2
sql不变,减少用户表数据
SELECT * FROM `order` INNER JOIN `user` ON `order`.user_id = `user`.id;
情况
用户总数 | 订单总数 | user_id索引 |
---|---|---|
4 | 36 | 无 |
减少了用户表的数目,从15变为4条
explain
可以看出,情况变了,先全局扫描user表再全局扫描order表,使用
BNL
(Using join buffer)
trace
与sql1的主要区别,在于plan2(先user再order)的cost大大降低,甚至小于plan1(先), 并又pruned_by_cost:true变为choose:true
cost的大大降低是由于user表的数据少了,全表扫描时间大大减小(这里就提现出小表驱动大表的优势了)
结论
小表驱动大表是一般情况(也是我们优化的方向),而不是mysql优化遵循的原则,mysql遵循的原则就是看哪种查法cost更低
三.sql3
这次sql还是不变,恢复用户表数据,再sql1的基础上,添加user_id索引
SELECT * FROM `order` INNER JOIN `user` ON `order`.user_id = `user`.id;
添加索引
ALTER TABLE `order`
ADD INDEX `user_id`(`user_id`) USING BTREE;
情况
用户总数 | 订单总数 | user_id索引 |
---|---|---|
15 | 36 | 有 |
explain
结果令我有点意外,我以为两边索引条件差不多,应该是小表带大表,但是事实不是,索引失效了,只是possible_keys有这个user_id,但实际没有使用,再通过trace分析
trace
首先第一个变化时优化可用索引增加了一个
plan2(先user再order也多出这个选项--索引)
但最终没有走这个plan2,原因还是plan2虽然用到索引,大大减少了cost,但是cost还是大于plan1(减少的不够)
总结
对比一下两个索引的rows
可以看到user.id的rows是1,而order.user_id的rows是9,cost也相对很大,所以之所以没有用plan2的原因是cost较大,而cost较大的主要原因普通索引没有主键索引快(猜想主要原因是首先order表比较大,而使用user_id索引还需要回表操作)
四.sql4
主要对标sql2,用户数据留3条,并且还给order.user_id加索引,修改为left join
SELECT * FROM `order` LEFT JOIN `user` ON `order`.user_id = `user`.id;
情况
用户总数 | 订单总数 | user_id索引 |
---|---|---|
4 | 36 | 有 |
explain
对比sql2,这里的用户表数据少,而且order还有索引,如虎添翼啊,如果是
inner join
肯定是先user再order,可left join
确是order驱动user,trace一下
trace
首先left join 不能把join优化为where
user明确依赖order,且row_may_not_be_null:true
而且不像inner join 有两个选项plan1和plan2, 明确只有一个plan(先order再user)
最终条件也有出入,加入了null的逻辑
总结
left join 一定是前表驱动后表,这种情况给order.user_id加索引肯定无效
RIGHT JOIN
和left join道理都一样,只不过颠倒了前后
五.sql5
与sql1对比,加入where条件
SELECT * FROM `order` INNER JOIN `user` ON `order`.user_id = `user`.id where name = '侯玉芬';
用户总数 | 订单总数 | user_id索引 |
---|---|---|
15 | 36 | 有 |
explain
与sql1同样的情况,而结果相反,这次先user再order,可见是where条件再join判断cost时起了作用
trace
先看plan1(order驱动user),order全scan然后user使用eq_ref基本没有变化,双scan变化很大,有了where的加持,user全scan的final_filtering_effect
由1变化为0.1,resulting_rows
由15变为1.5,而cost变化也非常大,使用scan甚至cost小于eq_ref
当然虽然plan1快,但plan2更快(user驱动order)
对比sql1时间(order->user路径)
总结
当join配合了where条件,加where条件的表扫描的速度会变快,会影响join的驱动表,而扫描变快的原因,可以感觉到mysql内部先用where条件筛选出结果,再用结果去与另一个表做join,筛选可以让一个大表再join中变成小表
六.sql6
上一个sql,有些人会做这样的优化
SELECT * FROM `order` INNER JOIN (select * from `user` where name = '侯玉芬') u ON `order`.user_id = `u`.id;
其实结果也显而易见了,思路倒是不错,但是你就是不这么写,mysql内部也是这么执行的(至少mysql8是,其它没验证),那何必要写这么蠢笨的sql
trace
{
"transformations_to_nested_joins": {
"transformations": [
"JOIN_condition_to_WHERE",
"parenthesis_removal"
] /* transformations */,
"expanded_query": "/* select#1 */ select `order`.`user_id` AS `user_id`,`order`.`product_name` AS `product_name`,`user`.`id` AS `id`,`user`.`name` AS `name` from `order` join `user` where ((`order`.`user_id` = `user`.`id`) and (`user`.`name` = '侯玉芬'))"
} /* transformations_to_nested_joins */
}
mysql又给转换回去了,后面的和sql5一模一样
总结
这种级别的优化,mysql都已经做好了,就不用可以这么写了
七.sql7
还是对标sql5,=
变成like
SELECT * FROM `order` INNER JOIN `user` ON `order`.user_id = `user`.id where name like '侯%';
explain
没变化
trace
稍微有点变化,cost比等于变大了
总结
基本和等于逻辑一样,直接导致final_filtering_effect变化进而resulting_rows变化最终导致cost变化,其实就是表变小的程度不一样,类似>
<
like
也都是这个逻辑,如果使用>
<
like
导致筛选范围过大可能又会导致驱动表变化
八.sql8
依然对标sql5,inner join
变left join
,情况不变
SELECT * FROM `order` LEFT JOIN `user` ON `order`.user_id = `user`.id where name = '侯玉芬';
explain
和inner join结果一样,关键left join并没有左表驱动右表(ps:想一下这个sql的结果,和inner join完全一样)
trace
对比sql5
基本证实了多了个outer_join_to_inner_join,就是把left join优化成join,其它基本一样
也就是说left join最终转换为inner join
还有个细小的变动,
row_may_be_null
遵循了left join的原则,但是没啥用
总结
left join右表加条件会自动优化为join(inner join)
八.一.sql8.1(补充)
SELECT * FROM `order` LEFT JOIN `user` ON `order`.user_id = `user`.id where name = '侯玉芬' or name is null;
新增条件or name is null
,这是一个小兄弟写出来的错误代码。
之前的场景是,发布一个调查问卷,每个人可以看到,但是答完之后需要显示已答完状态,这么写sql在只有一个用的时候可以满足,但bug就在于任何一个人答完了,其他人就无法查到这个调查问卷了
查询结果
查询结果就出现了问题,只能查到自己参与的订单和没有人参与的订单,如果别人参与了,查不出
explain
可以看到,又变成order驱动user,说明并没有转换inner join
trace
发现outer_join_to_inner_join没有了
总结
sql8总结:left join右表加条件会自动优化为join(inner join)
8.1扩展:如果右表条件是is null例外
所以这条语句的意思是: 查询某用户参与的和没有人参与的订单,如果想实现查询某用户参与的和某用户没有参与的订单,需要如下
九.sql9
SELECT * FROM `order` LEFT JOIN `user` ON `order`.user_id = `user`.id AND name = '侯玉芬';
修改where为and,让条件进入join
结果
查询出所有订单,是侯玉芬
的关联上用户,否则用户是空
- 与sql8相比最大的不同是意思不一样,一个where条件是对所有条件过滤,一个是join的on条件配合left join 没有满足条件自动补空
- 与sql8.1相比,意思也不一样,8.1是查询该用户的订单和没有关联用户的订单,9是查询所有订单如果是该用户则关联用户
那么更重要的问题来了: 问题where可以先筛选再join的优化,on可不可以
explain
由于是left join一定是order驱动user(不转换时),所以还真看不出来上面的问题
trace
可以与sql4对比,多了个on条件,结果基本差不多,就是多了个条件,sql基本没有优化
需要注意的是里的on条件就没有了JOIN_condition_to_WHERE 因为放到where上不就相当于sql8,意思完全不一样了
最终分配的条件也有了大大的不同
final_table_condition是
"final_table_condition ": "(is_not_null_compl(user), (`user`.`name` = '侯玉芬'), true)"
大概意思就是如果不符合条件就关联null,这就是left join条件放在on和where的主要区别,一个是关联建立的条件,一个是最终的条件筛选
总结
由于left join左表驱动,基本看不出on条件是否提前使用,还是得用inner join或right join试试,所以还得测试
sql9和sql8.1对应两种场景
sql8.1.抢单列表:查询我抢到的和所有没有被抢的单
sql9.公单列表:查询我完成的和我未完成的单
十.sql10
修改为inner join
SELECT * FROM `order` INNER JOIN `user` ON `order`.user_id = `user`.id AND name = '侯玉芬';
结果
与where一样,on条件不满足就join不上,join不上inner join就会过滤这条记录
explain
与where还一样
基本可以确定on也会先筛选再join(using where)
trace
{
"transformations_to_nested_joins": {
"transformations": [
"JOIN_condition_to_WHERE",
"parenthesis_removal"
] /* transformations */,
"expanded_query": "/* select#1 */ select `order`.`user_id` AS `user_id`,`order`.`product_name` AS `product_name`,`user`.`id` AS `id`,`user`.`name` AS `name` from `order` join `user` where ((`order`.`user_id` = `user`.`id`) and (`user`.`name` = '侯玉芬'))"
} /* transformations_to_nested_joins */
}
真相大白了,on的条件在inner join 情况下其实就相当于where,消除了以上疑惑
总结
on条件如果可以优化为where则可以先执行过滤再join
最终
大概把测试出来的结果整理下,join时
- inner join 不一定小表驱动大表,mysql会分析每种方式的cost,选取最小cost方案
- left join 一般左表驱动右表
- left join 在右表加where条件时一般会转换为inner join,可能打破左表驱动右表
- left join 右表的条件如果是is null就不会转换为inner join
- 查询主键索引的cost一般比使用普通索引快,使用普通索引的cost一般比全表扫描快
- inner join加where条件会先过滤再join,大表过滤成小表会导致扫描cost降低,因此可能会导致驱动表变化
- inner join中on的条件会被自动优化到where