[TOC]
# SQL 是一種聲明式語言
首先要把這個概念記在腦中:“聲明”。 SQL 語言是為計算機聲明了一個你想從原始數據中獲得什么樣的結果的一個范例,而不是告訴計算機如何能夠得到結果。這是不是很棒?
(注:簡單地說,SQL 語言聲明的是結果集的屬性,計算機會根據 SQL 所聲明的內容來從數據庫中挑選出符合聲明的數據,而不是像傳統編程思維去指示計算機如何操作。)
~~~
SELECT first_name, last_name FROM employees WHERE salary > 100000
~~~
上面的例子很容易理解,我們不關心這些雇員記錄從哪里來,我們所需要的只是那些高薪者的數據。
我們從哪兒學習到這些?
如果 SQL 語言這么簡單,那么是什么讓人們“聞 SQL 色變”?主要的原因是:我們潛意識中的是按照命令式編程的思維方式思考問題的。就好像這樣:“電腦,先執行這一步,再執行那一步,但是在那之前先檢查一下是否滿足條件 A 和條件 B ”。例如,用變量傳參、使用循環語句、迭代、調用函數等等,都是這種命令式編程的思維慣式。
# 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 中設定別名的字段作為判斷條件的原因。
~~~
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
~~~
…或者求助于衍生表、通用數據表達式或者視圖,以避免別名重用。請看下文中的例子。
3. 無論在語法上還是在執行順序上, UNION 總是排在在 ORDER BY 之前。很多人認為每個 UNION 段都能使用 ORDER BY 排序,但是根據 SQL 語言標準和各個數據庫 SQL 的執行差異來看,這并不是真的。盡管某些數據庫允許 SQL 語句對子查詢(subqueries)或者派生表(derived tables)進行排序,但是這并不說明這個排序在 UNION 操作過后仍保持排序后的順序
注意:并非所有的數據庫對 SQL 語句使用相同的解析方式。如 MySQL、PostgreSQL和 SQLite 中就不會按照上面第二點中所說的方式執行。
我們學到了什么?
既然并不是所有的數據庫都按照上述方式執行 SQL 預計,那我們的收獲是什么?我們的收獲是永遠要記得: SQL 語句的語法順序和其執行順序并不一致,這樣我們就能避免一般性的錯誤。如果你能記住 SQL 語句語法順序和執行順序的差異,你就能很容易的理解一些很常見的 SQL 問題。
當然,如果一種語言被設計成語法順序直接反應其語句的執行順序,那么這種語言對程序員是十分友好的,這種編程語言層面的設計理念已經被微軟應用到了 LINQ 語言中。
# SQL 語言的核心是對表的引用
由于 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)個字段。
這個聯合表里的數據是 ab,即 a 和 b 的笛卡爾積。換句話說,也就是 a 表中的每一條數據都要跟 b 表中的每一條數據配對。如果 a 表有3 條數據, b 表有 5 條數據,那么聯合表就會有 15 條數據。
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 語句寫成這樣:
~~~
SELECT DISTINCT first_name, last_name
FROM author
JOIN book ON author.id = book.author_id
~~~
這是一種很糟糕的寫法,原因如下:
SQL 語句性能低下:因為去重操作( DISTINCT )需要數據庫重復從硬盤中讀取數據到內存中。(譯者注: DISTINCT 的確是一種很耗費資源的操作,但是每種數據庫對于 DISTINCT 的操作方式可能不同)。
這么寫并非完全正確:盡管也許現在這么寫不會出現問題,但是隨著 SQL 語句變得越來越復雜,你想要去重得到正確的結果就變得十分困難。
更多的關于濫用 DISTINCT 的危害可以參考這篇博文 https://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 應該怎么辦,因為有一點背離本篇主題,就不詳細介紹,有興趣的同學可以讀一下 https://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.jobbole.com/40443/
# 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 所迷惑,mysql5.7堅持了這個標準。 GROUP BY 改變了對表引用的方式。你可以像這樣既在 SELECT 中引用某一字段,也在 GROUP BY 中對其進行分組。
我們學到了什么?
GROUP BY,再次強調一次,是在表的引用上進行了操作,將其轉換為一種新的引用方式
# SQL 語句中的 SELECT 實質上是對關系的映射
我個人比較喜歡“映射”這個詞,尤其是把它用在關系代數上。(譯者注:原文用詞為 projection ,該詞有兩層含義,第一種含義是預測、規劃、設計,第二種意思是投射、映射,經過反復推敲,我覺得這里用映射能夠更直觀的表達出 SELECT 的作用)。一旦你建立起來了表的引用,經過修改、變形,你能夠一步一步的將其映射到另一個模型中。 SELECT 語句就像一個“投影儀”,我們可以將其理解成一個將源表中的數據按照一定的邏輯轉換成目標表數據的函數。
通過 SELECT語句,你能對每一個字段進行操作,通過復雜的表達式生成所需要的數據。
SELECT 語句有很多特殊的規則,至少你應該熟悉以下幾條:
1. 你僅能夠使用那些能通過表引用而得來的字段;
2. 如果你有 GROUP BY 語句,你只能夠使用 GROUP BY 語句后面的字段或者聚合函數;
3. 當你的語句中沒有 GROUP BY 的時候,可以使用開窗函數代替聚合函數;
4. 當你的語句中沒有 GROUP BY 的時候,你不能同時使用聚合函數和其它函數;
5. 有一些方法可以將普通函數封裝在聚合函數中;
……
一些更復雜的規則多到足夠寫出另一篇文章了。比如:為何你不能在一個沒有 GROUP BY 的 SELECT 語句中同時使用普通函數和聚合函數?
原因如下:
1. 憑直覺,這種做法從邏輯上就講不通。
2. 如果直覺不能夠說服你,那么語法肯定能。 SQL : 1999 標準引入了 GROUPING SETS,SQL: 2003 標準引入了 group sets : GROUP BY() 。無論什么時候,只要你的語句中出現了聚合函數,而且并沒有明確的 GROUP BY 語句,這時一個不明確的、空的 GROUPING SET 就會被應用到這段 SQL 中。因此,原始的邏輯順序的規則就被打破了,映射(即 SELECT )關系首先會影響到邏輯關系,其次就是語法關系。(注:這段話原文就比較艱澀,可以簡單理解如下:在既有聚合函數又有普通函數的 SQL 語句中,如果沒有 GROUP BY 進行分組,SQL 語句默認視整張表為一個分組,當聚合函數對某一字段進行聚合統計的時候,引用的表中的每一條 record 就失去了意義,全部的數據都聚合為一個統計值,你此時對每一條 record 使用其它函數是沒有意義的)。
3.
糊涂了?是的,我也是。我們再回過頭來看點淺顯的東西吧。
我們學到了什么?
SELECT 語句可能是 SQL 語句中最難的部分了,盡管他看上去很簡單。其他語句的作用其實就是對表的不同形式的引用。而 SELECT 語句則把這些引用整合在了一起,通過邏輯規則將源表映射到目標表,而且這個過程是可逆的,我們可以清楚的知道目標表的數據是怎么來的。
想要學習好 SQL 語言,就要在使用 SELECT 語句之前弄懂其他的語句,雖然 SELECT 是語法結構中的第一個關鍵詞,但它應該是我們最后一個掌握的。
# 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 的不同語法可以參考這篇文章 https://www.jooq.org/doc/3.1/manual/sql-building/sql-statements/select-statement/limit-clause/
讓我們在工作中盡情的使用 SQL!
正如其他語言一樣,想要學好 SQL 語言就要大量的練習。上面的 10 個簡單的步驟能夠幫助你對你每天所寫的 SQL 語句有更好的理解。另一方面來講,從平時常見的錯誤中也能積累到很多經驗。下面的兩篇文章就是介紹一些 JAVA 和其他開發者所犯的一些常見的 SQL 錯誤:
https://blog.jooq.org/2013/07/30/10-common-mistakes-java-developers-make-when-writing-sql/
https://blog.jooq.org/2013/08/12/10-more-common-mistakes-java-developers-make-when-writing-sql/
- SQL
- 名詞
- mysql
- 初識mysql
- 備份和恢復
- 存儲引擎
- 數據表損壞和修復
- mysql工具
- 數據庫操作
- 增
- 刪
- 改
- 查
- 數據類型
- 整數類型
- 小數類型
- 日期時間類型
- 字符和文本型
- enum類型
- set類型
- 時間類型
- null與not null和null與空值''的區別
- 數據表操作
- 創建
- 索引
- 約束
- 表選項列表
- 表的其他語句
- 視圖
- sql增刪改查
- sql增
- sql刪
- sql改
- sql查
- sql語句練習
- 連接查詢和更新
- 常用sql語句集錦
- 函數
- 字符函數
- 數值運算符
- 比較運算符與函數
- 日期時間函數
- 信息函數
- 聚合函數
- 加密函數
- null函數
- 用戶權限管理
- 用戶管理
- 權限管理
- pdo
- 與pdo相關的幾個類
- 連接數據庫
- 使用
- pdo的錯誤處理
- pdo結果集對象
- pdo結果集對象常用方法
- pdo預處理
- 常用屬性
- mysql編程
- 事務
- 語句塊
- mysql中的變量
- 存儲函數
- 存儲過程
- 觸發器
- mysql優化
- 存儲引擎
- 字段類型
- 三范式和逆范式
- 索引
- 查詢緩存
- limit分頁優化
- 分區
- 介紹
- 分區算法
- list分區
- range范圍
- Hash哈希
- key鍵值
- 分區管理
- 特別注意
- 分表
- 數據碎片與維護
- innodb表壓縮
- 慢查詢
- explain執行計劃
- count和max,groupby優化
- 子查詢優化
- mysql鎖機制
- 介紹
- 演示
- 總結
- 樂觀鎖和悲觀鎖
- 扛得住的mysql
- 實例和故事
- 系統參數優化
- mysql體系結構
- mysql基準測試
- 索引
- mysql的復制
- win配置MySQL主從
- mysql5.7新特性
- 常見問題
- general log
- 忘記密碼
- uodo log與redo log
- 事務隔離級別
- mysql8密碼登錄
- explain
- 高效的Tree表
- on delete cascade 總結
- mongod
- 簡介
- 集合文檔操作語句
- 增刪改查
- 索引
- 數據導入和導出
- 主從復制
- php7操作mongod
- 權限管理
- redis
- redis簡介
- 3.2版本配置文件
- 3.0版本配置文件
- 2.8版本配置文件
- 配置文件總結
- 外網連接
- 持久化
- RDB備份方式保存數據
- AOF備份方式保存數據
- 總結
- win安裝redis和sentinel部署
- 事務
- Sentinel模式配置
- 分布式鎖
- 管道
- php中redis代碼
- 發布訂閱
- slowlog
- Redis4.0
- scan和keys
- elasticsearch
- 配置說明
- 啟動
- kibana
- kibana下載
- kibana配置文件
- kibana常用功能
- 常用術語
- Beats
- Beats簡介
- Filebeat
- Packetbeat
- Logstash
- 配置
- elasticsearch架構
- es1.7
- head和bigdesk插件
- 插件大全
- 倒排索引
- 單模式下API增刪改查
- mget獲取多個文檔
- 批量操作bulk
- 版本控制
- Mapping映射
- 基本查詢
- Filter過濾
- 組合查詢
- es配置文件
- es集群優化和管理
- logstash
- kibana
- es5.2
- 安裝
- 沖突處理
- 數據備份
- 缺陷不足
- 集群管理api
- 分布式事務
- CAP理論
- BASE模型
- 兩階段提交(2PC)
- TCC (Try-Confirm-Cancle)
- 異步確保型
- 最大努力通知型
- 總結