很多程序员认为SQL是一头难以驯服的野兽。它是为数不多的声明性语言之一,也因为这样,其展示了完全不同于其他的表现形式、命令式语言、面向对象语言甚至函数式编程语言(虽然有些人觉得SQL还是有些类似功能)。
我每天都写SQL,我的开源软件JOOQ中也包含SQL。因此我觉得有必要为还在为此苦苦挣扎的你呈现SQL的优美!下面的教程面向于:
已经使用过但没有完全理解SQL的读者
已经差不多了解SQL但从未真正考虑过它的语法的读者
想要指导他人学习SQL的读者
本教程将重点介绍SELECT语句。其他DML语句将在另一个教程中在做介绍。接下来就是…
1、SQL是声明性语言
首先你需要思考的是,声明性。你唯一需要做的只是声明你想获得结果的性质,而不需要考虑你的计算机怎么算出这些结果的。
SELECT first_name, last_name FROM employees WHERE salary > 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和GROUPBY之后。重要的是要理解当你觉得你可以从WHERE语句中引用你定义在SELECT语句当中的时候,。以下是不可行的:
SELECT A.x + A.y AS zFROM AWHERE z = 10 -- z is not available here!
如果你想重用z,您有两种选择。要么重复表达式:
SELECT A.x + A.y AS zFROM AWHERE (A.x + A.y) = 10
或者你使用派生表、公用表表达式或视图来避免代码重复。请参阅示例进一步的分析:
3、在语法和逻辑顺序里,UNION都是放在ORDER BY之前,很多人认为每个UNION子查询都可以进行排序,但根据SQL标准和大多数的SQL方言,并不是真的可行。虽然一些方言允许子查询或派生表排序,但却不能保证这种顺序能在UNION操作后保留。
需要注意的是,并不是所有的数据库都以相同的形式实现,例如规则2并不完全适用于MySQL,PostgreSQL,和SQLite上
从中我们学到了什么呢?
要时刻记住SQL语句的语法顺序和逻辑顺序来避免常见的错误。如果你能明白这其中的区别,就能明确知道为什么有些可以执行有些则不能。
如果能设计一种在语法顺序上实际又体现了逻辑顺序的语言就更好了,因为它是在微软的LINQ上实现的。
3、SQL是关于数据表引用的
因为语法顺序和逻辑顺序的差异,大多数初学者可能会误认为SQL中列的值是第一重要的。其实并非如此,最重要的是数据表引用。
该SQL标准定义了FROM语句,如下:
<from clause> ::= FROM <table reference> [ { <comma> <table reference> }... ]
ROM语句的"output"是所有表引用的结合程度组合表引用。让我们慢慢地消化这些。
FROM a, b
上述产生一个a+b度的组合表引用,如果a有3列和b有5列,那么"输出表"将有8(3+5)列。
包含在这个组合表引用的记录是交叉乘积/笛卡儿积的axb。换句话说,每一条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标准中有定义(简化的):
<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语句的一部分。某些数据库允许JOIN在插入、更新、删除中使用。
5、应使用SQL JOIN的表,而不是以逗号分隔表
前面,我们已经看到这语句:
FROM a, b
高级SQL开发人员可能会告诉你,最好不要使用逗号分隔的列表,并且一直完整的表达你的JOINs。这将有助于改进你的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,并且永远不在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)
例子是其中的区别最好的解释:
-- This table reference contains authors and their books.-- There is one record for each book and its author.-- authors without books are NOT includedauthor 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
SEMIJOIN(半连接)
这种关系的概念在SQL中用两种方式表达:使用IN谓词或使用EXISTS谓语。"Semi"是指在拉丁语中的"half"。这种类型的连接用于连接只有"half"的表引用。再次考虑上述加入的作者和书。让我们想象,我们想要作者/书的组合,但只是那些作者实际上也有书。然后我们可以这样写:
-- Using INFROM authorWHERE author.id IN (SELECT book.author_id FROM book)-- Using EXISTSFROM authorWHERE EXISTS (SELECT 1 FROM book WHERE book.author_id = author.id)
虽然不能肯定你到底是更加喜欢IN还是EXISTS,而且也没有规则说明,但可以这样说:
IN往往比EXISTS更具可读性
EXISTS往往比IN更富表现力(如它更容易表达复杂的半连接)
一般情况下性能上没有太大的差异,但,在某些数据库可能会有巨大的性能差异。
因为INNER JOIN有可能只产生有书的作者,因为很多初学者可能认为他们可以使用DISTINCT删除重复项。他们认为他们可以表达一个像这样的半联接:
-- Find only those authors who also have booksSELECT DISTINCT first_name, last_nameFROM author
这是非常不好的做法,原因有二:
它非常慢,因为该数据库有很多数据加载到内存中,只是要再删除重复项。
它不完全正确,即使在这个简单的示例中它产生了正确的结果。但是,一旦你JOIN更多的表引用,,你将很难从你的结果中正确删除重复项。
更多的关于DISTINCT滥用的问题,可以访问这里的博客。
ANTI JOIN(反连接)
这个关系的概念跟半连接刚好相反。您可以简单地通过将NOT关键字添加到IN或EXISTS中生成它。在下例中,我们选择那些没有任何书籍的作者:
-- Using INFROM authorWHERE author.id NOT IN (SELECT book.author_id FROM book)-- Using EXISTSFROM authorWHERE NOT EXISTS (SELECT 1 FROM book WHERE book.author_id = author.id)
同样的规则对性能、可读性、表现力都适用。然而,当使用NOTIN时对NULLs会有一个小警告,这个问题有点超出本教程范围。
CROSSJOIN(交叉连接)
结合第一个表中的内容和第二个表中的内容,引用两个join表交叉生成一个新的东西。我们之前已经看到,这可以在FROM语句中通过逗号分隔表引用来实现。在你确实需要的情况下,可以在SQL语句中明确地写一个CROSS JOIN。
-- Combine every author with every bookauthor CROSS JOIN book
DIVISION(除法)
关系分割就是一只真正的由自己亲自喂养的野兽。简而言之,如果JOIN是乘法,那么除法就是JOIN的反义词。在SQL中,除法关系难以表达清楚。由于这是一个初学者的教程,解释这个问题超出了我们的教程范围。当然如果你求知欲爆棚,那么就看这里,这里还有这里。
从中我们学到了什么呢?
让我们把前面讲到的内容再次牢记于心。SQL是表引用。连接表是相当复杂的表引用。但关系表述和SQL表述还是有点区别的,并不是所有的关系连接操作都是正规的SQL连接操作。对关系理论有一点实践与认识,你就可以选择JOIN正确的关系类型并能将其转化为正确的SQL。
7、SQL的派生表就像表的变量
前文,我们已经了解到SQL是一种声明性语言,因此不会有变量。(虽然在一些SQL语句中可能会存在)但你可以写这样的变量。那些野兽一般的表被称为派生表。
派生表只不过是包含在括号里的子查询。
-- A derived tableFROM (SELECT * FROM author)
需要注意的是,一些SQL方言要求派生表有一个关联的名字(也被称为别名)。
-- A derived table with an aliasFROM (SELECT * FROM author) a
当你想规避由SQL子句逻辑排序造成的问题时,你会发现派生表可以用帅呆了来形容。例如,如果你想在SELECT和WHERE子句中重用一个列表达式,只写(Oracle方言):
-- Get authors' first and last names, and their age in daysSELECT first_name, last_name, ageFROM (SELECT first_name, last_name, current_date - date_of_birth ageFROM author)-- If the age is greater than 10000 daysWHERE age > 10000
注意,一些数据库和SQL:1999标准里已将派生表带到下一级别,,引入公共表表达式。这将允许你在单一的SQL SELECT中重复使用相同的派生表。上面的查询将转化为类似这样的:
WITH a AS (SELECT first_name, last_name, current_date - date_of_birth ageFROM author)SELECT *FROM aWHERE age > 10000
很明显,对广泛重用的常见SQL子查询,你也可以灌输具体"a"到一个独立视图中。想要了解更多就看这里。
从中我们学到了什么呢?
再温习一遍,SQL主要是关于表引用,而不是列。好好利用这些表引用。不要害怕写派生表或其他复杂的表引用。
8、SQLGROUPBY转换之前的表引用
让我们重新考虑我们之前的FROM语句:
FROM a, b
现在,让我们来应用一个GROUP BY语句到上述组合表引用
GROUP BY A.x, A.y, B.z
这会产生一个只有其余三个列(!)的新的表引用。让我们再消化一遍。如果你用了GROUP BY,那么你在所有后续逻辑条款-包括选择中减少可用列的数量。这就是为什么你只可以从SELECT语句中的GROUP BY语句引用列语法的原因。
新闻热点
疑难解答