Skip to content

6. 高级 SQL

6.1. 简介

在本章中,我们将介绍

  • SELECT 语句的更多语法,这些语法使其成为一个非常强大的查询命令,特别是在同时查询多个表时。
  • 已介绍命令的扩展语法

为说明各种订单类型,我们将使用以下表,这些表用于一家中小型图书分销公司的订单管理:

6.1.1. CLIENTS 表

该表存储了公司客户的相关信息:

 

Image

ID
客户的唯一标识符——主键
姓名
客户姓名
状态
I=个人,E=企业,A=政府
名字
个人名字
联系方式
客户所在地(如为公司或政府机构)的联系人姓名
街道
客户地址 - 街道
城市
城市
邮编
邮编
电话
电话
您从什么时候开始成为我们的客户了?
债务人
如果客户欠公司钱,请选“Y”(是);否则请选“N”(否)。

6.1.2. ARTICLES 表

该表存储已售产品的信息,本例中为书籍。其结构如下:

Image

ISBN
书籍的唯一标识符(ISBN = 国际标准书号)——主键
书名
书籍的标题
出版社代码
用于唯一标识出版商的代码
作者
作者姓名
摘要
书籍简介
数量
今年销量
QTEANPREC
上一年度销量
最近一次销售
上次销售日期
QTERECUE
上次交货数量
LASTDELIVERY
上次交货日期
销售价格
售价
成本
进货成本
MINCDE
最低订购量
MINSTOCK
最低库存水平
QTESTOCK
库存数量

其内容可能如下:

Image

6.1.3. ORDERS 表

该表存储客户下单的相关信息。其结构如下:

Image

NOCMD
订单的唯一标识符——主键
CUSTID
该订单的客户ID - 外键 - 引用 CUSTOMERS(ID)
ORDER_DATE
本订单的录入日期
已取消
若订单已被取消,则为 O(是);否则为 N(否)。

Image

6.1.4. DETAILS 表

该表包含订单的详细信息,即所订购书籍的书名和数量。其结构如下:

Image

NOCMD
订单编号——外键,引用 COMMANDES 表的 NOCMD 列
ISBN
已订购图书编号 - 引用BOOKS表中ISBN列的外键
QTE
订购数量

其内容可能如下:

Image

上文可见,订单编号 3(NOCMD)涉及三本书。这意味着客户同时订购了三本书。该客户的记录可在 [ORDERS] 表中找到,其中显示订单编号 3 由客户编号 5 下达。而 [CUSTOMERS] 表显示,客户编号 5 是位于塞格雷的 NetLogos 公司。

6.2. SELECT 语句

在此,我们将通过介绍新的语法来加深对 SELECT 语句的理解。

6.2.1. 多表查询的语法

语法
SELECT 列1, 列2, ...
FROM 表1, 表2, ..., 表p
WHERE 条件
ORDER BY ...
操作
此处的创新之处在于,列 column1、column2、... 来自多个表 table1、table2、...。如果两个表中有同名的列,则使用 tablei.columnj 的表示法来消除歧义。条件可以应用于来自不同表的列。

工作原理

1
构建表 table1、table2、...、tablep 的笛卡尔积。若 n_i 表示表 table_i 的行数,则生成的表将包含 n₁*n₂*...*n_p 行,其中包含来自不同表的所有列。
2
WHERE 条件应用于该表。由此生成一个新表
3
该表将根据 ORDER 子句中指定的方法进行排序。
4
将显示 SELECT 语句后指定的列。

示例

我们使用前面展示的表格。我们想查看9月25日之后下单的订单详情:

SQL>select details.nocmd,isbn,qte from commandes,details
  where commandes.datecmd>'25-sep-91'
  and details.nocmd=commandes.nocmd

Image

请注意,在 FROM 之后,我们需要列出所有被引用的列所属的表名。在上一个示例中,所选列均属于 DETAILS 表。然而,条件语句引用了 ORDERS 表。因此,需要在 FROM 之后列出后者。用于测试两个不同表中列是否相等的操作通常被称为等值连接。

该 SELECT 查询也可以写成如下形式:

SQL> select details.nocmd,isbn,qte from commandes
    inner join details on details.nocmd=commandes.nocmd
    where commandes.datecmd>'25-sep-91'

让我们继续看示例。我们希望得到与之前相同的结果,但显示的是所订购书籍的书名,而不是其ISBN:

SQL>select commandes.nocmd, articles.titre, details.qte
  from commandes,articles,details
  where commandes.datecmd>'25-sep-91'
  and details.nocmd=commandes.nocmd
  and details.isbn=articles.isbn

Image

使用以下 SQL 查询也可得到相同的结果,但可读性较低:

SQL> select details.nocmd,articles.titre,details.qte from details
    inner join commandes on details.nocmd=commandes.nocmd
    inner join articles on  details.isbn=articles.isbn
    where commandes.datecmd>'25-sep-91'

上文对 [DETAILS] 表执行了两次内连接:

  • 一次与 [ORDERS] 表进行内连接,以获取某本书的订购日期
  • 另一个与 [ARTICLES] 表进行内连接,以获取所订购书籍的书名

我们还希望获取下单客户的姓名:

SQL>select commandes.nocmd, articles.titre, qte ,clients.nom
  from commandes,details,articles,clients
  where commandes.datecmd>'25-sep-91'
  and details.nocmd=commandes.nocmd
  and details.isbn=articles.isbn
  and commandes.idcli=clients.id

Image

我们还希望获取订单日期,并按降序显示这些日期:

SQL>select commandes.nocmd, commandes.datecmd, articles.titre, qte ,clients.nom
    from commandes,details,articles,clients
    where commandes.datecmd>'25-sep-91'
    and details.nocmd=commandes.nocmd
    and details.isbn=articles.isbn
    and commandes.idcli=clients.id
    order by commandes.datecmd descending

Image

创建连接时,请遵循以下几条规则:

  1. 在 SELECT 之后,列出要显示的列。如果该列存在于多个表中,请在列名前加上表名。
  2. 在 FROM 之后,列出所有将被 SELECT 语句查询的表,即包含 SELECT 和 WHERE 之后所列列的表。

6.2.2. 自连接

我们希望查找零售价高于《Using SQL》一书的书籍:

SQL>select a.titre from articles a, articles b
  where b.titre='Using SQL'
  and a.prixvente>b.prixvente

Image

此处的两个表在连接中是相同的:articles 表。为了区分它们,我们给它们起了别名:from articles a, articles b。第一个表的别名是 a,第二个表的别名是 b。即使表不同,也可以使用这种语法。使用别名时,必须在整个 SELECT 语句中使用该别名来代替它所指代的表。

6.2.3. 外连接

我们希望找出9月份进行过购买的客户,并显示其订单日期。其余客户则不显示该日期:

SQL>select clients.nom,commandes.datecmd from clients
    left outer join commandes  on clients.id=commandes.idcli
    where datecmd between '01-sep-91' and '30-sep-91'

Image

令人惊讶的是,这里我们并没有得到正确的结果。按理说,[CLIENTS] 表中的所有客户都应该出现在查询结果中,但实际情况并非如此。 当我们思考外连接的工作原理时,就会意识到:那些尚未下单的客户被匹配到了ORDERS表中的一行空记录,因此对应的日期为空(在SQL术语中称为NULL值)。这个日期不符合日期条件,所以相应的客户就没有显示出来。让我们尝试另一种方法:

SQL>select clients.nom,commandes.datecmd from clients
    left outer join commandes  on clients.id=commandes.idcli
    where (commandes.datecmd between '01-sep-91' and '30-sep-91')
        or (commandes.datecmd is null)

Image

这次,我们得到了问题的正确答案。

6.2.4. 嵌套查询

语法
SELECT 列[s] FROM 表[s]
WHERE 表达式 查询运算符
ORDER BY ...
工作原理
查询是一个 SELECT 语句,它返回 0 个、1 个或多个值的集合。然后我们有一个类型为
表达式 运算符 (val1, val2, ..., vali)
表达式和 vali 必须是同一种类型。如果查询返回单个值,则条件简化为
表达式 运算符 值
,这是我们所熟悉的。如果查询返回一组值,我们可以使用以下运算符:
IN
表达式 IN (val1, val2, ..., vali):如果表达式求值为列表 vali 中的某个元素,则返回 true
NOT IN
IN 的反义词
ANY
必须由 =、!=、>、>=、<、<= 引出
表达式 >= ANY (val1, val2, .., valn):表达式大于等于列表中任意一个值 vali,则返回 true
ALL
必须以 =、!=、>、>=、<、<= 开头
表达式 >= ALL (val1, val2, .., valn):如果表达式大于等于列表中的所有有效值,则返回 true
EXISTS
查询:若查询返回至少一行,则为真。

示例

让我们重新审视那个已经通过等值连接解决的问题:显示售价高于《Using SQL》一书的书名。

SQL>select titre from ARTICLES
    where prixvente > (select prixvente from ARTICLES where titre='Using SQL')

Image

这种解决方案似乎比等值连接更直观。我们先使用 SELECT 语句进行初始筛选,然后对结果集进行第二次筛选。通过这种方式,我们可以依次执行多个筛选操作。

我们希望找出售价高于平均售价的书名:

SQL> select titre from ARTICLES
    where prixvente > (select avg(prixvente) from ARTICLES)

Image

哪些客户订购了上一条查询返回的书目?

SQL>select distinct idcli from COMMANDES,DETAILS
    where DETAILS.isbn in
    (select isbn from ARTICLES where prixvente
        > (select avg(prixvente) from ARTICLES))
    and COMMANDES.nocmd=DETAILS.nocmd

Image

说明

  1. 我们从 DETAILS 表中筛选出价格高于平均书价的书籍所对应的 ISBN 码。
  2. 在上一步骤选出的行中,不包含客户 ID(IDCLI)。该字段位于 ORDERS 表中。通过订单号(NOCMD)建立两张表之间的关联,因此连接条件为 ORDERS.nocmd=DETAILS.nocmd。
  3. 单个客户可能多次购买了相关书籍中的某一本,这种情况下其 IDCLI 代码会多次出现。为避免这种情况,我们在 SELECT 语句后添加了 DISTINCT 关键字。DISTINCT 通常会从 SELECT 查询返回的行中消除重复项。
  4. 若要检索客户姓名,我们需要在 ORDERS 和 CUSTOMERS 表之间进行额外的连接,如下面的查询所示。
SQL> select distinct CLIENTS.nom from COMMANDES,DETAILS,CLIENTS
    where DETAILS.isbn in
    (select isbn from ARTICLES where prixvente
        > (select avg(prixvente) from ARTICLES))
    and COMMANDES.nocmd=DETAILS.nocmd
    and COMMANDES.IDCLI=CLIENTS.ID

Image

查找自9月24日以来未下单的客户:

SQL>select nom from CLIENTS
    where clients.id not in
    (select distinct commandes.idcli from commandes where datecmd>='24-sep-91')

Image

我们已经看到,除了使用 WHERE 子句外,还可以通过将 HAVING 子句与 GROUP BY 子句结合使用来过滤行。HAVING 子句用于过滤行组

与 WHERE 子句类似,其语法


     HAVING expression opérateur requête 

是可行的,但需遵守前述限制:该表达式必须是该子句中 expri 表达式之一


     GROUP BY expr1, expr2, ...

示例

售价超过 200F 的书籍销量是多少?

首先,让我们按书名显示销量:

SQL>select ARTICLES.titre,sum(qte) QTE from ARTICLES, DETAILS
    where DETAILS.isbn=ARTICLES.isbn
    group by titre

Image

现在,让我们筛选书名:

SQL> select ARTICLES.titre,sum(qte) QTE from ARTICLES, DETAILS
    where DETAILS.isbn=ARTICLES.isbn
    group by titre
    having titre in (select titre from ARTICLES where prixvente>200)

Image

或许更直观的是,我们可以这样写:

SQL>select ARTICLES.titre,sum(qte) QTE from ARTICLES, DETAILS
    where DETAILS.isbn=ARTICLES.isbn
    and ARTICLES.prixvente>200
    group by titre

Image

6.2.5. 嵌套查询

在嵌套查询中,存在父查询(最外层的查询)和子查询(最内层的查询)。只有在子查询完全执行完毕后,才会对父查询进行评估。

关联查询的语法与嵌套查询相同,但存在以下细微差别:子查询会对父查询的表执行连接操作。在这种情况下,父子查询对会针对父表中的每一行反复进行求值。

示例

让我们重新审视那个想要查询自9月24日以来未下单的客户姓名的示例:

SQL> 
select nom from clients
    where not exists
        (select idcli from commandes
            where datecmd>='24-sep-91'
                and commandes.idcli=clients.id)

Image

父查询操作的是 customers 表。子查询在 customers 表和 orders 表之间执行连接操作。因此,这是一个相关查询。对于 clients 表中的每一行,子查询都会运行:它会在 9 月 24 日之后下单的订单中搜索该客户的 ID。如果未找到(not exists),则显示该客户的姓名。然后,它会转到 clients 表中的下一行。

6.2.6. 编写 SELECT 语句的准则

我们多次看到,使用不同的 SELECT 语句可以得到相同的结果。让我们看一个例子:显示下过订单的客户:

连接

SQL>
select distinct nom from clients,commandes
    where clients.id=commandes.idcli

Image

嵌套查询

SQL> 
select nom from clients
    where id in (select idcli from commandes)

返回相同的结果。

相关查询

SQL>
select nom from clients
    where exists (select * from commandes where commandes.idcli=clients.id)

返回相同的结果。

作者克里斯蒂安·马雷(Christian MAREE)和盖伊·莱丹(Guy LEDANT)在其著作《SQL:入门、编程与精通》中提出了一些筛选标准:

性能

用户并不知道数据库管理系统(DBMS)是如何“处理”并找到他们所请求的结果的。因此,只有通过实践经验,他们才能发现某个查询比另一个更高效。马雷和勒丹根据经验断言,相关查询通常比嵌套查询或连接查询显得更慢。

表述

使用嵌套查询的表达通常比连接更易读且更直观。然而,这种方法并非总是适用。特别需要注意以下两点:

  • 包含 SELECT 子句中指定列(SELECT col1, col2, ...)的表必须列在 FROM 关键字之后。随后将对这些表执行笛卡尔积运算,这被称为连接。
  • 当查询显示来自单个表的结果,且筛选该表的行需要参考另一个表时,可以使用嵌套查询。

6.3. 语法扩展

为方便起见,我们此前主要介绍了各种命令的简写语法。在本节中,我们将介绍它们的完整语法。这些语法不言自明,因为它们与广为研究的 SELECT 命令的语法类似。

INSERT

语法1
INSERT INTO 表 (列1, 列2, ...) VALUES (值1, 值2, ...)
语法2
INSERT INTO 表 (col1, col2, ..) (查询)
说明
已介绍了这两种语法

DELETE

语法1
DELETE FROMWHERE 条件
说明
此语法众所周知。请注意,条件中可能包含使用 WHERE 表达式 运算符 (查询) 语法的查询

UPDATE

语法1
UPDATE
SET 列1=表达式1, 列2=表达式2, ...
WHERE 条件
说明
该语法已在前文介绍过。请注意,条件中可以包含使用 WHERE 表达式 运算符 (查询) 语法的查询
语法2
UPDATE
SET (col1, col2, ..) = query1, (cola, colb, ..) = query2, ...
WHERE 条件
说明
分配给各列的值可能来自一个查询。