深入理解SQL的十个步骤

很多程序员认为SQL十分令人讨厌,作为现存为数不多的声明式语言之一,SQL表现得与一些命令式语言(注:基于动作的语言,机器语言及汇编语言是最早的命令式语言,也称为过程式语言,Fortran、C、Ada、Pascal等都是命令式程序设计语言)、面向对象语言(注:一类以对象作为基本程序结构单位的程序设计语言,如C++、Objective-C等)、函数式语言(一种非冯·诺伊曼式的程序设计语言,如Lisp、Schema等)完全不同,虽然也有一些人说SQL具有一些函数式语言的特征。

作为一名SQL教员,我几乎每天都要和SQL打交道,因此我觉得有必要将SQL的美展示给为它苦苦挣扎的你们。

下面的教程是为这些人准备的:

  • 已经使用过SQL但不完全了解的读者
  • 知道SQL但从来没有认真分析过它的语法的读者
  • 想将SQL教给他人的读者

本教程将重点关注SQL的核心:查询语句(SELECT)

1、SQL是声明式的

开头就直接声明你要查询的数据,也就是你想显示的结果,而不需要告诉计算机如何计算出这些结果,很人性化吧。

SELECT first_name, last_name

FROM employees

WHERE salary > 100000

上面的语句很容易理解,再次强调:你不用关心员工的数据从哪来,你要做的只是查询出那些有体面工资的员工的信息。

我们能从中学到什么?

这么简单?那为什么很多人会害怕SQL呢? 问题在于我们常常想当然地按照过程式编程考虑问题:告诉你的电脑,先做这个,然后做那个,并且在此之前要做个检查,如果发生这样那样的错误就宣告失败,这个过程至少会包括存储临时结果变量,编写循环、迭代,调用函数等等。

在SQL中,忘记上面的一切,想想如何声明,别在告诉计算机如何计算了。

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

造成混淆的一个常见原因是SQL的语法元素不是按照他们的执行顺序排列的,常见语句顺序如下:

  • SELECT [ DISTINCT ]:指定要显示的属性列
  • FROM:说明要查询的数据来自那个/些表
  • WHERE:指定查询条件
  • GROUP BY:对查询结果进行分组
  • HAVING:筛选出满足指定条件的组
  • UNION:用于合并两个或多个 SELECT 语句的结果集
  • ORDER BY: 对查询结果表按指定列值得升序或降序排序

(为简单起见,这里并没有列出所有的SQL语句)这个语句顺序是不同于其逻辑顺序:(也可能不同于其执行顺序,这取决于优化器的选择)

  • FROM
  • WHERE
  • GROUP BY
  • HAVING
  • SELECT
  • DISTINCT
  • UNION
  • ORDER BY

这里有三点需要注意的:

(1)FROM语句一定是首先执行的,而不是SELECT语句,因为系统要先将磁盘中的数据加载到缓存当中,以便执行接下来操作。

(2)SELECT语句在大多数语句之后执行,尤其是FROM与GROUP语句。当你认为你可以在WHERE语句中引用SELECT语句声明的元素时,理解这一点就显得很重要了。比如下面的例子是不允许的:

SELECT A.x + A.y AS zFROM AWHERE z = 10 -- 执行WHERE语句时,z根本不存在,见上面的逻辑顺序

如果你想在这里再次使用z,有两种方法,一是用A.x + A.y代替,如下

SELECT A.x + A.y AS zFROM AWHERE (A.x + A.y) = 10

或者你也可以采用派生表,通用数据表达式,或者视图,以避免代码的重复,参见后续的例子。

(3) UNION语句不管在语法上还是逻辑上,都在ORDER BY之前,许多人认为SELECT的结果可以先排序后合并,但根据SQL标准和许多SQL方言(注:不同数据库的SQL语句略有差别,姑且称之为方言),这是不正确的,尽管一些方言允许对子查询的结果或者派生表进行排序,但这种结果在执行完UNION语句之后是不一定能保持的。

注意,不同数据库执行语句的方式是不同的,比如上面第二条就不适用于MySQL, PostgreSQL, 和SQLite。

我们能从中学到什么?

记住SQL语句的语法顺序和逻辑顺序以避免一些常见的错误,如果你理解他们的区别,你就能清楚地知道为什么有些程序有些却是错的。

如果这门语言的语法顺序和逻辑上顺序是一致的那会非常好,比如微软的LINQ(注:语言集成查询:Language Integrated Query,是一组用于C#和Visual Basic语言的扩展,它允许编写C#或者Visual Basic代码以查询数据库相同的方式操作内存数据)

3、SQL是关于表的引用的语言

由于语法顺序和逻辑顺序之间的差异,大多数初学者可能被骗,以为列值是SQL中最重要的元素,事实上他们不是,最重要的是表的引用。

SQL标准中是这样定义FROM语句的:

<from clause> ::=     FROM <table reference>         [ { <comma> <table reference> }... ]

可见FROM语句的输出结果是一张组合表。下面我们慢慢分析这一点:

FROM a, b

上面的语句根据表a、b的维度生成了一张组合表,例如如果a有3列,b有5列,那么输出表应当有3+5=8列,这张表中的数据是a与b的笛卡儿积(设A,B为集合(SQL中称一组具有相同数据类型的值的集合为“域”,表中的每一列对应一个域),用A中任一元素为第一元素,B中任一元素为第二元素构成有序对,所有这样的有序对组成的集合叫做A与B的“笛卡尔积”)。

这个结果经过WHERE语句过滤后进入GROUP BY语句,然后被转化为一个新的输出,稍后我们会讲到这一点。

如果我们从关系代数或者集合论的角度来看这个问题,一个SQL表就是一个关系或元组的集合,每个SQL语句都会改变一个或多个关系并产生新的关系。

我们从中学到了什么?

要多从表的引用角度来思考问题,这样才能理解各个SQL语句都是如何处理表中数据的。

4、表的引用功能十分强大

用一个简单的例子证明这一点: JOIN关键字(事实上它并不是SELECT语句的一部分,而是属于一种特殊的表的引用),在SQL标准中,JOIN定义(简化的)如下:

<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个维度。

派生表的功能甚至比表连接更加强大,我们后续会提到它。

我们从中学到了什么?

多从(一定要多从)表的引用角度来考虑问题,这不仅可以帮助你理解SQL语句是如何处理数据的,也将有助于你了解如何构造复杂的表引用,并且·,了解JOIN是构建联合表的关键字,而不是SELECT语句的一部分,一些数据库允许在INSERT, UPDATE, DELETE中使用JOIN。

5、SQL的连接操作中应多使用JOIN而不是逗号分隔表

之前,我们分析了下面的语句:

FROM a, b

高级SQL程序员可能会告诉你,最好不要使用逗号分隔表,要充分利用JOIN这个关键字,这不仅会提高SQL语句的可读性,而且会减少错误的发生。

比如下面的例子:

FROM a, b, c, d, e, f, g, hWHERE a.a1 = b.bxAND a.a2 = c.c1AND d.d1 = b.bc-- etc...

可见,使用join:

  • 更加安全:你可以将join放置在靠近要连接的表的地方,以此来避免错误;
  • 更具表达力:你可以区分外连接、内连接。

我们从中学到了什么?

一定要多使用JOIN。在FROM语句中尽量不使用逗号分隔表。

6、SQL中不同的连接操作

连接操作主要分为五种:

  • EQUI JOIN(等值连接)
  • SEMI JOIN(半连接)
  • ANTI JOIN(ANTI-SEMI JOIN)
  • CROSS JOIN(交叉连接)
  • DIVISION(除法连接)

这些术语是关系代数中常用的术语。SQL对以上概念使用的术语略有不同,让我们详细探讨一下:

EQUI JOIN

最常见的连接操作,它又分为:

  • INNER JOIN (JOIN)
  • OUTER JOIN (进一步分为LEFT, RIGHT, FULL JOIN)

(注:

LEFT JOIN: 即使右表中没有匹配,也从左表返回所有的行

RIGHT JOIN: 即使左表中没有匹配,也从右表返回所有的行

FULL JOIN: 只要其中一个表中存在匹配,就返回行)

它们的区别可以用下面这个例子很好的说明:

— 引用了“作者”表以及“著作”表–

每个作者对应一个著作–

不包含没有著作的作者

author JOIN book ON author.id = book.author_id

-- 引用了“作者”表以及“著作”表

-- 每个作者对应一个著作-- ... 

存在无著作的作者,记录为空--

 “空“意味着著作所在列为空

author LEFT OUTER JOIN book ON author.id = book.author_id

SEMI JOIN

这种关系的概念在SQL中可以用的两种方式表达:IN或者EXISTS。“Semi” 在拉丁语中意味着“一半”。这种类型的连接只引用表中的“一半”。这是什么意思?(注:通常出现在使用了exists或in的sql中,所谓semi-join即在两表关联时,当第二个表中存在一个或多个匹配记录时,返回第一个表的记录;与普通join的区别在于semi-join时,第一个表里的记录最多只返回一次)再考虑一下上面作者和书的连接。让我们想象一下,如果我们不想要作者/书籍的组合,而只想要有著书的作者的信息。那么我们可以这样写:

-- 用 IN

FROM author

WHERE author.id IN (SELECT book.author_id FROM book)

-- 用 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

这是非常糟糕的做法,原因有两点:

  • 性能低下,因为数据库需要将大量的数据加载到内存中,来删除重复的数据。
  • 这不是完全正确的,即使在这个简单的例子它会产生正确的结果,但是当你引用更多的表格,那时想从结果中去重就变得十分困难了。

更多有关DISTINCT的内容可以看这篇博文:

http://blog.jooq.org/2013/07/30/10-common-mistakes-java-developers-make-when-writing-sql/

ANTI JOIN

这个关系的概念与SEMI JOIN恰好相反,你可以通过简单地在IN或者EXISTS前添加NOT来实现它(注:而anti-join则与semi-join相反,即当在第二张表没有发现匹配记录时,才会返回第一张表里的记录;当使用not exists/not in的时候会用到,两者在处理null值的时候会有所区别,见下文所附链接),例如,我们来查询那些没有对应著作的作者信息:

-- Using IN

FROM author

WHERE author.id NOT IN (SELECT book.author_id FROM book)

-- Using EXISTSF

ROM author

WHERE NOT EXISTS (SELECT 1 FROM book WHERE book.author_id = author.id)

在性能、可读性、表现力方面,上面的规则同样适用。然而,在使用NOT IN时遇到NULLs就有点麻烦了,这个有点超出本教程的范围了。参看:

http://blog.jooq.org/2012/01/27/sql-incompatibilities-not-in-and-null-values/

CROSS JOIN

这个结果是产生两个表的笛卡儿积,我们之前说过,也可以用逗号分隔两个表来实现。在极少数情况下,如果确实需要的话,你也可以这样明白地来写一个CROSS JOIN:

-- Combine every author with every book

author CROSS JOIN book

DIVISION

DIVISION是个奇葩,你姑且这样理解吧,如果JOIN是乘法,DIVISION就是乘法的逆运算,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表述与关系表述之间是有区别的,不是所有的关系连接操作也正规SQL的连接操作。通过一点点的实践和对关系理论知识的进一步了解,你可以选择使用正确的JOIN类型,并能把它转化成正确的SQL语句。

7、SQL的派生表(可以看作SQL中的表变量)

再此之前,我们已经了解到,SQL是声明性语言,这种语言中变量是没有地位的,但你可以写一些类似于变量的奇葩东西,这中奇葩东西就被称为派生表,派生表其实不过是一个包含在括号内的子查询。

-- 一个派生表

FROM (SELECT * FROM author)

值得注意的是,一些SQL方言需要派生表有一个相关名(也称为别名)。

-- 带别名的派生表

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:1999标准已经将派生表发展到一个新的水平,引入了通用表表达式,允许你在一个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是关于表引用的,要充分利用这一点,不要害怕写派生表或者其他更复杂的表引用语句。

8、GROUP BY语句可以用来改变表引用的结果

我们再来看一下前面的例子:

FROM a, b

现在让我们对上面的联合表(FROM语句的操作结果)使用GROUP BY操作:

GROUP BY A.x, A.y, B.z

上述操作将会生成一个只有三列的新表,我们来整理一下思路,如果你使用GROUP,那么随后的逻辑语句中可操作的列的数目将会减少,包括SELECT语句,这就是为什么你在SELECT语句中只能引用GROUP BY语句产生的列的原因。

  • 注意,其他列仍可以作为聚合函数的参数可用:
SELECT A.x, A.y, SUM(A.z)

FROM A

GROUP BY A.x, A.y

SELECT A.x, A.y, SUM(A.z)

FROM A

GROUP BY A.x, A.y
  • 不幸的是MySQL没有使用这个标准,因此也造成了一些问题。不要被MySQL戏弄了,GROUP BY会改变表引用的方式,您只能引用GROUP BY操作产生的列。

我们从中学到了什么?

GROUP BY也是建立在表引用的基础上的,并且将它转化成了一个新表。

9、SQL的SELECT在关系代数中被称为投影(也称映射)

我个人更喜欢“投影”这个词,关系代数中就是用的这种说法。一旦你生成了新表(FROM)、并且经过了过滤(WHERE)、转化(GROUP BY),你就可以将结果投影到另一张表上,SELECT语句就像一个投影仪:使用某种行值表达式将先前建立的表中的数据映射到最终的结果表中。

使用SELECT语句,你可以对每一列进行操作,构建复杂的列表达式。

一些常用表达式和函数都有很多特殊的使用规则,你应当记住以下这些:

  1. 只能引用输出表(之前操作产生的表)中的列。
  2. 如果你使用了GROUP BY语句,那么你只能引用GROUP BY操作产生的列,聚合函数除外。
  3. 如果你的程序中没有GROUP BY语句,你可以使用窗口函数来代替聚合函数。
  4. 如果你的程序中没有GROUP BY,那么禁止同时使用聚合函数和非聚合函数。
  5. 在聚合函数中使用正则表达式也有一些特殊要求,反之亦然。
  6. 等等…

还有很多复杂的规则,足以用来写另一个教程。例如,在没有GROUP BY的SELECT语句中不能将聚合函数与非聚合函数结合起来使用的原因是:

  • 直觉告诉我,没意义。
  • 如果你没有这样的直觉(这对初学者来说很难),那么让语法来告诉你:SQL 1999标准引入了GROUPING SETS,SQL 2003标准引入了空分组集:GROUP BY ()。当聚合函数出现并且没有GROUP BY时,一个隐含的,空的GROUPING SET会被使用。因此,原有的关于逻辑顺序的规则将不再适用,即投影(SELECT)操作会先影响逻辑关系,然后影响到语法关系(GROUP BY)。

糊涂了吧?哈哈,我也是,我们来看点简单的。

我们从中学到了什么?

尽管看上去简单,SELECT语句其实是SQL中最复杂的部分,其他操作都不过是引用完这个表引用那个表。SELECT完全打乱了这些表,并且对它们应用了一些特殊的规则。

为了进一步了解SQL,应当在搞定SELECT语句之前掌握其他所有内容,尽管写程序时我们把它放在最前面,但事实上这是最难啃的部分。

10、DISTINCT, UNION, ORDER BY, 和OFFSET相对来说就容易很多了

见识过SELECT的复杂后,我们终于可以回到这些简单的内容上了(长松一口气):

  • 集合运算 (DISTINCT 和 UNION)
  • 排序操作 (ORDER BY, OFFSET .. FETCH)

集合运算:

集合运算是基于“集合”的操作,实际上,那就是……表!概念上也很容易理解:

  • DISTINCT :删除投影操作后的重复项
  • UNION:连接两个子查询并且删除重复项
  • UNION ALL : 连接两个子查询并且保留重复项
  • EXCEPT:删除第一个子查询中已在第二个子查询中存在的元素(然后删除重复项)
  • INTERSECT :只保留两个子查询中都存在的元素并删除重复项

通常来说,这些被删除的重复项都是无意义的,大多数时候,使用UNION ALL来连接两个子查询就够了。

排序操作

排序不具有关系理论的特征,它是SQL特有的特征,通常应用在语法顺序和逻辑顺序的最后,使用ORDER BY、OFFSET .. FETCH是保证数据能够通过索引访问的唯一方式,其他排序都是任意和随机的。

OFFSET .. FETCH的语法规则略有不同,其他的变体包括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,同时,也要善于从平时所犯的错误当中进一步去学习。



阅读更多
换一批

没有更多推荐了,返回首页