【譯】SQL 指引:如何寫出更好的查詢

SQL 指引:如何寫出更好的查詢

結構化查詢語言(SQL)是數據科學行業的一種不可或缺的技能,通常來講,學習這項技能是至關簡單的。然而大多數人都忘記 SQL 不只僅是寫查詢語句,這只是第一步。確保查詢高性能,或者符合上下文語意又徹底是另一回事了。前端

這就是爲何本篇 SQL 教程要引導你,能夠經過如下步驟來評估你的查詢:react

  • 首先,你將以數據科學工做中學習 SQL 的重要性的簡要概述爲開始。
  • 接着,你將學習更多有關如何 SQL 查詢處理和執行,這樣你纔可以正確地理解編寫高性能查詢的重要性:更具體地說,你會看到查詢被解析,重寫,優化和最終被執行;
  • 考慮到這一點,你不只能夠複習初學者編寫查詢時的一些反模式查詢,並且還能夠學習關於針對那些可能出現的錯誤的替代和解決方案,你還將學習更多有關基於集合仍是程序方法進行查詢的內容。
  • 你還將看到這些出於性能問題考慮的反模式,除了「手動」方法改進 SQL 查詢以外,你還能夠經過使用一些其餘可幫助你查看查詢計劃的工具,以更加結構化,深刻的方式分析你的查詢;並且,
  • 在執行查詢以前,你將簡要了解時間複雜度和大 O 符號來在你執行查詢以前瞭解執行計劃的時間複雜度;最後,
  • 你將簡要地瞭解如何進一步調整你的查詢

你對 SQL 課程感興趣嗎?那就來學習 DataCamp 的數據科學的 SQL 簡介課程吧!android

爲何我應該爲數據科學學習 SQL?

SQL 遠未消亡:不管你是申請數據分析師,數據工程師,數據科學家仍是任何其餘職位,你均可以從數據科學行業的職位描述中發現 SQL 是最須要的技能之一。參加 O'Reilly 數據科學工資調查報告的 70% 的受訪者證明了這一點,他們表示他們會在專業場景中使用 SQL。並且,在本次調查中,SQL(70%)遠勝於 R(57%)和 Python(54%)編程語言。ios

你得知一個狀況:當你正在努力找數據科學行業的工做時,SQL 是一項必須具有的技能。git

對於一個20世紀70年代初開發的語言來講,還不錯,對吧?github

可是爲何被使用的如此頻繁?爲何 SQL 不會消失,即便它已經存在了很長時間了?算法

有幾個緣由:第一個緣由是大多數公司將數據存儲在關係型數據庫管理系統(RDBMS)或關係數據流管理系統(RDSMS)中,你須要 SQL 才能訪問這些數據。 SQL 是數據的通用語言:它使你可以與幾乎任何數據庫進行交互,甚至能夠在本地創建本身的數據庫!sql

若是這還不夠,請記住有不少 SQL 的實如今供應商之間不兼容,並不必定遵照標準。於是,瞭解標準 SQL 是你在(數據科學)行業中找到一條路的要求之一。數據庫

除此以外,能夠確定地說,SQL 也被更新的技術所接受,例如 Hive,用於查詢和管理大型數據集的類 SQL 查詢語言界面,或可用於執行 SQL 查詢的 Spark SQL。雖然你發現標準可能與你已知的有所不一樣,但學習曲線將會更加容易。編程

若是你想作一個比較,認爲它和學線性代數同樣:經過把全部的精力放在這個主題上,你甚至可使用它來掌握機器學習!

簡而言之,這就是爲何你應該學習這門查詢語言:

  • 即便對於新手它也是至關容易學習的。學習曲線是至關容易和平滑的,以致於在學習的任何階段你都能寫出查詢。
  • 遵循「一旦學習,到處適用」的原則,因此這是一個對你時間的偉大投資!
  • 它是對編程語言的極好補充; 在某些狀況下,編寫查詢甚至比編寫代碼更爲優先,由於它性能更高!

你還在等什麼呢?

SQL 處理 & 查詢執行

爲了提升你 SQL 查詢的性能,當你按快捷方式運行查詢時,你首先須要知道內部發生了什麼。

首先,查詢被解析成「解析樹」;分析查詢,看是否符合語法和語義要求。解析器建立輸入查詢的內部表示。而後將輸出傳遞給重寫引擎。

而後,優化器的任務是找到給定查詢的最佳執行或查詢的計劃。執行計劃準確地定義了每一個操做使用什麼算法,以及如何協調操做的執行。

爲了找到最佳的執行計劃,優化器列舉全部可能的執行計劃,肯定每一個計劃的性質或成本,獲取有關當前數據庫狀態的信息,而後選擇其中最佳的一個做爲最終的執行計劃。因爲查詢優化器可能並不完善,所以數據庫用戶和管理員有時須要手動檢查並調整優化器生成的計劃以得到更好的性能。

如今你可能想知道什麼是一個「好的查詢計劃」。

如你所見,一個計劃的質量在查詢中起着重要的做用。更具體地說,評估計劃所需的磁盤 I/O,CPU成本和數據庫客戶端能夠觀察到的整體響應時間以及總執行時間等因素相當重要。這就涉及到了時間複雜度的概念,在後面你將會看到更多與此相關的內容。

接下來,執行所選擇的查詢計劃,由系統的執行引擎進行評估並返回查詢結果。

在上節中描述的可能不是很清楚的是,Garbage In, Garbage Out(GIGO)原則在查詢處理和執行中會天然地顯現:制定查詢的人掌握着你 SQL 查詢性能的關鍵,若是優化器獲得的是一個很差的查詢語句,那麼那麼它也只能作到這麼多...

這意味着在編寫查詢時能夠執行一些操做。如你在介紹中所見,責任是雙重的:它不只僅是寫出符合必定標準的查詢,並且還涉及收集查詢中性能問題可能潛伏在哪裏的意識。

一個理想的出發點是在你的查詢中考慮可能會潛入問題的「地方」。新手一般會在如下四個子句和關鍵字中遇到性能問題。

  • WHERE 子句
  • 任何 INNER JOINLEFT JOIN 關鍵字; 還有,
  • HAVING 子句;

固然,這種方法簡單而原始,但做爲初學者,這些子句和聲明是很好的指引,並且確切地說,當你剛開始時,這些地方就是容易出錯的地方,更諷刺的是這些錯誤很難被發現。

然而,你也應該意識到,性能只有在實際場景中才有意義:只是單純的說這些子句和關鍵字是很差的沒有任何意義。固然,查詢中有 WHEREHAVING 子句不必定意味着這是一個壞的查詢...

查看如下內容,瞭解更多有關的構建查詢的反模式和可替代的方法。這些提示和技巧可做爲指導。如何重寫以及是否真的須要重寫取決於數據量,數據庫,以及查詢所需的次數等等。它徹底取決於你查詢的目標,而且有一些你要查詢的數據庫的以前的瞭解也是相當重要的!

1. 僅檢索你須要的數據

當編寫 SQL 查詢時,「數據越多越好」的思惟方式是不該該的:獲取比你實際需求更多的數據不只會有看錯的風險,並且性能可能會由於查詢太多數據而受到影響。

這就是當心處理 SELECT 語句,DISTINCT 子句和 LIKE 運算符是個不錯的主意。

當你寫好你的查詢時,你能檢查的第一件事情就是 SELECT 語句是否已是最緊湊了。你的目標應該是從 SELECT 中刪除沒必要要的列。這樣,你強制本身只提取符合查詢目的的數據。

若是具備 EXISTS 的相關子查詢,則應嘗試在該子查詢的 SELECT 語句中使用常量,而不是選擇實際列的值。當你只檢查數據是否存在時,這是特別方便的。

記住相關子查詢是使用外部查詢中的值的子查詢。注意,儘管 NULL 能夠在此上下文中看成「常量」使用,可是這會使人很是困惑!

考慮下面這個例子,並理解使用常量的意義在哪:

SELECT driverslicensenr, name
FROM Drivers
WHERE EXISTS (SELECT '1' FROM Fines
              WHERE fines.driverslicensenr = drivers.driverslicensenr);複製代碼

提示:能夠很方便知道,使用相關子查詢一般不是一個好主意。你應該考慮使用 INNER JOIN 重寫來避免它們:

SELECT driverslicensenr, name
FROM drivers
INNER JOIN fines ON fines.driverslicensenr = drivers.driverslicensenr;複製代碼

SELECT DISTINCT 語句是用來返回不一樣的值的。若是能夠,你應該你要儘可能避免使用 DISTINCT 這個子句;就像你在其餘例子中看到的同樣,若是你把這個子句添加到你的查詢中,執行時間確定會增長。所以,常常考慮是否真的須要 DISTINCT 操做來獲取想要的結果是一個好主意。。

當你在一個查詢中使用 LIKE 操做符時,若是匹配模式以 % 或者 _ 開始,那麼是不會使用索引的。它將阻止數據庫使用索引(若是存在)。固然,在另外一個方面看,這種類型的查詢會潛在地返回過多的記錄,這不必定知足你的查詢目標。

再次,你對存儲在數據庫中的數據的瞭解程度能夠幫助你制訂一個模式,這能夠幫助你從全部數據中正確過濾出和你的查詢真正相關的行。

2. 不要輸出太多結果

當你不能過濾掉 SELECT 語句中的列時,你能夠考慮用其餘方法限制你的結果。如下是 LIMIT 語句和數據類型的轉換方法。

你能夠經過爲查詢添加 LIMIT 或者 TOP 子句來爲查詢結果設置最大行數。這兒是一些例子:

SELECT TOP 3 * FROM Drivers;複製代碼

注意 你能夠進一步指定 PERCENT,好比,你能夠經過 SELECT TOP 50 PERCENT * 這個查詢語句來替換第一行。

SELECT driverslicensenr, name FROM Drivers LIMIT 2;複製代碼

此外,你還能夠添加 ROWNUM 子句,這至關於在查詢中使用 LIMIT

SELECT *
FROM Drivers
WHERE driverslicensenr = 123456 AND ROWNUM <= 3;複製代碼

你應該始終使用最有效的,也就是最小的數據類型。當小的數據類型已經足夠的時候你提供一個巨大的數據類型老是有風險的。

然而,當你將數據類型轉換添加到查詢中時,你確定增長了它的執行時間。

一個替代方案是儘可能避免數據類型轉換。可是還要注意,數據類型轉換不是總能從查詢中被刪除或者省略的,並且當你在查詢語句包含它們的時候必定要注意,你能夠在執行查詢以前測試添加它們的影響。

3. 不要讓查詢比需求更復雜

數據類型轉換將你帶到了下一個關鍵點:你不該該過分設計你的查詢。試着保持簡單高效。做爲一個提示,這可能看起來太簡單或者愚蠢了,特別是在查詢可能變得複雜的狀況下。

然而,你將會在下一部分提到的示例中看到,你能夠很輕鬆的把本應更復雜的查詢變得簡單。

當你在你的查詢裏使用 OR 操做符時,極可能你沒有使用索引。

記住索引是一種數據結構,能夠提升數據庫表中的數據檢索速度,但它是有代價的:它須要額外的寫入和額外的存儲空間來維護索引結構。索引用來快速定位或查找數據而無需在每次訪問數據庫時查詢每一行。索引可使用數據庫表中的一列或多列來建立。

若是你不使用數據庫包含的索引,你的查詢會花費更長的時間來執行。這就是爲何最好在查詢中找到使用 OR 運算符的替換方案;

考慮如下查詢:

SELECT driverslicensenr, name
FROM Drivers
WHERE driverslicensenr = 123456 OR driverslicensenr = 678910 OR driverslicensenr = 345678;複製代碼

你能夠將運算符替換爲:

SELECT driverslicensenr, name
FROM Drivers
WHERE driverslicensenr IN (123456, 678910, 345678);複製代碼
  • 包含 UNION 的兩個 SELECT 語句。

提示:這兒你須要當心,沒有必要就不要使用 UNION 運算符,由於你會屢次查詢同一個表屢次,這是沒必要要的。同時,你必須意識到當你在查詢語句裏使用 UNION 時,執行時間會變長。UNION 操做符的替代是:將全部條件都放在一個 SELECT 結構中,或者使用 OUTER JOIN 替代 UNION 來從新構建查詢。

提示:在這裏也要記住的一點是,儘管 OR 以及下面將要提到的其餘運算符可能不使用索引,索引查找不老是更好的。

就像 OR 運算符同樣,當你的查詢包含 NOT 操做符時,也極可能不使用索引。這將不可避免的減慢你的查詢。若是你不明白這是什麼意思,考慮下如下查詢:

SELECT driverslicensenr, name FROM Drivers WHERE NOT (year > 1980);複製代碼

這個查詢跑起來確定比你預料還要慢,主要是由於它構建的太過於複雜了:在這樣的狀況下,最好尋找一個替代方案。考慮使用比較運算符替換 NOT,好比 ><> 或者 !>;上面的例子可能會被重寫爲這樣:

SELECT driverslicensenr, name FROM Drivers WHERE year <= 1980;複製代碼

看起來已經更加整潔了,不是嗎?

AND 是另外一個不使用索引的操做符,若是以過於複雜和低效的方式使用,它會減慢你的查詢,就像下面的例子:

SELECT driverslicensenr, name
FROM Drivers
WHERE year >= 1960 AND year <= 1980;複製代碼

最好使用 BETWEEN 運算符重寫這個查詢:

SELECT driverslicensenr, name
FROM Drivers
WHERE year BETWEEN 1960 AND 1980;複製代碼

ALLALL 運算符你也應該當心使用,將他們包含進查詢中會致使不使用索引。替代方法使用聚合功能,在這裏比較方便的方法是使用像 MIN 或者 MAX 的聚合函數。

提示:在你使用所提出的方案的狀況下,你應該意識到,全部的聚合函數好比 SUMAVGMINMAX 在多行的時候會致使很長時間的查詢,在這種狀況下,你能夠嘗試減小要處理的行數或預先計算這些值。當你決定使用哪一個查詢時,最重要的是清楚你的環境和查詢目標。

在使用列進行計算或者列做爲標量函數的參數時,也是不會使用索引的。一個特定的解決方案是簡單的隔離這個特殊列,使其再也不是計算或者函數的一部分或參數。請考慮一下示例:

SELECT driverslicensenr, name
FROM Drivers
WHERE year + 10 = 1980;複製代碼

這看起來頗有趣,是不?相反,試着從新考慮如何計算,而後像這樣重寫查詢:

SELECT driverslicensenr, name
FROM Drivers
WHERE year = 1970;複製代碼

4. 不要暴力查詢

最後一個提示,你不該該老是太限制查詢,由於這也會影響性能。特別是 join 語句和 HAVING 子句。

當你對兩個表使用 join 時,考慮你 join 的兩張表的順序是很重要的。若是一張表比另外一張大不少,你最好重寫你的查詢讓最大的表最後作 join 操做。

  • 減小 Joins 的條件

當你加了太多的條件到你的 joins 語句,你有義務選擇一個特定的路徑,雖然這個路徑並不老是最高效的那個。

HAVING 子句添加進 SQL 是由於 WHERE 關鍵字不能和聚合方法一塊兒使用。HAVING 的典型的用法就是和 GROUP BY 子句來約束分組聚合後的結果,使其知足一些精確匹配條件。然而,你知道的,使用這個子句是不會用到索引的,會致使查詢不能很好的執行。

若是你在尋找替代的方案,考慮使用 WHERE 子句,請看以下的查詢:

SELECT state, COUNT(*) FROM Drivers WHERE state IN ('GA', 'TX') GROUP BY state ORDER BY state

SELECT state, COUNT(*) FROM Drivers GROUP BY state HAVING state IN ('GA', 'TX') ORDER BY state複製代碼

第一個查詢使用 WHERE 子句限制須要求和的行數,而第二個查詢對錶中的全部行進行了求和,而後使用 HAVING 子句來捨棄其中的部分。在這種狀況下,選擇使用 WHERE 子句顯然是更好的,由於你不會浪費任查詢資源。

你會發現,這並非限制最終結果集,而是限制查詢中的中間記錄的數量。

注意 這兩個子句之間的區別在於,WHERE 子句引入了單行的條件,而 HAVING 子句引入了一個選擇集合或結果的條件,好比 MINMAXSUM,… 這些都已經從多行生成了的。

你看,當你想以儘量的提升性能爲前提的時候,評估語句質量,構建查詢還有改寫查詢並非一件容易的工做;當你構建運行在專業環境中的查詢的時候,避免反模式和考慮替代方案也將成爲你責任的一部分。

這個清單只是一些小的反模式的概述和技巧,可能對新手有些幫助;若是你想了解更多高級開發人員常見的反模式,查看 stackoverflow 的這個討論

基於集合與程序方法的查詢

上述反模式隱含的點實際上歸結爲基於集合與程序方法構建查詢的差別。

程序方法的查詢是一種很像編程的一種查詢方式:你告訴系統作什麼,怎麼作。

一個例子是你使用冗餘的鏈接操做或者濫用 HAVING 子句的狀況下,就像上面的例子,你能夠經過執行一個函數調用另外一個函數來查詢數據庫,或者使用包含循環,用戶定義方法,遊標等,來獲取最終結果。在這個方法中,你會常常發現你本身請求一個數據的子集,而後再請求這個數據的子集等等。

絕不奇怪,這個方法常常被稱爲「逐步」或者「逐行」查詢。

另外一種方法是基於集合的方法,你只須要指定作什麼。你的職責包含從查詢中指定要得到的結果集的條件或要求。至於你的數據是如何獲取到的,這取決於內部決定查詢實現的機制:讓數據庫引擎來肯定查詢最好的算法和執行邏輯。

因爲 SQL 是基於集合的,這種方法(基於集合)比程序方法更有效幾乎不會讓人感到驚訝,這也是一個驚喜,也解釋了爲何在某些狀況下,SQL 能夠比代碼更快的工做。

提示 在查詢中基於集合的方法也是數據科學行業最頂級的僱主所要求你掌握的方法!你常常須要在這兩種方法之間切換。

注意 若是你發現你本身有程序類型的查詢,你應該考慮重寫或者重構它。

從查詢到執行計劃

-------------知道反模式不是靜態的,而是隨着你作爲 SQL 開發者的成長而演進,當你考慮替代方案的時候也意味着你正在避免反模式查詢和重寫查詢的這個事實,這是一個十分困難的任務。任何幫助均可以派上用場,這就是爲何使用一些工具經過更結構化的方式來優化你的查詢或許是個不錯的選擇。

注意 還有一些上一節提到的反模式源於性能的問題的考慮,好比 ANDORNOT 操做符缺乏索引的使用。對性能的思考不只須要結構化的方法,還須要更多的深刻的方法。

然而可能的是,這種結構化和深刻的方法更可能是基於查詢計劃的,即首先被解析爲「解析樹」,而後在肯定每一個操做具體使用什麼算法,還有如何使執行操做更協調。

正如你在介紹中讀到的,你可能須要手動檢查優化器的生成計劃。在這種狀況下,你將須要經過查看查詢計劃來再次分析你的查詢。

要掌握這種查詢計劃,你將須要使用數據庫管理系統爲你提供工具,你可使用的工具以下:

  • 生成查詢計劃的圖形表示的一些工具包,看如下這個例子:

  • 其餘工具將可以爲你提供查詢計劃的文本描述。一個例子是 Oracle 中的 EXPLAIN PLAN 語句,但指令的名稱根據你使用的 RDBMS 而有所不一樣。在其餘數據庫,你可能會看到 EXPLAN(MySQL,PostgreSQL)或者 EXPLAIN QUERY PLAN(SQLite)。

注意若是你平時使用 PostgreSQL,你能夠在 EXPLAIN 之間作出區分,這裏你只獲得了一個描述,它是說明還未執行的查詢計劃會如何執行,而 EXPLAIN ANALYZE 實際上執行了查詢而後返回對預期與實際的查詢計劃的分析。通常來講,一個實際的執行計劃就是一個實際的查詢計劃,雖然在邏輯上是等價的,一個實際的執行計劃更爲有用,由於它包含執行查詢時實際發生的其餘細節和統計信息。

在本節的剩餘部分,你將會學習到更多關於 EXPLAINANALYZE 的信息,以及如何使用這兩個去了解更多你的查詢計劃和查詢性能的信息。

提示:若是你想了解更多關於 EXPLAIN 或更詳細的查看實例,考慮閱讀 Guillaume Lelarge 寫的這本書 「Understanding Explain」

時間複雜度和大 O

如今你已經簡要的檢查了查詢計劃,你能夠在複雜度計算的幫助下開始更深刻的研究具體的性能問題。理論計算機科學這一領域着重於根據難度對問題進行分類;這些計算問題能夠是算法,也能夠是查詢。

然而,對於查詢,你並不必定是根據他們的困難程度分類,而是根據運行它而後拿到返回結果的時間來分類。這個被叫作時間複雜度,你可使用大 O 符號來表達和衡量這種複雜性。

使用大 O 符號,輸入任意大時,你能夠根據輸入與運行時間的相對增加速度來衡量運行時間。大 O 表示法排除係數和低階的項,以便於你關注查詢運行時間的關鍵部分:增加率。當以這種方式表示時,丟棄係數與低階的項,時間複雜度被認爲是漸進式描述的。這意味着輸入會變爲無窮大。

在數據庫語言中,複雜度衡量了數據庫表數據增長以後,查詢該表數據所花時間相對增長了多少的過程。

注意你的數據庫大小不只僅由於表裏存儲的數據增多而變大,索引在其中對大小影響也起了很大的做用。

正如前面所述,執行計劃除了前面所說的之外,還定義了每一步操做使用什麼算法,這使得每次查詢執行的時間能夠在邏輯上表示爲查詢計劃中涉及表大小的函數。換句話說,你可使用大 O 符號和執行計劃預估查詢的複雜性和性能。

在接下來的小節中,你會了解關於四種時間複雜度類型的通常概念,你將會看到一些示例,說明查詢的時間複雜度如何根據你運行它們上下文的不一樣而有所不一樣的。

提示:索引是故事的一部分!

注意,由於不一樣的數據庫有不一樣類型的索引、不一樣的執行計劃、不一樣的實現,因此下面列出的幾個時間複雜度是很通用的,會根據你配置的不一樣而變化。

更多閱讀在這兒

總而言之,你能夠查看如下備忘單,以根據時間複雜度以及其執行狀況估計查詢的性能:

SQL 調優

考慮到查詢計劃和時間複雜性,你能夠考慮進一步調整 SQL 查詢,特別注意如下幾點:

  • 大表的全表掃描替換爲索引的掃描;
  • 確保你正在使用最佳的錶鏈接順序;
  • 確保的使用索引優化;還有
  • 緩存小表的全表掃描。

祝賀!你已經看到了這篇博文的結尾,這只是幫助你對 SQL 查詢性能的一瞥。你但願對反模式,查詢優化器,審查工具,預估和解釋查詢計劃的複雜性有更多的看法,然而,還有更多的東西等你去發現!若是你想知道更多,能夠考慮讀這本由R. Ramakrishnan 和 J. Gehrke 寫的「Database Management Systems」。

最後,我不想錯過這個來自 StackOverFlow 用戶那裏的引用

「我最喜歡的反模式不是測試你的查詢。

這適用於:

  • 你的查詢涉及了不止一張表。

  • 你認爲你的查詢有一個優化的設計,但不肯意去驗證你的假設。

  • 你會接受第一個成功的查詢,它是不是最優的,你並不清楚。」

如過你想開始使用 SQL,能夠考慮學習 DataCamp 的 Intro to SQL for Data Science 課程!


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索