許多程式設計師視 SQL 為洪水猛獸。 SQL 是一種少數的聲明性語言,它的運作方式完全不同於我們所熟知的命令列語言、物件導向的程式語言、甚至是函數語言(儘管有些人認為SQL 語言也是一種函數式語言)。
我們每天都在寫 SQL 並且應用在開源軟體 jOOQ 中。於是我想把SQL 之美介紹給那些仍然對它頭疼不已的朋友,所以本文是為了以下讀者而特地編寫的:
#1、 在工作中會用到SQL 但是對它並不完全了解的人。
2、 能夠熟練使用 SQL 但是並不了解其語法邏輯的人。
3、 想要教別人 SQL 的人。
本文著重介紹 SELECT 句式,其他的 DML (Data Manipulation Language 資料操縱語言指令)將會在別的文章中介紹。
首先要把這個概念記在腦海中:「宣告」。 SQL 語言是為電腦聲明了一個你想從原始資料中獲得什麼樣的結果的一個範例,而不是告訴電腦如何能夠得到結果。這是不是很棒?
(譯者註:簡單地說,SQL 語言宣告的是結果集的屬性,電腦會根據SQL 所宣告的內容來從資料庫中挑選出符合宣告的數據,而不是像傳統程式設計思維去指示電腦如何操作。哪裡來,我們所需要的只是那些高薪者的數據(譯者註: salary>100000 )。
我們要從哪裡學到這些?
如果 SQL 語言這麼簡單,那麼是什麼讓人們「聞 SQL 色變」?主要的原因是:我們潛意識中的是依照命令式程式設計的思考方式思考問題的。就好像這樣:「電腦,先執行這一步,再執行那一步,但在那之前先檢查是否符合條件 A 和條件 B 」。例如,用變數傳參、使用循環語句、迭代、呼叫函數等等,都是這種命令式程式設計的思考慣式。
2、 SQL 的語法不依照語法順序執行SQL 語句有一個讓大部分人都感到困惑的特性,就是:SQL 語句的執行順序跟其語句的語法順序並不一致。 SQL 語句的語法順序是:
##GROUP BY
HAVING
SELECT
#關於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 等常用數據庫為例,數據是從硬碟中抽取到資料緩衝區中進行操作。要理解這一點是非常重要的,這就是為什麼你不能在 WHERE 中使用在 SELECT 中設定別名的欄位作為判斷條件的原因。 ###SELECT A.x + A.y AS z FROM A WHERE z = 10 -- z 在此处不可用,因为SELECT是最后执行的语句!
SELECT A.x + A.y AS z FROM AWHERE (A.x + A.y) = 10
既然并不是所有的数据库都按照上述方式执行 SQL 预计,那我们的收获是什么?我们的收获是永远要记得: SQL 语句的语法顺序和其执行顺序并不一致,这样我们就能避免一般性的错误。如果你能记住 SQL 语句语法顺序和执行顺序的差异,你就能很容易的理解一些很常见的 SQL 问题。
当然,如果一种语言被设计成语法顺序直接反应其语句的执行顺序,那么这种语言对程序员是十分友好的,这种编程语言层面的设计理念已经被微软应用到了 LINQ 语言中。
由于 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 ( =53)条数据。
FROM 输出的结果被 WHERE 语句筛选后要经过 GROUP BY 语句处理,从而形成新的输出结果。我们后面还会再讨论这方面问题。
如果我们从集合论(关系代数)的角度来看,一张数据库的表就是一组数据元的关系,而每个 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 。
我们先看看刚刚这句话:
FROM a, b
高级 SQL 程序员也许学会给你忠告:尽量不要使用逗号来代替 JOIN 进行表的连接,这样会提高你的 SQL 语句的可读性,并且可以避免一些错误。
利用逗号来简化 SQL 语句有时候会造成思维上的混乱,想一下下面的语句:
FROM a, b, c, d, e, f, g, h WHERE a.a1 = b.bxAND a.a2 = c.c1AND d.d1 = b.bc -- etc...
我们不难看出使用 JOIN 语句的好处在于:
安全。 JOIN 和要连接的表离得非常近,这样就能避免错误。
更多连接的方式,JOIN 语句能去区分出来外连接和内连接等。
我们学到了什么?
记着要尽量使用 JOIN 进行表的连接,永远不要在 FROM 后面使用逗号连接表。
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语句表达:
author CROSS JOIN book
DIVISION
DIVISION 的确是一个怪胎。简而言之,如果 JOIN 是一个乘法运算,那么 DIVISION 就是 JOIN 的逆过程。DIVISION 的关系很难用 SQL 表达出来,介于这是一个新手指南,解释 DIVISION 已经超出了我们的目的。但是有兴趣的同学还是可以来看看这三篇文章
(http://blog.jooq.org/2012/03/30/advanced-sql-relational-pision-in-jooq/)
(http://en.wikipedia.org/wiki/Relational_algebra#Division)
(https://www.simple-talk.com/sql/t-sql-programming/pided-we-stand-the-sql-of-relational-pision/)。
我们学到了什么?
学到了很多!让我们在脑海中再回想一下。 SQL 是对表的引用, JOIN 则是一种引用表的复杂方式。但是 SQL 语言的表达方式和实际我们所需要的逻辑关系之间是有区别的,并非所有的逻辑关系都能找到对应的 JOIN 操作,所以这就要我们在平时多积累和学习关系逻辑,这样你就能在以后编写 SQL 语句中选择适当的 JOIN 操作了。
在这之前,我们学习到过 SQL 是一种声明性的语言,并且 SQL 语句中不能包含变量。但是你能写出类似于变量的语句,这些就叫做派生表:
说白了,所谓的派生表就是在括号之中的子查询:
-- A derived table FROM (SELECT * FROM author)
需要注意的是有些时候我们可以给派生表定义一个相关名(即我们所说的别名)。
-- A derived table with an aliasFROM (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 ”创建一个单独的视图,这样你就可以在更广泛的范围内重用这个派生表了。更多信息可以阅读下面的文章(http://en.wikipedia.org/wiki/View_%28SQL%29)。
我们学到了什么?
我们反复强调,大体上来说 SQL 语句就是对表的引用,而并非对字段的引用。要好好利用这一点,不要害怕使用派生表或者其他更复杂的语句。
让我们再回想一下之前的 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 AGROUP BY A.x, A.y
还有一点值得留意的是: MySQL 并不坚持这个标准,这的确是令人很困惑的地方。(译者注:这并不是说 MySQL 没有 GROUP BY 的功能)但是不要被 MySQL 所迷惑。 GROUP BY 改变了对表引用的方式。你可以像这样既在 SELECT 中引用某一字段,也在 GROUP BY 中对其进行分组。
我们学到了什么?
GROUP BY,再次强调一次,是在表的引用上进行了操作,将其转换为一种新的引用方式。
我个人比较喜欢“映射”这个词,尤其是把它用在关系代数上。(译者注:原文用词为 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 是文法結構中的第一個關鍵字,但它應該是我們最後一個掌握的。
在學習完複雜的SELECT 豫劇之後,我們再看一點簡單的東西:
集合運算( DISTINCT 和UNION )
#排序運算(ORDER BY,OFFSET…FETCH)
#集合運算( set operation):
集合運算主要操作在於集合上,事實上指的就是對錶的一種運算。從概念上來說,他們很好理解:UNION 將兩個子查詢拼接起來並去重
UNION ALL 將兩個子查詢拼接起來但不去重排序運算跟邏輯關係無關。這是一個 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 錯誤:###############10 Common Mistakes Java Developers Make when Writing SQL#### ##############10 More Common Mistakes Java Developers Make when Writing SQL################## 相關推薦:##### #######解析SQL中樹形分層資料的查詢最佳化#########在資料查詢中,從2008開始SQLServer提供了一個新的資料類型hierarchyid,專門用來操作. ....###以上是完美解析SQL只需要簡單的十個步驟的詳細內容。更多資訊請關注PHP中文網其他相關文章!