Administrator
发布于 2022-11-25 / 61 阅读
0
0

MySQL基于规则的优化

前言

MySQL 本质上是一个软件,MySQL 的设计者并不能要求使用这个软件的人个个都是数据库高高手。

说的就是我啦!!!!小菜鸡!!!!
文中关于物化表等情况复现的场景sql我就写不出来了。模拟都不会,唉!
学海无涯

也就是说菜鸡无法避免写一些执行起来十分耗费性能的语句。即使是这样,MySQL 还是会依据一些规则,竭尽全力的把这个很糟糕的语句转换成某种可以比较高效执行的形式,这个过程也可以被称作查询重写(就是人家觉得你写的语句不好,自己再重写一遍)。

sql重写规则

1、条件化简

这部分就记一些比较现实一点的点吧!
像什么	
移除不必要的括号;常量传递;等值传递;移除没用的条件等问题不会有人会写在实际开发的sql里吧
遭不住

1、表达式计算

之前就说过,搜索条件中索引列和常数使用某些运算符连接起来才可能使用到索引,所以如果可以的话,最好让索引列以单独的形式出现在表达式中:一部分是因为被操作过的索引列会用不上索引,另一部分就是因为优化器是不会尝试对这些表达式进行化简的,在sql中实现运算也是要增加成本的吧?

所以说:某个列并不是以单独的形式作为表达式的操作数时,比如出现在函数中,出现在某个更复杂表达式中,就像这样:

ABS(a) > 5
或者:
-a < -8

2、外连接消除

内连接 的驱动表和被驱动表的位置可以相互转换,而 左(外)连接 和 右(外)连接 的驱动表和被驱动表是固定的。这就导致 内连接 可能通过优化表的连接顺序来降低整体的查询成本,而 外连接 却无法优化表的连接顺序。

而外连接和内连接的本质区别就是:对于外连接的驱动表的记录来说,如果无法在被驱动表中找到匹配ON子句中的过滤条件的记录,那么该记录仍然会被加入到结果集中,对应的被驱动表记录的各个字段使用NULL值填充;而内连接的驱动表的记录如果无法在被驱动表中找到匹配ON子句中的过滤条件的记录,那么该记录会被舍弃。

在WHERE 子句中,凡是不符合WHERE子句中条件的记录都不会参与连接。只要我们在搜索条件中指定关于被驱动表相关列的值不为 NULL ,那么外连接中在被驱动表中找不到符合 ON 子句条件的驱动表记录也就被排除出最后的结果集了,也就是说:在这种情况下:外连接和内连接也就没有什么区别了!

eg:

SELECT * FROM t1 INNER JOIN t2 ON t1.m1 = t2.m2;

SELECT * FROM t1 LEFT JOIN t2 ON t1.m1 = t2.m2 WHERE t2.n2 IS NOT NULL;

这两句虽然分别是内连接和外连接,但查询的结果并没有区别

这种在外连接查询中,指定的 WHERE 子句中包含被驱动表中的列不为 NULL 值的条件称之为 空值拒绝(英文名: reject-NULL )。在被驱动表的WHERE子句符合空值拒绝的条件后,外连接和内连接可以相互转换。这种转换带来的好处就是查询优化器可以通过评估表的不同连接顺序的成本,选出成本最低的那种连接顺序来执行查询。

3、子查询优化

在MySQL中,可以使用连接(JOIN)查询来替代子查询。连接查询不需要建立临时表,其速度比子查询要快,如果查询中使用索引的话,性能就会更好。感觉就是半连接的思想

结论:尽量不要使用NOT IN 或者 NOT EXISTS,用LEFT JOIN xxx ON xx WHERE xx IS NULL替代

1、子查询语法

这里就不记录这些了,不记得的再去查

补一个相关子查询

不相关子查询
如果子查询可以单独运行出结果,而不依赖于外层查询的值,我们就可以把这个子查询称之为 不相关子查
询 。我们前边介绍的那些子查询全部都可以看作不相关子查询,所以也就不举例子了哈。

相关子查询
如果子查询的执行需要依赖于外层查询的值,我们就可以把这个子查询称之为 相关子查询 。比如:
 SELECT * FROM t1 WHERE m1 IN (SELECT m2 FROM t2 WHERE n1 = n2);
例子中的子查询是 (SELECT m2 FROM t2 WHERE n1 = n2) ,可是这个查询中有一个搜索条件是 n1 = n2 ,别
忘了 n1 是表 t1 的列,也就是外层查询的列,也就是说子查询的执行需要依赖于外层查询的值,所以这个子
查询就是一个 相关子查询 。

2、子查询在MySQL中是怎么执行的

将前面的表作为例子:

CREATE TABLE single_table (
 id INT NOT NULL AUTO_INCREMENT,
 key1 VARCHAR(100),
 key2 INT,
 key3 VARCHAR(100),
 key_part1 VARCHAR(100),
 key_part2 VARCHAR(100),
 key_part3 VARCHAR(100),
 common_field VARCHAR(100),
 PRIMARY KEY (id),
 KEY idx_key1 (key1),
 UNIQUE KEY idx_key2 (key2),
 KEY idx_key3 (key3),
 KEY idx_key_part(key_part1, key_part2, key_part3)
) Engine=InnoDB CHARSET=utf8;

1、子查询的执行方式

1、如果该子查询是不相关子查询,比如下边这个查询:

 SELECT * FROM s1 
 WHERE key1 IN (SELECT common_field FROM s2);

先单独执行 (SELECT common_field FROM s2) 这个子查询。
然后在将上一步子查询得到的结果当作外层查询的参数再执行外层查询

SELECT * FROM s1 WHERE key1 IN (...) 。

2、如果该子查询是相关子查询,比如下边这个查询:

 SELECT * FROM s1 
 WHERE key1 IN (SELECT common_field FROM s2 WHERE s1.key2 = s2.key2);

这个查询中的子查询中出现了 s1.key2 = s2.key2 这样的条件,意味着该子查询的执行依赖着外层查询的值,所以这个查询的执行方式是这样的:

先从外层查询中获取一条记录,本例中也就是先从 s1 表中获取一条记录。
然后从上一步骤中获取的那条记录中找出子查询中涉及到的值,本例中就是从 s1 表中获取的那条记录
中找出 s1.key2 列的值,然后执行子查询。
最后根据子查询的查询结果来检测外层查询 WHERE 子句的条件是否成立,如果成立,就把外层查询的那
条记录加入到结果集,否则就丢弃。
再次执行第一步,获取第二条外层查询中的记录,依次类推~

2、IN子查询优化

补派生表

把子查询放在外层查询的 FROM 子句后,那么这个子查询的结果相当于一个 派生表

含有 派生表 的查询, MySQL 提供了两种执行策略:

1、派生表物化。
我们可以将派生表的结果集写到一个内部的临时表中,然后就把这个物化表当作普通表一样参与查询。当
然,在对派生表进行物化时,MySQL 使用了一种称为 延迟物化 的策略,也就是在查询中真正使
用到派生表时才回去尝试物化派生表,而不是还没开始执行查询呢就把派生表物化掉。
如果采用物化派生表的方式来执行这个查询的话,如果压根儿找不到,说明参与连接的 s1 表记录就是空的,	
所以整个查询的结果集就是空的,所以也就没有必要去物化查询中的派生表了。

2、将派生表和外层的表合并,也就是将查询重写为没有派生表的形式
通过将外层查询和派生表合并的方式成功的消除了派生表,也就意味着我们没必要再付出创建和访问临
时表的成本了。可是并不是所有带有派生表的查询都能被成功的和外层查询合并,当派生表中有这些语句就
不可以和外层查询合并:
聚集函数,比如MAX()、MIN()、SUM()啥的
DISTINCT
GROUP BY
HAVING
LIMIT
UNION 或者 UNION ALL
派生表对应的子查询的 SELECT 子句中含有另一个子查询
所以 MySQL 在执行带有派生表的时候,优先尝试把派生表和外层查询合并掉,
如果不行的话,再把派生表物化掉执行查询。
1、物化表的提出

对于不相关的 IN 子查询,比如这样:

SELECT * FROM s1 
 WHERE key1 IN (SELECT common_field FROM s2 WHERE key3 = 'a');

但是如果单独执行子查询后的结果集太多的话,就会导致这些问题:

结果集太多,可能内存中都放不下

对于外层查询来说,如果子查询的结果集太多,那就意味着 IN 子句中的参数特别多,这就导致:

无法有效的使用索引,只能对外层查询进行全表扫描(个人理解是太多可能在使用索引回表不如直接全表扫描性价比高)。

在对外层查询执行全表扫描时,由于 IN 子句中的参数太多,这会导致检测一条记录是否符合和 IN 子句
中的参数匹配花费的时间太长。

对此: MySQL的处理是:不直接将不相关子查询的结果集当作外层查询的参数,而是将该结果集写入一个临时表里。写入临时表的过程是这样的:

该临时表的列就是子查询结果集中的列。
写入临时表的记录会被去重。

IN 语句是判断某个操作数在不在某个集合中,集合中的值重不重复对整个 IN 语句的结果并没有啥子
关系,所以我们在将结果集写入临时表时对记录进行去重可以让临时表变得更小,更省地方

一般情况下子查询结果集不会大的离谱,所以会为它建立基于内存的使用 Memory 存储引擎的临时表,而且
会为该表建立哈希索引。

如果子查询的结果集非常大,超过了系统变量 tmp_table_size 或者 max_heap_table_size ,临时表会转而使用基于磁盘的存储引擎来保存结果集中的记录,索引类型也对应转变为 B+ 树索引。

这个将子查询结果集中的记录保存到临时表的过程称之为 物化 (英文名:Materialize )。为了方便起见,我们就把那个存储子查询结果集的临时表称之为 物化表 。正因为物化表中的记录都建立了索引(基于内存的物化表有哈希索引,基于磁盘的有B+树索引),通过索引执行 IN 语句判断某个操作数在不在子查询结果集中变得非常快,从而提升了子查询语句的性能。

关于物化表的其他东西这有篇文章可以看看:https://www.bigspring.cn/post/16456085037553/

2、物化表转连接

将满足条件的子查询结果集变成一张临时表后呢?不能没有作用吧!

回到刚才的sql语句

SELECT * FROM s1 
 WHERE key1 IN (SELECT common_field FROM s2 WHERE key3 = 'a');
 
 现在(SELECT common_field FROM s2 WHERE key3 = 'a')的结果已被转成一张临时表,加入叫materialized_table
 那么这个sql最后的样子就是
 
 SELECT * FROM s1 , materialized_table WHERE  s1.key1 materialized_table.common_field
 
 变成两张表的连接查询

如图:假设临时表中的列为 m_val:
从s1表的角度来看:

image-1669377621000

从临时表的角度来看

image-1669377645425

如果使用 s1 表作为驱动表的话,总查询成本由下边几个部分组成:
物化子查询时需要的成本
扫描 s1 表时的成本
s1表中的记录数量 × 通过 m_val = xxx 对 materialized_table 表进行单表访问的成本(我们前边说过
物化表中的记录是不重复的,并且为物化表中的列建立了索引,所以这个步骤显然是非常快的)。

如果使用 materialized_table 表作为驱动表的话,总查询成本由下边几个部分组成:
物化子查询时需要的成本
扫描物化表时的成本
物化表中的记录数量 × 通过 key1 = xxx 对 s1 表进行单表访问的成本(非常庆幸 key1 列上建立了索
引,所以这个步骤是非常快的)。
MySQL 查询优化器会通过运算来选择上述成本更低的方案来执行查询。

3、将子查询转换为semi-join

前些篇文里说的半连接终于要记回来了、、、、

前提:将子查询进行物化之后再执行查询都会有建立临时表的成本,然而;能不能不进行物化操作直接把子查询转换为连接呢?
eg:

SELECT * FROM s1 
 WHERE key1 IN (SELECT common_field FROM s2 WHERE key3 = 'a');
上面sql会有三种情况:
情况一:对于 s1 表的某条记录来说, s2 表中没有任何记录满足 s1.key1 = s2.common_field 这个条件,
那么该记录自然也不会加入到最后的结果集。

情况二:对于 s1 表的某条记录来说, s2 表中有且只有记录满足 s1.key1 = s2.common_field 这个条件,
那么该记录会被加入最终的结果集。

情况三:对于 s1 表的某条记录来说, s2 表中至少有2条记录满足 s1.key1 = s2.common_field 这个条件,
那么该记录会被多次加入最终的结果集。

对于 s1 表的某条记录来说,由于我们只关心 s2 表中是否存在记录满足 s1.key1 = s2.common_field 这个条件,而不关心具体有多少条记录与之匹配,又因为有 情况三 的存在,我们上边所说的 IN 子查询和两表连接之间并不完全等价。但是将子查询转换为连接又真的可以充分发挥优化器的作用,所以MySQL在这里提出了一个新概念 — 半连接 (英文名: semi-join )。将 s1 表和 s2 表进行半连接的意思就是:对于 s1 表的某条记录来说,我们只关心在 s2 表中是否存在与之匹配的记录是否存在,而不关心具体有多少条记录与之匹配,最终的结果集中只保留 s1 表的记录。

效果大约看上去类似是这样的:

SELECT s1.* FROM s1 INNER JOIN s2 
 ON s1.key1 = s2.common_field WHERE s2.key3 = 'a';

这里另举一个例子(因为我的表不一样~~~~~):

第一条sql:
select * from db2 WHERE key1 IN (select  key1 from db1 WHERE name = 'a' ) ;

第二条sql:
SELECT * FROM db2 INNER JOIN db1 WHERE db2.key1 = db1.key1 AND db1.name = "a";

第一条sql半连接真的是像第二条那样嘛?
不是:
开启 optimizer_trace可以看到,其半连接的语法是这样的(我修改了一下):
select * from `db2` semi join (`db1`) where ((`db1`.`name` = 'a') and (`db2`.`key1` = `db1`.`key1`))

好像真的差不多喔!!!

但是吧!
第一条的sql会被转换成半连接;第二条不会。两者之间的效率貌似差别也不大。

貌似感觉不出半连接的好处!!!毕竟我将sql写成第二条那样的也是很正常的好嘛!!
所以对此先保留意见,可能是没感受到呢

image-1669380882236

如何实现半连接

上面说到第一条sql会转成半连接,第二条不会。那就是半连接的实现是有条件的。

在mysql5.7中。子查询有 5 种优化策略:子查询表上拉(table pullout)、重复值消除(duplicate weedout)、
首次匹配(first match)、松散扫描(loose scan)、物化连接(materialization)。

在8.0中,table pullout貌似名字改成了semijoin???

在8.0里使用命令
show variables like 'optimizer_switch';	查看
其中:

materialization=on,
semijoin=on,
loosescan=on,
firstmatch=on,
duplicateweedout=on,
五种策略都是开启的
1、Table pullout (子查询中的表上拉)

子查询的查询列表处只有主键或者唯一索引列时,可以直接把子查询中的表 上拉 到外层查询的 FROM 子句中,并把子查询中的搜索条件合并到外层查询的搜索条件中。

例子嘛,就是上面的那第一条sql(我的db1和db2的结构是一样的,key1都是唯一索引)。

为啥当子查询的查询列表处只有主键或者唯一索引列时,就可以直接将子查询转换为连接查询呢?
因为主键或者唯一索引列中的数据本身就是不重复的!所以对于同一条 s1 表中的记录,你不可能找到两条以上
的符合 db1.key1 = db2.key1 的记录
2、DuplicateWeedout execution strategy (重复值消除)

eg:

select * from db2 WHERE name IN (select  NAME from db1 WHERE key3 = "a") ;

sql转换为半连接查询后, db2表中的某条记录可能在 db1表中有多条匹配的记录,所以该条记录可能多次被添加到最后的结果集中,为了消除重复;建立一个临时表,比方说这个临时表长这样:

 CREATE TABLE tmp (
 id PRIMARY KEY
 );

这样在执行连接查询的过程中,每当某条 db2 表中的记录要加入结果集时,就首先把这条记录的 id 值加入到这个临时表里,如果添加成功,说明之前这条 db2 表中的记录并没有加入最终的结果集,现在把该记录添加到最终的结果集;如果添加失败,说明这条之前这条 db2 表中的记录已经加入过最终的结果集,这里直接把它丢弃就好了,这种使用临时表消除 semi-join 结果集中的重复值的方式称之为 DuplicateWeedout 。

这里的sql不能很好的复现,估计是表的设计和数量的问题

下面有些东西不懂可以看:https://juejin.cn/post/6854573204879441928#heading-3

{
            "transformation": {
              "select#": 2,
              "from": "IN (SELECT)",
              "to": "semijoin",	//半连接的转换都显示这个
              "chosen": true,
              "transformation_to_semi_join": {
                "subquery_predicate": "`db2`.`name` in (/* select#2 */ select `db1`.`name` from `db1` where (`db1`.`key3` = 'a'))",
                "embedded in": "WHERE",
                "evaluating_constant_semijoin_conditions": [
                ],
                "semi-join condition": "((`db1`.`key3` = 'a') and (`db2`.`name` = `db1`.`name`))",
                "decorrelated_predicates": [
                  {
                    "outer": "`db2`.`name`",
                    "inner": "`db1`.`name`"
                  }
                ]
              }
              
      半连接策略选择,看注释的地方就行        
    ---------------------------------------------          
              
  "semijoin_strategy_choice": [	//以下会列出可供选择的策略
                ],
                "rest_of_plan": [
                  {
                    "plan_prefix": [	//当前执行计划的前置计划
                      "`db1`"
                    ],
                    "table": "`db2`",
                    "best_access_path": {
                      "considered_access_paths": [
                        {
                          "rows_to_scan": 99410,
                          "filtering_effect": [
                          ],
                          "final_filtering_effect": 1,
                          "access_type": "scan",
                          "using_join_cache": true,
                          "buffers_needed": 1,
                          "resulting_rows": 99410,
                          "cost": 10110.6,
                          "chosen": true
                        }
                      ]
                    },
                    "condition_filtering_pct": 10,
                    "rows_for_plan": 9941,
                    "cost_for_plan": 10111.4,
                    "semijoin_strategy_choice": [
                      {
                        "strategy": "LooseScan",	//LooseScan execution strategy (松散索引扫描)
                        "recalculate_access_paths_and_cost": {
                          "tables": [
                            {
                              "table": "`db1`",
                              "best_access_path": {
                                "considered_access_paths": [
                                  {
                                    "access_type": "ref",
                                    "index": "key3",
                                    "rows": 1,
                                    "cost": 0.88414,
                                    "chosen": true
                                  },
                                  {
                                    "access_type": "range",
                                    "range_details": {
                                      "used_index": "key3"
                                    },
                                    "chosen": false,
                                    "cause": "heuristic_index_cheaper"
                                  }
                                ]
                              },
                              "unknown_key_1": {
                                "searching_loose_scan_index": {
                                  "indexes": [
                                    {
                                      "index": "key3",
                                      "index_handles_needed_semijoin_equalities": false
                                    }
                                  ]
                                }
                              }
                            }
                          ]
                        },
                        "chosen": false
                      },
                      {
                        "strategy": "DuplicatesWeedout",	//DuplicateWeedout execution strategy (重复值消除)
                        "cost": 12100.6,
                        "rows": 9941,
                        "duplicate_tables_left": true,
                        "chosen": true
                      }
                    ],
                    "chosen": true
                  }
                ]
              },
              -----------------------------------------
              上面的plan_prefix是db1,这里没有
              {
                "plan_prefix": [
                ],
                "table": "`db2`",
                "best_access_path": {
                  "considered_access_paths": [
                    {
                      "rows_to_scan": 99410,
                      "filtering_effect": [
                      ],
                      "final_filtering_effect": 1,
                      "access_type": "scan",
                      "resulting_rows": 99410,
                      "cost": 10109.2,
                      "chosen": true
                    }
                  ]
                },
                "condition_filtering_pct": 100,
                "rows_for_plan": 99410,
                "cost_for_plan": 10109.2,
                "semijoin_strategy_choice": [
                ],
                "rest_of_plan": [
                  {
                    "plan_prefix": [
                      "`db2`"
                    ],
                    "table": "`db1`",
                    "best_access_path": {
                      "considered_access_paths": [
                        {
                          "access_type": "ref",
                          "index": "key3",
                          "rows": 1,
                          "cost": 87892.4,
                          "chosen": true
                        },
                        {
                          "access_type": "range",
                          "range_details": {
                            "used_index": "key3"
                          },
                          "chosen": false,
                          "cause": "heuristic_index_cheaper"
                        }
                      ]
                    },
                    "condition_filtering_pct": 10,
                    "rows_for_plan": 9941,
                    "cost_for_plan": 98001.6,
                    "semijoin_strategy_choice": [
                      {
                        "strategy": "FirstMatch",	//FirstMatch execution strategy (首次匹配)
                        "recalculate_access_paths_and_cost": {
                          "tables": [
                          ]
                        },
                        "cost": 98001.6,
                        "rows": 99410,
                        "chosen": true
                      },
                      {
                        "strategy": "DuplicatesWeedout",	//DuplicateWeedout execution strategy (重复值消除)
                        "cost": 108938,
                        "rows": 99410,
                        "duplicate_tables_left": false,
                        "chosen": false
                      }
                    ],
                    "chosen": true,
                    "cause": "previous_plan_used_disabled_strategy"
                  }
                ]
              },
              {
                "final_semijoin_strategy": "FirstMatch",	//最终选择的策略
                "recalculate_access_paths_and_cost": {
                  "tables": [
                  ]
                }
              }
            ]
          },             
            
            
            还会创建临时表
             "refine_plan": [
              {
                "creating_tmp_table": {
                  "tmp_table_info": {
                    "columns": 1,
                    "row_length": 6,
                    "key_length": 4,
                    "unique_constraint": false,
                    "makes_grouped_rows": false,
                    "cannot_insert_duplicates": false,
                    "location": "TempTable"	//这是8.0后的样子,貌似5.几版本这里提示的是索引类型
                  }
                }
              },
         
3、LooseScan execution strategy (松散索引扫描)

eg:

select * from db2 WHERE key3 IN (select  key2 from db1 WHERE key2 > "a" AND key2 < 'b') ;

在子查询中,对于 db1 表的访问可以使用到 key2 列的索引,而恰好子查询的查询列表处就是 key2 列,这样
在将该查询转换为半连接查询后,如果将 db2 作为驱动表执行查询的话,如果能在 db2 表中找到对应的记录,那么就把对应的记录加入到结果集。依此类推,其他值相同的二级索引记录,也只需要取第一条记录的值到 db2 表中找匹配的记录,这种虽然是扫描索引,但只取值相同的记录的第一条去做匹配操作的方式称之为 松散索引扫描 。

有点类似于去重,但这个是利用索引选出第一个相同的记录,因为索引列相同的情况下回进行排序,但既然是索引列作为搜索条件的话,那么看索引列就行,所以只需选出索引列相同的一条记录,也有点像哈希表的感觉。

image-1669431940058

2和3两个策略容易一起出现,至于选哪个,最后由其成本决定。这里的具体执行看上面贴出来的就行,差不多,都有两个策略,只是在执行这条条件是范围查询的时候,松散索引扫描成本更低。

  {
                        "strategy": "DuplicatesWeedout",
                        "cost": 12100.3,
                        "rows": 9941,
                        "duplicate_tables_left": false,
                        "chosen": false
                      }
                    ],
                    "chosen": true
                  }
                ]
              },
              {
                "plan_prefix": [
                ],
                "table": "`db2`",
                "best_access_path": {
                  "considered_access_paths": [
                    {
                      "rows_to_scan": 99410,
                      "filtering_effect": [
                      ],
                      "final_filtering_effect": 1,
                      "access_type": "scan",
                      "resulting_rows": 99410,
                      "cost": 10109.2,
                      "chosen": true
                    }
                  ]
                },
                "condition_filtering_pct": 100,
                "rows_for_plan": 99410,
                "cost_for_plan": 10109.2,
                "semijoin_strategy_choice": [
                ],
                "pruned_by_heuristic": true
              },
              {
                "final_semijoin_strategy": "LooseScan",
                "recalculate_access_paths_and_cost": {
4、FirstMatch execution strategy (首次匹配)

FirstMatch 是一种最原始的半连接执行方式,跟相关子查询的执行方式是一样一样的,就是说先取一条外层查询的中的记录,然后到子查询的表中寻找符合匹配条件的记录,如果能找到一条,则将该外层查询的记录放入最终的结果集并且停止查找更多匹配的记录,如果找不到则把该外层查询的记录丢弃掉;然后再开始取下一条外层查询中的记录,重复上边这个过程。

对于某些使用 IN 语句的相关子查询,转为半连接后,就可以使用我们上边介绍过的 DuplicateWeedout 、 LooseScan 、 FirstMatch 等半连接执行策略来执行查询,当然,如果子查询的查询列表处只有主键或者唯一二级索引列,还可以直接使用 table pullout 的策略来执行查询,但是需要大家注意的是,由于相关子查询并不是一个独立的查询,所以不能转换为物化表来执行查询。

5、Semi-join Materialization execution strategy

先把外层查询的 IN 子句中的不相关子查询进行物化,然后再进行外层查询的表和物化表的连接本质上也算是一种 semi-join ,只不过由于物化表中没有重复的记录,所以可以直接将子查询转为连接查询。

这里和第一条的策略我没能复现,先留着吧。

semi-join的适用条件
该子查询必须是和 IN 语句组成的布尔表达式,并且在外层查询的 WHERE 或者 ON 子句中出现。
外层查询也可以有其他的搜索条件,只不过和 IN 子查询的搜索条件必须使用 AND 连接起来。
该子查询必须是一个单一的查询,不能是由若干查询由 UNION 连接起来的形式。
该子查询不能包含 GROUP BY 或者 HAVING 语句或者聚集函数。

如果 IN 子查询不符合转换为 semi-join 的条件,那么查询优化器会从下边两种策略中找出一种成本更低的方式执行子查询:

先将子查询物化之后再执行查询
执行 IN to EXISTS 转换。

Hash Join

在之前讲到基于块的嵌套循环连接(Block Nested-Loop Join),但书里是基于5版本的mysql。

现在我用的是8版本。在8.0中,block_nested_loop或许还存在,但应该也不使用了。

block_nested_loop=on,

image-1669432953395

从MySQL8.0.18版本开始就加入了hash join。默认都会使用hash join

  • Nested Loop:对于被连接的数据子集较小的情况下,Nested Loop是个较好的选择。
  • Hash Join是做大数据集连接时的常用方式,优化器使用两个表中较小(相对较小)的表利用Join Key在内存中建立散列值,然后扫描较大的表并探测散列值,找出与Hash表匹配的行。
    • 这种方式适用于较小的表完全可以放入内存中的情况,这样总成本就是访问两个表的成本之和。
    • 在表很大的情况下并不能完全放入内存,这时优化器会将它分割成若干不同的分区,不能放入内存的部分就把该分区写入磁盘的临时段,此时要求有较大的临时段从而尽量提高I/O的性能。
    • 它能够很好的工作于没有索引的大表和并行查询的环境中,并提供最好的性能。Hash Join只能应用于等值连接,这是由Hash的特点决定的。

评论