nested loops/hash join

最近看執行計劃時,老是看到nested loops/hash join這些詞,於是做了個實驗,貼出來如果錯誤請指出。
ps:順便也談了下CBO


1.創建表t1,t2 都只有很少的幾行數據,對錶t1加個索引

SQL> create table t1(id number, name varchar2(10));
Table created.
SQL> create table t2(id number, name varchar2(10));
Table created.
SQL> insert into t1 values(1, 'test');
1 row created.
SQL> insert into t1 values(2, 'test');
1 row created.
SQL> insert into t2 values(2, 'test');
1 row created.
SQL> commit;
Commit complete.
SQL> create index idx_t1  on t1(id);
Index created.
打開執行計劃跟蹤:
SQL> set autotrace trace exp;
SQL> set linesize 180
SQL> select * from t1, t2 where t1.id=t2.id;
Execution Plan
----------------------------------------------------------
Plan hash value: 580336636
--------------------------------------------------------------------------------------
| Id  | Operation   | Name   | Rows  | Bytes | Cost (%CPU)| Time|
--------------------------------------------------------------------------------------
|  0 | SELECT STATEMENT | |   1 |  40 |   3   (0)| 00:00:01 |
|  1 |  TABLE ACCESS BY INDEX ROWID| T1  | 1 | 0 |1 (0)| 00:00:01 |
| 2 | NESTED LOOPS |    |  1 |    40 |     3   (0)| 00:00:01 |
| 3 |   TABLE ACCESS FULL  | T2   |  1 |   20 |  2 (0)|00:00:01|
|* 4 |   INDEX RANGE SCAN   | IDX_T1 |  1 |  |  0  (0)| 00:00:01 |
--------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   4 - access("T1"."ID"="T2"."ID")
Note
-----
   - dynamic sampling used for this statement(請注意,只有第一次纔會自動分析,以後都要自己來手動分析)
因爲表t1在連接字段id上加了索引所以對於t1表的掃描是走的索引,而t2是全表掃描。兩個表的連接方式是nested loops(等下會說明原因)

(2)接着對t2的id上建立一個索引

        接着再執行對上面的SQL語句查看執行計劃,依然一樣。第一感覺是覺得不對,對t2表應該也走索引的,但是仔細一想,因爲這個特殊情況t2表裏面只有全爲id=2的字段,如果還走索引代價反而更高。而在10g中默認的是採用的CBO優化器,所以出現這種情況就很正常了。
(3)接着對錶t2插入一些數據。
Declare
Begin
        For I int 2..1000 loop
                Insert into t2 values(3,’test’);--這裏其實應該綁定變量的
        End loop;
End;
然後insert into t1 values(3, ‘test’);
再執行selec * from t1, t2 where t1.id=t2.id
執行計劃依然和上面一樣:
SQL> select * from t2, t1 where t2.id=t1.id;

Execution Plan
----------------------------------------------------------
Plan hash value: 580336636

--------------------------------------------------------------------------------------
| Id  | Operation   | Name   | Rows  | Bytes | Cost (%CPU)| Time|
--------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   | | 999 | 15984 |  4  (25)| 00:00:01 |
| 1 |  TABLE ACCESS BY INDEX ROWID| T1 |  1 |  8 |  1 (0)| 00:00:01 |
|   2 |   NESTED LOOPS |  |   999 | 15984 | 4  (25)| 00:00:01 |
|  3 | TABLE ACCESS FULL  | T2 | 999 |  7992 |  2   (0)| 00:00:01 |
|*  4 |  INDEX RANGE SCAN  | IDX_T1 | 1 | | 0   (0)|00:00:01|
--------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   4 - access("T2"."ID"="T1"."ID")
這又是爲什麼呢?同上面一樣,雖然在兩個表在連接時在id=2這行記錄上使用,對t2使用inde range scan(使用索引)效率會很高,但是對於id=3這行就不同了,因爲在t2中存在幾乎所有的行都是id=3,CBO做這樣的決定是明智的。
接着我將SQL語句改爲
SQL> select * from t2, t1 where t2.id=t1.id and t2.id=2;紅色部分限制了等下連接時,t2的cardinality值(篩選出來行的佔表總行數比)很小,那麼使用索引的價值就出來了。於是看到執行計劃:
Execution Plan
----------------------------------------------------------
Plan hash value: 4102592341

----------------------------------------------------------------------------------------
| Id  | Operation   | Name   | Rows  | Bytes | Cost (%CPU)| Time  |
----------------------------------------------------------------------------------------
|  0 | SELECT STATEMENT  | |  1 |    16 |     3   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID  | T1     |     1 |     8 |     1   (0)| 00
:00:01 |
|   2 |   NESTED LOOPS   |  |   1 | 16 |  3   (0)| 00:00:01 |
|  3 | TABLE ACCESS BY INDEX ROWID| T2  | 1 |   8 | 2   (0)| 00:00:01 |
|*  4 |     INDEX RANGE SCAN          | IDX_T2 |     1 |       |     1   (0)| 00
:00:01 |
|*  5 |    INDEX RANGE SCAN           | IDX_T1 |     1 |       |     0   (0)| 00
:00:01 |
--------------------------------------------------------------------------------
--------

Predicate Information (identified by operation id):
---------------------------------------------------

   4 - access("T2"."ID"=2)
   5 - access("T1"."ID"=2)

(4)接着對t1也插入大量數據。

Declare
Begin
        For I in 2..1000 loop
                Insert into t2 values(3,’test’);--這裏其實應該綁定變量的
        End loop;
End;
當然必須先手動執行分析,因爲我們已經對錶插入了數據。
SQL> exec dbms_stats.gather_table_stats(user,'t1',cascade=>true);

PL/SQL procedure successfully completed.

SQL> exec dbms_stats.gather_table_stats(user,'t2',cascade=>true);

PL/SQL procedure successfully completed.

SQL> select * from t1,t2 where t1.id=t2.id;

Execution Plan
----------------------------------------------------------
Plan hash value: 2959412835

---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |   998K|    15M|    22  (82)| 00:00:01 |
|*  1 |  HASH JOIN         |      |   998K|    15M|    22  (82)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| T2   |   999 |  7992 |     2   (0)| 00:00:01 |
|   3 |   TABLE ACCESS FULL| T1   |  1002 |  8016 |     2   (0)| 00:00:01 |
---------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - access("T1"."ID"="T2"."ID")
可以看到,此時兩個表都是全表掃描了,CBO的功力展現了因爲兩個表中用索引篩選效果不明顯,在連接字段幾乎都是一樣。同時也應該注意,兩個表之間的連接方式是hash join了。

(5)那麼什麼是hash join/nested loops,什麼情況下選擇hash join,什麼情況選擇 nested loops?

        Hash join:在將內表中的符合條件的行全部篩選出來,然後對連接字段進行hash,得到一個hash系列集,放置內存區域,然後再外表的行的那個連接字段進行hash,如果得到的hash值在之前的那個hash集合裏面存在的話,那麼這行就是符合條件的。更詳細的解答請看這篇文章:http://www.eygle.com/rss/20101128.html 
Nested loops:就是對於內表符合條件的結果集較少的情況使用,然後在內表每次取出一行,再掃描外表,看是否有符合的。循環下去,知道內表符合條件的每行都遍歷了一遍。
        那麼爲什麼情況使用它們呢?因爲對於hash join來說,它的時間複雜度大致相當於兩個for循環(並非嵌套)O(n+m)(n,m表示內外表掃描的時間複雜度,也可以簡單的理解爲是內外表符合條件的行數,雖然有些不恰當),爲什麼這麼說呢?因爲hash join是先把內表的結果集全部算出來,完了之後再對外表做一個全表掃描。所以說是O(n+m)。nested loops,對於內表的符合條件的每行,都會在外表去掃描一下,看是外表否有符合條件的行。這個就相當於兩個for循環嵌套了時間複雜度O(n*m)。所以在對於m或是n中有一個很小的情況下(內表的符合條件很少的情況下),那麼O(n*m)的時間比O(m+n)的時間可能更少。但是對於m和n都很大的情況下,當然就會選擇O(m+n)了。


這就是我自己的一點點心得,如果有什麼理解錯誤的地方請指出。

順便問個問題:
因爲有資料說,只有SQL第一次執行時才做動態採樣分析,那麼以後如果表裏面數據變化了都要我們手動做嗎?在應該程序中,不會時間久了就會效率越來越低啊? 難道是過那麼一段時間有DBA來手動做動態採樣分析?或者寫個觸發器,隔那麼久做一次?
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章