pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。
与大多数现代关系语言一样,SQL 基于元组关系演算。 因此,每个能用元组关系演算(或者等价地,关系代数)表述的查询也都能用 SQL 表述。不过,SQL 也有一些超出 关系代数或演算范围的能力。下面列出 SQL 提供的一些 不属于关系代数或演算的附加特性:
用于插入、删除或修改数据的命令。
算术能力:在 SQL 中可以引入 算术运算和比较,例如 A < B + 3. 注意 + 或其他算术运算符既不出现在关系代数中也不出现在关系演算中。
赋值和打印命令:可以打印由查询构造出的关系,也可以把计算得到的关系 赋给一个关系名。
聚合函数:可以对关系的列应用诸如平均值、 求和、最大值等操作, 以获得单一的量。
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 语句的复杂语法。示例所用的表定义于 供应商与零件数据库。
下面是一些使用 SELECT 语句的简单示例:
例 59.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 提供聚合运算符(例如 AVG、COUNT、SUM、MIN、MAX),它们接受属性名作为参数。聚合运算符的值是在整张表的指定属性(列)的所有值上计算的。如果查询中指定了组,则只在该组的值上计算(见下一节)。
例 59.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 上 都一致。
例 59.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 子句中的属性使用聚合函数。
HAVING 子句的作用与 WHERE 子句很像,用于只考虑满足 HAVING 子句中限定条件的那些组。HAVING 子句中允许的表达式必须涉及聚合函数。每个只使用普通属性的表达式都属于 WHERE 子句。另一方面,每个涉及聚合函数的表达式都必须放到 HAVING 子句中。
例 59.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 的表达能力。
例 59.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 的当前元组。
这些运算计算由两个子查询导出的元组的并集、交集和集合论差集。
例 59.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
The only tuple returned by both parts of the query is the one having $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[, ...]]);
例 59.10. 创建表
要创建供应商与零件数据库中定义的表, 使用以下 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 位在小数点右边。 (15 ≥ p ≥ qq ≥ 0). 如果省略 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);
例 59.11. 创建索引
要在关系 SUPPLIER 的属性 SNAME 上创建名为 I 的索引,使用以下语句:
CREATE INDEX I
ON SUPPLIER (SNAME);
创建的索引会被自动维护,即每当有新元组插入关系 SUPPLIER 时,索引 I 都会随之调整。注意,存在索引时用户能察觉到的唯一变化是速度的提高。
视图可以被看作一张虚拟表,即一张在数据库中 并不物理存在、但在用户看来好像存在的表。相比之下, 当我们谈论基表时,表的每一行在物理存储中的 某处确实有一个物理存储的对应物。
视图没有自己的、物理上分离且可区分的存储数据。相反,系统把视图的定义(即如何访问物理存储的基表以物化该视图的规则)存储在系统目录的某处(见 系统目录)。关于实现视图的不同技术的讨论,参见 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。它通过对这些基表执行视图定义中给出的 查询来完成这一步。之后,再应用(针对视图的查询中给出的)附加限定条件, 即可得到结果表。
要销毁一个表(包括该表中存储的所有元组),使用 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 数据库系统中,都使用 系统目录来记录 数据库中定义了哪些表、视图、索引等。这些系统目录可以像普通关系一样查询。例如有一个目录用于视图的定义。该目录存储视图定义中的查询。每当对视图发起查询时,系统先从目录中取出视图定义查询并物化该视图,然后再处理用户的查询(见 SIM98,那里有更详细的描述)。关于系统目录的更多信息,参见 DATE。
本节将概述如何把 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 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。