不少程序員視 SQL 爲洪水猛獸。SQL 是一種爲數很少的聲明性語言,它的運行方式徹底不一樣於咱們所熟知的命令行語言、面向對象的程序語言、甚至是函數語言(儘管有些人認爲 SQL 語言也是一種函數式語言)。 java
一、 SQL 是一種聲明式語言 程序員
首先要把這個概念記在腦中:「聲明」。 SQL 語言是爲計算機聲明瞭一個你想從原始數據中得到什麼樣的結果的一個範例,而不是告訴計算機如何可以獲得結果。這是否是很棒? sql
(譯者注:簡單地說,SQL 語言聲明的是結果集的屬性,計算機會根據 SQL 所聲明的內容來從數據庫中挑選出符合聲明的數據,而不是像傳統編程思惟去指示計算機如何操做。)
SELECT first_name, last_name FROM employees WHERE salary > 100000
上面的例子很容易理解,咱們不關心這些僱員記錄從哪裏來,咱們所須要的只是那些高薪者的數據(譯者注: salary>100000 )。 數據庫
咱們從哪兒學習到這些? 編程
若是 SQL 語言這麼簡單,那麼是什麼讓人們「聞 SQL 色變」?主要的緣由是:咱們潛意識中的是按照命令式編程的思惟方式思考問題的。就好像這樣:「電腦,先執行這一步,再執行那一步,可是在那以前先檢查一下是否知足條件 A 和條件 B 」。例如,用變量傳參、使用循環語句、迭代、調用函數等等,都是這種命令式編程的思惟慣式。 安全
二、 SQL 的語法並不按照語法順序執行 app
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 語句的執行順序,有三個值得咱們注意的地方: 編程語言
一、 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 等經常使用數據庫爲例,數據是從硬盤中抽取到數據緩衝區中進行操做。) ide
二、 SELECT 是在大部分語句執行了以後才執行的,嚴格的說是在 FROM 和 GROUP BY 以後執行的。理解這一點是很是重要的,這就是你不能在 WHERE 中使用在 SELECT 中設定別名的字段做爲判斷條件的緣由。
SELECT A.x + A.y AS z
FROM A
WHERE z = 10 -- z 在此處不可用,由於SELECT是最後執行的語句
若是你想重用別名z,你有兩個選擇。要麼就從新寫一遍 z 所表明的表達式:
SELECT A.x + A.y AS z
FROM A
WHERE (A.x + A.y) = 10 函數
…或者求助於衍生表、通用數據表達式或者視圖,以免別名重用。請看下文中的例子。
三、 不管在語法上仍是在執行順序上, UNION 老是排在在 ORDER BY 以前。不少人認爲每一個 UNION 段都能使用 ORDER BY 排序,可是根據 SQL 語言標準和各個數據庫 SQL 的執行差別來看,這並非真的。儘管某些數據庫容許 SQL 語句對子查詢(subqueries)或者派生表(derived tables)進行排序,可是這並不說明這個排序在 UNION 操做事後仍保持排序後的順序。
注意:並不是全部的數據庫對 SQL 語句使用相同的解析方式。如 MySQL、PostgreSQL和 SQLite 中就不會按照上面第二點中所說的方式執行。
咱們學到了什麼?
既然並非全部的數據庫都按照上述方式執行 SQL 預計,那咱們的收穫是什麼?咱們的收穫是永遠要記得: SQL 語句的語法順序和其執行順序並不一致,這樣咱們就能避免通常性的錯誤。若是你能記住 SQL 語句語法順序和執行順序的差別,你就能很容易的理解一些很常見的 SQL 問題。
固然,若是一種語言被設計成語法順序直接反應其語句的執行順序,那麼這種語言對程序員是十分友好的,這種編程語言層面的設計理念已經被微軟應用到了 LINQ 語言中。
三、 SQL 語言的核心是對錶的引用(table references)
因爲 SQL 語句語法順序和執行順序的不一樣,不少同窗會認爲SELECT 中的字段信息是 SQL 語句的核心。其實真正的核心在於對錶的引用。
根據 SQL 標準,FROM 語句被定義爲:
<from clause> ::= FROM <table reference> [ { <comma> <table reference> }... ]
FROM 語句的「輸出」是一張聯合表,來自於全部引用的表在某一維度上的聯合。咱們們慢慢來分析:
FROM a, b
上面這句 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 語句的「流水線」上進行了什麼樣的變更。
四、 靈活引用表能使 SQL 語句變得更強大
靈活引用表能使 SQL 語句變得更強大。一個簡單的例子就是 JOIN 的使用。嚴格的說 JOIN 語句並不是是 SELECT 中的一部分,而是一種特殊的表引用語句。 SQL 語言標準中表的鏈接定義以下:
<table reference> ::=
<table name>
| <derived table>
| <joined table>
就拿以前的例子來講:
FROM a, b
a 可能輸以下表的鏈接:
a1 JOIN a2 ON a1.id = a2.id
將它放到以前的例子中就變成了:
FROM a1 JOIN a2 ON a1.id = a2.id, b
儘管將一個鏈接表用逗號跟另外一張表聯合在一塊兒並非經常使用做法,可是你的確能夠這麼作。結果就是,最終輸出的表就有了 a1+a2+b 個字段了。
(譯者注:原文這裏用詞爲 degree ,譯爲維度。若是把一張表視圖化,咱們能夠想象每一張表都是由橫縱兩個維度組成的,橫向維度即咱們所說的字段或者列,英文爲columns;縱向維度即表明了每條數據,英文爲 record ,根據上下文,做者這裏所指的應該是字段數。)
在 SQL 語句中派生表的應用甚至比錶鏈接更增強大,下面咱們就要講到錶鏈接。
咱們學到了什麼?
思考問題時,要從表引用的角度出發,這樣就很容易理解數據是怎樣被 SQL 語句處理的,而且可以幫助你理解那些複雜的表引用是作什麼的。
更重要的是,要理解 JOIN 是構建鏈接表的關鍵詞,並非 SELECT 語句的一部分。有一些數據庫容許在 INSERT 、 UPDATE 、 DELETE 中使用 JOIN 。
五、 SQL 語句中推薦使用錶鏈接
咱們先看看剛剛這句話:
FROM a, b
高級 SQL 程序員也許學會給你忠告:儘可能不要使用逗號來代替 JOIN 進行表的鏈接,這樣會提升你的 SQL 語句的可讀性,而且能夠避免一些錯誤。
利用逗號來簡化 SQL 語句有時候會形成思惟上的混亂,想一下下面的語句:
FROM a, b, c, d, e, f, g, h
WHERE a.a1 = b.bx
AND a.a2 = c.c1
AND d.d1 = b.bc
-- etc...
咱們不難看出使用 JOIN 語句的好處在於:
安全。 JOIN 和要鏈接的表離得很是近,這樣就能避免錯誤。
更多鏈接的方式,JOIN 語句能去區分出來外鏈接和內鏈接等。
咱們學到了什麼?
記着要儘可能使用 JOIN 進行表的鏈接,永遠不要在 FROM 後面使用逗號鏈接表。
六、 SQL 語句中不一樣的鏈接操做
SQL 語句中,錶鏈接的方式從根本上分爲五種:
EQUI JOIN
SEMI JOIN
ANTI JOIN
CROSS JOIN
DIVISION
EQUI JOIN
這是一種最普通的 JOIN 操做,它包含兩種鏈接方式:
INNER JOIN(或者是 JOIN )
OUTER JOIN(包括: LEFT 、 RIGHT、 FULL OUTER JOIN)
用例子最容易說明其中區別:
-- This table reference contains authors and their books.
-- There is one record for each book and its author.
-- authors without books are NOT included
author JOIN book ON author.id = book.author_id
-- This table reference contains authors and their books
-- There is one record for each book and its author.
-- ... OR there is an "empty" record for authors without books
-- ("empty" meaning that all book columns are NULL)
author LEFT OUTER JOIN book ON author.id = book.author_id
SEMI JOIN
這種鏈接關係在 SQL 中有兩種表現方式:使用 IN,或者使用 EXISTS。「 SEMI 」在拉丁文中是「半」的意思。這種鏈接方式是隻鏈接目標表的一部分。這是什麼意思呢?再想一下上面關於做者和書名的鏈接。咱們想象一下這樣的狀況:咱們不須要做者 / 書名這樣的組合,只是須要那些在書名錶中的書的做者信息。那咱們就能這麼寫:
-- Using IN
FROM author
WHERE author.id IN (SELECT book.author_id FROM book)
-- Using EXISTS
FROM author
WHERE EXISTS (SELECT 1 FROM book WHERE book.author_id = author.id)
儘管沒有嚴格的規定說明你什麼時候應該使用 IN ,什麼時候應該使用 EXISTS ,可是這些事情你仍是應該知道的:
IN比 EXISTS 的可讀性更好
EXISTS 比IN 的表達性更好(更適合複雜的語句)
兩者之間性能沒有差別(但對於某些數據庫來講性能差別會很是大)
由於使用 INNER JOIN 也能獲得書名錶中書所對應的做者信息,因此不少初學者機會認爲能夠經過 DISTINCT 進行去重,而後將 SEMI JOIN 語句寫成這樣:
-- Find only those authors who also have books
SELECT DISTINCT first_name, last_name
FROM author
JOIN book ON author.id = book.author_id
這是一種很糟糕的寫法,緣由以下:
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 關鍵字就能使用這種鏈接。舉個例子來講,咱們列出書名錶裏沒有書的做者:
-- Using IN
FROM author
WHERE author.id NOT IN (SELECT book.author_id FROM book)
-- Using EXISTS
FROM author
WHERE NOT EXISTS (SELECT 1 FROM book WHERE book.author_id = author.id)
關於性能、可讀性、表達性等特性也徹底能夠參考 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語句表達:
-- Combine every author with every book
author CROSS JOIN book
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 操做了。
七、 SQL 中如同變量的派生表
在這以前,咱們學習到過 SQL 是一種聲明性的語言,而且 SQL 語句中不能包含變量。可是你能寫出相似於變量的語句,這些就叫作派生表:
說白了,所謂的派生表就是在括號之中的子查詢:
-- A derived table
FROM (SELECT * FROM author)
須要注意的是有些時候咱們能夠給派生表定義一個相關名(即咱們所說的別名)。
-- A derived table with an alias
FROM (SELECT * FROM author) a
派生表能夠有效的避免因爲 SQL 邏輯而產生的問題。舉例來講:若是你想重用一個用 SELECT 和 WHERE 語句查詢出的結果,這樣寫就能夠(以 Oracle 爲例):
-- Get authors' first and last names, and their age in days
SELECT first_name, last_name, age
FROM (
SELECT first_name, last_name, current_date - date_of_birth age
FROM author
)
-- If the age is greater than 10000 days
WHERE age > 10000
須要咱們注意的是:在有些數據庫,以及 SQL : 1990 標準中,派生表被歸爲下一級——通用表語句( common table experssion)。這就容許你在一個 SELECT 語句中對派生表屢次重用。上面的例子就(幾乎)等價於下面的語句:
WITH a AS (
SELECT first_name, last_name, current_date - date_of_birth age
FROM author
)
SELECT *
FROM a
WHERE age > 10000
固然了,你也能夠給「 a 」建立一個單獨的視圖,這樣你就能夠在更普遍的範圍內重用這個派生表了。
咱們學到了什麼?
咱們反覆強調,大致上來講 SQL 語句就是對錶的引用,而並不是對字段的引用。要好好利用這一點,不要懼怕使用派生表或者其餘更復雜的語句。
八、 SQL 語句中 GROUP BY 是對錶的引用進行的操做
讓咱們再回想一下以前的 FROM 語句:
FROM a, b
如今,咱們將 GROUP BY 應用到上面的語句中:
GROUP BY A.x, A.y, B.z
上面語句的結果就是產生出了一個包含三個字段的新的表的引用。咱們來仔細理解一下這句話:當你應用 GROUP BY 的時候, SELECT 後沒有使用聚合函數的列,都要出如今 GROUP BY 後面。(譯者注:原文大意爲「當你是用 GROUP BY 的時候,你可以對其進行下一級邏輯操做的列會減小,包括在 SELECT 中的列」)。
須要注意的是:其餘字段可以使用聚合函數:
SELECT A.x, A.y, SUM(A.z)
FROM A
GROUP BY A.x, A.y
還有一點值得留意的是: MySQL 並不堅持這個標準,這的確是使人很困惑的地方。(譯者注:這並非說 MySQL 沒有 GROUP BY 的功能)可是不要被 MySQL 所迷惑。 GROUP BY 改變了對錶引用的方式。你能夠像這樣既在 SELECT 中引用某一字段,也在 GROUP BY 中對其進行分組
咱們學到了什麼?
GROUP BY,再次強調一次,是在表的引用上進行了操做,將其轉換爲一種新的引用方式。
九、 SQL 語句中的 SELECT 實質上是對關係的映射
我我的比較喜歡「映射」這個詞,尤爲是把它用在關係代數上。(譯者注:原文用詞爲 projection ,該詞有兩層含義,第一種含義是預測、規劃、設計,第二種意思是投射、映射,通過反覆推敲,我以爲這裏用映射可以更直觀的表達出 SELECT 的做用)。一旦你創建起來了表的引用,通過修改、變形,你可以一步一步的將其映射到另外一個模型中。 SELECT 語句就像一個「投影儀」,咱們能夠將其理解成一個將源表中的數據按照必定的邏輯轉換成目標表數據的函數。
經過 SELECT語句,你能對每個字段進行操做,經過複雜的表達式生成所須要的數據。
SELECT 語句有不少特殊的規則,至少你應該熟悉如下幾條:
你僅可以使用那些能經過表引用而得來的字段;
若是你有 GROUP BY 語句,你只可以使用 GROUP BY 語句後面的字段或者聚合函數;
當你的語句中沒有 GROUP BY 的時候,可使用開窗函數代替聚合函數;
當你的語句中沒有 GROUP BY 的時候,你不能同時使用聚合函數和其它函數;
有一些方法能夠將普通函數封裝在聚合函數中;
一些更復雜的規則多到足夠寫出另外一篇文章了。好比:爲什麼你不能在一個沒有 GROUP BY 的 SELECT 語句中同時使用普通函數和聚合函數?(上面的第 4 條)
緣由以下:
憑直覺,這種作法從邏輯上就講不通。
若是直覺不可以說服你,那麼語法確定能。 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 是語法結構中的第一個關鍵詞,但它應該是咱們最後一個掌握的。
集合運算( 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 的不一樣語法能夠參考這篇文章