来自:http://blog.jobbole.com/55086/java
不少程序员视 SQL 为洪水猛兽。SQL 是一种为数很少的声明性语言,它的运行方式彻底不一样于咱们所熟知的命令行语言、面向对象的程序语言、甚至是函数语言(尽管有些人认为 SQL 语言也是一种函数式语言)。程序员
咱们天天都在写 SQL 而且应用在开源软件 jOOQ 中。因而我想把 SQL 之美介绍给那些仍然对它头疼不已的朋友,因此本文是为了如下读者而特意编写的:sql
一、 在工做中会用到 SQL 可是对它并不彻底了解的人。数据库
二、 可以熟练使用 SQL 可是并不了解其语法逻辑的人。编程
三、 想要教别人 SQL 的人。安全
本文着重介绍 SELECT 句式,其余的 DML (Data Manipulation Language 数据操纵语言命令)将会在别的文章中进行介绍。app
首先要把这个概念记在脑中:“声明”。 SQL 语言是为计算机声明了一个你想从原始数据中得到什么样的结果的一个范例,而不是告诉计算机如何可以获得结果。这是否是很棒?编程语言
(译者注:简单地说,SQL 语言声明的是结果集的属性,计算机会根据 SQL 所声明的内容来从数据库中挑选出符合声明的数据,而不是像传统编程思惟去指示计算机如何操做。)ide
1
|
SELECT first_name, last_name FROM employees WHERE salary > 100000
|
上面的例子很容易理解,咱们不关心这些雇员记录从哪里来,咱们所须要的只是那些高薪者的数据(译者注: salary>100000 )。函数
咱们从哪儿学习到这些?
若是 SQL 语言这么简单,那么是什么让人们“闻 SQL 色变”?主要的缘由是:咱们潜意识中的是按照命令式编程的思惟方式思考问题的。就好像这样:“电脑,先执行这一步,再执行那一步,可是在那以前先检查一下是否知足条件 A 和条件 B ”。例如,用变量传参、使用循环语句、迭代、调用函数等等,都是这种命令式编程的思惟惯式。
SQL 语句有一个让大部分人都感到困惑的特性,就是:SQL 语句的执行顺序跟其语句的语法顺序并不一致。SQL 语句的语法顺序是:
为了方便理解,上面并无把全部的 SQL 语法结构都列出来,可是已经足以说明 SQL 语句的语法顺序和其执行顺序彻底不同,就以上述语句为例,其执行顺序为:
关于 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 等经常使用数据库为例,数据是从硬盘中抽取到数据缓冲区中进行操做。)
二、 SELECT 是在大部分语句执行了以后才执行的,严格的说是在 FROM 和 GROUP BY 以后执行的。理解这一点是很是重要的,这就是你不能在 WHERE 中使用在 SELECT 中设定别名的字段做为判断条件的缘由。
1
2
3
|
SELECT A.x + A.y AS z
FROM A
WHERE z = 10 -- z 在此处不可用,由于SELECT是最后执行的语句!
|
若是你想重用别名z,你有两个选择。要么就从新写一遍 z 所表明的表达式:
1
2
3
|
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 语句语法顺序和执行顺序的不一样,不少同窗会认为SELECT 中的字段信息是 SQL 语句的核心。其实真正的核心在于对表的引用。
根据 SQL 标准,FROM 语句被定义为:
1
|
< from clause> ::= FROM < table reference> [ { <comma> < table reference> }... ]
|
FROM 语句的“输出”是一张联合表,来自于全部引用的表在某一维度上的联合。咱们们慢慢来分析:
1
|
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 语句变得更强大。一个简单的例子就是 JOIN 的使用。严格的说 JOIN 语句并不是是 SELECT 中的一部分,而是一种特殊的表引用语句。 SQL 语言标准中表的链接定义以下:
1
2
3
4
|
< table reference> ::=
< table name >
| <derived table >
| <joined table >
|
就拿以前的例子来讲:
1
|
FROM a, b
|
a 可能输以下表的链接:
1
|
a1 JOIN a2 ON a1.id = a2.id
|
将它放到以前的例子中就变成了:
1
|
FROM a1 JOIN a2 ON a1.id = a2.id, b
|
尽管将一个链接表用逗号跟另外一张表联合在一块儿并非经常使用做法,可是你的确能够这么作。结果就是,最终输出的表就有了 a1+a2+b 个字段了。
(译者注:原文这里用词为 degree ,译为维度。若是把一张表视图化,咱们能够想象每一张表都是由横纵两个维度组成的,横向维度即咱们所说的字段或者列,英文为columns;纵向维度即表明了每条数据,英文为 record ,根据上下文,做者这里所指的应该是字段数。)
在 SQL 语句中派生表的应用甚至比表链接更增强大,下面咱们就要讲到表链接。
咱们学到了什么?
思考问题时,要从表引用的角度出发,这样就很容易理解数据是怎样被 SQL 语句处理的,而且可以帮助你理解那些复杂的表引用是作什么的。
更重要的是,要理解 JOIN 是构建链接表的关键词,并非 SELECT 语句的一部分。有一些数据库容许在 INSERT 、 UPDATE 、 DELETE 中使用 JOIN 。
咱们先看看刚刚这句话:
1
|
FROM a, b
|
高级 SQL 程序员也许学会给你忠告:尽可能不要使用逗号来代替 JOIN 进行表的链接,这样会提升你的 SQL 语句的可读性,而且能够避免一些错误。
利用逗号来简化 SQL 语句有时候会形成思惟上的混乱,想一下下面的语句:
1
2
3
4
5
|
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 进行表的链接,永远不要在 FROM 后面使用逗号链接表。
SQL 语句中,表链接的方式从根本上分为五种:
EQUI JOIN
这是一种最普通的 JOIN 操做,它包含两种链接方式:
用例子最容易说明其中区别:
1
2
3
4
5
6
7
8
9
10
|
-- 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 ”在拉丁文中是“半”的意思。这种链接方式是只链接目标表的一部分。这是什么意思呢?再想一下上面关于做者和书名的链接。咱们想象一下这样的状况:咱们不须要做者 / 书名这样的组合,只是须要那些在书名表中的书的做者信息。那咱们就能这么写:
1
2
3
4
5
6
7
|
-- 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 ,可是这些事情你仍是应该知道的:
由于使用 INNER JOIN 也能获得书名表中书所对应的做者信息,因此不少初学者机会认为能够经过 DISTINCT 进行去重,而后将 SEMI JOIN 语句写成这样:
1
2
3
4
|
-- Find only those authors who also have books
SELECT DISTINCT first_name, last_name
FROM author
JOIN book ON author.id = book.author_id
|
这是一种很糟糕的写法,缘由以下:
更多的关于滥用 DISTINCT 的危害能够参考这篇博文
(http://blog.jooq.org/2013/07/30/10-common-mistakes-java-developers-make-when-writing-sql/)。
ANTI JOIN
这种链接的关系跟 SEMI JOIN 恰好相反。在 IN 或者 EXISTS 前加一个 NOT 关键字就能使用这种链接。举个例子来讲,咱们列出书名表里没有书的做者:
1
2
3
4
5
6
7
|
-- 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语句表达:
1
2
|
-- 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 是对表的引用, JOIN 则是一种引用表的复杂方式。可是 SQL 语言的表达方式和实际咱们所须要的逻辑关系之间是有区别的,并不是全部的逻辑关系都能找到对应的 JOIN 操做,因此这就要咱们在平时多积累和学习关系逻辑,这样你就能在之后编写 SQL 语句中选择适当的 JOIN 操做了。
在这以前,咱们学习到过 SQL 是一种声明性的语言,而且 SQL 语句中不能包含变量。可是你能写出相似于变量的语句,这些就叫作派生表:
说白了,所谓的派生表就是在括号之中的子查询:
1
2
|
-- A derived table
FROM ( SELECT * FROM author)
|
须要注意的是有些时候咱们能够给派生表定义一个相关名(即咱们所说的别名)。
1
2
|
-- A derived table with an alias
FROM ( SELECT * FROM author) a
|
派生表能够有效的避免因为 SQL 逻辑而产生的问题。举例来讲:若是你想重用一个用 SELECT 和 WHERE 语句查询出的结果,这样写就能够(以 Oracle 为例):
1
2
3
4
5
6
7
8
|
-- 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 语句中对派生表屡次重用。上面的例子就(几乎)等价于下面的语句:
1
2
3
4
5
6
7
|
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 语句:
1
|
FROM a, b
|
如今,咱们将 GROUP BY 应用到上面的语句中:
1
|
GROUP BY A.x, A.y, B.z
|
上面语句的结果就是产生出了一个包含三个字段的新的表的引用。咱们来仔细理解一下这句话:当你应用 GROUP BY 的时候, SELECT 后没有使用聚合函数的列,都要出如今 GROUP BY 后面。(译者注:原文大意为“当你是用 GROUP BY 的时候,你可以对其进行下一级逻辑操做的列会减小,包括在 SELECT 中的列”)。
1
2
3
|
SELECT A.x, A.y, SUM (A.z)
FROM A
GROUP BY A.x, A.y
|
咱们学到了什么?
GROUP BY,再次强调一次,是在表的引用上进行了操做,将其转换为一种新的引用方式。
我我的比较喜欢“映射”这个词,尤为是把它用在关系代数上。(译者注:原文用词为 projection ,该词有两层含义,第一种含义是预测、规划、设计,第二种意思是投射、映射,通过反复推敲,我以为这里用映射可以更直观的表达出 SELECT 的做用)。一旦你创建起来了表的引用,通过修改、变形,你可以一步一步的将其映射到另外一个模型中。 SELECT 语句就像一个“投影仪”,咱们能够将其理解成一个将源表中的数据按照必定的逻辑转换成目标表数据的函数。
经过 SELECT语句,你能对每个字段进行操做,经过复杂的表达式生成所须要的数据。
SELECT 语句有不少特殊的规则,至少你应该熟悉如下几条:
一些更复杂的规则多到足够写出另外一篇文章了。好比:为什么你不能在一个没有 GROUP BY 的 SELECT 语句中同时使用普通函数和聚合函数?(上面的第 4 条)
缘由以下:
糊涂了?是的,我也是。咱们再回过头来看点浅显的东西吧。
咱们学到了什么?
SELECT 语句多是 SQL 语句中最难的部分了,尽管他看上去很简单。其余语句的做用其实就是对表的不一样形式的引用。而 SELECT 语句则把这些引用整合在了一块儿,经过逻辑规则将源表映射到目标表,并且这个过程是可逆的,咱们能够清楚的知道目标表的数据是怎么来的。
想要学习好 SQL 语言,就要在使用 SELECT 语句以前弄懂其余的语句,虽然 SELECT 是语法结构中的第一个关键词,但它应该是咱们最后一个掌握的。
在学习完复杂的 SELECT 豫剧以后,咱们再来看点简单的东西:
集合运算( set operation):
集合运算主要操做在于集合上,事实上指的就是对表的一种操做。从概念上来讲,他们很好理解:
排序运算( 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 错误: