十步完全理解 SQL

水果泡騰片發表於2013-07-08

很多程式設計師視 SQL 為洪水猛獸。SQL 是一種為數不多的宣告性語言,它的執行方式完全不同於我們所熟知的命令列語言、物件導向的程式語言、甚至是函式語言(儘管有些人認為 SQL 語言也是一種函式式語言)。

我們每天都在寫 SQL 並且應用在開源軟體 jOOQ 中。於是我想把 SQL 之美介紹給那些仍然對它頭疼不已的朋友,所以本文是為了以下讀者而特地編寫的:

1、 在工作中會用到 SQL 但是對它並不完全瞭解的人。

2、 能夠熟練使用 SQL 但是並不瞭解其語法邏輯的人。

3、 想要教別人 SQL 的人。

sql logo

本文著重介紹 SELECT 句式,其他的 DML (Data Manipulation Language 資料操縱語言命令)將會在別的文章中進行介紹。

10個簡單步驟,完全理解SQL

1、 SQL 是一種宣告式語言

首先要把這個概念記在腦中:“宣告”。 SQL 語言是為計算機宣告瞭一個你想從原始資料中獲得什麼樣的結果的一個範例,而不是告訴計算機如何能夠得到結果。這是不是很棒?

(譯者注:簡單地說,SQL 語言宣告的是結果集的屬性,計算機會根據 SQL 所宣告的內容來從資料庫中挑選出符合宣告的資料,而不是像傳統程式設計思維去指示計算機如何操作。)

上面的例子很容易理解,我們不關心這些僱員記錄從哪裡來,我們所需要的只是那些高薪者的資料(譯者注: salary>100000 )。

我們從哪兒學習到這些?

如果 SQL 語言這麼簡單,那麼是什麼讓人們“聞 SQL 色變”?主要的原因是:我們潛意識中的是按照指令式程式設計的思維方式思考問題的。就好像這樣:“電腦,先執行這一步,再執行那一步,但是在那之前先檢查一下是否滿足條件 A 和條件 B ”。例如,用變數傳參、使用迴圈語句、迭代、呼叫函式等等,都是這種指令式程式設計的思維慣式。

2、 SQL 的語法並不按照語法順序執行

SQL 語句有一個讓大部分人都感到困惑的特性,就是:SQL 語句的執行順序跟其語句的語法順序並不一致。SQL 語句的語法順序是:

  • SELECT[DISTINCT]
  • FROM
  • WHERE
  • GROUP BY
  • HAVING
  • UNION
  • ORDER BY

為了方便理解,上面並沒有把所有的 SQL 語法結構都列出來,但是已經足以說明 SQL 語句的語法順序和其執行順序完全不一樣,就以上述語句為例,其執行順序為:

  • FROM
  • WHERE
  • GROUP BY
  • HAVING
  • SELECT
  • DISTINCT
  • UNION
  • ORDER BY

關於 SQL 語句的執行順序,有三個值得我們注意的地方:

1、 FROM 才是 SQL 語句執行的第一步,並非 SELECT 。資料庫在執行 SQL 語句的第一步是將資料從硬碟載入到資料緩衝區中,以便對這些資料進行操作。(譯者注:原文為“The first thing that happens is loading data from the disk into memory, in order to operate on such data.”,但是並非如此,以 Oracle 等常用資料庫為例,資料是從硬碟中抽取到資料緩衝區中進行操作。)

2、 SELECT 是在大部分語句執行了之後才執行的,嚴格的說是在 FROM 和 GROUP BY 之後執行的。理解這一點是非常重要的,這就是你不能在 WHERE 中使用在 SELECT 中設定別名的欄位作為判斷條件的原因。

如果你想重用別名z,你有兩個選擇。要麼就重新寫一遍 z 所代表的表示式:

…或者求助於衍生表、通用資料表示式或者檢視,以避免別名重用。請看下文中的例子。

3、 無論在語法上還是在執行順序上, UNION 總是排在在 ORDER BY 之前。很多人認為每個 UNION 段都能使用 ORDER BY 排序,但是根據 SQL 語言標準和各個資料庫 SQL 的執行差異來看,這並不是真的。儘管某些資料庫允許 SQL 語句對子查詢(subqueries)或者派生表(derived tables)進行排序,但是這並不說明這個排序在 UNION 操作過後仍保持排序後的順序。

注意:並非所有的資料庫對 SQL 語句使用相同的解析方式。如 MySQL、PostgreSQL和 SQLite 中就不會按照上面第二點中所說的方式執行。

我們學到了什麼?

既然並不是所有的資料庫都按照上述方式執行 SQL 預計,那我們的收穫是什麼?我們的收穫是永遠要記得: SQL 語句的語法順序和其執行順序並不一致,這樣我們就能避免一般性的錯誤。如果你能記住 SQL 語句語法順序和執行順序的差異,你就能很容易的理解一些很常見的 SQL 問題。

當然,如果一種語言被設計成語法順序直接反應其語句的執行順序,那麼這種語言對程式設計師是十分友好的,這種程式語言層面的設計理念已經被微軟應用到了 LINQ 語言中。

3、 SQL 語言的核心是對錶的引用(table references)

由於 SQL 語句語法順序和執行順序的不同,很多同學會認為SELECT 中的欄位資訊是 SQL 語句的核心。其實真正的核心在於對錶的引用。

根據 SQL 標準,FROM 語句被定義為:

FROM 語句的“輸出”是一張聯合表,來自於所有引用的表在某一維度上的聯合。我們們慢慢來分析:

上面這句 FROM 語句的輸出是一張聯合表,聯合了表 a 和表 b 。如果 a 表有三個欄位, b 表有 5 個欄位,那麼這個“輸出表”就有 8 ( =5+3)個欄位。

這個聯合表裡的資料是 a*b,即 a 和 b 的笛卡爾積。換句話說,也就是 a 表中的每一條資料都要跟 b 表中的每一條資料配對。如果 a 表有3 條資料, b 表有 5 條資料,那麼聯合表就會有 15 ( =5*3)條資料。

FROM 輸出的結果被 WHERE 語句篩選後要經過 GROUP BY 語句處理,從而形成新的輸出結果。我們後面還會再討論這方面問題。

如果我們從集合論(關係代數)的角度來看,一張資料庫的表就是一組資料元的關係,而每個 SQL 語句會改變一種或數種關係,從而產生出新的資料元的關係(即產生新的表)。

我們學到了什麼?

思考問題的時候從表的角度來思考問題提,這樣很容易理解資料如何在 SQL 語句的“流水線”上進行了什麼樣的變動。

4、 靈活引用表能使 SQL 語句變得更強大

靈活引用表能使 SQL 語句變得更強大。一個簡單的例子就是 JOIN 的使用。嚴格的說 JOIN 語句並非是 SELECT 中的一部分,而是一種特殊的表引用語句。 SQL 語言標準中表的連線定義如下:

就拿之前的例子來說:

a 可能輸如下表的連線:

將它放到之前的例子中就變成了:

儘管將一個連線表用逗號跟另一張表聯合在一起並不是常用作法,但是你的確可以這麼做。結果就是,最終輸出的表就有了 a1+a2+b 個欄位了。

(譯者注:原文這裡用詞為 degree ,譯為維度。如果把一張表檢視化,我們可以想象每一張表都是由橫縱兩個維度組成的,橫向維度即我們所說的欄位或者列,英文為columns;縱向維度即代表了每條資料,英文為 record ,根據上下文,作者這裡所指的應該是欄位數。)

在 SQL 語句中派生表的應用甚至比表連線更加強大,下面我們就要講到表連線。

我們學到了什麼?

思考問題時,要從表引用的角度出發,這樣就很容易理解資料是怎樣被 SQL 語句處理的,並且能夠幫助你理解那些複雜的表引用是做什麼的。

更重要的是,要理解 JOIN 是構建連線表的關鍵詞,並不是 SELECT 語句的一部分。有一些資料庫允許在 INSERT 、 UPDATE 、 DELETE 中使用 JOIN 。

5、 SQL 語句中推薦使用表連線

我們先看看剛剛這句話:

高階 SQL 程式設計師也許學會給你忠告:儘量不要使用逗號來代替 JOIN 進行表的連線,這樣會提高你的 SQL 語句的可讀性,並且可以避免一些錯誤。

利用逗號來簡化 SQL 語句有時候會造成思維上的混亂,想一下下面的語句:

我們不難看出使用 JOIN 語句的好處在於:

  • 安全。 JOIN 和要連線的表離得非常近,這樣就能避免錯誤。
  • 更多連線的方式,JOIN 語句能去區分出來外連線和內連線等。

我們學到了什麼?

記著要儘量使用 JOIN 進行表的連線,永遠不要在 FROM 後面使用逗號連線表。

6、 SQL 語句中不同的連線操作

SQL 語句中,表連線的方式從根本上分為五種:

  • EQUI JOIN
  • SEMI JOIN
  • ANTI JOIN
  • CROSS JOIN
  • DIVISION

EQUI JOIN

這是一種最普通的 JOIN 操作,它包含兩種連線方式:

  • INNER JOIN(或者是 JOIN )
  • OUTER JOIN(包括: LEFT 、 RIGHT、 FULL OUTER JOIN)

用例子最容易說明其中區別:

SEMI JOIN

這種連線關係在 SQL 中有兩種表現方式:使用 IN,或者使用 EXISTS。“ SEMI ”在拉丁文中是“半”的意思。這種連線方式是隻連線目標表的一部分。這是什麼意思呢?再想一下上面關於作者和書名的連線。我們想象一下這樣的情況:我們不需要作者 / 書名這樣的組合,只是需要那些在書名錶中的書的作者資訊。那我們就能這麼寫:

儘管沒有嚴格的規定說明你何時應該使用 IN ,何時應該使用 EXISTS ,但是這些事情你還是應該知道的:

  • IN比 EXISTS 的可讀性更好
  • EXISTS 比IN 的表達性更好(更適合複雜的語句)
  • 二者之間效能沒有差異(但對於某些資料庫來說效能差異會非常大)

因為使用 INNER JOIN 也能得到書名錶中書所對應的作者資訊,所以很多初學者機會認為可以通過 DISTINCT 進行去重,然後將 SEMI JOIN 語句寫成這樣:

這是一種很糟糕的寫法,原因如下:

  • SQL 語句效能低下:因為去重操作( DISTINCT )需要資料庫重複從硬碟中讀取資料到記憶體中。(譯者注: DISTINCT 的確是一種很耗費資源的操作,但是每種資料庫對於 DISTINCT 的操作方式可能不同)。
  • 這麼寫並非完全正確:儘管也許現在這麼寫不會出現問題,但是隨著 SQL 語句變得越來越複雜,你想要去重得到正確的結果就變得十分困難。

更多的關於濫用 DISTINCT 的危害可以參考這篇博文

http://blog.jooq.org/2013/07/30/10-common-mistakes-java-developers-make-when-writing-sql/)。

ANTI JOIN

這種連線的關係跟 SEMI JOIN 剛好相反。在 IN 或者 EXISTS 前加一個 NOT 關鍵字就能使用這種連線。舉個例子來說,我們列出書名錶裡沒有書的作者:

關於效能、可讀性、表達性等特性也完全可以參考 SEMI JOIN。

這篇博文介紹了在使用 NOT IN 時遇到 NULL 應該怎麼辦,因為有一點背離本篇主題,就不詳細介紹,有興趣的同學可以讀一下

http://blog.jooq.org/2012/01/27/sql-incompatibilities-not-in-and-null-values/)。

CROSS JOIN

這個連線過程就是兩個連線的表的乘積:即將第一張表的每一條資料分別對應第二張表的每條資料。我們之前見過,這就是逗號在 FROM 語句中的用法。在實際的應用中,很少有地方能用到 CROSS JOIN,但是一旦用上了,你就可以用這樣的 SQL語句表達:

DIVISION

DIVISION 的確是一個怪胎。簡而言之,如果 JOIN 是一個乘法運算,那麼 DIVISION 就是 JOIN 的逆過程。DIVISION 的關係很難用 SQL 表達出來,介於這是一個新手指南,解釋 DIVISION 已經超出了我們的目的。但是有興趣的同學還是可以來看看這三篇文章

http://blog.jooq.org/2012/03/30/advanced-sql-relational-division-in-jooq/

http://en.wikipedia.org/wiki/Relational_algebra#Division

https://www.simple-talk.com/sql/t-sql-programming/divided-we-stand-the-sql-of-relational-division/)。

推薦閱讀 →_→ 畫圖解釋SQL聯合語句

我們學到了什麼?

學到了很多!讓我們在腦海中再回想一下。 SQL 是對錶的引用, JOIN 則是一種引用表的複雜方式。但是 SQL 語言的表達方式和實際我們所需要的邏輯關係之間是有區別的,並非所有的邏輯關係都能找到對應的 JOIN 操作,所以這就要我們在平時多積累和學習關係邏輯,這樣你就能在以後編寫 SQL 語句中選擇適當的 JOIN 操作了。

7、 SQL 中如同變數的派生表

在這之前,我們學習到過 SQL 是一種宣告性的語言,並且 SQL 語句中不能包含變數。但是你能寫出類似於變數的語句,這些就叫做派生表:

說白了,所謂的派生表就是在括號之中的子查詢:

需要注意的是有些時候我們可以給派生表定義一個相關名(即我們所說的別名)。

派生表可以有效的避免由於 SQL 邏輯而產生的問題。舉例來說:如果你想重用一個用 SELECT 和 WHERE 語句查詢出的結果,這樣寫就可以(以 Oracle 為例):

需要我們注意的是:在有些資料庫,以及 SQL : 1990 標準中,派生表被歸為下一級——通用表語句( common table experssion)。這就允許你在一個 SELECT 語句中對派生表多次重用。上面的例子就(幾乎)等價於下面的語句:

當然了,你也可以給“ a ”建立一個單獨的檢視,這樣你就可以在更廣泛的範圍內重用這個派生表了。更多資訊可以閱讀下面的文章(http://en.wikipedia.org/wiki/View_%28SQL%29)。

我們學到了什麼?

我們反覆強調,大體上來說 SQL 語句就是對錶的引用,而並非對欄位的引用。要好好利用這一點,不要害怕使用派生表或者其他更復雜的語句。

8、 SQL 語句中 GROUP BY 是對錶的引用進行的操作

讓我們再回想一下之前的 FROM 語句:

現在,我們將 GROUP BY 應用到上面的語句中:

上面語句的結果就是產生出了一個包含三個欄位的新的表的引用。我們來仔細理解一下這句話:當你應用 GROUP BY 的時候, SELECT 後沒有使用聚合函式的列,都要出現在 GROUP BY 後面。(譯者注:原文大意為“當你是用 GROUP BY 的時候,你能夠對其進行下一級邏輯操作的列會減少,包括在 SELECT 中的列”)。

  • 需要注意的是:其他欄位能夠使用聚合函式:

  • 還有一點值得留意的是: MySQL 並不堅持這個標準,這的確是令人很困惑的地方。(譯者注:這並不是說 MySQL 沒有 GROUP BY 的功能)但是不要被 MySQL 所迷惑。 GROUP BY 改變了對錶引用的方式。你可以像這樣既在 SELECT 中引用某一欄位,也在 GROUP BY 中對其進行分組。

我們學到了什麼?

GROUP BY,再次強調一次,是在表的引用上進行了操作,將其轉換為一種新的引用方式。

9、 SQL 語句中的 SELECT 實質上是對關係的對映

我個人比較喜歡“對映”這個詞,尤其是把它用在關係代數上。(譯者注:原文用詞為 projection ,該詞有兩層含義,第一種含義是預測、規劃、設計,第二種意思是投射、對映,經過反覆推敲,我覺得這裡用對映能夠更直觀的表達出 SELECT 的作用)。一旦你建立起來了表的引用,經過修改、變形,你能夠一步一步的將其對映到另一個模型中。 SELECT 語句就像一個“投影儀”,我們可以將其理解成一個將源表中的資料按照一定的邏輯轉換成目標表資料的函式。

通過 SELECT語句,你能對每一個欄位進行操作,通過複雜的表示式生成所需要的資料。

SELECT 語句有很多特殊的規則,至少你應該熟悉以下幾條:

  1. 你僅能夠使用那些能通過表引用而得來的欄位;
  2. 如果你有 GROUP BY 語句,你只能夠使用 GROUP BY 語句後面的欄位或者聚合函式;
  3. 當你的語句中沒有 GROUP BY 的時候,可以使用開窗函式代替聚合函式;
  4. 當你的語句中沒有 GROUP BY 的時候,你不能同時使用聚合函式和其它函式;
  5. 有一些方法可以將普通函式封裝在聚合函式中;
  6. ……

一些更復雜的規則多到足夠寫出另一篇文章了。比如:為何你不能在一個沒有 GROUP BY 的 SELECT 語句中同時使用普通函式和聚合函式?(上面的第 4 條)

原因如下:

  1. 憑直覺,這種做法從邏輯上就講不通。
  2. 如果直覺不能夠說服你,那麼語法肯定能。 SQL : 1999 標準引入了 GROUPING SETS,SQL: 2003 標準引入了 group sets : GROUP BY() 。無論什麼時候,只要你的語句中出現了聚合函式,而且並沒有明確的 GROUP BY 語句,這時一個不明確的、空的 GROUPING SET 就會被應用到這段 SQL 中。因此,原始的邏輯順序的規則就被打破了,對映(即 SELECT )關係首先會影響到邏輯關係,其次就是語法關係。(譯者注:這段話原文就比較艱澀,可以簡單理解如下:在既有聚合函式又有普通函式的 SQL 語句中,如果沒有 GROUP BY 進行分組,SQL 語句預設視整張表為一個分組,當聚合函式對某一欄位進行聚合統計的時候,引用的表中的每一條 record 就失去了意義,全部的資料都聚合為一個統計值,你此時對每一條 record 使用其它函式是沒有意義的)。

糊塗了?是的,我也是。我們再回過頭來看點淺顯的東西吧。

我們學到了什麼?

SELECT 語句可能是 SQL 語句中最難的部分了,儘管他看上去很簡單。其他語句的作用其實就是對錶的不同形式的引用。而 SELECT 語句則把這些引用整合在了一起,通過邏輯規則將源表對映到目標表,而且這個過程是可逆的,我們可以清楚的知道目標表的資料是怎麼來的。

想要學習好 SQL 語言,就要在使用 SELECT 語句之前弄懂其他的語句,雖然 SELECT 是語法結構中的第一個關鍵詞,但它應該是我們最後一個掌握的。

10、 SQL 語句中的幾個簡單的關鍵詞: DISTINCT , UNION , ORDER BY 和 OFFSET

在學習完複雜的 SELECT 豫劇之後,我們再來看點簡單的東西:

  • 集合運算( DISTINCT 和 UNION )
  • 排序運算( ORDER BY,OFFSET…FETCH)

集合運算( set operation):

集合運算主要操作在於集合上,事實上指的就是對錶的一種操作。從概念上來說,他們很好理解:

  • DISTINCT 在對映之後對資料進行去重
  • UNION 將兩個子查詢拼接起來並去重
  • UNION ALL 將兩個子查詢拼接起來但不去重
  • EXCEPT 將第二個字查詢中的結果從第一個子查詢中去掉
  • INTERSECT 保留兩個子查詢中都有的結果並去重

排序運算( ordering operation):

排序運算跟邏輯關係無關。這是一個 SQL 特有的功能。排序運算不僅在 SQL 語句的最後,而且在 SQL 語句執行的過程中也是最後執行的。使用 ORDER BY 和 OFFSET…FETCH 是保證資料能夠按照順序排列的最有效的方式。其他所有的排序方式都有一定隨機性,儘管它們得到的排序結果是可重現的。

OFFSET…SET是一個沒有統一確定語法的語句,不同的資料庫有不同的表達方式,如 MySQL 和 PostgreSQL 的 LIMIT…OFFSET、SQL Server 和 Sybase 的 TOP…START AT 等。具體關於 OFFSET..FETCH 的不同語法可以參考這篇文章

http://www.jooq.org/doc/3.1/manual/sql-building/sql-statements/select-statement/limit-clause/)。

讓我們在工作中盡情的使用 SQL!

正如其他語言一樣,想要學好 SQL 語言就要大量的練習。上面的 10 個簡單的步驟能夠幫助你對你每天所寫的 SQL 語句有更好的理解。另一方面來講,從平時常見的錯誤中也能積累到很多經驗。下面的兩篇文章就是介紹一些 JAVA 和其他開發者所犯的一些常見的 SQL 錯誤:

相關文章