简单十步让你全面理解SQL

baba007 发布于 2013/12/24 15:45
阅读 6K+
收藏 122
SQL

很多程序员认为SQL是一头难以驯服的野兽。它是为数不多的声明性语言之一,也正因为这样,其展示了完全不同于其他表现形式、命令式语言、面向对象语言甚至函数式编程语言(虽然有些人觉得SQL还是有些类似功能)。

我每天都写SQL,我的开源软件JOOQ中也包含SQL。因此我觉得有必要为还在为此苦苦挣扎的你呈现SQL的优美!下面的教程面向于:

  • 已经使用过但没有完全理解SQL的读者;

  • 已经差不多了解SQL但从未真正考虑过它的语法的读者;

  • 想要指导他人学习SQL的读者;

本教程将重点介绍SELECT语句。其他DML语句将在另一个教程中在做介绍。

1、SQL是声明性语言

首先你需要思考的是声明性。你唯一需要做的只是声明你想获得结果的性质,而不需要考虑你的计算机怎么算出这些结果的。

1
2
3
SELECTfirst_name, last_name
FROMemployees
WHEREsalary > 100000

这很容易理解,你无须关心员工的身份记录从哪来,你只需要知道谁有着不错的薪水。

从中我们学到了什么呢?

那么如果它是这样的简单,会出现什么问题吗?问题就是我们大多数人会直观地认为这是命令式编程。如:“机器,做,再做那,但在这之前,如果和那都发生错误,那么会运行一个检测”。这包括在变量中存储临时的编写循环、迭代、调用函数等等结果。

把那些都忘了吧,想想怎么去声明,而不是怎么告诉机器去计算。

2、SQL语法不是“有序的”

常见的混乱源于一个简单的事实,SQL语法元素并不会按照它们的执行方式排序。语法顺序如下:

  • SELECT [DISTINCT]

  • FROM

  • WHERE

  • GROUP BY

  • HAVING

  • UNION

  • ORDER BY

为简单起见,并没有列出所有SQL语句。这个语法顺序与逻辑顺序从根本上就不同,即执行顺序: 

  • FROM

  • WHERE

  • GROUP BY

  • HAVING

  • SELECT

  • DISTINCT

  • UNION

  • ORDER BY

这有三点需要注意:

1、第一句是FROM,而不是SELECT。首先是将数据从磁盘加载到内存中,以便对这些数据进行操作。

2、SELECT是在其他大多数语句后执行,最重要的是,在FROM和GROUP BY之后。重要的是要理解当你觉得你可以从WHERE语句中引用你定义在SELECT语句当中的时候。以下是不可行的:

1
2
3
4
5
SELECTA.x + A.y ASz
 
FROMA
 
WHEREz = 10 -- z is not available here!

如果你想重用z,您有两种选择。要么重复表达式: 

1
2
3
4
5
SELECTA.x + A.y ASz
 
FROMA
 
WHERE(A.x + A.y) = 10

或者你使用派生表、公用表表达式或视图来避免代码重复。可参阅下文示例。

3、在语法和逻辑顺序里,UNION都是放在ORDER BY之前,很多人认为每个UNION子查询都可以进行排序,但根据SQL标准和大多数的SQL方言,并不是真的可行。虽然一些方言允许子查询或派生表排序,但却不能保证这种顺序能在UNION操作后保留。

需要注意的是,并不是所有的数据库都以相同的形式实现,例如规则2并不完全适用于MySQL、PostgreSQL和SQLite上。

从中我们学到了什么呢?

要时刻记住SQL语句的语法顺序和逻辑顺序来避免常见的错误。如果你能明白这其中的区别,就能明确知道为什么有些可以执行有些则不能。

如果能设计一种在语法顺序上实际又体现了逻辑顺序的语言就更好了,因为它是在微软的LINQ上实现的。

3、SQL是关于数据表引用的 

因为语法顺序和逻辑顺序的差异,大多数初学者可能会误认为SQL中列的值是最重要的。其实并非如此,最重要的是数据表引用。

该SQL标准定义了FROM语句,如下:

1
<fromclause> ::= FROM<tablereference> [ { <comma> <tablereference> }... ]

FROM语句的"output"是所有表引用的结合程度组合表引用。让我们慢慢地消化这些。 

1
FROMa, b

上述产生一个a+b度的组合表引用,如果a有3列和b有5列,那么"输出表"将有8(3+5)列。

包含在这个组合表引用的记录是交叉乘积/笛卡儿积的a×b。换句话说,每一条a记录都会与每一条b记录相对应。如果a有3个记录和b有5条记录,然后上面的组合表引用将产生15条记录(3×5)。

在WHERE语句筛选后,GROUP BY语句中"output"是"fed"/"piped",它已转成新的"output",我们会稍后再去处理。

如果我们从关系代数/集合论的角度来看待这些东西,一个SQL表是一个关系或一组元素组合。每个SQL语句将改变一个或几个关系,来产生新的关系。

从中我们学到了什么呢?

一直从数据表引用角度去思考,有助于理解数据怎样通过你的sql语句流水作业的。

4、SQL数据表引用可以相当强大

表引用是相当强大的东西。举个简单的例子,JOIN关键字其实不是SELECT语句的一部分,但却是"special"表引用的一部分。连接表,在SQL标准中有定义(简化的):

1
2
3
4
5
6
7
<tablereference> ::=
 
<tablename>
 
| <derived table>
 
| <joined table>

如果我们又拿之前的例子来分析: 

1
FROMa, b

a可以作为一个连接表,如:

1
a1 JOINa2 ONa1.id = a2.id

这扩展到前一个表达式,我们会得到:

1
FROMa1 JOINa2 ONa1.id = a2.id, b

虽然结合了数据表引用语法与连接表语法的逗号分隔表让人很无语,但你肯定还会这样做的。结果,结合数据表引用将有a1+a2+b度。

派生表甚至比连接表更强大,我们接下来将会说到。

从中我们学到了什么呢?

要时时刻刻考虑表引用,重要的是这不仅让你理解数据怎样通过你的sql语句流水作业的,它还将帮助你了解复杂表引用是如何构造的。

而且,重要的是,了解JOIN是构造连接表的关键字。不是的SELECT语句的一部分。某些数据库允许JOIN在插入、更新、删除中使用。

5、应使用SQL JOIN的表,而不是以逗号分隔表 

前面,我们已经看到这语句: 

1
FROM a, b

高级SQL开发人员可能会告诉你,最好不要使用逗号分隔的列表,并且一直完整的表达你的JOINs。这将有助于改进你的SQL语句的可读性从而防止错误出现。

一个非常常见的错误是忘记某处连接谓词。思考以下内容:

1
2
3
4
5
6
7
8
9
FROMa, b, c, d, e, f, g, h
 
WHEREa.a1 = b.bx
 
ANDa.a2 = c.c1
 
ANDd.d1 = b.bc
 
-- etc...

使用join来查询表的语法

  • 更安全,你可以把连接谓词与连接表放一起,从而防止错误。

  • 更富于表现力,你可以区分外部连接,内部连接,等等。

从中我们学到了什么呢?

使用JOIN,并且永远不在FROM语句中使用逗号分隔表引用。 

6、SQL的不同类型的连接操作

连接操作基本上有五种

  • EQUI JOIN

  • SEMI JOIN

  • ANTI JOIN

  • CROSS JOIN

  • DIVISION

这些术语通常用于关系代数。对上述概念,如果他们存在,SQL会使用不同的术语。让我们仔细看看:

EQUI JOIN(同等连接)

这是最常见的JOIN操作。它有两个子操作:

  • INNER JOIN(或者只是JOIN)

  • OUTER JOIN(可以再次拆分为LEFT、RIGHT、FULL OUTER JOIN)

例子其中的区别最好的解释:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 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 JOINbook ONauthor.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 LEFTOUTERJOINbook ONauthor.id = book.author_id

SEMI JOIN(半连接)

这种关系的概念在SQL中用两种方式表达:使用IN谓词或使用EXISTS谓语。"Semi"是指在拉丁语中的"half"。这种类型的连接用于连接只有"half"的表引用。再次考虑上述加入的作者和书。让我们想象,我们想要作者/书的组合,但只是那些作者实际上也有书。然后我们可以这样写:

1
2
3
4
5
6
7
8
9
10
11
-- Using IN
 
FROMauthor
 
WHEREauthor.id IN(SELECTbook.author_id FROMbook)
 
-- Using EXISTS
 
FROMauthor
 
WHEREEXISTS (SELECT1 FROMbook WHEREbook.author_id = author.id)

虽然不能肯定你到底是更加喜欢IN还是EXISTS,而且也没有规则说明,但可以这样说:

  • IN往往比EXISTS更具可读性;

  • EXISTS往往比IN更富表现力(如它更容易表达复杂的半连接);

  • 一般情况下性能上没有太大的差异,但,在某些数据库可能会有巨大的性能差异。

因为INNER JOIN有可能只产生有书的作者,因为很多初学者可能认为他们可以使用DISTINCT删除重复项。他们认为他们可以表达一个像这样的半联接:

1
2
3
4
5
-- Find only those authors who also have books
 
SELECTDISTINCTfirst_name, last_name
 
FROMauthor

这是非常不好的做法,原因有二:

  • 它非常慢,因为该数据库有很多数据加载到内存中,只是要再删除重复项;

  • 它不完全正确,即使在这个简单的示例中它产生了正确的结果。但是,一旦你JOIN更多的表引用,你将很难从你的结果中正确删除重复项。

更多的关于DISTINCT滥用的问题,可以访问这里的博客

ANTI JOIN(反连接)

这个关系的概念跟半连接刚好相反。您可以简单地通过将NOT关键字添加到IN或EXISTS中生成它。在下例中,我们选择那些没有任何书籍的作者:

1
2
3
4
5
6
7
8
9
10
11
-- Using IN
 
FROMauthor
 
WHEREauthor.id NOTIN(SELECTbook.author_id FROMbook)
 
-- Using EXISTS
 
FROMauthor
 
WHERENOTEXISTS (SELECT1 FROMbook WHEREbook.author_id = author.id)

同样的规则对性能、可读性、表现力都适用。然而,当使用NOT IN时对NULLs会有一个小警告,这个问题有点超出本教程范围

CROSS JOIN(交叉连接)

结合第一个表中的内容和第二个表中的内容,引用两个join表交叉生成一个新的东西。我们之前已经看到,这可以在FROM语句中通过逗号分隔表引用来实现。在你确实需要的情况下,可以在SQL语句中明确地写一个CROSS JOIN。

1
2
3
-- Combine every author with every book
 
author CROSSJOINbook

DIVISION(除法)

关系分割就是一只真正的由自己亲自喂养的野兽。简而言之,如果JOIN是乘法,那么除法就是JOIN的反义词。在SQL中,除法关系难以表达清楚。由于这是一个初学者的教程,解释这个问题超出了我们的教程范围。当然如果你求知欲爆棚,那么就看这里这里还有这里

从中我们学到了什么呢?

让我们把前面讲到的内容再次牢记于心。SQL是表引用,连接表是相当复杂的表引用。但关系表述和SQL表述还是有点区别的,并不是所有的关系连接操作都是正规的SQL连接操作。对关系理论有一点实践与认识,你就可以选择JOIN正确的关系类型并能将其转化为正确的SQL。

7、SQL的派生表就像表的变量

前文,我们已经了解到SQL是一种声明性语言,因此不会有变量。(虽然在一些SQL语句中可能会存在)但你可以写这样的变量,那些野兽一般的表被称为派生表。

派生表只不过是包含在括号里的子查询。

1
2
3
-- A derived table
 
FROM(SELECT* FROMauthor)

需要注意的是,一些SQL方言要求派生表有一个关联的名字(也被称为别名)。

1
2
3
-- A derived table with an alias
 
FROM(SELECT* FROMauthor) a

当你想规避由SQL子句逻辑排序造成的问题时,你会发现派生表可以用帅呆了来形容。例如,如果你想在SELECT和WHERE子句中重用一个列表达式,只写(Oracle方言):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- Get authors' first and last names, and their age in days
 
SELECTfirst_name, last_name, age
 
FROM(
 
SELECTfirst_name, last_name, current_date- date_of_birth age
 
FROMauthor
 
)
 
-- If the age is greater than 10000 days
 
WHEREage > 10000

注意,一些数据库和SQL:1999标准里已将派生表带到下一级别,引入公共表表达式。这将允许你在单一的SQL SELECT中重复使用相同的派生表。上面的查询将转化为类似这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
WITHa AS(
 
SELECTfirst_name, last_name, current_date- date_of_birth age
 
FROMauthor
 
)
 
SELECT*
 
FROMa
 
WHEREage > 10000

很明显,对广泛重用的常见SQL子查询,你也可以灌输具体"a"到一个独立视图中。想要了解更多就看这里

从中我们学到了什么呢?

再温习一遍,SQL主要是关于表引用,而不是列。好好利用这些表引用。不要害怕写派生表或其他复杂的表引用。

8、SQL GROUP BY转换之前的表引用

让我们重新考虑我们之前的FROM语句:

1
FROMa, b

现在,让我们来应用一个GROUP BY语句到上述组合表引用

1
GROUPBYA.x, A.y, B.z

这会产生一个只有其余三个列(!)的新的表引用。让我们再消化一遍,如果你用了GROUP BY,那么你在所有后续逻辑条款——包括选择中减少可用列的数量。这就是为什么你只可以从SELECT语句中的GROUP BY语句引用列语法的原因。

  • 请注意,其他列仍然可能可作为聚合函数的参数:
1
2
3
SELECTA.x, A.y, SUM(A.z)
FROMA
GROUPBYA.x, A.y
  • 值得注意并很不幸的是,MySQL不遵守这一标准,只会造成混乱。不要陷入MySQL的把戏。GROUP BY转换表引用,因此你可以只引用列也引用GROUP BY语句。

从中我们学到了什么呢?

GROUP BY,在表引用上操作,将它们转换成一个新表。

9、SQL SELECT在关系代数中被称为投影

当它在关系代数中使用时,我个人比较喜欢用“投影”一词中。一旦你生成你的表引用、过滤、转换它,你可以一步将它投影到另一个表中。SELECT语句就像一个投影机。表函数利用行值表达式将之前构造的表引用的每个记录转化为最终结果。

在SELECT语句中,你终于可以在列上操作,创建复杂的列表达式作为记录/行的一部分。

有很多关于可用的表达式,函数性质等的特殊规则。最重要的是,你应该记住这些:

1、你只能使用从“output”表引用产生的列引用;

2、如果你有GROUP BY语句,你只可能从该语句或聚合函数引用列;

3、当你没有GROUP BY语句时,你可以用窗口函数替代聚合函数;

4、如果你没有GROUP BY语句,你就不能将聚合函数与非聚合函数结合起来;

5、这有一些关于在聚合函数包装常规函数的规则,反之亦然;

6、还有……

嗯,这有很多复杂的规则。他们可以填补另一个教程。例如,之所以你不能将聚合函数与非聚合函数结合起来投影到没有GROUP BY的SELECT语句中是因为:

1、凭直觉,没有任何意义。

2、对一个SQL初学者来说,直觉还是毫无帮助的,语法规则则可以。SQL:1999年引入了分组集,SQL:2003引入了空分组集GROUP BY()。每当存在没有显式GROUP BY语句的聚合函数时就会应用隐式的空分组集(规则2)。因此,最初关于逻辑顺序的那个规则就不完全正确了,SELECT的投影也会影响前面的逻辑结果,但语法语句GROUP BY却不受影响。

是不是有点迷糊?其实我也是,让我们看一些简单的吧。

从中我们学到了什么呢?

在SQL语句中,SELECT语句可能是最复杂的语句之一,即使它看起来是那么的简单。所有其他语句只不过是从这个到另一个表引用的的输送管道。通过将它们完全转化,后期地对它们应用一些规则,SELECT语句完完全全地搅乱了这些表引用的美。

为了了解SQL,重要的是要理解其他的一切,都要尝试在SELECT之前解决。即便SELECT在语法顺序中排第一的语句,也应该将它放到最后。

10.相对简单一点的SQL DISTINCT,UNION,ORDER BY,和OFFSET

看完复杂的SELECT之后,我们看回一些简单的东西。

  • 集合运算(DISTINCT和UNION)

  • 排序操作(ORDER BY,OFFSET..FETCH)

集合运算

集合运算在除了表其实没有其他东西的“集”上操作。嗯,差不多是这样,从概念上讲,它们还是很容易理解的:

  • DISTINCT投影后删除重复项。

  • UNION求并集,删除重复项。

  • UNION ALL求并集,保留重复项。

  • EXCEPT求差集(在第一个子查询结果中删除第二个子查询中也含有的记录删除),删除重复项。

  • INTERSECT求交集(保留所有子查询都含有的记录),删除重复项。

所有这些删除重复项通常是没有意义的,很多时候,当你想要连接子查询时,你应该使用UNION ALL。

排序操作

排序不是一个关系特征,它是SQL仅有的特征。在你的SQL语句中,它被应用在语法排序和逻辑排序之后。保证可以通过索引访问记录的唯一可靠方法是使用ORDER BY a和OFFSET..FETCH。所有其他的排序总是任意的或随机的,即使它看起来像是可再现的。

OFFSET..FETCH是唯一的语法变体。其他变体包括MySQL'和PostgreSQL的LIMIT..OFFSET,或者SQL Server和Sybase的TOP..START AT(这里)。

让我们开始应用吧

跟其他每个语言一样,要掌握SQL语言需要大量的实践。上述10个简单的步骤将让你每天编写SQL时更有意义。另一方面,你也可以从常见的错误中学习到更多。下面的两篇文章列出许多Java(和其他)开发者写SQL时常见的错误:

· 10 Common Mistakes Java Developers Make when Writing SQL

· 10 More Common Mistakes Java Developers Make when Writing SQL

来源声明本文来自Tech.Pro的博文《10 Easy Steps to a Complete Understanding of SQL》,由IDF实验室陈民慧翻译。

(全文完)

加载中
0
宏哥
宏哥
歪门邪道
0
young7
young7
好像不错,收藏下来慢慢看
0
张亦俊
张亦俊
写得有点跳了,但内容无疑是非常不错的。
0
ueharaai
ueharaai
sql就是个残疾语言,你不能从中学到什么,你唯一值得做的就是尽量不用他,等他的替代品出现。
0
湘中朱生
湘中朱生
写得挺好的
0
whaon
whaon
该评论暂时无法显示,详情咨询 QQ 群:点此入群
0
wharf_zhang
wharf_zhang
不错的文,谢谢作者和译者。对第二个话题,我是深有感触的。在写我的私有数据库时,我起初是首先实现from和select的,但后来发现应该先实现where,再后来发现作为底层开发,必须考虑更多。比如没有提到的那些子句的执行逻辑,以及更复杂的存储函数,这些都会直接影响和决定数据库的特性和性能等。在开发私有数据库过程中,深切感受到用好数据库的最终关键还是深入了解底层。开源数据库的优点正在于此。
OSCHINA
登录后可查看更多优质内容
返回顶部
顶部