5.1 查询数据

SQL语言提供了SELECT语句进行数据查询,该语句具有灵活的使用方式和丰富的功能。其一般格式如下:

SELECT [DISTINCT] <目标列表达式>[,…n]
FROM <表名或视图名> [,…n ]
[WHERE <条件表达式>]
[GROUP BY <列名1> [HAVING <条件表达式>] ]
[ORDER BY <列名2> [ASC | DESC] ]

整个SELECT语句的含义是,根据WHERE子句的条件表达式,从FROM子句指定的基本表或视图中找出满足条件的元组,再按SELECT子句中的目标列表达式,选出元组中的属性值形成结果表。如果有GROUP子句,则将结果按<列名1>的值进行分组,该属性列值相等的元组分为一个组,每个组产生结果表中的一条记录。通常会在每组中作用集函数。如果GROUP子句带HAVING短语,则只输出满足HAVING条件的组。如果有ORDER子句,则还要按<列名2>值的升序或降序对结果表进行排序。

SELECT语句既可以完成简单的单表查询,也可以完成复杂的连接查询和嵌套查询。本节将以第1章给出的5张表为基础,举例说明SELECT语句的各种用法。

5.1.1 简单查询

简单查询指的是仅涉及一张表的查询,例如,查询一张表中的某些列,查询一张表中满足给定条件的元组,等等。

1.最简单的查询

将SELECT语句的大部分可选成分省略后,就得到如下最简单的查询命令:

SELECT [DISTINCT] <目标列表达式> [,…n ]
FROM <表名或视图名>

最简单的查询命令可以对一张表中的某些列进行操作,实现下列查询功能。

(1)查询指定列

【例5-1】查询全体学生的姓名、学号和电话号码。

SELECT 姓名,学号,移动电话
FROM 学生表

注意,<目标列表达式>中各个列的先后顺序可以与表中的顺序不一致。也就是说,用户在查询时可以根据应用的需要改变列的显示顺序。

(2)查询所有列

SELECT语句用“*”表示表的所有列,常用来将表中的所有列按照表中的顺序全部输出。

【例5-2】查询全体学生的详细信息。

SELECT *
FROM 学生表

(3)查询计算列

SELECT语句中的<目标列表达式>不仅可以是表中的属性列,也可以是由常量、变量和函数构成的表达式,即可以将查询出来的属性列经过一定的计算后列出结果。

【例5-3】将每个学生的累计学分降低10%后显示出来。

SELECT 姓名,累计学分,累计学分- 累计学分*0.1
FROM 学生表

查询结果如下:

<目标列表达式>不仅可以是算术表达式,还可以是字符串常量、函数等。

(4)为列起别名

若要按照用户的习惯显示查询结果的列名,或者为无列名的计算列起一个名字,就要用到列的别名。为列起别名的方法有以下两种。

①用AS关键字。格式如下:

<目标列表达式> [AS] <别名>

注意,关键字AS是可选的。

②用SQL Server支持的“=”来连接别名和目标列表达式。格式如下:

<别名> = <目标列表达式>

【例5-4】将每个学生的累计学分降低10%后显示出来,要求查询结果表的标题用汉语显示。

SELECT 姓名 AS name,累计学分 Ogpa,Ngpa=累计学分- 累计学分*0.1
FROM   学生表

查询结果如下:

注意,当列的别名含有空格时要用单引号括起。例如:

SELECT 姓名 AS '学生表 Name',累计学分 'Old Gpa','New Gpa'=累计学分- 累计学
    分*0.1
FROM 学生表

(5)使用DISTINCT关键字消除重复元组

如果不指定DISTINCT关键字,SQL Server在执行SELECT语句时会搜索所有满足条件的元组,按照目标列表达式指定的列进行输出,而不管查询结果表中是否存在重复的元组。指定DISTINCT关键字后,SQL Server会自动消除结果表中的重复元组。

【例5-5】查询每个院系有在读学生的专业。

SELECT 所在院系,专业
FROM   学生表

查询结果如下:

上述查询结果表中包含了许多重复的元组,需要指定DISTINCT关键字进行消除:

SELECT DISTINCT 所在院系,专业
FROM   学生表

查询结果如下:

注意:DISTINCT关键字的作用范围是整个查询列表,而不是单独的某个列,因此应该紧跟在SELECT之后书写。例如,下列DISTINCT关键字的用法是错误的。

SELECT 所在院系,DISTINCT 专业
FROM   学生表

2.查询满足条件的元组

查询满足条件的元组是通过在WHERE子句中指定查询条件来实现的。在SQL Server中,查询条件是一个返回TRUE(真)、FALSE(假)或UNKNOWNUNKNOWN是由值为NULL的数据参与逻辑表达式运算所返回的结果。(未知)三种逻辑值的逻辑表达式。若一个元组使WHERE子句中的查询条件为真,则称该元组满足查询条件,否则称该元组违反查询条件。表5-1、表5-2和表5-3分别列出了NOT、AND和OR三种逻辑运算在各种情况下的结果。

表5-1 NOT运算情况

表5-2 AND运算情况

表5-3 OR运算情况

WHERE子句常用的查询条件如表5-4所示,下面逐一进行介绍。

表5-4 常用的查询条件

(1)比较大小

比较大小就是用比较运算符(θ)连接两个同类操作数(op1op2)来表达查询条件,

其一般格式如下:

op1 θ op2

其中,op1op2是由常量、变量、函数构成的算术表达式或字符串表达式;比较运算符θ包括

= 等于 <= 小于等于

> 大于 !=或<> 不等于

< 小于 !> 不大于

>= 大于等于 !< 不小于

【例5-6】查询来自杭州的所有学生。

SELECT *
FROM 学生表
WHERE 籍贯='杭州'

【例5-7】查询累计学分在160分以下的学生姓名和累计学分。

SELECT 姓名,累计学分
FROM 学生表
WHERE 累计学分<160

(2)确定范围

确定范围就是用三目运算符[NOT] BETWEEN AND连接三个同类操作数(op1op2op3)来表达查询条件,其一般格式如下:

op1 [NOT] BETWEEN op2 AND op3

其中,op1op2op3是由常量、变量、函数构成的算术表达式或字符串表达式。

上述查询条件的含义是:若op1(不)在op2op3之间,则条件为真,否则为假。与确定范围等价的查询条件见表5-5。

表5-5 与确定范围等价的查询条件

【例5-8】查询累计学分不在150和159之间的学生姓名和累计学分。

SELECT 姓名,累计学分
FROM 学生表
WHERE 累计学分 NOT BETWEEN 150 AND 159

【例5-9】查询姓名在“陈”和“李”之间的学生学号和姓名。

SELECT 学号,姓名
FROM 学生表
WHERE 姓名 BETWEEN '陈' AND '李'

查询结果如下:

由字符串定义的范围是根据字符内码的顺序确定的(一般按字典顺序)。

(3)确定集合

确定集合就是用运算符[NOT] IN连接两个操作数(op1op2)来表达查询条件,其一般格式如下:

op1 [NOT] IN op2

其中,op1是由常量、变量、函数构成的算术表达式或字符串表达式;op2是一个集合,在SQL语言中常表示为(e1,e2,…,en)的形式,其中e1,e2,…,en为集合的元素,它们可以是与op1同类型的常量、变量和函数构成的表达式。

上述查询条件的含义是:如果op1(不)是集合op2中的元素,则条件为真,否则为假。与确定集合等价的查询条件见表5-6。

表5-6 与确定集合等价的查询条件

【例5-10】查询来自杭州、宁波或温州的学生学号和姓名。

SELECT 学号,姓名
FROM 学生表
WHERE 籍贯 IN ('杭州','宁波','温州')

【例5-11】查询既不来自杭州,也不来自宁波的学生学号和姓名。

SELECT 学号,姓名
FROM 学生表
WHERE 籍贯 NOT IN ('杭州','宁波')

【例5-12】查询学号后两位是“09”,或者等于学号前两位或中间两位的学生学号和姓名。

SELECT 学号,姓名
FROM 学生表
WHERE SUBSTRING(学号,6,2) IN ('09',SUBSTRING(学号,2,2),SUBSTRING (学
    号,4,2) )

查询结果如下:

本例用到了SQL Server提供的取子串函数SUBSTRING(spc),其含义是返回字符串s中从第p个字符开始,长度为c的子串。在本例中,参与IN运算的操作数和集合元素是函数或常量。

(4)字符串匹配

字符串匹配就是用运算符[NOT] LIKE连接两个字符串操作数(s1s2)来表达查询条件,其一般格式如下:

s1 [NOT] LIKE s2 [ESCAPE '<换码字符>']

其中,s1s2是由常量、变量、函数构成的字符串表达式。s1称为主字符串,s2称为模式字符串(ESCAPE短语稍后介绍)。模式字符串除了包含普通字符外,还包含下列特殊字符(称为通配符):

% 匹配任意长度的字符串(长度可以为0)

_ 匹配任意一个字符

[c1c2…cn] 匹配字符c1,c2,…,cn中的一个。当c1,c2,…,cn连续时可简化为[c1-cn]

[^c1c2…cn] 匹配除c1,c2,…,cn外的一个字符。当c1,c2,…,cn连续时可简化为[^c1-cn]

上述查询条件的含义是:如果主字符串s1(不)与模式字符串s2相匹配,则条件为真,否则为假。字符串匹配常用来实现模糊查询。

【例5-13】查询姓名中第二个字为“鹏”的学生学号和姓名。

SELECT 学号,姓名
FROM 学生表
WHERE 姓名 LIKE '_鹏%'

【例5-14】查询学号长度不等于7,或者学号后6位含有非数字字符的学生学号和姓名。

SELECT 学号,姓名
FROM 学生表
WHERE 学号 NOT LIKE 'S[0-9][0-9][0-9][0-9][0-9][0-9]'

【例5-15】查询学号最后一位既不是“1”或“3”,也不是“9”的学生学号和姓名。

SELECT 学号,姓名
FROM 学生表
WHERE 学号 LIKE '%[^139]'

我们知道,模式字符串中的通配符(%、_、[、])已被赋予了特殊含义。要让某个通配符恢复原来的含义,就要用ESCAPE短语对通配符进行转义。

比如,要查课程名以“DB_”开头的课程信息。将模式串写成“DB_%”是错误的,因为此时找到的是课程名以“DB”开头,第三个字符任意的课程信息,显然不符合题意。例5-16用ESCAPE短语给出了解决办法。

【例5-16】查询课程名以“DB_”开头的课程信息。

SELECT *
FROM 课程表
WHERE 课名 LIKE 'DB\_%' ESCAPE '\'

ESCAPE '\'短语表示“\”为换码字符,这样模式串中紧跟在“\”后面的字符“_”不再具有通配符的含义,而被转义为普通的“_”字符。但“%”仍是通配符。

(5)涉及空值的查询

涉及空值的查询就是用运算符IS [NOT] NULL对一个表达式(exp)的值进行判断,判断它是否为NULL,其一般格式如下:

exp IS [NOT] NULL

其中,exp是由常量、变量、函数构成的表达式。

上述查询条件的含义是:如果exp的值(不)为空值,则条件为真,否则为假。

注意,与exp IS NOT NULL等价的查询条件是NOT exp IS NULL。

【例5-17】查询没有成绩的学号和开课计划编号。

SELECT 学号,开课号
FROM 选课表
WHERE 成绩 IS NULL

注意,“IS”不能用“=”代替。

【例5-18】查询有成绩的学号和开课计划编号。

SELECT 学号,开课号
FROM 选课表
WHERE 成绩 IS NOT NULL

注意,“IS NOT”不能用“!=”或“<>”代替。本例的查询条件等价于NOT成绩IS NULL。

(6)多重条件查询

多重条件查询就是用逻辑运算符NOT、AND、OR和括号将多个逻辑表达式连接起来,形成一个更为复杂的逻辑表达式,作为WHERE子句中的查询条件。在逻辑表达式中,括号的优先级最高,NOT次之,AND再次之,OR的优先级最低。

【例5-19】查询这样的男生,他的电话号码前3位是“130”,他来自杭州或者宁波,他既不主修电子商务专业,也不主修信息管理专业。

SELECT *
FROM 学生表
WHERE   性别 = '男' AND SUBSTRING(移动电话,1,3) = '130' AND
       ( 籍贯 = '杭州' OR 籍贯 = '宁波' ) AND
        NOT 专业 IN ('电子商务','信息管理')

3.对查询结果排序

如果没有指定查询结果的显示顺序,DBMS将按其最方便的顺序(通常是元组在表中的先后顺序)输出查询结果。用户也可以用ORDER BY子句指定按照一个或多个属性列的升序(ASC)或降序(DESC)重新排列查询结果,其中升序ASC为默认值。OEDER BY子句的语法如下:

ORDER BY {<排序列> [ASC | DESC]}[,…n]

其中,<排序列>用来定义排序所依据的列。可以按照多列的值进行排序,各列在ORDER BY子句中的先后次序决定了排序过程的优先级。例如,ORDER BY 姓名,年龄 DESC,表示对结果集先按姓名升序排列,姓名相同时按年龄降序排列。

【例5-20】查询选修了开课计划编号为“010101”的课程的学生学号和成绩,查询结果按分数降序排列。

SELECT 学号,成绩
FROM 选课表
WHERE 开课号 = '010101'
ORDER BY 成绩 DESC

可以使用列在SELECT子句中的顺序编号来指定排序列,例如,上例可以改写为

SELECT 学号,成绩
FROM 选课表
WHERE 开课号 = '010101'
ORDER BY 2 DESC

如果SELECT子句中使用了计算列,并且需要按照这个计算列进行排序,则在ORDER BY子句中可以采用三种方法来表示这个计算列:①这个计算列的表达式;②这个计算列的顺序编号;③这个计算列的别名。

【例5-21】查询选修了开课编号为“010101”的课程的学生学号、成绩,以及加了10分后的新成绩,查询结果按原成绩降序、按新成绩升序排列。

SELECT 学号,成绩,成绩+10 AS New成绩
FROM 选课表
WHERE 开课号 = '010101'
ORDER BY 成绩 DESC,成绩+10

上例ORDER BY子句中的成绩+10也可改写为“New成绩或3”。

也可根据SELECT子句中没有出现的列进行排序,此时不能用顺序编号来表示排序列。

【例5-22】查询选修了开课编号为“010101”的课程的学生学号,并按成绩降序排列。

SELECT 学号
FROM 选课表
WHERE 开课号 = '010101'
ORDER BY 成绩 DESC

5.1.2 统计

为了有效处理使用SQL查询得到的数据集合,SQL Server提供了一系列的统计函数,用来实现对数据集的汇总、求平均等各种运算。

1.常用的统计函数

表5-7列出了最常用的统计函数,其中DISTINCT表示统计时要剔除重复值。

表5-7 最常用的统计函数

这些函数常在SELECT子句中直接作为计算列或参与计算列的运算,对数据集进行一定的统计运算并返回结果。

【例5-23】查询所有课本的总价格和平均价格,以及打七折后的总价格和平均价格。

SELECT SUM(定价),AVG(定价),SUM(定价*0.7),AVG(定价*0.7)
FROM 课程表

查询结果如下:

关于本例有如下几条说明:

①这条查询语句搜索了课程表的所有行,但只返回一行结果。

②用统计函数表示的列和计算列一样,返回结果都没有列名,因此也可以像计算列一样指定列的别名,例如:

SELECT SUM(定价) AS 原总价,AVG(定价) AS 原均价,
      SUM(定价*0.7) 折扣总价,折扣均价=AVG(定价*0.7)
FROM 课程表

查询结果如下:

③统计列值为空的元组不参与统计计算。例如,课程表中含有4门课本价格为空的课程,它们不参与求总价和均价的计算。

如果结合WHERE子句来使用统计函数,则只有满足WHERE条件的行才参与统计。

【例5-24】查询课程编号前两位数字是“02”的课程所用课本的总价格和平均价格。

SELECT SUM(定价),AVG(定价)
FROM 课程表
WHERE 课号 LIKE 'C02%'

在统计函数中可以用DISTINCT关键字来剔除重复值。

【例5-25】查询至少选修了一门课程的学生总数。

SELECT COUNT(DISTINCT 学号)
FROM 选课表

在本例中,DISTINCT关键字使得一个学生只统计一次,而无论他选修了多少门课程。

COUNT(*)用来统计满足条件的元组个数。

【例5-26】查询课程编号前两位数字是“02”的课程总数。

SELECT COUNT(*)
FROM 课程表
WHERE 课号 LIKE 'C02%'

2.分组查询

以上关于统计函数的例子都是针对满足WHERE条件的查询结果集进行的统计。如果要先对查询结果集进行分组,然后再对每个组进行统计,就要用到GROUP BY子句了。GROUP BY子句可以将查询结果集按一列或多列取值相等的原则进行分组。含GROUP BY子句的查询称为分组查询。

(1)使用GROUP BY子句进行分组

SQL Server用GROUP BY子句对查询结果集进行分组,目的是为了细化统计函数的作用对象。如果未对查询结果集分组,统计函数将作用于整个查询结果集,即整个查询结果集只有一个统计值。否则,统计函数将作用于每个组,即每一个组都有一个统计值。GROUP BY子句的语法如下:

GROUP BY <分组列>[,…n]

【例5-27】查询各门课程的课程号及相应的选课人数。

SELECT 开课号,COUNT(学号)
FROM 选课表
GROUP BY 开课号

该SELECT语句先对选课表按开课号的取值进行分组,所有具有相同开课号值的元组被分为一组,然后用COUNT函数统计每一组的学生人数。

查询结果如下:

注意:

①GROUP BY子句中的列名只能是FROM子句所列表的列名,不能是列的别名。例如,下列查询是错误的:

SELECT 开课号 AS 开课计划编号,COUNT(学号)
FROM 选课表
GROUP BY 开课计划编号

②使用GROUP BY子句后,SELECT子句的目标列表达式所涉及的列必须满足:要么在GROUP BY子句中,要么在某个统计函数中。例如,下列查询是错误的:

SELECT 开课号,学号
FROM 选课表
GROUP BY 开课号

因为学号既不在GROUP BY子句中,也不在任何统计函数中。

(2)使用HAVING短语来筛选组

如果分组后还要求按一定的条件对这些组进行筛选,最终只输出满足指定条件的组,则可以使用HAVING短语指定筛选条件。

【例5-28】查询学号前5位为“S0601”且选修了两门以上(含两门)课程的学生学号。

SELECT 学号
FROM 选课表
WHERE 学号 LIKE 'S0601%'
GROUP BY 学号
HAVING COUNT(*)>=2

本例首先通过WHERE子句从选课表中求出学号前5位为“S0601”的学生。然后求其中每个学生选修了几门课,为此需要用GROUP BY子句按学号进行分组,再用统计函数COUNT对每个组的元组数进行计数。如果某一组的元组数大于等于2,则表示此学生选修了两门以上的课程,应将他的学号选出来。HAVING短语指定选择组的条件,只有满足条件(即元组数>=2)的组才会被选出来。

WHERE子句与HAVING短语的区别有二。一是作用对象不同。WHERE子句作用于FROM子句所列的表,从中选择满足条件的元组,而HAVING短语作用于组,从中选择满足条件的组。二是选择条件的构成有差异。WHERE条件不能直接包含统计函数,而HAVING条件所涉及的列必须要么在GROUP BY子句中,要么在某个统计函数中。

5.1.3 连接查询

到目前为止,我们所介绍的查询只涉及一张表,但大多数查询需要从多个相关的表中获取信息。例如,要查询选修了开课计划编号为“010101”的课程的学生姓名,就需要从选课表和学生表两张表中获取信息。如果将只涉及一张表的查询称为单表查询的话,那么涉及多张表的查询就称为多表查询或连接查询。

1.连接查询和单表查询的区别和联系

从形式上看,连接查询和单表查询的主要区别在于:单表查询的FROM子句只涉及一张表,而连接查询的FROM子句要涉及多张表。但是,如果我们将FROM子句中的多个表看成由这些表的笛卡尔积构成的一张大表,那么连接查询实际上就是这张大表上的单表查询,因此,连接查询和单表查询并没有本质的区别。

尽管如此,连接查询还是有一些特殊性的:

(1)由于不同表的某些列可能重名,故如果查询的某些子句涉及这些重名列,则需要在列名前加“<表名>.”作为限定。

(2)在连接查询中,如果没有WHERE子句,查询结果将是没有实际意义的笛卡尔积。为了避免这种情况,WHERE条件应包括必须的连接条件和可选的普通查询条件。

(3)在涉及n张表的连接查询中,至少应包括n−1个连接条件,否则,这n张表之间的某些位置将蜕化为没有实际意义的笛卡尔积。

【例5-29】查询学生的基本信息及其选课信息。

SELECT 学生表.*,开课号,成绩
FROM 学生表,选课表
WHERE 学生表.学号= 选课表.学号

【例5-30】查询选修了开课计划编号为“010101”的课程的学生学号和姓名。

SELECT 学生表.学号,姓名
FROM 学生表,选课表
WHERE 学生表.学号= 选课表.学号 AND 开课号 = '010101'

2.为FROM子句后的表起别名

为FROM子句后的表起别名的格式如下:

<表名> [AS] <别名>

为表起别名的目的有两个。

一是为了缩短涉及重名列的子句的书写。因为重名列前需要加“<表名>.”作为限定,如果<表名>很长,则涉及重名列的子句也会很长;如果为长的<表名>起一个短的别名,就可以用“<别名>.”作为重名列的限定,从而缩短涉及重名列的子句。

二是为了表达自身连接。自身连接指的是一张表和自己连接,也就是说,FROM子句含有多张相同的表。如果不起别名,重名列前的“<表名>.”限定将失效。为这些相同的表起不同的别名,就可以用“<别名>.”作为重名列的限定,从而解决这个问题。

【例5-31】查询至少选修了学号为“S060110”的学生所选一门课程的学生学号和姓名。

SELECT DISTINCT Z.学号,姓名
FROM 选课表 AS X,选课表 AS Y,学生表 AS Z
WHERE X.学号= 'S060110' AND Y.学号!=X.学号 AND Y.开课号 = X.开课号 AND Y.学
      号=Z.学号

3.使用JOIN和ON关键字

可以用JOIN和ON关键字将连接条件和普通查询条件分开。普通查询条件仍写在WHERE子句中。JOIN用于连接两张表,ON则用于给出这两张表的连接条件。用JOIN和ON表达的连接查询有如下格式:

SELECT子句
FROM <表名> {JOIN <表名> ON <连接条件> }[ …n ]
[WHERE <普通查询条件>]
[其他子句]

【例5-32】用JOIN和ON关键字实现例5-31的查询。

SELECT DISTINCT Z.学号,姓名
FROM 选课表 X JOIN 选课表 Y ON Y.学号!=X.学号 AND Y.开课号 = X.开课号
     JOIN 学生表 Z ON Y.学号=Z.学号
WHERE X.学号= 'S060110'

4.外连接

通常的连接查询只输出满足连接条件的元组。例如,在例5-29的结果表中没有关于S060109和S060308两个学生的信息,原因是他们没有选课,在选课表中没有相应的元组。有时要以学生表为主体列出每个学生的基本情况及其选课情况,对没有选课的学生则只输出基本情况,相应的选课信息为空,这就需要使用外连接(Outer Join)。

外连接又分为左外连接、右外连接和全外连接三种。相应地可将普通连接称为内连接。

左外连接是指在连接时要将左边关系中的未用元组配上空值加到结果集中。可以通过将连接条件中的“=”改成“*=”,或者将JOIN关键字改成LEFT OUTER JOIN来表示左外连接。

右外连接是指在连接时要将右边关系中的未用元组配上空值加到结果集中。可以通过将连接条件中的“=”改成“=*”,或者将JOIN关键字改成RIGHT OUTER JOIN来表示右外连接。

全外连接是指在连接时要将右边关系和右边关系中的未用元组都配上空值加到结果集中。可以通过将JOIN关键字改成FULL OUTER JOIN来表示全外连接。

此外,内连接除了可以用JOIN关键字表达外,还可以用INNER JOIN关键字进行表达。

【例5-33】查询学生的学号、姓名、籍贯信息及选课信息,分别以左外连接、右外连接和全外连接的形式显示。

(1)左外连接

SELECT 学生表.学号,姓名,籍贯,开课号,成绩
FROM 学生表,选课表
WHERE 学生表.学号*= 选课表.学号

这种外连接形式是基本SQL中的,建议使用下面的形式。

查询结果如下:

(2)右外连接

SELECT 学生表.学号,姓名,籍贯,开课号,成绩
FROM 学生表 RIGHT OUTER JOIN 选课表 ON 学生表.学号= 选课表.学号

(3)全外连接

SELECT 学生表.学号,姓名,籍贯,开课号,成绩
FROM 学生表 FULL OUTER JOIN 选课表 ON 学生表.学号= 选课表.学号

在本例中,由于选课表中没有未用元组,因此右外连接的结果和内连接相同,全外连接的结果和左外连接相同。

5.1.4 子查询

在SQL语言中,一条SELECT语句称为一个查询块。将一个查询块用圆括号括起,就表示由这个查询块返回的元组构成的集合。可以通过将这个集合放在另一个查询块的WHERE子句或HAVING短语中进行各种集合检查,来表达查询条件。这种查询称为嵌套查询,其中上层查询块称为父查询,下层查询块称为子查询。

对子查询结果集的检查包括①检查给定值是否在结果集中;②用给定值和结果集中的元素进行大小比较;③检查结果集是否为空。下面分别进行介绍。

1.检查给定值是否在结果集中

检查给定值是否在结果集中是指父查询与子查询之间用IN进行连接,判断父查询的某个属性列的值是否在子查询的结果中。

【例5-34】查询选修了课程名为“数据库原理”的学生学号和姓名。

SELECT 学号,姓名
FROM 学生表
WHERE 学号 IN (SELECT 学号
    FROM 选课表
    WHERE 开课号 IN (SELECT 开课号
        FROM 开课表
        WHERE 课号 IN (SELECT 课号
            FROM 课程表
            WHERE 课名='数据库原理'
        )
    )
)

本例首先从课程表中找出课程名为“数据库原理”的课程号集合,然后在开课表中找出这些课程号的开课计划号集合,接着在选课表中找出选修了这些开课计划号的学号集合,最后在学生表中找出与这些学号对应的学生姓名。

从本例我们可以看出嵌套查询的如下特点:

(1)嵌套查询的求解顺序是由内向外进行的。即子查询的求解先于父查询的求解,子查询的结果用于建立父查询的查询条件。

(2)嵌套查询使得可以用一系列简单查询构成复杂的查询,从而明显地增强了SQL的查询能力。子查询的嵌套层数没有限制。

(3)如果父查询与子查询用IN或比较运算符连接,则子查询的SELECT子句只能有一个列表达式。

(4)如果父查询与子查询用IN或比较运算符连接,则IN或比较运算符左边的列表达式和右边SELECT子句中的列表达式应具有相同的含义,否则得不到正确的结果。

2.用给定值和结果集中的元素进行大小比较

用给定值和结果集中的元素进行大小比较是指,父查询与子查询之间用比较运算符进行连接。这种比较又分为单值比较和多值比较两类。

(1)单值比较

当用户能确切知道子查询的结果集只包含一个值时,可以用比较运算符直接连接父查询的列表达式和子查询的结果集,实现其间的大小比较。例如,在例5-34中,若同名课程只有一个课程号,则课程号后的“IN”可以用“=”替代。

在SQL Server中,返回单值的子查询可以作为一个值参加任何合法的表达式运算。

【例5-35】查询累计学分比“胡汉民”多2分以上(含2分)的学生学号、姓名和累计学分。

SELECT 学号,姓名,累计学分
FROM  学生表
WHERE 累计学分 >= (SELECT 累计学分 FROM 学生表 WHERE 姓名 = '胡汉民' ) + 2

【例5-36】查询学生S060101的姓名和各门课程的平均成绩。

SELECT 姓名,(SELECT SUM(成绩) FROM 选课表 WHERE 学号='S060101')
    FROM 学生表
    WHERE 学号='S060101'

本例用两个返回单值的子查询分别计算学生S060101各门课程的总成绩和选修的课程门数,然后将这两个子查询相除而得的平均成绩作为最终结果表的第2列。

(2)多值比较

多值比较是指当子查询的结果集包含多个值时,用给定值和结果集中的某个值进行的比较。此时父查询与子查询之间要用比较运算符后缀ANY或ALL进行连接,其含义见表5-8。

表5-8 多值比较

【例5-37】查询累计学分比计算机专业和信息管理专业所有学生都低的学生名单。

SELECT 姓名
FROM 学生表
WHERE 专业 <> '计算机' AND 专业 <> '信息管理' AND
    累计学分 <ALL (SELECT 累计学分 FROM 学生表
        WHERE 专业 IN ('计算机' ,'信息管理')  )

本例也可以用统计函数实现:

SELECT 姓名
FROM 学生表
WHERE 专业 <> '计算机' AND 专业 <> '信息管理' AND
    累计学分 < (SELECT MIN(累计学分) FROM 学生表
        WHERE 专业 IN ('计算机' ,'信息管理')  )

事实上,用统计函数实现子查询通常比直接用ANY或ALL实现查询效率要高。ANY和ALL与统计函数的对应关系如表5-9所示。

表5-9 ANY和ALL与统计函数的对应关系

3.检查结果集是否为空

可以用关键字EXISTS来检查子查询的结果集是否为空,检查结果是逻辑值“真”或“假”。如果EXISTS检查返回“真”,则子查询结果集不空,否则子查询结果集为空。同理,也可以用关键字NOT EXISTS来检查子查询的结果集是否为空。如果NOT EXISTS检查返回“真”,则子查询结果集为空,否则子查询结果集不空。

【例5-38】查询选修了开课计划编号为010101的课程的学生姓名。

SELECT 姓名
FROM 学生表 AS S
WHERE EXISTS ( SELECT *
                FROM 选课表 AS E
                WHERE E.学号=S.学号 AND 开课号='010101'  )

这类子查询具有如下特点:

(1)子查询的条件往往要引用上层查询所涉及的表。例如,在例5-38中,子查询的WHERE子句引用了父查询涉及的表。父查询不允许引用子查询涉及的表。

(2)子查询的SELECT子句一般写成SELECT *即可,无须给出具体的列名。

5.1.5 联合查询

每一个SELECT语句都能获得一个或一组元组。若要把多个SELECT语句的结果合并为一个结果,可用UNION操作来完成。使用UNION将多个查询结果合并起来,形成一个完整的查询结果时,系统会自动去掉重复的元组。需要注意的是,参加UNION操作的各结果表的列数必须相同,对应列的数据类型也必须相同,结果表的列名取第1个SELECT语句定义的列名。

【例5-39】查询计算机专业和信息管理专业的学生信息。

SELECT *
FROM 学生表
WHERE 专业 ='计算机'
UNION
SELECT *
FROM 学生表
WHERE 专业 ='信息管理'