1、單表訪問之索引合併 - index merge
MySQL中使用多個索引來完成一次查詢的執行方法稱之為 索引合併(index merge)。索引合併演算法有 Intersection合併、Union合併、Sort-Union合併。
1.1、Intersection合併
Intersection合併,某個查詢可以使用多個二級索引,將從多個二級索引中查詢到的結果取交集。
SELECT * FROM order_info WHERE order_no = 'a' AND expire_time = 'b';
若這個查詢使用 Intersection合併的方式執行,過程如下:先從 idx_order_no 二級索引對應的B+樹中取出order_no='a'的相關記錄;再從 idx_expire_time 二級索引對應的B+樹中取出expire_time='b'的相關記錄。
二級索引的記錄都是由索引列 + 主鍵構成的,獲得這兩個結果集中id值的交集。按照生成的id值列表進行回表操作,即從聚簇索引中把指定id值的完整使用者記錄取出來,返回給使用者。
1.1.1、執行成本
思考:為什麼不直接使用 idx_order_no或者idx_expire_time 只根據某個搜尋條件去讀取一個二級索引,回表後再過濾另外一個搜尋條件呢?因為兩種查詢執行方式之間的成本代價不同。
·只讀取一個二級索引的成本:按照某個搜尋條件讀取一個二級索引 -> 根據從該二級索引得到的主鍵值進行回表操作 -> 然後再過濾其他的搜尋條件。
·讀取多個二級索引之後取交整合本:按照不同的搜尋條件分別讀取不同的二級索引 -> 將從多個二級索引得到的主鍵值取交集 -> 最後根據主鍵值進行回表操作。
讀取多個二級索引比讀取一個二級索引消耗效能,但大部分讀取二級索引的操作是順序I/O,而回表操作是隨機I/O,所以,若只讀取一個二級索引時需要回表的記錄數很多,而 讀取二級索引後取交集的記錄數非常少,當節省的因為回表而造成的效能損耗比訪問多個二級索引帶來的效能損耗更高時,讀取多個二級索引後取交集比只讀取一個二級索引的成本更低。
1.1.2、Intersection索引合併的必要條件
MySQL在某些特定的情況下才可能會使用到Intersection索引合併,詳情如下:
2.1、等值匹配
二級索引列必須是等值匹配的情況
對於聯合索引來說,在聯合索引中的每個列都必須等值匹配,不能出現只匹配部分列的情況。
2.2、主鍵列可以是範圍匹配
主鍵的索引是有序的,按照有序的主鍵值去回表取記錄稱為:Rowid Ordered Retrieval,簡稱ROR。而二級索引的使用者記錄是由索引列 + 主鍵構成的,所以根據範圍匹配出來 的主鍵就是亂序的,導致回表開銷很大。
在二級索引列都是等值匹配的情況下可使用Intersection索引合併,因為只有在這種場景下根據二級索引查詢出的結果集是按照主鍵值排序的。
Intersection索引合併會把從多個二級索引中查詢出的主鍵值求交集,若從各個二級索引中查詢的到的結果集本身就是已經按照主鍵排好序的,求交集的過程比較容易。
上邊說的兩種情況只是發生Intersection索引合併的必要條件,不是充分條件。也就是說即使符合Intersection的條件,也不一定發生Intersection索引合併,是否進行索引合併,需要看最佳化器的判斷。
最佳化器只有在單獨根據搜尋條件從某個二級索引中獲取的記錄數太多,導致回表開銷太大,而透過Intersection索引合併後需要回表的記錄數大大減少時才會使用Intersection索引合併。
1.2、Union合併
查詢時,將符合某個搜尋條件的記錄取出來,也將符合另外的某個搜尋條件的記錄取出來,不同的搜尋條件之間是OR關係,對於OR關係的不同搜尋條件會使用到不同的索引。
SELECT * FROM order_exp WHERE order_no = 'a' OR expire_time = 'b'
Intersection是交集的意思,適用於使用不同索引的搜尋條件之間使用AND連線起來的情況;Union是並集的意思,適用於使用不同索引的搜尋條件之間使用OR連線起來的情況。
與Intersection索引合併類似,MySQL在某些特定的情況下才可能會使用到Union索引合併。
1.2.1、Union合併必要條件
等值匹配、主鍵列可以是範圍匹配。
1.2.2、使用Intersection索引合併的搜尋條件
搜尋條件的某些部分使用Intersection索引合併的方式得到的主鍵集合和其他方式得到的主鍵集合取交集,如下面的查詢:
SELECT * FROM order_info WHERE create_time = 'a' AND order_status = 'b' AND expire_time = 'c' OR (order_no = 'a' AND expire_time = 'b');
最佳化器可能採用這樣的方式來執行這個查詢:
1、先按照搜尋條件order_no = 'a' AND expire_time = 'b'從索引idx_order_no和idx_expire_time中使用Intersection索引合併的方式得到一個主鍵集合。
2、再按照搜尋條件 create_time ='a' AND order_status = 'b' AND expire_time = 'c'從聯合索引 u_idx_time_status 中得到另一個主鍵集合。
3、採用Union索引合併的方式把上述兩個主鍵集合取並集,然後進行回表操作,將結果返回給使用者。
查詢條件符合上述情況不一定會採用Union索引合併,需要最佳化器進行判斷。最佳化器只有在單獨根據搜尋條件從某個二級索引中獲取的記錄數比較少,透過Union索引合併後進行訪問的代價比全表掃描更小的時候才會使用Union索引合併。
1.3、Sort-Union合併
Union索引合併必須保證各個二級索引列在進行等值匹配的條件下才可能被用到,如下面的查詢就無法使用到Union索引合併:
SELECT * FROM order_info WHERE order_no < 'a' OR expire_time > 'z'
因為根據order_no < 'a'從idx_order_no索引中獲取的二級索引記錄的主鍵值不是排好序的,同時根據 expire_time> 'z'從idx_expire_time 索引中獲取的二級索引記錄的主鍵值也不是排好序的,但是order_no < 'a'和 expire_time> 'z''這兩個條件又是必須要使用的,所以可以這樣執行:
1、先根據order_no < 'a' 條件從idx_order_no二級索引中獲取記錄,並按照記錄的主鍵值進行排序;
2、再根據expire_time > 'z'條件從idx_expire_time二級索引中獲取記錄,並按照記錄的主鍵值進行排序;
3、因為上述的兩個二級索引主鍵值都是排好序的,剩下的操作與Union索引合併方式一樣。
先按照二級索引記錄的主鍵值進行排序,之後按照Union索引合併方式執行的方式稱之為Sort-Union索引合併,Sort-Union索引合併比單純的Union索引合併多了一步對二級索引記錄的主鍵值排序的過程。
查詢條件符合上述情況也不一定會採用Sort-Union索引合併,得看最佳化器的判斷。最佳化器只有在單獨根據搜尋條件從某個二級索引中獲取的記錄數比較少,透過Sort-Union索引合併後進行訪問的代價比全表掃描更小時才會使用Sort-Union索引合併。
2、連線查詢
2.1、連線的本質
把各個連線表中的記錄都取出來依次匹配的組合加入結果集並返回給使用者。將e1和e2兩個表連線起來的過程如下圖所示:
這個過程是把e1表的記錄和e2的記錄連起來組成新的更大的記錄,這個查詢過程稱之為連線查詢。連線查詢的結果集中包含一個表中的每一條記錄與另一個表中的每一條記錄相互匹配的組合,這樣的結果集稱之為 笛卡爾積 。
表e1中有3條記錄,表e2中也有3條記錄,兩個表連線之後的笛卡爾積就有3×3=9行記錄。
在MySQL中,連線查詢的語法只要在FROM語句後邊跟多個表名就好了,如把e1表和e2表連線起來的查詢語句可以寫成這樣:
SELECT * FROM e1, e2;
2.2、連線過程
SELECT * FROM e1, e2 WHERE e1.m1 > 1 AND e1.m1 = e2.m2 AND e2.n2 < 'd';
涉及單表的條件:e1.m1 > 1 只針對e1表;e2.n2 < 'd' 只針對e2表。
涉及兩表的條件:e1.m1 = e2.m2 涉及兩個表。
連線查詢大致執行過程如下:
2.2.1、確定驅動表(t1)
先確定第一個需要查詢的表,該表稱之為驅動表。單表中執行查詢語句只需要選取代價最小的那種訪問方法去執行單表查詢語句,即從執行計劃中找const、ref、ref_or_null、range、index、all等這些執行方法中選取代價最小的去執行查詢。
此處假設使用e1作為驅動表,需要到e1表中找滿足e1.m1 > 1的記錄,因表中的資料太少,未在表上建立二級索引,此處查詢e1表的訪問方法設定為all,即採用全表掃描的方式執行單表查詢。
2.2.2、遍歷驅動表結果,到被驅動表(t2)中查詢匹配記錄
針對上一步驟中從驅動表產生的結果集中的每一條記錄,分別需要到e2表中查詢匹配的記錄,指的是符合過濾條件的記錄。
因為是根據e1表中的記錄去找e2表中的記錄,e2表也被稱之為 被驅動表。上一步驟從驅動表中得到了2條記錄,所以需要查詢2次e2表。
整個連線查詢的執行過程如下圖所示:
整個連線查詢最後的結果只有兩條符合過濾條件的記錄:
從上邊兩個步驟可以看出來,這個兩表連線查詢共需要查詢1次e1表,2次e2表。
在特定的過濾條件下的結果,若把e1.m1 > 1這個條件去掉,那麼從e1表中查出的記錄就有3條,就需要查詢3次e2表了。即在兩表連線查詢中, 驅動表只需要訪問一次,被驅動表可能被訪問多次。
2.3、內連線和外連線
內連線
|
驅動表中的記錄在被驅動表中找不到匹配的記錄,該記錄不會加入到最後的結果集
|
外連線
|
驅動表中的記錄即使在被驅動表中沒有匹配的記錄,也仍然需要加入到結果集
|
根據選取驅動表的不同,外連線可以細分為2種:
左外連線
|
選取左側的表為驅動表
|
右外連線
|
選取右側的表為驅動表
|
2.3.1、過濾條件
WHERE子句中的過濾條件:
不論是內連線還是外連線,凡是不符合WHERE子句中的過濾條件的記錄都不會被加入最後的結果集。
ON子句中的過濾條件:
外連線的驅動表的記錄,若無法在被驅動表中找到匹配ON子句中的過濾條件的記錄,該記錄仍然會被加入到結果集中,對應的被驅動表記錄的各個欄位使用NULL值填充。
ON子句是專門為外連線驅動表中的記錄在被驅動表找不到匹配記錄時應不應該把該記錄加入結果集這個場景下提出的,若把ON子句放到內連線中,MySQL會把它和WHERE子句一樣對待,即:內連線中的WHERE子句和ON子句是等價的。
一般情況下,把只涉及單表的過濾條件放到WHERE子句中,把涉及兩表的過濾條件都放到ON子句中,把放到ON子句中的過濾條件也稱之為連線條件。
2.3.2、左(外)連線
左連線語法:
SELECT * FROM e1 LEFT [OUTER] JOIN e2 ON 連線條件 [WHERE 普透過濾條件];
中括號裡的OUTER單詞是可以省略的。
對於LEFT JOIN型別的連線來說,把放在左邊的表稱之為外表或者驅動表,右邊的表稱之為內表或者被驅動表。
e1為外表或者驅動表,e2是內標或者被驅動表。對於左(外)連線和右(外)連線來說,必須使用ON子句來指出連線條件。
2.3.3、右(外)連線
右(外)連線和左(外)連線的原理相同,只是LEFT換成RIGHT,語法如下:
SELECT * FROM e1 RIGHT [OUTER] JOIN e2 ON 連線條件 [WHERE 普透過濾條件];
驅動表是右邊的表e2,被驅動表是左邊的表e1。
2.3.4、內連線
內連線和外連線的根本區別:在驅動表中的記錄不符合ON子句中的連線條件時不會把該記錄加入到最後的結果集。
SELECT * FROM e1 [INNER | CROSS] JOIN e2 [ON 連線條件] [WHERE 普透過濾條件];
即在MySQL中,如下幾種內連線的寫法都是等價的:
SELECT * FROM e1 JOIN e2; SELECT * FROM e1 INNER JOIN e2; SELECT * FROM e1 CROSS JOIN e2;
內連線,上述寫法和直接把需要連線的表名放到FROM語句之後,用逗號,分隔開的寫法是等價的:
SELECT * FROM e1, e2;
連線的本質是把各個連線表中的記錄都取出來依次匹配的組合加入結果集並返回給使用者。
對於內連線來說,驅動表和被驅動表是可以互換的,並不會影響最後的查詢結果。
對於外連線來說,由於驅動表中的記錄即使在被驅動表中找不到符合ON子句條件的記錄時也要將其加入到結果集,所以此時驅動表和被驅動表的關係非常重要,即左外連線和右外連線的驅動表和被驅動表不能輕易互換。
3、MySQL執行連線
3.1、巢狀迴圈連線(Nested-LoopJoin)
對於兩表連線來說,驅動表只會被訪問一遍,但被驅動表卻要被訪問到好多遍,具體訪問幾遍取決於對驅動表執行單表查詢後的結果集中的記錄條數。
對於內連線來說,選取哪個表為驅動表都沒關係,而外連線的驅動表是固定的,即左(外)連線的驅動表就是左邊的那個表,右(外)連線的驅動表就是右邊的那個表。
若有3個表進行連線,首先兩表連線得到的結果集為新的驅動表,第三個表為被驅動表,虛擬碼的過程如下:
# 遍歷滿足對e1單表查詢結果集中的每一條記錄,N條 for each row in e1 { # 對於某條e1表的記錄來說,遍歷滿足對e2單表查詢結果集中的每一條記錄,M條 for each row in e2 { # 對於某條e1和e2表的記錄組合,對t3表進行單表查詢,L條 for each row in t3 { ... } } }
該過程就像巢狀的迴圈,驅動表只訪問一次,但被驅動表卻可能被多次訪問,訪問次數取決於對驅動表執行單表查詢後的結果集中的記錄條數的連線執行方式稱之為 巢狀迴圈連線( Nested-Loop Join ),時間複雜度是O(N * M * L)。
3.2.使用索引加快連線速度
利用索引來加快查詢速度。e1表和e2表進行內連線查詢如下:
SELECT * FROM e1, e2 WHERE e1.m1 > 1 AND e1.m1 = e2.m2 AND e2.n2 < 'd';
巢狀迴圈連線演算法執行的其實是連線查詢,查詢驅動表e1後的結果集中有兩條記錄,巢狀迴圈連線演算法需要對被驅動表查詢2次,當e1.m1 = 2時,去查詢一遍e2表,對e2表的查詢語句相當於:
SELECT * FROM e2 WHERE e2.m2 = 2 AND e2.n2 < 'd';
當e1.m1 = 3時,再去查詢一遍e2表,此時對e2表的查詢語句相當於:
SELECT * FROM e2 WHERE e2.m2 = 3 AND e2.n2 < 'd';
原來的e1.m1 = e2.m2這個涉及兩個表的過濾條件在針對e2表做查詢時關於e1表的條件已經確定,只需要單單最佳化對e2表的查詢,上述兩個對e2表的查詢語句中利用到的列是m2和n2列,可以在e2表的m2列上建立索引。
3.3.基於塊的巢狀迴圈連線(Block Nested-Loop Join)
掃描一個表的過程實際上是先把這個表從磁碟上載入到記憶體中,然後從記憶體中比較匹配條件是否滿足。
記憶體裡可能並不能完全存放大表中所有的記錄,在掃描表前邊記錄的時候後邊的記錄可能還在磁碟上,等掃描到後邊記錄的時候可能記憶體不足,需要把前邊的記錄從記憶體中釋放掉。
採用巢狀迴圈連線演算法的兩表連線過程中,被驅動表要被訪問好多次,若這個被驅動表中的資料特別多且不能使用索引進行訪問,即相當於從磁碟上讀好幾次這個表,I/O代價非常大,需要想辦法儘量減少訪問被驅動表的次數。
當被驅動表中的資料很多時,每次訪問被驅動表,被驅動表的記錄會被載入到記憶體中,在記憶體中的每一條記錄只會和驅動表結果集的一條記錄做匹配,之後會被從記憶體中清除掉。再從驅動表結果集中拿出另一條記錄,再一次把被驅動表的記錄載入到記憶體中一遍,驅動表結果集中有多少條記錄,就把被驅動表從磁碟上載入到記憶體中多少次。
MySQL提出了 join buffer 的概念,可以一次性和多條驅動表中的記錄做匹配,大大減少了重複從磁碟上載入被驅動表的代價。join buffer是執行連線查詢前申請的一塊固定大小的記憶體,先把若干條驅動表結果集中的記錄裝在此 join buffer 中,然後開始掃描被驅動表,每一條被驅動表的記錄一次性和 join buffer 中的多條驅動表記錄做匹配,匹配的過程都是在記憶體中完成的,這樣可以顯著減少被驅動表的I/O代價。使用join buffer 過程如下圖所示:
當join buffer能容納驅動表結果集中的所有記錄,這種加入了 join buffer 的巢狀迴圈連線演算法稱之為 基於塊的巢狀連線( Block Nested-Loop Join )演算法。join buffer的大小是可以透過啟動引數或者系統變數join_buffer_size進行配置,預設大小為 262144位元組(也就是256KB),最小可設定為128位元組。
show variables like 'join_buffer_size' ;
對於最佳化被驅動表的查詢來說,最好是為被驅動表加上效率高的索引,若不能使用索引,並且自己的機器的記憶體可以嘗試調大 join_buffer_size的值來對連線查詢進行最佳化。
驅動表的記錄並不是所有列都會被放到join buffer中,只有查詢列表中的列和過濾條件中的列才會被放到join buffer中,為了讓join buffer中放置更多的記錄,只需要把需要的列放到查詢列表中,最好不要把 * 作為查詢列表。
4、MySQL查詢成本
4.1、查詢成本
MySQL執行一個查詢可以有不同的執行方案,它會選擇其中成本最低,或者代價最低的方案去真正的執行查詢。MySQL中一條查詢語句的執行成本由 I/O成本 和 CPU成本 組成。
I/O成本:MyISAM、InnoDB儲存引擎都是將資料和索引儲存到磁碟上,當想查詢表中的記錄時,需要先把資料或者索引載入到記憶體中然後再操作。從 磁碟到記憶體這個載入的過程損耗的時間稱之為 I/O 成本。
CPU成本:讀取以及檢測記錄是否滿足對應的搜尋條件、對結果集進行排序等這些操作損耗的時間稱之為CPU成本。
對InnoDB儲存引擎而言,頁是磁碟和記憶體之間互動的基本單位。
MySQL規定讀取一個頁面花費的成本預設是1.0(I/O成本),讀取以及檢測一條記錄是否符合搜尋條件的成本預設是0.2(CPU成本)。讀取記錄時需不需要檢測是否滿足搜尋條件,哪怕是空資料,成本都算是0.2。
4.2、單表查詢的成本
在一條單表查詢語句真正執行前,MySQL的查詢最佳化器會找出執行該語句所有可能使用的方案,對比之後找出成本最低的方案,這個成本最低的方案就是所謂的執行計劃,之後才會呼叫儲存引擎提供的介面真正的執行查詢,過程如下:
4.2.1、根據搜尋條件,找出所有可能使用的索引
B+樹索引,只要索引列和常數使用=、<=>、IN、NOT IN、IS NULL、IS NOT NULL、>、<、>=、<=、BETWEEN、!=或者LIKE運算子連線起來,就能產生一個範圍區間,MySQL把一個查詢中可能使用到的索引稱之為possible keys。
4.2.2、計算全表掃描的代價
對InnoDB儲存引擎而言,全表掃描就是把聚簇索引(主鍵索引)中的記錄都依次和給定的搜尋條件做一下比較,把符合搜尋條件的記錄加入到結果集,所以需要將聚簇索引對應的頁面載入到記憶體中,然後再檢測記錄是否符合搜尋條件。查詢成本=I/O成本+CPU成本,計算全表掃描的代價需要兩個資訊:
2.1、聚簇索引佔用的頁面數
2.2、該表中的記錄數
MySQL為每個表維護了一系列的統計資訊,透過SHOW TABLE STATUS語句來檢視錶的統計資訊,若要看指定的某個表的統計資訊,就在該語句後加對應的LIKE語句,如要檢視 order_info 這個表的統計資訊,詳情如下:
SHOW TABLE STATUS LIKE 'order_info'
查詢後會出現很多統計選項,只需要關注兩個選項:
·Rows
該選項表示表中的記錄條數,對於使用MyISAM儲存引擎的表而言,該值是準確的;對於使用InnoDB儲存引擎的表而言,該值是一個估計值。
·Data_length
該選項表示表佔用的儲存空間位元組數,對於使用MyISAM儲存引擎的表而言,該值是資料檔案的大小;對於使用InnoDB儲存引擎的表而言,該值相當於聚簇索引佔用的儲存空間大小,該值大小:
Data_length = 聚簇索引的頁面數量 × 每個頁面的大小
4.2.3、計算使用不同索引執行查詢的代價
MySQL查詢最佳化器先分析使用唯一二級索引的成本,再分析使用普通索引的成本
MySQL計算查詢成本依賴兩個方面的資料:範圍區間數量、需要回表的記錄數。
範圍區間數量:不論某個範圍區間的二級索引到底佔用了多少頁面,查詢最佳化器認為讀取索引的一個範圍區間的I/O成本和讀取一個頁面是相同的。
需要回表的記錄數:最佳化器需要計算二級索引的某個範圍區間到底包含多少條記錄。
4.2.4、對比各種執行方案的代價,找出成本最低的一個
4.3、連線查詢的成本
MySQL中連線查詢採用的是巢狀迴圈連線演算法,驅動表會被訪問一次,被驅動表可能會被訪問多次,對於兩表連線查詢來說,它的查詢成本由 單次查詢驅動表的成本 + 多次查詢被驅動表的成本(具體查詢多少次取決於對驅動表查詢的結果集中有多少條記錄) 兩部分構成。
對驅動表進行查詢後得到的記錄條數稱之為驅動表的 扇出(英文名:fanout)。驅動表的扇出值越小,對被驅動表的查詢次數越少,連線查詢的總成本也越低。當查詢最佳化器想計算整個連線查詢所使用的成本時,需要計算出驅動表的扇出值。
連線查詢總成本 = 單次訪問驅動表的成本 + 驅動表扇出數 × 單次訪問被驅動表的成本。
對於左(外)連線和右(外)連線查詢來說,它們的驅動表示固定的,想要得到最優的查詢方案只需要分別為驅動表和被驅動表選擇成本最低的訪問方法。
對於內連線而言,驅動表和被驅動表的位置是可以互換的,要考慮兩個方面的問題:不同的表作為驅動表最終的查詢成本可能是不同的,需要考慮最優的表連線順序,分別為驅動表和被驅動表選擇成本最低的訪問方法。
5、MySQL的查詢重寫規則
5.1、條件化簡
5.1.1、移除不必要的括號
((a = 5 AND b =c) OR ((a > c) AND (c < 5)))
最佳化器將用不到的括號去除:
(a = 5 and b =c) OR (a > c AND c < 5)
5.1.2、常量傳遞(constant_propagation)
當表示式和其他涉及列a的表示式使用AND連線起來時,可將其他表示式中的a的值替換為5,如:
a = 5 AND b >a
可被轉換為:
a = 5 AND b >5
等值傳遞(equality_propagation),當多個列之間存在等值匹配的關係,如:
a = b and b = c and c = 5
表示式可以被簡化為:
a = 5 and b = 5 and c = 5
5.1.3、移除沒用的條件(trivial_condition_removal)
對於一些明顯永遠為TRUE或者FALSE的表示式,最佳化器會移除掉它們,如:
(a < 1 and b= b) OR (a = 6 OR 5 != 5)
簡化後表示式如下:
(a < 1 and TRUE) OR (a = 6 OR FALSE)
繼續被簡化為:
a < 1 OR a =6
5.2、外連線消除
內連線的驅動表和被驅動表的位置可以相互轉換,而左(外)連線和右(外)連線的驅動表和被驅動表是固定的,會導致內連線可能透過最佳化表的連線順序來降低整體的查詢成本,但外連線卻無法最佳化表的連線順序。
外連線和內連線的本質區別:對於外連線的驅動表的記錄而言,若無法在被驅動表中找到匹配ON子句中的過濾條件的記錄,那麼該記錄仍然會被加入到結果集中,對應的被驅動表記錄的各個欄位使用NULL值填充;而內連線的驅動表的記錄,若無法在被驅動表中找到匹配ON子句中的過濾條件的記錄,那麼該記錄會被捨棄。
查詢效果如下:
SELECT * FROM e1 INNER JOIN e2 ON e1.m1 = e2.m2;
SELECT * FROM e1 LEFT JOIN e2 ON e1.m1 = e2.m2;
上述例子中的(左)外連線來說,由於驅動表e1中m1=1, n1='a'的記錄無法在被驅動表e2中找到符合ON子句條件e1.m1 = e2.m2的記錄,所以就直接把這條記錄加入到結果集,對應的e2表的m2和n2列的值都設定為NULL。