69.4. SQL 语言 #
与大多数现代关系语言一样,SQL 基于元组关系演算。因此,每个能用元组关系演算(或者等价地,关系代数)表述的查询也都能用 SQL 表述。不过,SQL 也有一些超出关系代数或演算范围的能力。下面列出 SQL 提供的一些不属于关系代数或演算的附加特性:
用于插入、删除或修改数据的命令。
算术能力:在 SQL 中可以使用算术运算和比较,例如:
A < B + 3.
注意 + 或其他算术操作符既不出现在关系代数中,也不出现在关系演算中。
赋值和打印命令:可以打印由查询构造出的关系,也可以把计算得到的关系赋给一个关系名。
聚合函数:可以对关系的列应用诸如平均值、求和、最大值等操作,以获得单一的量。
69.4.1. Select #
SQL 中最常用的命令是用于检索数据的 SELECT 语句。其语法为:
SELECT [ALL|DISTINCT]
{ * | expr_1 [AS c_alias_1] [, ...
[, expr_k [AS c_alias_k]]]}
FROM table_name_1 [t_alias_1]
[, ... [, table_name_n [t_alias_n]]]
[WHERE condition]
[GROUP BY name_of_attr_i
[,... [, name_of_attr_j]] [HAVING condition]]
[{UNION [ALL] | INTERSECT | EXCEPT} SELECT ...]
[ORDER BY name_of_attr_i [ASC|DESC]
[, ... [, name_of_attr_j [ASC|DESC]]]];
下面我们将用各种示例来说明 SELECT 语句的复杂语法。示例所用的表定义于供应商与零件数据库 [原文引用目标缺失] [查看原文章节]。
69.4.1.1. 简单 Select
下面是一些使用 SELECT 语句的简单示例:
例 69.4. 带限定条件的简单查询
要检索表 PART 中属性 PRICE 大于 10 的所有元组,我们写出如下查询:
SELECT * FROM PART
WHERE PRICE > 10;
并得到表:
PNO | PNAME | PRICE -----+---------+-------- 3 | Bolt | 15 4 | Cam | 25
在 SELECT 语句中使用 "*" 将交付该表的所有属性。如果只想检索表 PART 的属性 PNAME 和 PRICE,我们使用如下语句:
SELECT PNAME, PRICE
FROM PART
WHERE PRICE > 10;
此时结果为:
PNAME | PRICE
--------+--------
Bolt | 15
Cam | 25
注意,SQL 的 SELECT 对应于关系代数中的"投影"而不是"选择"(更多细节见关系代数 [原文引用目标缺失] [查看原文章节])。
WHERE 子句中的限定条件也可以用关键字 OR、AND 和 NOT 进行逻辑连接:
SELECT PNAME, PRICE
FROM PART
WHERE PNAME = 'Bolt' AND
(PRICE = 0 OR PRICE < 15);
将得到结果:
PNAME | PRICE --------+-------- Bolt | 15
目标列表和 WHERE 子句中可以使用算术运算。例如,如果我们想知道取一个零件的两件要花多少钱,可以用如下查询:
SELECT PNAME, PRICE * 2 AS DOUBLE
FROM PART
WHERE PRICE * 2 < 50;
我们得到:
PNAME | DOUBLE --------+--------- Screw | 20 Nut | 16 Bolt | 30
注意,关键字 AS 之后的单词 DOUBLE 是第二列的新标题。此技术可用于目标列表的每个元素,为结果列指派新标题。这个新标题通常称为别名。别名不能在查询的其余部分中使用。
69.4.1.2. 连接
下面的示例演示了SQL中如何实现连接。
要通过公共属性连接 SUPPLIER、PART 和 SELLS 三张表,我们表述如下语句:
SELECT S.SNAME, P.PNAME
FROM SUPPLIER S, PART P, SELLS SE
WHERE S.SNO = SE.SNO AND
P.PNO = SE.PNO;并得到以下表作为结果:
SNAME | PNAME
-------+-------
Smith | Screw
Smith | Nut
Jones | Cam
Adams | Screw
Adams | Bolt
Blake | Nut
Blake | Bolt
Blake | Cam
在 FROM 子句中,我们为每个关系引入了一个别名,因为这些关系之间存在同名属性(SNO 和 PNO)。现在只需在属性名前加上别名和一个点号,就可以区分这些同名属性。连接的计算方式与一个内连接 [原文引用目标缺失] [查看原文章节]中所示的相同。首先导出笛卡尔积 SUPPLIER × PART × SELLS 然后只选出满足 WHERE 子句所给条件的元组(即同名属性必须相等)。最后把除 S.SNAME 和 P.PNAME 之外的所有列都投影出去。
69.4.1.3. 聚合函数
SQL 提供聚合运算符(例如 AVG、COUNT、SUM、MIN、MAX),它们接受属性名作为参数。聚合运算符的值是在整张表的指定属性(列)的所有值上计算的。如果查询中指定了组,则只在该组的值上计算(见下一节)。
例 69.5. 聚合
如果我们想知道表 PART 中所有零件的平均价格,使用以下查询:
SELECT AVG(PRICE) AS AVG_PRICE
FROM PART;
结果是:
AVG_PRICE ----------- 14.5
如果我们想知道表 PART 中存储了多少个零件,我们使用如下语句:
SELECT COUNT(PNO)
FROM PART;
并得到:
COUNT ------- 4
69.4.1.4. 按组聚合
SQL 允许把表的元组划分成组。然后就可以把上面描述的聚合运算符应用到这些组上(即聚合运算符的值不再是对指定列的所有值计算,而是对一个组的所有值计算。这样,聚合运算符会对每个组分别求值。)
把元组划分成组的工作通过关键字 GROUP BY 后跟定义组的属性列表来完成。如果有
GROUP BY A1, ⃛, Ak,我们就把关系划分为若干组,使得两个元组处于同一组当且仅当它们在所有属性 A1, ⃛, Ak 上都一致。
例 69.6. 聚合
如果我们想知道每个供应商销售多少个零件,可以表述如下查询:
SELECT S.SNO, S.SNAME, COUNT(SE.PNO)
FROM SUPPLIER S, SELLS SE
WHERE S.SNO = SE.SNO
GROUP BY S.SNO, S.SNAME;
并得到:
SNO | SNAME | COUNT -----+-------+------- 1 | Smith | 2 2 | Jones | 1 3 | Adams | 2 4 | Blake | 3
现在来看看这里发生了什么。首先导出表 SUPPLIER 和 SELLS 的连接:
S.SNO | S.SNAME | SE.PNO -------+---------+-------- 1 | Smith | 1 1 | Smith | 2 2 | Jones | 4 3 | Adams | 1 3 | Adams | 3 4 | Blake | 2 4 | Blake | 3 4 | Blake | 4
接下来,把在 S.SNO 和 S.SNAME 两个属性上都一致的元组放在一起,将元组划分为组:
S.SNO | S.SNAME | SE.PNO
-------+---------+--------
1 | Smith | 1
| 2
--------------------------
2 | Jones | 4
--------------------------
3 | Adams | 1
| 3
--------------------------
4 | Blake | 2
| 3
| 4
在本例中我们得到了四个组,现在可以对每个组应用聚合运算符 COUNT,从而得到上面给出的查询的最终结果。
注意,要让使用 GROUP BY 和聚合运算符的查询结果有意义,分组所用的属性也必须出现在目标列表中。其他未出现在 GROUP BY 子句中的属性只能通过聚合函数来选择。另一方面,不能对出现在 GROUP BY 子句中的属性使用聚合函数。
69.4.1.5. Having
HAVING 子句的作用与 WHERE 子句很像,用于只考虑满足 HAVING 子句中限定条件的那些组。HAVING 子句中允许的表达式必须涉及聚合函数。每个只使用普通属性的表达式都属于 WHERE 子句。另一方面,每个涉及聚合函数的表达式都必须放到 HAVING 子句中。
例 69.7. Having
如果我们只想要销售多于一个零件的供应商,我们使用如下查询:
SELECT S.SNO, S.SNAME, COUNT(SE.PNO)
FROM SUPPLIER S, SELLS SE
WHERE S.SNO = SE.SNO
GROUP BY S.SNO, S.SNAME
HAVING COUNT(SE.PNO) > 1;
并得到:
SNO | SNAME | COUNT -----+-------+------- 1 | Smith | 2 3 | Adams | 2 4 | Blake | 3
69.4.1.6. 子查询
在 WHERE 和 HAVING 子句中,凡是期望一个值的地方都允许使用子查询(子 SELECT)。此时该值必须先通过求值子查询得出。子查询的使用扩展了 SQL 的表达能力。
例 69.8. 子选择
如果我们想知道所有比名为 'Screw' 的零件价格更高的零件,我们使用如下查询:
SELECT *
FROM PART
WHERE PRICE > (SELECT PRICE FROM PART
WHERE PNAME='Screw');
结果是:
PNO | PNAME | PRICE -----+---------+-------- 3 | Bolt | 15 4 | Cam | 25
观察上面的查询可以看到关键字 SELECT 出现了两次。第一次在查询的开头——我们称之为外层 SELECT——另一次在 WHERE 子句中,它开始一个嵌套查询——我们称之为内层 SELECT。对外层 SELECT 的每个元组,都必须求值内层 SELECT。每次求值后我们就知道了名为 'Screw' 的元组的价格,从而可以检查当前元组的价格是否更大。
如果我们想知道没有销售任何零件的供应商(例如,以便能从数据库中删除这些供应商),使用:
SELECT *
FROM SUPPLIER S
WHERE NOT EXISTS
(SELECT * FROM SELLS SE
WHERE SE.SNO = S.SNO);
在本例中结果将为空,因为每个供应商都至少销售一个零件。注意我们在内层 SELECT 的 WHERE 子句中使用了来自外层 SELECT 的 S.SNO。如上所述,子查询对外层查询的每个元组求值,即 S.SNO 的值总是取自外层 SELECT 的当前元组。
69.4.1.7. Union, Intersect, Except
这些运算计算由两个子查询导出的元组的并集、交集和集合论差集。
例 69.9. Union, Intersect, Except
下面的查询是 UNION 的例子:
SELECT S.SNO, S.SNAME, S.CITY
FROM SUPPLIER S
WHERE S.SNAME = 'Jones'
UNION
SELECT S.SNO, S.SNAME, S.CITY
FROM SUPPLIER S
WHERE S.SNAME = 'Adams';给出结果:
SNO | SNAME | CITY -----+-------+-------- 2 | Jones | Paris 3 | Adams | Vienna
下面是 INTERSECT 的一个例子:
SELECT S.SNO, S.SNAME, S.CITY
FROM SUPPLIER S
WHERE S.SNO > 1
INTERSECT
SELECT S.SNO, S.SNAME, S.CITY
FROM SUPPLIER S
WHERE S.SNO > 2;
给出结果:
SNO | SNAME | CITY -----+-------+-------- 2 | Jones | Paris
查询两部分都返回的唯一元组是 $SNO=2$ 的那一个。
最后是 EXCEPT 的例子:
SELECT S.SNO, S.SNAME, S.CITY
FROM SUPPLIER S
WHERE S.SNO > 1
EXCEPT
SELECT S.SNO, S.SNAME, S.CITY
FROM SUPPLIER S
WHERE S.SNO > 3;
给出结果:
SNO | SNAME | CITY -----+-------+-------- 2 | Jones | Paris 3 | Adams | Vienna
69.4.2. 数据定义 #
SQL 语言中包含一组用于数据定义的命令。
69.4.2.1. 创建表 #
数据定义最基本的命令是创建新关系(新表)的命令。CREATE TABLE 命令的语法为:
CREATE TABLEtable_name(name_of_attr_1type_of_attr_1[,name_of_attr_2type_of_attr_2[, ...]]);
例 69.10. 创建表
69.4.2.2. SQL 中的数据类型
下面是 SQL 支持的一些数据类型的列表:
INTEGER:有符号全字二进制整数(31 位精度)。
SMALLINT:有符号半字二进制整数(15 位精度)。
DECIMAL (
p[,q]):有符号压缩十进制数,精度为p位数字,并假定其中q位在小数点右边。(15 ≥p≥q≥ 0). 如果省略q,则假定为 0。FLOAT:有符号双字浮点数。
CHAR(
n):长度为n的定长字符串。VARCHAR(
n):最大长度为n的变长字符串。
69.4.2.3. 创建索引
索引用来加快对关系的访问。如果关系
R
在属性
A
上有一个索引,那么检索所有满足
t(A) = a
的元组 t
时,所需时间大致与这类元组 t
的数量成正比,而不再与
R
的大小成正比。
要在 SQL 中创建索引,使用 CREATE INDEX 命令。其语法为:
CREATE INDEXindex_nameONtable_name(name_of_attribute);
例 69.11. 创建索引
要在关系 SUPPLIER 的属性 SNAME 上创建名为 I 的索引,使用以下语句:
CREATE INDEX I ON SUPPLIER (SNAME);
创建的索引会被自动维护,即每当有新元组插入关系 SUPPLIER 时,索引 I 都会随之调整。注意,存在索引时用户能察觉到的唯一变化是速度的提高。
69.4.2.4. 创建视图
视图可以被看作一张虚拟表,即一张在数据库中并不物理存在、但在用户看来好像存在的表。相比之下,当我们谈论基表时,表的每一行在物理存储中的某处确实有一个物理存储的对应物。
视图没有自己的、物理上分离且可区分的存储数据。相反,系统把视图的定义(即如何访问物理存储的基表以物化该视图的规则)存储在系统目录的某处(见系统目录 [原文引用目标缺失] [查看原文章节])。关于实现视图的不同技术的讨论,参见 SIM98。
在 SQL 中使用 CREATE VIEW
命令定义视图。语法为:
CREATE VIEWview_nameASselect_stmt
其中 select_stmt 是一个如Select [原文引用目标缺失] [查看原文章节]中所定义的有效的
select 语句。注意,创建视图时并不执行
select_stmt,它只是被存储在系统目录中,每当对视图进行查询时才被执行。
给定下面的视图定义(我们再次使用供应商与零件数据库 [原文引用目标缺失] [查看原文章节]中的表):
CREATE VIEW London_Suppliers
AS SELECT S.SNAME, P.PNAME
FROM SUPPLIER S, PART P, SELLS SE
WHERE S.SNO = SE.SNO AND
P.PNO = SE.PNO AND
S.CITY = 'London';
现在我们就可以像使用另一张基表一样使用这个虚拟关系 London_Suppliers:
SELECT * FROM London_Suppliers
WHERE P.PNAME = 'Screw';
它将返回以下表:
SNAME | PNAME
-------+-------
Smith | Screw
为了计算这个结果,数据库系统必须先隐蔽地访问基表 SUPPLIER、SELLS 和 PART。它通过对这些基表执行视图定义中给出的查询来完成这一步。之后,再应用(针对视图的查询中给出的)附加限定条件,即可得到结果表。
69.4.2.5. Drop Table、Drop Index、Drop View
要销毁一个表(包括该表中存储的所有元组),使用 DROP TABLE 命令:
DROP TABLE table_name;
要销毁 SUPPLIER 表,使用以下语句:
DROP TABLE SUPPLIER;
要销毁一个索引,使用 DROP INDEX 命令:
DROP INDEX index_name;
最后,要销毁一个给定的视图,使用命令 DROP VIEW:
DROP VIEW view_name;
69.4.3. 数据操纵
69.4.3.1. Insert Into
一旦创建了表(见创建表 [原文引用目标缺失] [查看原文章节]),就可以用
INSERT INTO 命令向其中填入元组。语法为:
INSERT INTOtable_name(name_of_attr_1[,name_of_attr_2[,...]]) VALUES (val_attr_1[,val_attr_2[, ...]]);
要向关系 SUPPLIER(来自供应商与零件数据库 [原文引用目标缺失] [查看原文章节])插入第一个元组,使用以下语句:
INSERT INTO SUPPLIER (SNO, SNAME, CITY)
VALUES (1, 'Smith', 'London');
要向关系 SELLS 插入第一个元组,使用:
INSERT INTO SELLS (SNO, PNO)
VALUES (1, 1);
69.4.3.2. Update
要更改关系中元组的一个或多个属性值,使用 UPDATE 命令。其语法为:
UPDATEtable_nameSETname_of_attr_1=value_1[, ... [,name_of_attr_k=value_k]] WHEREcondition;
要修改关系 PART 中零件'Screw'的属性 PRICE 的值,使用:
UPDATE PART
SET PRICE = 15
WHERE PNAME = 'Screw';
名为'Screw'的元组的属性 PRICE 的新值现在是 15。
69.4.3.3. Delete
要从特定的表中删除元组,使用 DELETE FROM 命令。语法为:
DELETE FROMtable_nameWHEREcondition;
要删除表 SUPPLIER 中名为'Smith'的供应商,使用以下语句:
DELETE FROM SUPPLIER
WHERE SNAME = 'Smith';
69.4.4. 系统目录 #
在每个 SQL 数据库系统中,都使用系统目录来记录数据库中定义了哪些表、视图、索引等。这些系统目录可以像普通关系一样被查询。例如,有一个用于视图定义的目录,该目录存储视图定义中的查询。每当对视图进行查询时,系统首先从目录中取出视图定义查询并物化该视图,然后再继续处理用户的查询(更详细的描述见 Simkovics, 1998 )。关于系统目录的更多信息,参见 Date, 1994 。
69.4.5. 嵌入式 SQL
本节将概述如何把 SQL 嵌入到宿主语言(例如 C)中。我们想从宿主语言中使用
SQL 有两个主要原因:
有些查询无法用纯 SQL 表述(即递归查询)。要能够执行这类查询,我们需要一种表达能力比 SQL 更强的宿主语言。
我们只是想从用宿主语言编写的应用程序访问数据库(例如,一个带图形用户界面的订票系统用 C 编写,而关于还剩哪些票的信息存储在可以用嵌入式 SQL 访问的数据库中)。
在宿主语言中使用嵌入式 SQL 的程序由宿主语言的语句和嵌入式 SQL(ESQL)语句组成。每条 ESQL
语句都以关键字 EXEC SQL 开头。ESQL 语句由预编译器转换成宿主语言的语句(预编译器通常会插入对库例程的调用,由这些例程执行各种
SQL 命令)。
纵观Select [原文引用目标缺失] [查看原文章节]中的示例可以意识到,查询的结果常常是一个元组的集合。而大多数宿主语言并非为操作集合而设计,因此我们需要一种机制来访问 SELECT 语句返回的元组集合中的每一个元组。这一机制可以通过声明一个游标来提供。之后我们就可以用 FETCH 命令检索一个元组并把游标移到下一个元组。
关于嵌入式 SQL 的详细讨论,参见 Date and Darwen, 1997 、 Date, 1994 或 Ullman, 1988 。