1. 程式人生 > 其它 >Semi-join Materialization 子查詢優化策略

Semi-join Materialization 子查詢優化策略

技術標籤:MySQL相關mysql

什麼是 Semi-join

常規聯接中,結果可能會出現重複值,而子查詢可以獲得無重複的結果。比如需要找出有人口大於 2000 萬的城市的國家,如果用普通聯接,則可能出現重複結果:

select country.* from country join city on country.code=city.country_code and population>20000000;
+---------+----------+
| code    | name     |
+---------+----------+
|    1    | china    |
|    1
| china | +---------+----------+ 2 rows in set (0.00 sec)

而子查詢則不會:

select * from country where code in
	(select country_code from city where population>20000000);
+------+---------+
| code | name    |
+------+---------+
|  1   | china   |
+------+---------+
1 row in set (0.00 sec)

在子查詢中,優化器可以識別出 in 子句中每組只需要返回一個值,在這種情況下,可以使用半聯接 Semi-join 來優化子查詢,提升查詢效率。

Semi-join 限制

不過並不是所有子查詢都是半聯接,必須滿足以下條件:

  • 子查詢必須是出現在頂層的 WHERE、ON 子句後面的 IN 或者 =ANY
  • 子查詢必須是單個 select,不能是 union;
  • 子查詢不能有 group by 或者 having 子句(可以用 semijoin materialization 策略,其他不可以 );
  • It must not be implicitly grouped (it must contain no aggregate functions). (不知道啥意思,保持原文);
  • 子查詢不能有 order by with limit;
  • 父查詢中不能有 STRAIGHT_JOIN 指定聯接順序;
  • The number of outer and inner tables together must be less than the maximum number of tables permitted in a join.

Semi-join 實現策略

子查詢可以是相關子查詢,如果子查詢滿足以上條件,MySQL 會將其轉換為 semijoin,並從以下的策略中基於成本進行選擇其中一種:

  • Duplicate Weedout
  • FirstMatch
  • LooseScan
  • Materialize

對應 optimizer_switch 引數中的:

  • semijon=ON,控制 semijoin 是否開啟的開關
  • firstmatch、loosescan、duplicateweedout、materialization 分別是四種策略的開關,預設都是開啟的

通過 explain 輸出資訊可以判斷使用了哪種優化策略:

  • extra 中出現 Start temporary、End temporary,表示使用了 Duplicate Weedout 策略
  • extra 中出現 FirstMatch(tbl_name) ,表示使用了 FirstMatch 策略
  • extra 中出現 LooseScan(m…n),表示使用了 LooseScan 策略
  • select_type 列為 MATERIALIZED,以及 table 列為 ,表示使用了 Materialize 策略

接下來介紹 Semi-join Materialization 優化策略。

Semi-join Materialization

Semi-join Materialization 策略就是把子查詢結果物化成臨時表,再用於 semijoin 的一種特殊的子查詢實現,它實際上也可以分為兩種策略:

  • Materialization-scan
  • Materialization-lookup
    以下 SQL 為例:
select * from Country
where Country.code IN (select City.Country
                       from City
                       where City.Population > 7*1000*1000)
      and Country.continent='Europe'

這是一個不相關子查詢,查出歐洲有人口超過 700 萬的大城市的國家。Semi-join Materialization 優化策略的做法就是:把人口超過 700 萬的大城市所在的國家,即 City.Country 欄位值填充到一個臨時表中,並且 Country 欄位為主鍵(用來去重),然後與 Country 表進行聯接:
在這裡插入圖片描述

這個join可以從兩個方向進行:
1.從物化表到國家表
2.從國家表到物化表

第一個方向涉及一個全表掃描(在物化表上的全表掃描),因此被稱為"Materialization-scan"
如果從第二個方向進行,最廉價的方式是使用主鍵從物化表中lookup出匹配的記錄。這種方式被稱為"Materialization-lookup"。

Materialization-scan

如果我們尋找人口超過700萬的城市,優化器將使用materialize-scan,EXPLAIN輸出結果也會顯示這一點:

MariaDB [world]> explain select * from Country where Country.code IN (select City.Country from City where  City.Population > 7*1000*1000);
+----+--------------+-------------+--------+--------------------+------------+---------+--------------------+------+-----------------------+
| id | select_type  | table       | type   | possible_keys      | key        | key_len | ref                | rows | Extra                 |
+----+--------------+-------------+--------+--------------------+------------+---------+--------------------+------+-----------------------+
|  1 | PRIMARY      | <subquery2> | ALL    | distinct_key       | NULL       | NULL    | NULL               |   15 |                       |
|  1 | PRIMARY      | Country     | eq_ref | PRIMARY            | PRIMARY    | 3       | world.City.Country |    1 |                       |
|  2 | MATERIALIZED | City        | range  | Population,Country | Population | 4       | NULL               |   15 | Using index condition |
+----+--------------+-------------+--------+--------------------+------------+---------+--------------------+------+-----------------------+
3 rows in set (0.01 sec)

從上可以看到:

  1. 仍然有兩個select(id=1和id=2)
  2. 第二個select(id=2)的select_type是MATERIALIZED。這表示會執行並將結果儲存在一個在所有列上帶有一個唯一性索引的臨時表。這個唯一性索引可以避免有重複的記錄
  3. 第一個select中接收到一個名為subquery2的表,這是從第二個select(id=2)獲取的物化的表優化器選擇在物化的表上執行全表掃描。這就是Materialization-Scan策略的示例。

至於執行成本,我們將從表City讀取15行,將15行寫入物化表,然後讀取它們(優化器假設不會有任何重複),然後對錶Country執行15次eq_ref訪問。總共,我們將進行45次讀取和15次寫入。

相比之下,如果你在MySQL中執行EXPLAIN,你會得到如下結果:

MySQL [world]> explain select * from Country where Country.code IN (select City.Country from City where  City.Population > 7*1000*1000);
+----+--------------------+---------+-------+--------------------+------------+---------+------+------+------------------------------------+
| id | select_type        | table   | type  | possible_keys      | key        | key_len | ref  | rows | Extra                              |
+----+--------------------+---------+-------+--------------------+------------+---------+------+------+------------------------------------+
|  1 | PRIMARY            | Country | ALL   | NULL               | NULL       | NULL    | NULL |  239 | Using where                        |
|  2 | DEPENDENT SUBQUERY | City    | range | Population,Country | Population | 4       | NULL |   15 | Using index condition; Using where |
+----+--------------------+---------+-------+--------------------+------------+---------+------+------+------------------------------------+

讀的記錄是(239 + 239*15) = 3824。

Materialization-Lookup

讓我們稍微修改一下查詢,看看哪些國家的城市人口超過1百萬(而不是7百萬):

MariaDB [world]> explain select * from Country where Country.code IN (select City.Country from City where  City.Population > 1*1000*1000) ;
+----+--------------+-------------+--------+--------------------+--------------+---------+------+------+-----------------------+
| id | select_type  | table       | type   | possible_keys      | key          | key_len | ref  | rows | Extra                 |
+----+--------------+-------------+--------+--------------------+--------------+---------+------+------+-----------------------+
|  1 | PRIMARY      | Country     | ALL    | PRIMARY            | NULL         | NULL    | NULL |  239 |                       |
|  1 | PRIMARY      | <subquery2> | eq_ref | distinct_key       | distinct_key | 3       | func |    1 |                       |
|  2 | MATERIALIZED | City        | range  | Population,Country | Population   | 4       | NULL |  238 | Using index condition |
+----+--------------+-------------+--------+--------------------+--------------+---------+------+------+-----------------------+
3 rows in set (0.00 sec)

explain的輸出結果和Materialization-scan類似,除了:

  1. subquery2表是通過eq_ref訪問的
  2. access使用了索引distinct_key

這意味著優化器計劃對物化表執行索引查詢。換句話說,我們將使用Materialization-lookup策略。

在MySQL中(或者使用optimizer_switch=‘semi-join=off,materialization=off’),會得到這樣的執行計劃:

MySQL [world]> explain select * from Country where Country.code IN (select City.Country from City where  City.Population > 1*1000*1000) ;
+----+--------------------+---------+----------------+--------------------+---------+---------+------+------+-------------+
| id | select_type        | table   | type           | possible_keys      | key     | key_len | ref  | rows | Extra       |
+----+--------------------+---------+----------------+--------------------+---------+---------+------+------+-------------+
|  1 | PRIMARY            | Country | ALL            | NULL               | NULL    | NULL    | NULL |  239 | Using where |
|  2 | DEPENDENT SUBQUERY | City    | index_subquery | Population,Country | Country | 3       | func |   18 | Using where |
+----+--------------------+---------+----------------+--------------------+---------+---------+------+------+-------------+

可以看出,這兩個執行計劃都將對國家表進行全面掃描。對於第二步,MariaDB將填充物化表(238行從表City讀取並寫入臨時表),然後對錶Country中的每個記錄執行惟一的鍵查詢,結果是238個惟一的鍵查詢。總的來說,第二步將花費(239+238)= 477讀取和238 temp.table的寫入。

MySQL的第二步計劃是使用City上的索引讀取18行。它為表國家接收的每個記錄的國家。計算出來的成本為(18*239)= 4302讀取。如果有更少的子查詢呼叫,這個計劃將比物化的計劃更好。順便說一下,MariaDB也可以選擇使用這樣的查詢計劃(請參閱FirstMatch策略),但是它沒有選擇。

帶有group by的子查詢

當子查詢帶有分組的時候,MariaDB可以使用semi-join物化策略(這種場景下,其他semi-join策略不適用)
這允許高效地執行搜尋某個組中最佳/最後一個元素的查詢。

舉個例子,我們來看看每個大陸上人口最多的城市:

explain
select * from City
where City.Population in (select max(City.Population) from City, Country
                          where City.Country=Country.Code
                          group by Continent)
+------+--------------+-------------+------+---------------+------------+---------+----------------------------------+------+-----------------+
| id   | select_type  | table       | type | possible_keys | key        | key_len | ref                              | rows | Extra           |
+------+--------------+-------------+------+---------------+------------+---------+----------------------------------+------+-----------------+
|    1 | PRIMARY      | <subquery2> | ALL  | distinct_key  | NULL       | NULL    | NULL                             |  239 |                 |
|    1 | PRIMARY      | City        | ref  | Population    | Population | 4       | <subquery2>.max(City.Population) |    1 |                 |
|    2 | MATERIALIZED | Country     | ALL  | PRIMARY       | NULL       | NULL    | NULL                             |  239 | Using temporary |
|    2 | MATERIALIZED | City        | ref  | Country       | Country    | 3       | world.Country.Code               |   18 |                 |
+------+--------------+-------------+------+---------------+------------+---------+----------------------------------+------+-----------------+
4 rows in set (0.00 sec)

城市是:

+------+-------------------+---------+------------+
| ID   | Name              | Country | Population |
+------+-------------------+---------+------------+
| 1024 | Mumbai (Bombay)   | IND     |   10500000 |
| 3580 | Moscow            | RUS     |    8389200 |
| 2454 | Macao             | MAC     |     437500 |
|  608 | Cairo             | EGY     |    6789479 |
| 2515 | Ciudad de México  | MEX     |    8591309 |
|  206 | São Paulo         | BRA     |    9968485 |
|  130 | Sydney            | AUS     |    3276207 |
+------+-------------------+---------+------------+

Semi-join materialization總結

1.可以用於非相關的in子查詢。子查詢可以含有分組、和/或聚合函式
2.在explain輸出中,子查詢會有type=Materialized;父表子查詢中有table=<subqueryN>
3.開啟需要將變數optimizer_switch中的materialization=on、semijoin=on
4.Non-semijoin materialization與materialization=on|off標記共享

參考

https://blog.csdn.net/ActionTech/article/details/108710418
https://mariadb.com/kb/en/library/semi-join-materialization-strategy/
https://dev.mysql.com/doc/refman/5.7/en/semijoins.html