GreatSQL優化技巧:半連接(semijoin)優化

来源:https://www.cnblogs.com/greatsql/p/18139862
-Advertisement-
Play Games

何為半連接? 半連接是在GreatSQL內部採用的一種執行子查詢的方式,semi join不是語法關鍵字,不能像使用inner join、left join、right join這種語法關鍵字一樣提供給用戶來編寫SQL語句。 兩個表t1表和t2表進行半連接的含義是:對於t1表的某條記錄來說,我們只關 ...


何為半連接?

半連接是在GreatSQL內部採用的一種執行子查詢的方式,semi join不是語法關鍵字,不能像使用inner joinleft joinright join這種語法關鍵字一樣提供給用戶來編寫SQL語句。

兩個表t1表和t2表進行半連接的含義是:對於t1表的某條記錄來說,我們只關心在t2表中是否存在與之匹配的記錄,而不關心有多少條記錄與之匹配,最終的結果集中只保留t1表的記錄。

前面文章也提到過,含in、exists子查詢的語句通常會採用半連接方式執行查詢,但這不絕對,也有一些情況不適用半連接。比如:

(1)外查詢的where子句中,存在其他搜索條件使用OR操作符與IN子查詢的條件連接起來

(2)IN子查詢位於Select子句中

(3)IN子查詢中含有union的情況

(4)IN子查詢中含group by、having或聚合函數的情況

GreatSQL執行半連接的優化策略

本文實驗使用資料庫版本為GreatSQL 8.0.32-25。

創建兩張實驗表來說明。

greatsql> create table t1(
c1 varchar(30),
c2 int
);
greatsql> create table t2(
id int primary key,
c1 varchar(30),
key idx_c1(c1)
);
--插入幾條測試數據
greatsql> insert into t1 values('a',1);
greatsql> insert into t1 values('b',3);
greatsql> insert into t1 values('a',5);
greatsql> insert into t1 values('c',7);
greatsql> insert into t1 values('d',9);
greatsql> insert into t2 values(1,'a');
greatsql> insert into t2 values(2,'a');
greatsql> insert into t2 values(3,'b');
greatsql> insert into t2 values(4,'b');
greatsql> insert into t2 values(5,'c');
greatsql> insert into t2 values(6,'b');

GreatSQL執行半連接的方式大致有以下5種:

1.Table pullout(子查詢中的表上拉)

當子查詢的查詢列表處只有主鍵或者唯一索引列時,可以直接把子查詢中的表上拉到外層查詢的FROM子句中,並把子查詢的查詢條件合併到外層查詢的搜索條件中。所以選擇這種方式是有先決條件的,子查詢的查詢列表處必須只有主鍵或唯一索引列。有沒有選擇這種方式,可以通過執行explain展示計劃後,使用show warnings命令查看優化器改寫後的語句。

例如下麵這個語句:

select * from t1 where c2 in (select id from t2 where t2.c1='b');

這個語句種子查詢的id列是t2表的主鍵列,滿足這種方式的先決條件,看一下執行計劃。

greatsql> explain select * from t1 where c2 in (select id from t2 where t2.c1='b');
+----+-------------+-------+------------+------+----------------+--------+---------+-------+------+----------+--------------------------------------------+
| id | select_type | table | partitions | type | possible_keys  | key    | key_len | ref   | rows | filtered | Extra                                      |
+----+-------------+-------+------------+------+----------------+--------+---------+-------+------+----------+--------------------------------------------+
|  1 | SIMPLE      | t2  | NULL       | ref  | PRIMARY,idx_c1 | idx_c1 | 123     | const |    3 |   100.00 | Using index                                |
|  1 | SIMPLE      | t1  | NULL       | ALL  | NULL           | NULL   | NULL    | NULL  |    4 |    25.00 | Using where; Using join buffer (hash join) |
+----+-------------+-------+------------+------+----------------+--------+---------+-------+------+----------+--------------------------------------------+
2 rows in set, 1 warning (0.00 sec)

greatsql> show warnings;
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                                                                                                                            |
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note  | 1003 | /* select#1 */ select `test`.`t1`.`c1` AS `c1`,`test`.`t1`.`c2` AS `c2` from `test`.`t2` join `test`.`t1` where ((`test`.`t1`.`c2` = `test`.`t2`.`id`) and (`test`.`t2`.`c1` = 'b')) |
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.01 sec)

從warning信息可以看出,優化器改執行連接方式是,t1表與t2表通過內連接來關聯,原子查詢內部t2表的過濾條件放到了整個語句where條件的後面,原語句與優化器執行的語句之所以等價,是因為子查詢的查詢列id列是主鍵列,不會有重覆值,跟外表t1使用inner join連接後,不會造成關聯後結果集數據量的放大。一般情況下子查詢的查詢列表處只有主鍵或者唯一索引列時都會轉化為這種方式來執行。對於這種業務,無論開發者怎麼編寫SQL,使用inner join 也好,exists也好,最後優化器執行方式可能都是一樣的。

可以看一下將原語句改造為inner join 與 exists語句的執行計劃,是不是都是一樣的。

greatsql> explain select * from t1 where exists (select 1 from t2 where t2.id=t1.c2 and t2.c1='b');
+----+-------------+-------+------------+------+----------------+--------+---------+-------+------+----------+--------------------------------------------+
| id | select_type | table | partitions | type | possible_keys  | key    | key_len | ref   | rows | filtered | Extra                                      |
+----+-------------+-------+------------+------+----------------+--------+---------+-------+------+----------+--------------------------------------------+
|  1 | SIMPLE      | t2  | NULL       | ref  | PRIMARY,idx_c1 | idx_c1 | 123     | const |    3 |   100.00 | Using index                                |
|  1 | SIMPLE      | t1  | NULL       | ALL  | NULL           | NULL   | NULL    | NULL  |    4 |    25.00 | Using where; Using join buffer (hash join) |
+----+-------------+-------+------------+------+----------------+--------+---------+-------+------+----------+--------------------------------------------+
2 rows in set, 2 warnings (0.00 sec)

greatsql> show warnings;
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                                                                                                                            |
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note  | 1276 | Field or reference 'test.t1.c2' of SELECT #2 was resolved in SELECT #1                                                                                                                           |
| Note  | 1003 | /* select#1 */ select `test`.`t1`.`c1` AS `c1`,`test`.`t1`.`c2` AS `c2` from `test`.`t2` join `test`.`t1` where ((`test`.`t1`.`c2` = `test`.`t2`.`id`) and (`test`.`t2`.`c1` = 'b')) |
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
2 rows in set (0.00 sec)

greatsql> explain select t1.* from t1 inner join t2 on t1.c2=t2.id where t2.c1='b';
+----+-------------+-------+------------+------+----------------+--------+---------+-------+------+----------+--------------------------------------------+
| id | select_type | table | partitions | type | possible_keys  | key    | key_len | ref   | rows | filtered | Extra                                      |
+----+-------------+-------+------------+------+----------------+--------+---------+-------+------+----------+--------------------------------------------+
|  1 | SIMPLE      | t2  | NULL       | ref  | PRIMARY,idx_c1 | idx_c1 | 123     | const |    3 |   100.00 | Using index                                |
|  1 | SIMPLE      | t1  | NULL       | ALL  | NULL           | NULL   | NULL    | NULL  |    4 |    25.00 | Using where; Using join buffer (hash join) |
+----+-------------+-------+------------+------+----------------+--------+---------+-------+------+----------+--------------------------------------------+
2 rows in set, 1 warning (0.01 sec)

greatsql> show warnings;
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                                                                                                                            |
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note  | 1003 | /* select#1 */ select `test`.`t1`.`c1` AS `c1`,`test`.`t1`.`c2` AS `c2` from `test`.`t1` join `test`.`t2` where ((`test`.`t1`.`c2` = `test`.`t2`.`id`) and (`test`.`t2`.`c1` = 'b')) |
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

這種執行方式本質上已經轉換為內連接了。

2.FirstMatch(首次匹配)

這種方式先取外層查詢的一條記錄,到子查詢的表中尋找符合匹配條件的記錄,如果能找到一條,則將外層查詢的記錄放入到最終結果集中並且停止查找匹配更多的記錄,如果找不到,則把該外層查詢的記錄丟棄掉,然後再開始取下一條外層查詢中的記錄,這個過程一直持續到外層查詢獲取不到記錄為止。

看一個簡單語句的執行計劃

select * from t1 where c1 in (select c1 from t2);
greatsql> explain select * from t1 where c1 in (select c1 from t2);
+----+-------------+-------+------------+------+---------------+--------+---------+--------------+------+----------+-------------------------------+
| id | select_type | table | partitions | type | possible_keys | key    | key_len | ref          | rows | filtered | Extra                         |
+----+-------------+-------+------------+------+---------------+--------+---------+--------------+------+----------+-------------------------------+
|  1 | SIMPLE      | t1  | NULL       | ALL  | NULL          | NULL   | NULL    | NULL         |    4 |   100.00 | Using where                   |
|  1 | SIMPLE      | t2  | NULL       | ref  | idx_c1        | idx_c1 | 123     | test.t1.c1 |    2 |   100.00 | Using index; FirstMatch(t1) |
+----+-------------+-------+------------+------+---------------+--------+---------+--------------+------+----------+-------------------------------+
2 rows in set, 1 warning (0.01 sec)

greatsql> show warnings;
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                                                                                                  |
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note  | 1003 | /* select#1 */ select `test`.`t1`.`c1` AS `c1`,`test`.`t1`.`c2` AS `c2` from `test`.`t1` semi join (`test`.`t2`) where (`test`.`t2`.`c1` = `test`.`t1`.`c1`) |
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

從warning信息可以看到 semi join 的字樣,優化器使用半連接方式執行的子查詢。從執行計劃可以看到 extra 列有FirstMatch(t1) 的字樣,表示對t1表外查詢傳入的每個c1值在t2表上都進行了首次匹配,這種方式也是我最初理解的in子查詢的含義,只關心有無匹配上,不關心匹配上多少。

3.LooseScan(鬆散掃描)

LooseScan是使用子查詢的查詢列上的索引,只針對相同索引列值的第一條記錄,去外查詢找對應的記錄。使用了這種優化方式的半連接,在explain的計劃的Extra列會有LooseScan字樣。

還是上面的語句,使用semijoin的hint干涉優化器,使其選擇LooseScan的優化策略。

select /*+ semijoin(@subq1 loosescan) */  * from t1 where c1 in (select /*+ qb_name(subq1)*/ c1 from t2 );
greatsql> explain select /*+ semijoin(@subq1 loosescan) */  * from t1 where c1 in (select /*+ qb_name(subq1)*/ c1 from t2 );            
+----+-------------+-------+------------+-------+---------------+--------+---------+------+------+----------+--------------------------------------------+
| id | select_type | table | partitions | type  | possible_keys | key    | key_len | ref  | rows | filtered | Extra                                      |
+----+-------------+-------+------------+-------+---------------+--------+---------+------+------+----------+--------------------------------------------+
|  1 | SIMPLE      | t2  | NULL       | index | idx_c1        | idx_c1 | 123     | NULL |    6 |    50.00 | Using index; LooseScan                     |
|  1 | SIMPLE      | t1  | NULL       | ALL   | NULL          | NULL   | NULL    | NULL |    5 |    20.00 | Using where; Using join buffer (hash join) |
+----+-------------+-------+------------+-------+---------------+--------+---------+------+------+----------+--------------------------------------------+
2 rows in set, 1 warning (0.01 sec)

greatsql> show warnings;
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                                                                                                                                      |
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note  | 1003 | /* select#1 */ select /*+ SEMIJOIN(@`subq1` LOOSESCAN) */ `test`.`t1`.`c1` AS `c1`,`test`.`t1`.`c2` AS `c2` from `test`.`t1` semi join (`test`.`t2`) where (`test`.`t1`.`c1` = `test`.`t2`.`c1`) |
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

從執行計劃可以看出,子查詢的表t2作為驅動表,t2表的c1列上有索引,對錶t2進行訪問時,使用其c1列的索引,對相同的索引列值只取第一條記錄去t1表中找對應記錄,將所有外查詢表t1對應的記錄都加入到最終結果集,可以理解為對子查詢t2表的索引掃描方式是跳躍式的。

4.Duplicate Weedout重覆值消除

這種方式是藉助臨時表來消除重覆值,explain展示計劃時,在extra列會出現Start temporaryEnd temporary的字樣。

還是上面的語句,我們使用semijoin的hint干涉優化器,使其選擇dupsweedout優化策略。

greatsql> explain select /*+ semijoin(@subq1 dupsweedout)*/ * from t1 where c1 in (select /*+ qb_name(subq1)*/ c1 from t2);
+----+-------------+-------+------------+------+---------------+--------+---------+--------------+------+----------+---------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key    | key_len | ref          | rows | filtered | Extra                                       |
+----+-------------+-------+------------+------+---------------+--------+---------+--------------+------+----------+---------------------------------------------+
|  1 | SIMPLE      | t1  | NULL       | ALL  | NULL          | NULL   | NULL    | NULL         |    4 |   100.00 | Using where                                 |
|  1 | SIMPLE      | t2  | NULL       | ref  | idx_c1        | idx_c1 | 123     | test.t1.c1 |    2 |   100.00 | Using index; Start temporary; End temporary |
+----+-------------+-------+------------+------+---------------+--------+---------+--------------+------+----------+---------------------------------------------+
2 rows in set, 1 warning (0.00 sec)

greatsql> show warnings;
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                                                                                                                                        |
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note  | 1003 | /* select#1 */ select /*+ SEMIJOIN(@`subq1` DUPSWEEDOUT) */ `test`.`t1`.`c1` AS `c1`,`test`.`t1`.`c2` AS `c2` from `test`.`t1` semi join (`test`.`t2`) where (`test`.`t2`.`c1` = `test`.`t1`.`c1`) |
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

例如:t1表的記錄('b',3),可以匹配上t2表的兩條記錄(3,'b'),(4,'b'),為了消除關聯結果的重覆值,可以想象建立這樣一個臨時表:

create table tmp(rowid int primary key);

當把t1表的記錄加入到結果集時,先把這條記錄的rowid加入到臨時表中,如果添加成功,說明這條記錄並沒有加入到最後的結果集,如果添加失敗,則說明t1表的這條記錄已經加入到最終結果集了

個人感覺這種方式比其他方式效率低。

5.Semi-join Materialization(半連接物化)

先把IN 子句中的不相關子查詢進行物化,然後再將外層查詢的表與物化表進行連接。子查詢內部有分組聚合運算時通常會先進行物化處理。

還是上面的語句,使用semijoin的hint干涉優化器,使其選擇materialization的優化策略。

select /*+ semijoin(@subq1 materialization) */  * from t1 where c1 in (select /*+ qb_name(subq1)*/ c1 from t2 );
greatsql> explain select /*+ semijoin(@subq1 materialization) */  * from t1 where c1 in (select /*+ qb_name(subq1)*/ c1 from t2 );
+----+--------------+-------------+------------+--------+---------------------+---------------------+---------+--------------+------+----------+-------------+
| id | select_type  | table       | partitions | type   | possible_keys       | key                 | key_len | ref          | rows | filtered | Extra       |
+----+--------------+-------------+------------+--------+---------------------+---------------------+---------+--------------+------+----------+-------------+
|  1 | SIMPLE       | t1        | NULL       | ALL    | NULL                | NULL                | NULL    | NULL         |    5 |   100.00 | Using where |
|  1 | SIMPLE       | <subquery2> | NULL       | eq_ref | <auto_distinct_key> | <auto_distinct_key> | 123     | test.t1.c1 |    1 |   100.00 | NULL        |
|  2 | MATERIALIZED | t2        | NULL       | index  | idx_c1              | idx_c1              | 123     | NULL         |    6 |   100.00 | Using index |
+----+--------------+-------------+------------+--------+---------------------+---------------------+---------+--------------+------+----------+-------------+
3 rows in set, 1 warning (0.00 sec)

greatsql> show warnings;
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                                                                                                                                            |
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note  | 1003 | /* select#1 */ select /*+ SEMIJOIN(@`subq1` MATERIALIZATION) */ `test`.`t1`.`c1` AS `c1`,`test`.`t1`.`c2` AS `c2` from `test`.`t1` semi join (`test`.`t2`) where (`<subquery2>`.`c1` = `test`.`t1`.`c1`) |
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

從執行計劃可以看出,先對子查詢t2表做了物化表處理,物化表會生成自動索引<auto_distinct_key>,外查詢表t1再與物化表做Nest loop連接。

補充說明

對於上面的語句 select * from t1 where c1 in (select c1 from t2);,優化器預設選擇了firstmatch方式,其他方式都是使用hint來干涉的優化器的選擇,可以看到這個hint包含兩部分,一個是使用qb_name()給子查詢分配一個名稱,一個是使用semijoin([@query_block_name] [strategy]),指定子查詢塊使用半連接策略,可以指定多個策略。同時semijoin的優化策略的選擇還受優化開關參數optimize_switch的影響,該參數里有semijoin,loosescan,firstmatch,duplicateweedout的開關,預設都是開啟的,所以也可以使用優化開關來干涉優化器的選擇。

優化舉例

select count(*)
  from t1 a
 where substr(a.modifytime, 1, 8) = '20240301'
   and a.sospecnumber in
       (select a.sospecnumber
          from t1 a
         where substr(a.modifytime, 1, 8) < '20240301');

這條SQL只涉及一張表t1,表中數據200萬左右,modify_time為字元類型,存儲從2009年開始的時間串。看一下該表的索引情況。

greatsql> show index from t1;
+-------+------------+------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table | Non_unique | Key_name         | Seq_in_index | Column_name  | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+-------+------------+------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| t1    |          1 | idx_sospecnumber |            1 | SOSPECNUMBER | A         |         133 |     NULL |   NULL | YES  | BTREE      |         |               | YES     | NULL       |
| t1    |          1 | idx_modifytime   |            1 | MODIFYTIME   | A         |      634186 |     NULL |   NULL |      | BTREE      |         |               | YES     | NULL       |
+-------+------------+------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
2 rows in set (0.01 sec)

explain的執行計劃如下:

greatsql> explain
    -> select count(*)
    ->   from t1 a
    ->  where substr(a.modifytime, 1, 8) ='20240301'
    ->    and a.sospecnumber  in
    ->        (select a.sospecnumber
    ->           from t1 a
    ->          where substr(a.modifytime, 1, 8) < '20240301') ;
+----+--------------+-------------+------------+--------+---------------------+---------------------+---------+---------------------+---------+----------+-------------+
| id | select_type  | table       | partitions | type   | possible_keys       | key                 | key_len | ref                 | rows    | filtered | Extra       |
+----+--------------+-------------+------------+--------+---------------------+---------------------+---------+---------------------+---------+----------+-------------+
|  1 | SIMPLE       | a           | NULL       | ALL    | idx_sospecnumber    | NULL                | NULL    | NULL                | 2426414 |   100.00 | Using where |
|  1 | SIMPLE       | <subquery2> | NULL       | eq_ref | <auto_distinct_key> | <auto_distinct_key> | 131     | test.a.SOSPECNUMBER |       1 |   100.00 | NULL        |
|  2 | MATERIALIZED | a           | NULL       | ALL    | idx_sospecnumber    | NULL                | NULL    | NULL                | 2426414 |   100.00 | Using where |
+----+--------------+-------------+------------+--------+---------------------+---------------------+---------+---------------------+---------+----------+-------------+
3 rows in set, 1 warning (0.00 sec)

優化器選擇的半連接優化策略是物化的方式。

explain analyze的實際計劃如下:

greatsql> explain analyze
    -> select count(*)
    ->   from t1 a
    ->  where substr(a.modifytime, 1, 8) ='20240301'
    ->    and a.sospecnumber  in
    ->        (select a.sospecnumber
    ->           from t1 a
    ->          where substr(a.modifytime, 1, 8) < '20240301') \G
*************************** 1. row ***************************
EXPLAIN: -> Aggregate: count(0)  (cost=1177497474524.58 rows=1) (actual time=4442.499..4442.500 rows=1 loops=1)
    -> Nested loop inner join  (cost=588748984584.98 rows=5887484899396) (actual time=4438.967..4442.408 rows=1346 loops=1)
        -> Filter: ((substr(a.MODIFYTIME,1,8) = '20240301') and (a.SOSPECNUMBER is not null))  (cost=252003.98 rows=2426414) (actual time=1550.096..1552.027 rows=1346 loops=1)
            -> Table scan on a  (cost=252003.98 rows=2426414) (actual time=0.050..1189.136 rows=2493198 loops=1)
        -> Single-row index lookup on <subquery2> using <auto_distinct_key> (sospecnumber=a.SOSPECNUMBER)  (cost=494645.48..494645.48 rows=1) (actual time=2.147..2.147 rows=1 loops=1346)
            -> Materialize with deduplication  (cost=494645.38..494645.38 rows=2426414) (actual time=2888.845..2888.845 rows=165 loops=1)
                -> Filter: (a.SOSPECNUMBER is not null)  (cost=252003.98 rows=2426414) (actual time=0.215..1927.315 rows=2487547 loops=1)
                    -> Filter: (substr(a.MODIFYTIME,1,8) < '20240301')  (cost=252003.98 rows=2426414) (actual time=0.214..1745.562 rows=2487547 loops=1)
                        -> Table scan on a  (cost=252003.98 rows=2426414) (actual time=0.211..1235.738 rows=2493198 loops=1)

1 row in set (4.45 sec)

優化分析:

這條SQL總體耗時4.45s,耗時主要分佈在兩處:

一處消耗在外表的查詢,對t1進行了全表掃描,回表過濾後剩餘1346行數據,耗時1552ms,此處雖然modifytime列有索引,但是因為在條件列上施加了substr函數,導致索引用不上,改為modifytime like '20240301%'的方式,也表示了查詢2024年3月1日的數據,同時用上了索引。

另一處消耗在子查詢的物化上,子查詢結果集有2487547行數據,表掃描、過濾、物化整個過程耗時約2888ms,對大結果集進行物化消耗比較大,同時IN子查詢的查詢列sospecnumber列上是有索引的,雖然選擇性不好,但是這個子查詢的含義是只需要判斷子查詢結果集中有無記錄能匹配上,而不關心匹配上多少條,所以這種情況採用first match方式比較好。

SQL改寫如下:

select /*+ semijoin(@subq firstmatch)*/
 count(*)
  from t1 a
 where a.modifytime like '20240301%'
   and a.sospecnumber in
       (select /*+ qb_name(subq)*/
         a.sospecnumber
          from t1 a
         where substr(a.modifytime, 1, 8) < '20240301')

改寫後執行計劃如下:

*************************** 1. row ***************************
EXPLAIN: -> Aggregate: count(0)  (cost=11052513.72 rows=1) (actual time=157.570..157.570 rows=1 loops=1)
    -> Nested loop semijoin  (cost=8596909.70 rows=24556040) (actual time=0.203..157.450 rows=1346 loops=1)
        -> Filter: (a.SOSPECNUMBER is not null)  (cost=606.05 rows=1346) (actual time=0.057..7.610 rows=1346 loops=1)
            -> Index range scan on a using idx_modifytime over ('20240301' <= MODIFYTIME <= '20240301????????????????????????????????????????????????'), with index condition: (a.MODIFYTIME like '20240301%')  (cost=606.05 rows=1346) (actual time=0.055..7.406 rows=1346 loops=1)
        -> Filter: (substr(a.MODIFYTIME,1,8) < '20240301')  (cost=83255911.06 rows=18244) (actual time=0.111..0.111 rows=1 loops=1346)
            -> Index lookup on a using idx_sospecnumber (SOSPECNUMBER=a.SOSPECNUMBER)  (cost=83255911.06 rows=18244) (actual time=0.111..0.111 rows=1 loops=1346)

1 row in set, 1 warning (0.16 sec)

改寫後耗時0.16s,性能提升近30倍,在對子查詢通過索引idx_sospecnumber搜索數據時,查到一條就會停止繼續搜索了。

結語

GreatSQL的 IN 子查詢適用於半連接時,優化器提供了5種優化策略:Table pullout、FirstMatch、LooseScan、Duplicate weedout、materialize。

一般外查詢表結果集小,子查詢結果集太大時,不希望通過物化這種方式來執行連接,因為物化表的代價太大,可能通過FirstMatch或者LooseScan很快就可以執行出結果了。那反之外查詢結果集大,子查詢結果集小時,通過物化表這種方式可能就會取得很好的效果。很多時候都不用過多干涉優化器做選擇,但是如果懂得原理,當優化器選錯的時候我們也可以通過hint來穩定計劃,讓SQL保持高效的執行。


Enjoy GreatSQL

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1、安裝Docker Centos7.6-centos7.9 # 配置主機名: hostnamectl set-hostname master1 && bash #關閉防火牆 systemctl stop firewalld && systemctl disable firewalld #關閉ipt ...
  • Linux 的重要性不用我多說了吧,大多數互聯網公司,伺服器都是採用的Linux操作系統 Linux是一個主要通過命令行來進行管理的操作系統。 只有熟練掌握Linux核心命令,在使用起來我們才會得心應手 這裡給大家整理了Linux一些核心命令,掌握這些核心命令,工作中應該游刃有餘了 一、腦圖 二、詳 ...
  • Part1:sleep 實驗要求與提示 可以參考 user/echo.c, user/grep.c 和 user/rm.c 文件 如果用戶忘記傳遞參數,sleep 應該列印一條錯誤消息 命令行參數傳遞時為字元串,可以使用 atoi 函數將字元串轉為數字 使用系統調用 sleep,有關實現 sleep ...
  • 深度解析GaussDB(DWS)+Flink如何增強湖倉增量數據在不同數據模型層之間的實時流動能力,如何為消息數據流提供高性能通用入庫能力,又如何構建極致的端到端實時數倉解決方案。 ...
  • 提要(廢話): 最近我將筆記本重裝了,為了保留之前的程式,我把相關的註冊表和環境備份了下來,重裝之後重新導入成功再現了部分軟體。如MySQL這樣的程式,都是預設安裝在C盤之中的,雖然C盤的程式文件我也做了備份並且重新拷貝到了新系統C盤裡,但MySQL無法啟動了,同時我更新了系統之後就把安裝源MSI文 ...
  • 在實際項目中,從Kafka到HDFS的數據是每天自動生成一個文件,按日期區分。而且Kafka在不斷生產數據,因此看看kettle是不是需要時刻運行?能不能按照每日自動生成數據文件? 為了測試實際項目中的海豚定時調度從Kafka到HDFS的Kettle任務情況,特地提前跑一下海豚定時調度這個任務,看看 ...
  • 在當前快速發展的技術格局中,企業尋求創新解決方案來簡化運營並提高效率成為一種趨勢。 Apache DolphinScheduler作為一個強大的工具,允許跨分散式系統進行複雜的工作流任務調度。本文將深入探討如何將Apache DolphinScheduler適配並整合進現代IT環境,提升其在雲原生部 ...
  • 本文分享自華為雲社區《DTC2024,華為雲資料庫創新融合大發展,打造世界級資料庫!》,作者:GaussDB 資料庫。 4月12日-13日,以“智能·雲原生·一體化——DB與Al協同創新,模型與架構融合發展”為主題的第十三屆數據技術嘉年華(DTC 2024)在北京新雲南皇冠假日酒店成功舉行。作為本次 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...