原話題:
是關於一個left join的,沒有技術難度,但不想清楚不一定能回答出正確答案來:
TabA表有三個字段Id,Col1,Col2 且里面有一條數據1,1,2
TabB表有兩個字段Id,Col1且里面有四條數據
- 1,1
- 2,2
- 3,2
- 4,2
問題:
如下語句會返回多少條數據? 在不寫測試腳本的情況下,如果你能在5分鍾內准備回答出答案,且能說出些所以然來(及不是憑感覺猜出來的結果),那么請繼續看后面的問題。
Select * from TabA a Left join TabB b1 on a.Col1=b1.Col1 Left join TabB b2 on a.Col2=b2.Col1
延深問題:
現在表A多增加一條數據2,3,4 ,此時再運行上面的語句會幾條數據?如果你能在2分鍾內回答出正常答案,那么請繼續看后面的問題。
理論問題:
- 是否知道 sql server的join包含 hash匹配,嵌套循環以及合並聯接?不同於left join, inner join的概念,屬於執行計划中的概念。
- 上面三種的查詢機制是否能畫簡單的示意圖?
- 上面三種查詢機制的應用場景是什么?即什么樣的情況下適合應用三種中的哪一種?
我發現就上面這個問題不少人回答不正確,這其中也包括我自己。為什么如此簡單的問題往往會回答錯誤,我認為可能有如下原因:
- 本身對SQL查詢知識就很欠缺,比如不知道left join與inner join的區別等等;
- 平時工作中也寫SQL查詢,只知道怎么用,不知道稍微詳細一點的細節;
- 沒經過大腦思考,隨口說的,往往仔細想想就能回答正確。
對於第一種情況的人,短時間內無法解決,只有通過自身的學習來補救,對於第二種情況的人就需要稍微學習一些基本的理論知識就夠用,對於第三種情況的人是一個態度問題。
left join的概念
簡單來講就是以左表做為外層循環表,每條每條去內層表去查找匹配記錄,如果找到就返回join好的值,如果沒找到返回外層表的值,內層表統一賦值為null。這里之所以說成簡單來講,是因為我是拿嵌套循環的例子來分析,因為這比較容易讓非SQL方面的程序員明白,畢竟對於.net程序員來講編寫雙層或者多層循環的例子會很多。而對於hash匹配以及合並聯接的應用場景在.net程序中相對較少,類似如下的雙層循環。
foreach(var colA in tabA) { foreach(var colB in tabB) { if(colA==colB) { ...... } } }
這里需要注意下,上面說到的外層表的記錄循環去內層表查找時,這里有個問題,看這條語句:
Select * from TabA a Left join TabB b1 on a.Col1=b1.Col1
這里的TabA 就是我這里講的外層表,TabB就是內層表,外層表就一行數據,內層表有4行數據,從上面給出的數據來看,用來做等值判斷的條件是外層表的Col1字段與內層表的Col1字段,拿外層表的Cole=1這行數據去內層表查詢時,內層表的第一條數據符合條件,其它三條不符合,此時的結果會是下面的哪一種呢?
- 4條記錄
a.Id a.Col1 a.Col2 b.Id b.Col1
1 1 2 1 1
1 1 2 null null
1 1 2 null null
1 1 2 null null
- 一條記錄
a.Id a.Col1 a.Col2 b.Id b.Col1
1 1 1 2 1
這要理解當在內層表中找到數據以及找不到數據的區別,我們拿外層表Col1=1這條數據去內層表查找時,需要查找4次,其中有一條符合,三條不符合,這說明找到了匹配數據,所以只返回匹配的數據行,即一條數據,而不會出現上面的第一種結果返回4條數據。
這是我當時遇到這個問題時產生的誤解。
再看后面的那個left join
Select * from TabA a Left join TabB b1 on a.Col1=b1.Col1 Left join TabB b2 on a.Col2=b2.Col1
容易產生的問題,再進行第二次left join 的時候,外層表是TabA原始表呢還是第一次left join 之后的結果集呢? 看下我列出來的表頭,就很容易理解了,這里的a.Col2就是第一次left join后的結果集。( a.Id a.Col1 a.Col2 b.Id b.Col1)
我們可以做下測試,這里使用inner join來做測試,因為這加容易比較出差異,運行下面的語句,此時TabA中有兩條數據,就是上面延深問題中添加的2,3,4這條。
Select * from TabA a inner join TabB b1 on a.Col1=b1.Col1 inner join TabB b2 on a.Col2=b2.Col1
分兩步來看:
Select * from TabA a inner join TabB b1 on a.Col1=b1.Col1
這里只會返回一條數據,因為inner join返回的交集。
a.Id a.Col1 a.Col2 b.Id b.Col1
1 1 2 1 1
如果第二次join 時,如果連接的是原始表TablA,那么循環查詢的次數應該是TabA的總條數2,但從下面的執行計划圖可以分析出執行順序。
- 上圖一的結構圖很明顯,第二次join的是第一次join的結果集而不是原始表TabA。
- 上圖二的實際行數也足以說明關聯的不是原始表TabA
解決了上面兩個問題,那么應該能容易分析出文章前面提到的兩個問題的答案了。但這只是解決了一個小問題,如果從學習的角度來講我們應該通過這一個問題來將其周邊涉及的主要知識都學習一下,這里我們非常有必要了解了執行計划的join分類。
Join在執行計划中的分類
我只是簡單的對這三種分類做簡單的概述,后續為這三種join分別進行稍微詳細點的總結。執行計划中的三種Join各有各的優缺點,不能說哪一種絕對是最好的,也不能說哪一種能夠適用於所有的查詢應用場景,我下面提供的常見應用場景只是一些案例,且是有運行環境的,因為執行計划的選擇非常復雜,有時只要有一個環境因素不同就會造成執行計划的不同,比如會受到下面因素的因素的影響:
- 數據量,當數據量比較小時可能是一種執行計划,當數據量慢慢增大時執行計划可能會發生改變。
- join關聯的不同也會使執行計划發生改變,比如用inner join與left join時產生的執行計划有可能是不同的。
Hash匹配
常見適用場景:
- 條件列沒有索引,這里說的條件列是關聯表的所有關聯鍵都沒有索引
- 大數據表關聯
約束條件:只能用於等值條件,比如a.Col1=b.Col1這種類型的,值的注意的是這里講的等值條件,不是說所有的條件都需要是等值的,只有其中有一個是等值條件就行,比如下面這幾種都符合等值條件:
Select * from TabA a inner join TabB b1 on a.Col1=b1.Col1 AND a.Col3 LIKE '%1%'
上面提到的適用場景以及約束條件,不是絕對的,受很多其它因素影響,比如上面提到的join方式的不同,例如下面這兩條SQL的執行計划就不同,這里就不貼圖了,情況很復雜。
- 下面是嵌套查詢
Select * from TabA a
Left join TabB b1 on a.Col1=b1.Col1
Left join TabB b2 on a.Col2=b2.Col1
- 下面是Hash匹配
Select * from TabA a inner join TabB b1 on a.Col1=b1.Col1 inner join TabB b2 on a.Col2=b2.Col1
Hash匹配還可以根據需要生成的Hash表的大小細分,分為In-Memory,grace以及recursive 這三種,它們對於內存的要求逐步提高。
Hash匹配的優點:只需要掃描兩次表,IO占用相對較少。
Hash匹配的缺點:構建Hash表,比較消耗CPU資源。
嵌套循環
常見適用場景:一個表數據量大,一個表數據量小,且關聯鍵有索引。當只有一個表的關聯鍵有索引時,將具有索引的表做為內層表可以獲取最佳的IO性能。不局限於等值條件。
合並聯接
常見適用場景,關聯鍵上需要有已經經過排序后的索引做為數據源,一般情況下需要有一個關聯鍵是唯一索引。當兩個關聯表數據量相當時且具有排序后的索引那么比較適合用合並聯接,不局限於等值條件。
總結:
sql查詢機制非常復雜,受各種因素的影響,很難有統一的標准去衡量一條SQL語句的性能。而只有了解了它的一些基本原理后,才有可能不被一些看起來非常簡單的問題難倒,也才有可能編寫也適用於當前項目性能最佳的SQL來。