pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。
与大多数现代关系语言一样,SQL 基于元组关系演算。 因此,每个能用元组关系演算(或者等价地,关系代数)表述的查询也都能用 SQL 表述。不过,SQL 也有一些超出 关系代数或演算范围的能力。下面列出 SQL 提供的一些 不属于关系代数或演算的附加特性:
用于插入、删除或修改数据的命令。
算术能力:在 SQL 中可以使用算术运算和比较,例如:
A < B + 3.
注意 + 或其他算术操作符既不出现在关系代数中,也不出现在关系演算中。
赋值和打印命令:可以打印由查询构造出的关系,也可以把计算得到的关系 赋给一个关系名。
聚合函数:可以对关系的列应用诸如平均值、 求和、最大值等操作, 以获得单一的量。
SQL 中最常用的命令是用于检索数据的 SELECT 语句。其语法为:
SELECT [ ALL | DISTINCT [ ON (expression[, ...] ) ] ] * |expression[ ASoutput_name] [, ...] [ INTO [ TEMPORARY | TEMP ] [ TABLE ]new_table] [ FROMfrom_item[, ...] ] [ WHEREcondition] [ GROUP BYexpression[, ...] ] [ HAVINGcondition[, ...] ] [ { UNION | INTERSECT | EXCEPT [ ALL ] }select] [ ORDER BYexpression[ ASC | DESC | USINGoperator] [, ...] ] [ FOR UPDATE [ OFclass_name[, ...] ] ] [ LIMIT {count| ALL } [ { OFFSET | , }start]]
下面我们将用各种示例来说明 SELECT 语句的复杂语法。示例所用的表定义于 供应商与零件数据库。
下面是一些使用 SELECT 语句的简单示例:
例 1.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 是第二列的新标题。此技术可用于目标列表的每个元素,为结果列指派新标题。这个新标题通常称为别名。别名不能在查询的其余部分中使用。
下面的示例演示了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 之外的所有列都投影出去。
执行连接的另一种方式是使用 SQL 的 JOIN 语法,如下所示:
select sname, pname from supplier
JOIN sells USING (sno)
JOIN part USING (pno);
同样得到:
sname | pname
-------+-------
Smith | Screw
Adams | Screw
Smith | Nut
Blake | Nut
Adams | Bolt
Blake | Bolt
Jones | Cam
Blake | Cam
(8 rows)
使用 JOIN 语法创建的连接表是出现在 FROM 子句中、并且位于任何 WHERE、 GROUP BY 或 HAVING 子句之前的表引用列表项。其他表引用(包括表名或 其他 JOIN 子句)只要用逗号分隔,也可以包含在 FROM 子句中。连接表在 逻辑上与 FROM 子句中列出的其他任何表一样。
SQL 的 JOIN 分为两大类型:CROSS JOIN(非限定连接)和 限定 JOIN。限定连接还可以进一步按 连接条件的指定方式(ON、USING 或 NATURAL) 以及应用方式(INNER 或 OUTER 连接)细分。
连接类型
{ T1 } CROSS JOIN { T2 }
交叉连接取分别有 N 行和 M 行的两张表 T1 和 T2,返回包含所有 N*M 种可能连接行的连接表。对 T1 的每一行 R1,T2 的每一行 R2 都与 R1 连接,产生由 R1 和 R2 的所有字段组成的连接表行 JR。 CROSS JOIN 等价于 INNER JOIN ON TRUE。
{ T1 } [ NATURAL ] [[ INNER ] | [ { LEFT | RIGHT | FULL } [ OUTER ] ]] JOIN { T2 } {[ ON search condition] | [ USING ( join column list ) ]}
限定 JOIN 必须通过给出 NATURAL、ON 或 USING 三者之一(且仅一者) 来指定其连接条件。ON 子句接受一个 search condition(搜索条件),它与 WHERE 子句中的相同。 USING 子句接受一个逗号分隔的列名列表,这些列是两张被连接表所 共有的,连接在这些列相等的基础上进行。NATURAL 是 USING 子句的 简写形式,它列出两张表所有公共列名。USING 和 NATURAL 都有一个 副作用:每个被连接列只有一份副本会输出到结果表中(对比前面给出 的 JOIN 的关系代数定义)。
[ INNER ] JOIN
对 T1 的每一行 R1,连接表都为 T2 中满足与 R1 的连接条件的 每一行各包含一行。
对所有 JOIN 而言,单词 INNER 和 OUTER 都是可选的。 INNER 是默认值。LEFT、RIGHT 和 FULL 意味着 OUTER JOIN。
LEFT [ OUTER ] JOIN
首先执行 INNER JOIN。然后,对 T1 中不满足与 T2 任何行的 连接条件的每一行,额外返回一个连接行,其来自 T2 的列为 空值。
连接表无条件地为 T1 的每一行各包含一行。
RIGHT [ OUTER ] JOIN
首先执行 INNER JOIN。然后,对 T2 中不满足与 T1 任何行的 连接条件的每一行,额外返回一个连接行,其来自 T1 的列为 空值。
连接表无条件地为 T2 的每一行各包含一行。
FULL [ OUTER ] JOIN
首先执行 INNER JOIN。然后,对 T1 中不满足与 T2 任何行的 连接条件的每一行,额外返回一个连接行,其来自 T2 的列为 空值。 同样,对 T2 中不满足与 T1 任何行的连接条件的每一行,额外返回 一个连接行,其来自 T1 的列为空值。
连接表无条件地为 T1 的每一行和 T2 的每一行各包含一行。
所有类型的 JOIN 都可以串联或嵌套在一起, T1 和 T2 之一或两者都可以是 连接表。可以在 JOIN 子句周围使用圆括号来控制 JOIN 的顺序, 否则 JOIN 按从左到右的顺序处理。
SQL 提供聚合运算符 (例如 AVG、COUNT、SUM、MIN、MAX),它们接受一个表达式作为参数。该表达式在满足 WHERE 子句的每一行上求值,聚合运算符就在这组输入值上计算。通常,一个聚合为整个 SELECT 语句交付单个结果。但如果查询中指定了分组,则会对每个组的行分别计算,每个组交付一个聚合结果(见下一节)。
例 1.5. 聚合
如果我们想知道表 PART 中所有零件的平均价格,使用以下查询:
SELECT AVG(PRICE) AS AVG_PRICE
FROM PART;
结果是:
AVG_PRICE
-----------
14.5
如果我们想知道表 PART 中定义了多少个零件,使用语句:
SELECT COUNT(PNO)
FROM PART;
并得到:
COUNT
-------
4
SQL 允许把表的元组划分成组。然后就可以把上面描述的聚合运算符应用到这些组上——即聚合运算符的值不再是对指定列的所有值计算,而是对一个组的所有值计算。这样,聚合运算符会对每个组分别求值。
把元组划分成组的工作通过关键字 GROUP BY 后跟定义 组的属性列表来完成。如果有 GROUP BY A1, [tdot ], Ak, 我们就把关系划分为若干组,使得两个元组处于同一组当且仅当它们在所有 属性 A1, [tdot ], Ak 上 都一致。
例 1.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 和聚合函数的查询有意义,目标列表只能直接 引用被分组的那些属性。其他属性只能用在聚合函数的参数内部。否则, 其他属性就无法关联到一个唯一的值。
还要注意,请求聚合的聚合(例如 AVG(MAX(sno)))是没有意义的,因为 SELECT 只做一轮分组和聚合。你可以通过临时表或 FROM 子句中的子 SELECT 完成第一级聚合来得到这类结果。
HAVING 子句的作用与 WHERE 子句很像,用于只考虑满足 HAVING 子句中限定条件的那些组。本质上,WHERE 在分组和聚合之前过滤掉不需要的输入行,而 HAVING 在 GROUP 之后过滤掉不需要的组行。因此,WHERE 不能引用聚合函数的结果。另一方面,写不涉及聚合函数的 HAVING 条件毫无意义!如果你的条件不涉及聚合,不妨直接把它写进 WHERE,从而避免为反正要丢弃的组计算聚合。
例 1.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
在 WHERE 和 HAVING 子句中,凡是期望一个值的地方都允许使用子查询(子 SELECT)。此时该值必须先通过求值子查询得出。子查询的使用扩展了 SQL 的表达能力。
例 1.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 的当前元组。
使用子查询的一种略有不同的方式是把它们放到 FROM 子句中。这是一个 有用的特性,因为这种子查询可以输出多列多行,而用在表达式中的子查询 必须只给出单个结果。它还让我们无需借助临时表就能进行不止一轮的 分组/聚合。
例 1.9. FROM 中的子选择
如果我们想知道所有供应商中最高的平均零件价格,不能写成 MAX(AVG(PRICE)),但可以写成:
SELECT MAX(subtable.avgprice)
FROM (SELECT AVG(P.PRICE) AS avgprice
FROM SUPPLIER S, PART P, SELLS SE
WHERE S.SNO = SE.SNO AND
P.PNO = SE.PNO
GROUP BY S.SNO) subtable;
子查询为每个供应商返回一行(因为它有 GROUP BY),然后我们在外层 查询中对这些行进行聚合。
这些操作计算两个子查询所得元组的并集、交集和集合论差。
例 1.10. 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 < 3;
给出结果:
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
SQL 语言中包含一组用于数据定义的命令。
数据定义最基本的命令是创建新关系(新表)的命令。 CREATE TABLE 命令的语法为:
CREATE TABLEtable_name(name_of_attr_1type_of_attr_1[,name_of_attr_2type_of_attr_2[, ...]]);
例 1.11. 创建表
要创建供应商与零件数据库中定义的表, 使用以下 SQL 语句:
CREATE TABLE SUPPLIER
(SNO INTEGER,
SNAME VARCHAR(20),
CITY VARCHAR(20));
CREATE TABLE PART
(PNO INTEGER,
PNAME VARCHAR(20),
PRICE DECIMAL(4 , 2));
CREATE TABLE SELLS
(SNO INTEGER,
PNO INTEGER);
下面是 SQL 支持的一些数据类型的列表:
INTEGER:有符号全字二进制整数(31 位精度)。
SMALLINT:有符号半字二进制整数(15 位精度)。
DECIMAL(p[,q]): 有符号压缩十进制数,最多 p 位数字,其中小数点右边有 q 位数字。如果省略 q,则假定为 0。
FLOAT:有符号双字浮点数。
CHAR(n): 长度为 n 的定长 字符串。
VARCHAR(n): 最大长度为 n 的 变长字符串。
索引用来加快对关系的访问。如果关系 R 在属性 A 上有一个索引,那么检索所有满足 t(A) = a 的元组 t 时,所需时间大致与这类元组 t 的数量成正比,而不再与 R 的大小成正比。
要在 SQL 中创建索引,使用 CREATE INDEX 命令。其语法为:
CREATE INDEXindex_nameONtable_name(name_of_attribute);
例 1.12. 创建索引
要在关系 SUPPLIER 的属性 SNAME 上创建名为 I 的索引,使用以下语句:
CREATE INDEX I ON SUPPLIER (SNAME);
创建的索引会被自动维护,即每当有新元组插入关系 SUPPLIER 时,索引 I 都会随之调整。注意,存在索引时用户能察觉到的唯一变化是 SELECT 的速度提高和更新的速度降低。
视图可以被看作一张虚拟表,即一张在数据库中 并不物理存在、但在用户看来好像存在的表。相比之下, 当我们谈论基表时,表的每一行在物理存储中的 某处确实有一个物理存储的对应物。
视图没有自己的、物理上独立、可区分的存储数据。系统只是把视图的定义 (即如何访问物理存储的基表以物化该视图的规则)存储在系统目录的某个 地方(见 系统目录)。 关于实现视图的不同技术的讨论,参见 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 PNAME = 'Screw';
它将返回以下表:
SNAME | PNAME
-------+-------
Smith | Screw
为了计算这个结果,数据库系统必须先隐蔽地访问 基表 SUPPLIER、SELLS 和 PART。它通过对这些基表执行视图定义中给出的 查询来完成这一步。之后,再应用(针对视图的查询中给出的)附加限定条件, 即可得到结果表。
要销毁一个表(包括该表中存储的所有元组),使用 DROP TABLE 命令:
DROP TABLE table_name;
要销毁 SUPPLIER 表,使用以下语句:
DROP TABLE SUPPLIER;
要销毁一个索引,使用 DROP INDEX 命令:
DROP INDEX index_name;
最后,要销毁一个给定的视图,使用命令 DROP VIEW:
DROP VIEW view_name;
一旦创建了表(见 创建表),就可以用 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);
要更改关系中元组的一个或多个属性值,使用 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。
要从特定的表中删除元组,使用 DELETE FROM 命令。语法为:
DELETE FROMtable_nameWHEREcondition;
要删除表 SUPPLIER 中名为'Smith'的供应商,使用以下语句:
DELETE FROM SUPPLIER
WHERE SNAME = 'Smith';
在每个 SQL 数据库系统中,都使用 系统目录来记录数据库中定义了哪些表、视图、 索引等。这些系统目录可以像普通关系一样被查询。例如,有一个用于视图 定义的目录,该目录存储视图定义中的查询。每当对视图进行查询时,系统 首先从目录中取出视图定义查询并物化该视图, 然后再继续处理用户的查询(更详细的描述见 Simkovics, 1998 )。关于系统目录的更多信息,参见 Date, 1994 。
本节将概述如何把 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 。
译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。