SQL 语言

正如大多数现代的关系语言一样,SQL 是基于记录关系微积分的。结果是每个可以用记录关系微积分表示的查询(相等地,或者是关系代数),同样也可以用SQL 表示。不过,SQL 还有一些超出关系代数或者微积分的能力。下面是一个SQL 提供的并非关系代数或者关系微积分的内容的一个列表:

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 语句复杂的语法。用于这些例子的表在 供应商和部件数据库 里定义。
 

简单的 Selects

这里是一些使用 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 语句对应关系代数里面的 "projection" (映射),而不是 "selection"(选择)(参阅 关系代数  获取详细信息)。

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 是第二个列的新名字。这个技巧可以用于目标列表里的每个元素,给它们赋予一个在结果列中显示的新的标题。这个新的标题通常称为别名。这个别名不能在该查询的其他地方使用。

Joins(联接)

下面的例子显示了 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)。聚集操作符的结果是对整个表中的所有声明的属性(列)的值进行计算的结果。如果在一个查询里面声明了组,那么这种(聚集)计算将只发生在一个组的值上(见下节)。
 
例 69-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 后面跟着一个定义组的构成的属性列表。如果我们使用语句 GROUP BY A1, &tdot, Ak,我们把关系分成了组,这样如果两条记录在(也只是在)所有属性 A1, &tdot, 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 子句里面的属性使用聚集函数。

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

子查询

在 WHERE 和 HAVING 子句里,允许在任何要产生数值的地方使用子查询(子选择)。这种情况下,该值来自首先对子查询的计算。子查询的使用扩展了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);
在我们的例子里,结果列将是空的,因为每个供应商至少销售一个部件。请注意我们在 WHERE 子句的内层 SELECT 里使用了来自外层 SELECT 的 S.SNO。正如前面所说的,子查询为每个外层查询计算一次,也就是说,S.SNO 的值总是从外层 SELECT 的实际记录中取得的。

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
The only tuple returned by both parts of the query is the one having $SNO=2$.(两个查询都会返回的记录是那条 $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 TABLE 命令的语法如下:
 
   CREATE TABLE table_name
                (name_of_attr_1 type_of_attr_1
                 [, name_of_attr_2 type_of_attr_2 
                 [, ...]]);
例 69-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 里的数据类型

下面是 SQL 一些数据类型的列表:
    INTEGER: 有符号全长二进制整数(31位精度)。

    SMALLINT: 有符号半长二进制整数(15位精度)。

    DECIMAL (p[,q]): 有符号封装的十进制小数,共 p 位数,并假设有 q 位数在小数点右边。(15 ≥ pqq ≥ 0)。如果省略 q,则认为是 0。

    FLOAT: 有符号双字浮点数。

    CHAR(n): 长度为 n 的定长字符串。

    VARCHAR(n): 最大长度为 n 的变长字符串。

Create Index

索引用于加速对关系的访问。如果关系 R 有一个建立在属性 A 上的索引,那么我们检索满足 t(A) = a 的所有记录 t 用的时间和这样的记录数 t 成比例,而不是与 R 的大小成比例。

在 SQL 里创建一个索引,使用 CREATE INDEX 命令。语法是:

   CREATE INDEX index_name 
   ON table_name ( name_of_attribute );
例 69-11. 创建索引

要在关系 SUPPLIER 的属性 SNAME 上创建一个名为 I 的索引,我们使用下面语法:

   CREATE INDEX I
   ON SUPPLIER (SNAME);
所创建的索引是自动维护的,也就是说,当向关系 SUPPLIER 插入新的记录时,索引 I 相应做调节。请注意有索引存在时,用户能感觉到的唯一的变化就是速度的提升。

Create View(创建视图)

一个视图可以看作一个虚拟表,也就是说,数据库里的一个物理上不存在的,但是用户看起来却存在的表。与之相比,当我们谈到一个基本表时,则是在物理存储中的确物理地存放着表中每一行的内容。

视图没有它们自身的,物理上分开的,可区分的存储区。实际上,系统把视图的定义(也就是说,为物化试图应如何访问物理上存储在基本表中内容的规则)存放在系统表里的某个地方(参阅 系统表)。关于实现视图的不同技巧,请参考 SIM98

在 SQL 里,使用 CREATE VIEW 命令定义视图。语法是:

   CREATE VIEW view_name
   AS select_stmt
这里 select_stmt 是一个与定义在 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 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;

数据操作

Insert Into(插入)

一旦表创建完成(参阅 Create Table),就可以用命令 INSERT INTO 向里面填充记录。语法是:
   INSERT INTO table_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(更新)

要修改记录中的一个或者多个属性的值,使用 UPDATE 命令。语法是:
   UPDATE table_name
   SET name_of_attr_1 = value_1 
       [, ... [, name_of_attr_k = value_k]]
   WHERE condition;
要该表关系 PART 中部件 'Screw' 的属性 PRICE 的值,我们使用:
   UPDATE PART
   SET PRICE = 15
   WHERE PNAME = 'Screw';
记录名为 'Screw' 的属性 PRICE 的新值现在是 15。

Delete

要从一个表中删除一条记录,使用 DELETE FROM 命令。语法是:
   DELETE FROM table_name
   WHERE condition;
要删除 SUPPLIER 表中名为 'Smith' 的供应商,使用下面的语句:
   DELETE FROM SUPPLIER
   WHERE SNAME = 'Smith';

系统表

在所有 SQL 数据库系统里面,系统表都被用于跟踪表,视图索引等在数据库中定义的东西。这些系统表可以象普通关系一样检索。例如,有一个系统表用于视图的定义。这个表存储来自视图定义的查询。每当对视图进行查询时,系统在处理用户查询之前首先从该表中取出视图定义查询并且执行该查询(参阅 Simkovics, 1998 获取更详细的描述)。关于系统表的更多信息,请参考 Date, 1994

嵌入的 SQL

在这一节里,我们将勾勒如何把 SQL 嵌入到一个宿主语言里(例如,C)。我们需要从一种宿主语言里使用SQL 的原因主要有两个: 一个在宿主语言里面使用嵌入 SQL 的程序包含宿主语言的语句和嵌入 SQLESQL)的语句。每条ESQL 语句以关键字 EXEC SQL 开始。ESQL 语句被一个预编译器(它通常向源文件中插入库过程的调用以执行各种SQL 命令)转换成宿主语言。

当我们观察例子 Select 时,我们认识到一个查询的结果大多是记录的集合。大多数宿主语言不是为集合而设计的,因此我们需要一个机制用来访问一条 SELECT 语句返回结果集中的每一条记录。这个机制可以通过定义一个游标来实现。之后我们就可以使用 FETCH 命令检索一条记录并把游标定义到下一条记录。

关于嵌入 SQL 的详细讨论,请参考 Date and Darwen, 1997Date, 1994,或 Ullman, 1988