十条sql语句玩转join的索引优化

准备

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;

分别从explaintrace两个角度分析sql的执行
测试工作准备完毕,开始测试,场景就是查询订单同时携带用户姓名

一.sql1

SELECT * FROM `order` INNER JOIN `user` ON `order`.user_id = `user`.id;
疑问

mysql 是否遵循小表驱动大表,先查user再查order

情况
用户总数 订单总数 user_id索引
15 36

用户表

explain

image.png

可以看出先全表扫描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

explain

可以看出,情况变了,先全局扫描user表再全局扫描order表,使用BNL(Using join buffer)

trace

与sql1的主要区别,在于plan2(先user再order)的cost大大降低,甚至小于plan1(先), 并又pruned_by_cost:true变为choose:true


image.png

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
image.png

结果令我有点意外,我以为两边索引条件差不多,应该是小表带大表,但是事实不是,索引失效了,只是possible_keys有这个user_id,但实际没有使用,再通过trace分析

trace

首先第一个变化时优化可用索引增加了一个

image.png

plan2(先user再order也多出这个选项--索引)
image.png

但最终没有走这个plan2,原因还是plan2虽然用到索引,大大减少了cost,但是cost还是大于plan1(减少的不够)

总结

对比一下两个索引的rows


user.id

order.user_id

可以看到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

image.png

对比sql2,这里的用户表数据少,而且order还有索引,如虎添翼啊,如果是inner join肯定是先user再order,可left join确是order驱动user,trace一下

trace

首先left join 不能把join优化为where


image.png

user明确依赖order,且row_may_not_be_null:true


image.png

而且不像inner join 有两个选项plan1和plan2, 明确只有一个plan(先order再user)
image.png

最终条件也有出入,加入了null的逻辑


image.png
总结

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
image.png

与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

image.png

当然虽然plan1快,但plan2更快(user驱动order)
image.png

image.png

对比sql1时间(order->user路径)
image.png
总结

当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 joinleft 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,其它基本一样

image.png

也就是说left join最终转换为inner join
还有个细小的变动,row_may_be_null遵循了left join的原则,但是没啥用
image.png

总结

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
image.png

可以看到,又变成order驱动user,说明并没有转换inner join

trace

发现outer_join_to_inner_join没有了


image.png
总结

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基本没有优化


image.png

需要注意的是里的on条件就没有了JOIN_condition_to_WHERE 因为放到where上不就相当于sql8,意思完全不一样了
最终分配的条件也有了大大的不同


image.png

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
image.png

与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

你可能感兴趣的:(十条sql语句玩转join的索引优化)