查詢操作是關系資料庫中使用最為頻繁的操作,也是構成其他SQL語句(如DELETE、UPDATE)的基礎。當要删除或更新某些記錄時,首先要查詢出這些記錄,然後再對其進行相應的SQL操作。是以基于SELECT的查詢操作就顯得非常重要。對于查詢處理,可将其分為邏輯查詢處理及實體查詢處理。邏輯查詢處理表示執行查詢應該産生什麼樣的結果,而實體查詢代表MySQL資料庫是如何得到該結果的。兩種查詢的方法可能完全不同,但是得到的結果必定是相同的
SQL語言不同于其他程式設計語言(如C、C++、Java、Python),最明顯的不同展現在處理代碼的順序上。在大多數程式設計語言中,代碼按編碼順序被處理。但在SQL語言中,第一個被處理的子句總是FROM子句
查詢語句中的序号為該查詢語句的處理順序
可以看到一共有11個步驟,最先執行的是FROM操作,最後執行的是LIMIT操作。每個操作都會産生一張虛拟表,該虛拟表作為一個處理的輸入。這些虛拟表對使用者是透明的,隻有最後一步生成的虛拟表才會傳回給使用者。如果沒有在查詢中指定某一子句,則将跳過相應的步驟
具體分析查詢處理的各個階段:
FROM:對FROM子句中的左表<left_table>和右表<right_table>執行笛卡兒積(Cartesianproduct),産生虛拟表VT1
ON:對虛拟表VT1應用ON篩選,隻有那些符合<join_condition>的行才被插入虛拟表VT2中
JOIN:如果指定了OUTER JOIN(如LEFT OUTER JOIN、RIGHT OUTER JOIN),那麼保留表中未比對的行作為外部行添加到虛拟表VT2中,産生虛拟表VT3。如果FROM子句包含兩個以上表,則對上一個連接配接生成的結果表VT3和下一個表重複執行步驟1)~步驟3),直到處理完所有的表為止
WHERE:對虛拟表VT3應用WHERE過濾條件,隻有符合<where_condition>的記錄才被插入虛拟表VT4中
GROUP BY:根據GROUP BY子句中的列,對VT4中的記錄進行分組操作,産生VT5
CUBE|ROLLUP:對表VT5進行CUBE或ROLLUP操作,産生表VT6
HAVING:對虛拟表VT6應用HAVING過濾器,隻有符合<having_condition>的記錄才被插入虛拟表VT7中。
SELECT:第二次執行SELECT操作,選擇指定的列,插入到虛拟表VT8中
DISTINCT:去除重複資料,産生虛拟表VT9
ORDER BY:将虛拟表VT9中的記錄按照<order_by_list>進行排序操作,産生虛拟表VT10。11)11. LIMIT:取出指定行的記錄,産生虛拟表VT11,并傳回給查詢使用者
下面通過一個查詢示例來較長的描述邏輯處理的11個階段。首先根據下面的代碼,建立一個使用者資料表customers和orders,并填充一定量的資料
customers表記錄

orders表記錄
【通過如下語句來查詢來自杭州且訂單數少于2的客戶,并且查詢出他們的訂單數量,查詢結果按訂單數從小到大排序】
來自杭州且訂單數少于2的顧客
下邊分析該SQL的執行過程
(1)執行笛卡爾積 第一步需要做的是對FROM子句前後的兩張表進行笛卡兒積操作,也稱做交叉連接配接(CrossJoin),生成虛拟表VT1。如果FROM子句前的表中包含a行資料,FROM子句後的表中包含b行資料,那麼虛拟表VT1中将包含a*b行資料。虛拟表VT1的列由源表定義。對于前面的SQL查詢語句,會先執行表orders和customers的笛卡兒積操作
笛卡兒積傳回的虛拟表VT1
(2)應用ON過濾器 SELECT查詢一共有3個過濾過程,分别是ON、WHERE、HAVING。ON是最先執行的過濾過程。根據上一小節産生的虛拟表VT1,過濾條件為:
對于大多數的程式設計語言而言,邏輯表達式的值隻有兩種:TRUE和FALSE。但是在關系資料庫中起邏輯表達式作用的并非隻有兩種,還有一種稱為三值邏輯的表達式。這是因為在資料庫中對NULL值的比較與大多數程式設計語言不同。在C語言中, NULL ==NULL的比較傳回的是1,即相等,而在關系資料庫中,NULL的比較則完全不是這麼回事,例如:
第一個NULL值的比較傳回的是NULL而不是0,第二個NULL值的比較傳回的仍然是NULL,而不是1。對于比較傳回值為NULL的情況,使用者應該将其視為UNKNOWN,即表示未知的。因為在某些情況下,NULL傳回值可能代表1,即NULL等于NULL,而有時NULL傳回值可能代表0。
對于在ON過濾條件下的NULL值比較,此時的比較結果為UNKNOWN,卻被視為FALSE來進行處理,即兩個NULL并不相同。但是在下面兩種情況下認為兩個NULL值的比較是相等的:
GROUP BY子句把所有NULL值分到同一組
ORDER BY子句中把所有NULL值排列在一起 (這個大家可以自行測試)
是以在産生虛拟表VT2時,會增加一個額外的列來表示ON過濾條件的傳回值,傳回值有TRUE、FALSE、UNKNOWN。取出比較值為TRUE的記錄,産生虛拟表VT2 虛拟表VT2
(3)添加外部行
這一步隻有在連接配接類型為OUTER JOIN時才發生,如LEFT OUTER JOIN、RIGHT OUTERJOIN、FULL OUTER JOIN。雖然在大多數時候我們可以省略OUTER關鍵字,但OUTER代表的就是外部行。LEFT OUTER JOIN把左表記為保留表,RIGHT OUTER JOIN把右表記為保留表,FULL OUTER JOIN把左右表都記為保留表。添加外部行的工作就是在VT2表的基礎上添加保留表中被過濾條件過濾掉的資料,非保留表中的資料被賦予NULL值,最後生成虛拟表VT3 虛拟表VT3
在這個例子中,保留表是customers,顧客baidu在VT2表中由于沒有訂單而被過濾,是以baidu作為外部行被添加到虛拟表VT2中,将非保留表中的資料指派為NULL。如果需要連接配接表的數量大于2,則對虛拟表VT3重做本節首的步驟(1)~步驟(3),最後産生的虛拟表作為下一個步驟的輸出
(4)應用WHERE過濾器 對上一步驟産生的虛拟表VT3進行WHERE條件過濾,隻有符合<where_condition>的記錄才會輸出到虛拟表VT4中
在目前應用WHERE過濾器時,有兩種過濾是不被允許的:
由于資料還沒有分組,是以現在還不能在WHERE過濾器中使用where_condition=MIN(col)這類對統計的過濾
由于沒有進行列的選取操作,是以在SELECT中使用列的别名也是不被允許的,如SELECT city as c FROM t WHERE c='ShangHai'是不允許出現的
看一個在WHERE過濾條件中使用分組過濾查詢導緻出錯的例子
可以看到MySQL資料庫提示錯誤地使用了分組函數。接着來看一個列别名使用出錯的例子:
因為在目前的步驟中還未進行SELECT選取列名的操作,是以此時的列别名是不被支援的,MySQL資料庫抛出了錯誤,提示未知的列c
應用WHERE過濾器:WHERE c.city='HangZhou',最後得到的虛拟表VT4 虛拟表VT4
此外,在WHERE過濾器中進行的過濾和在ON過濾器中進行的過濾是有所不同的。對于OUTERJOIN中的過濾,在ON過濾器過濾完之後還會添加保留表中被ON條件過濾掉的記錄,而WHERE條件中被過濾掉的記錄則是永久的過濾
(5)分組 在本步驟中根據指定的列對上個步驟中産生的虛拟表進行分組,最後得到虛拟表VT5
虛拟表VT5
(6)應用ROLLUP或CUBE 如果指定了ROLLUP選項,那麼将建立一個額外的記錄添加到虛拟表VT5的最後,并生成虛拟表VT6。因為我們的查詢并未用到ROLLUP,是以将跳過本步驟
(7)應用HAVING過濾器 這是最後一個條件過濾器了,之前已經分别應用了ON和WHERE過濾器。在該步驟中對于上一步産生的虛拟表應用HAVING過濾器,HAVING是對分組條件進行過濾的篩選器。對于示例的查詢語句,其分組條件為
是以将customer_id為163的訂單從虛拟表中删除,生成的虛拟表VT6 虛拟表VT6
需要特别注意的是,在這個分組中不能使用COUNT(1)或COUNT(*),因為這會把通過OUTER JOIN添加的行統計入内而導緻最終查詢結果與預期結果不同。在這個例子中隻能使用COUNT o.order_id才能得到預期的結果
注意:子查詢不能用做分組的聚合函數,如HAVING COUNT(SELECT ...)<2是不合法的
(8)處理SELECT清單 雖然SELECT是查詢中最先被指定的部分,但是直到步驟8)時才真正進行處理。在這一步中,将SELECT中指定的列從上一步産生的虛拟表中選出 SELECT部分為:
虛拟表VT7
(9)應用DISTINCT子句 如果在查詢中指定了DISTINCT子句,則會建立一張記憶體臨時表(如果記憶體中存放不下就放到磁盤上)。這張記憶體臨時表的表結構和上一步産生的虛拟表一樣,不同的是對進行DISTINCT操作的列增加了一個唯一索引,以此來去除重複資料。
由于在這個SQL查詢中未指定DISTINCT,是以跳過本步驟。另外,對于使用了GROUP BY的查詢,再使用DISTINCT是多餘的,因為已經進行分組,不會移除任何行
(10)應用ORDER BY子句 根據ORDER BY子句中指定的列對上一步輸出的虛拟表進行排列,傳回新的虛拟表。還可以在ORDER BY子句中指定SELECT清單中列的序列号,如下面的語句:
通常情況下,并不建議采用這種方式來進行排序,因為程式員可能修改了SELECT清單中的列,而忘記修改ORDER BY中的清單。但是,如果使用者對網絡傳輸要求很高,這也不失為一種節省網絡傳輸位元組的方法
對于示例中,ORDER BY子句為:
最後得到虛拟表
相信很多DBA和開發人員都錯誤地認為在選取表中的資料時,記錄會按照表中主鍵的大小順序地取出,即結果像進行了ORDER BY一樣。導緻這個經典錯誤的原因主要是沒有了解什麼才是真正的關系資料庫
關系資料庫是在數學的基礎上發展起來的,關系對應于數學中集合的概念。資料庫中常見的查詢操作其實對應的是集合的某些運算:選擇、投影、連接配接、并、交、差、除。最終的結果雖然是以一張二維表的方式呈現在使用者面前,但是從資料庫内部來看是一系列的集合操作。是以,對于表中的記錄,使用者需要以集合的思想來了解
因為表中的資料是集合中的元素,而集合是無序的。是以對于沒有ORDER BY子句的SQL語句,其解析結果應為:從集合中選擇期望的子集合。這表明結果并不一定要有序
注意:在MySQL資料庫中,NULL值在升序過程中總是首先被選出,即NULL值在ORDER BY子句中被視為最小值
(11)LIMIT子句 在該步驟中應用LIMIT子句,從上一步驟的虛拟表中選出從指定位置開始的指定行資料。對于沒有應用ORDER BY的LIMIT子句,結果同樣可能是無序的,是以LIMIT子句通常和ORDER BY子句一起使用
由于示例中SQL語句沒有LIMIT子句,是以最後得到的結果應如下:
上邊介紹了邏輯查詢處理,并且描述了執行查詢應該得到什麼樣的結果。但是資料庫也許并不會完全按照邏輯查詢處理的方式來進行查詢。我們知道在MySQL資料庫層有Parser和Optimizer兩個元件。Parser的工作就是分析SQL語句,而Optimizer的工作就是對這個SQL語句進行優化,選擇一條最優的路徑來選取資料,但是必須保證實體查詢處理的最終結果和邏輯查詢處理是相等的
如果表上建有索引,那麼優化器就會判斷SQL語句是否可以利用該索引來進行優化。如果沒有可以利用的索引,可能整個SQL語句的執行代價非常大。可以舉個例子:
假設分别向x、y表中分别插入10w和18w條資料,兩張表中均沒有建立索引,是以最終SQL解析器解析的執行結果為邏輯處理的步驟,也就是按照上邊中分析的,總共經過11個步驟來進行資料的查詢。最先根據笛卡兒積生成一張虛拟表VT1,表x有10萬行資料,表y有18萬行資料,這意味着進行笛卡兒積後産生的虛拟表VT1總共有180億行的資料!是以運作這個SQL語句,在一個雙核筆記本上,InnoDB緩沖池配置為128M,總共需要執行50多分鐘。
可能有人會認為,128MB的InnoDB緩沖池太小,進而導緻記憶體中無法存放這麼多資料而使執行需要花費這麼長的時間。其實不然,表x和表y的大小都沒有超過20MB,足夠存放在128MB的記憶體緩沖池中,語句執行速度慢的主要原因是需要産生180億次的資料。即便是在記憶體中産生這麼多次的資料,也需要花費很長的時間。然而,如果這時對表y添加一個主鍵值,再執行這條SQL語句,你會驚訝地發現隻需要不到1秒的時間
性能提高了3000多倍!促使這個查詢時間大幅減少的原因很簡單,就是在添加索引後避免了笛卡兒表的産生,是以大幅縮短了語句運作的時間。我們可以通過EXPLAIN指令來檢視經SQL優化器優化後MySQL資料庫實際選擇的執行方式。關于MySQL中的索引,以及EXPLAIN執行計劃中每一列的含義,我之前也整理過一篇文章《一篇文章帶你熟悉myql索引》