主要内容
数据查询
数据查询是数据库的核心操作,该篇将着重介绍数据查询的五种方式:单表查询、连接查询、嵌套查询、集合查询和基于派生表的查询。
基本语法
一般格式(尖括号“<>”表示必填,“[]”表示可选,“|”用于区别不同选择):
SELECT [ ALL | DISTINCT ] <目标列表达式> [,<其余目标列表达式>]
FROM <表名或视图名> [,<其余表名或视图名>]
[ WHERE <条件表达式> ]
[ GROUP BY <列名> [ HAVING <条件表达式> ] ]
[ ORDER BY <列名> [ ASC | DESC ] ] ;
(1)整个SELECT语句的含义是,根据WHERE子句的条件表达式,从FROM子句指定的范围(基本表、视图或派生表)中找出满足条件的元组,再按SELECT子句中的目标列表达式选出元组中的属性值形成结果表。
(2)形成的结果表可能会含有重复行,若SELECT子句中指定ALL关键词,则保留结果表中的所有重复行。反之,若指定DISTINCT关键词,则去掉重复行。默认为ALL。
(3)GROUP BY子句用于对查询结果(执行SELECT-FROM语句后的结果)按属性值(同一列或几列相应的值)相等的规则进行分组,随后可用HAVING子句对划分好的组依组处理。
(4)ORDER BY子句用于对查询结果根据某一列或几列的属性值,按升序(ASC)或降序(DESC)排列。
可能看完上面这一坨拗口(虽然我已经很努力在概括了)的句子后,大家还是云里雾里的。不用担心,后面会继续作介绍。
单表查询
单表查询是指仅涉及一个表的查询。
1. 选择表中的若干列
SELECT-FROM语句对应关系代数的“投影”(我是链接)
(1)指定查询
如查询全体学生的学号与名字:
SELECT Sno,Sname
FROM Student;
(2)全部查询
如查询全体学生的详细信息(学号,姓名,性别,年龄,院系):
SELECT Sno,Sname,Ssex,Sage,Sdept
FROM Student;
SQL提供了用于全选语法糖,如果你允许结果表中属性列的排列顺序与原表相同,则上面的语句等价于
SELECT *
FROM Student;
(3)表达式查询
如查询学生姓名、出生年份(假设今年是2018年)和所在院系,并且所在院系用小写字母表示:
SELECT Sname, 'Year of Birth:', 2018-Sage BIRTHDAY, LOWER(Sdept) DEPARTMENT
FROM Student;
分析:
'Year of Birth:' —— 字符串常量直接输出
2018-Sage BIRTHDAY —— 算术表达式求出生年份,并且该列属性名取别名为“BIRTHDAY”
LOWER(Sdept) DEPARTMENT —— 使用SQL函数LOWER()实现字母转换成小写的功能,并取别名为“DEPARTMENT”
2. 选择表中的若干元组
(1)对重复行的处理判断
SQL语句先处理列,再处理行。两个本来不完全相同的元组,在投影到某些列后,可能会出现重复行,若指定DISTINCT关键词,重复行会被消除,若指定ALL关键词,则重复行被保留,系统默认为ALL。
(2)查询满足条件的元组
SELECT-FROM-WHERE语句对应关系代数的“选择”。
查询条件 | 谓词 |
比较 | [NOT]比较运算符 |
确定范围(包括端点) | [NOT] BETWEEN AND |
确定集合 | [NOT] IN |
字符匹配 | [NOT] LIKE |
空值 | IS NULL,IS NOT NULL |
多重条件 | AND,OR,NOT |
//查询年龄在20~23岁之间(包括20和23岁)的学生的姓名:
SELECT Sname
FROM Student
WHERE Sage BETWEEN 20 AND 23;
//查询计算机科学(CS)、数学系(MA)的学生姓名:
SELECT Sname
FROM Student
WHERE Sdept IN ('CS','MA');
//查询无成绩的学生的姓名和学号:
SELECT Sname,Sno
FROM Student
WHERE Grade IS NULL;
(3)字符匹配
在字符匹配中,“LIKE”等价于“=”。
此外,在字符匹配中涉及到两个通配符(%和_)和换码字符短语(ESCAPE '\')
%代表任意长度的字符串,例如a%b,表示以a为开头,b为结尾的字符串;
_代表任意单个字符;
ESCAPE '\'表示“\”后紧跟的字符“%”或“_”不作为通配符使用。
//查询所有姓刘的学生的姓名和学号:
SELECT Sname,Sno
FROM Student
WHERE Sname LIKE '刘%';
//查询姓“欧阳”且名字只有三个字的学生的姓名和学号:
SELECT Sname,Sno
FROM Student
WHERE Sname LIKE '欧阳_';
//查询DB_Design课程的课程号和学分:
SELECT Cno,Ccredit
FROM Student
WHERE Cname LIKE 'DB\_Design' ESCAPE '\';
3. ORDER BY子句
//查询选修了3号课程的学生的学号及其成绩,查询结果按分数的降序排序:
SELECT Sno,Grade
FROM SC
WHERE Cno = '3'
ORDER BY Grade DESC;
需要留意的是,对于空值,排序时显示的顺序由具体系统来决定。
4. 聚集函数
COUNT(*) | 统计元组个数 |
COUNT( [ DISTINCT | ALL ] ) | 统计一列中值的个数 |
SUM( [ DISTINCT | ALL ] ) | 计算一列值的总和 |
AVG( [ DISTINCT | ALL ] ) | 计算一列值的平均值 |
MAX( [ DISTINCT | ALL ] ) | 求一列值中的最大值 |
MIN( [ DISTINCT | ALL ] ) | 求一列值中的最小值 |
注意点:
(1)指定DISTINCT关键词,表示计算时要忽略重复值。如果不指定DISTINCT,则默认为ALL关键词,将重复值计算在内。
(2)WHERE子句不能用聚集函数作为条件表达式,聚集函数只能在SELECT子句和HAVING子句中使用。WHERE子句与HAVING子句的区别在于作用对象不同。WHERE子句作用于基本表或视图,从中选择满足条件的元组,而HAVING子句作用于分组,从中选择满足条件的组。
(3)除COUNT(*)外,其他聚集函数遇到空值后都会自动跳过。
//查询学生总人数:
SELECT COUNT(*)
FROM Student;
//查询选修了课程的学生人数:
SELECT COUNT(DISTINCT Sno)
FROM SC;
//计算选修1号课程的学生平均分:
SELECT AVG(Grade)
FROM SC
WHERE Cno = '1';
5. GROUP BY子句
(1)GROUP BY子句将查询结果按一列或多列的属性值分组,组相等的行归为一组。若不对查询结果进行分组,聚集函数将作用于整个查询结果。分组后,聚集函数会作用于每个小组并返回一个函数值。
//求各课程号及相应的选课人数:
SELECT Cno,COUNT(Sno) /*分组后,COUNT函数会对每个组都进行计算*/
FROM SC
GROUP BY Cno; /*对查询结果按Cno的值分组*/
(2)GROUP BY子句还可以配合上HAVING短语(回顾上面的注意点2)指定筛选条件。
//查询选修了三门以上课程的学生学号和选修的课程数量:
SELECT Sno
FROM SC
GROUP BY Sno /*对查询结果按Sno的值分组*/
HAVING COUNT(*) > 3; /*分组后 * 会依次表示不同的组,COUNT函数再计算每个组的元组数*/
连接查询
连接查询是指同时涉及两个以上的表的查询。
1. 等值与非等值连接查询
连接查询中的WHERE子句是由连接条件和选择条件组成的复合条件。执行复合条件的连接查询时是先执行选择条件,再执行连接条件,这是一种高效执行过程。
连接条件是指用比较运算符连接两个表中的某个属性列,连接条件中的列名称为连接字段,字段的类型必须是可比的,但名字不必相同。如果属性列名称不同,可以省略表名前缀。
当连接条件中的比较运算符为“=”时,连接称为等值连接,否则称为非等值连接。
有时等值连接后会出现重复列,将重复列取消的连接称为自然连接。(可回顾“专门的关系运算”)
//查询每个学生及其选修课程的情况:
<等值连接>
SELECT Student.*,SC.* /*Student.*和SC.*表示两表中的所有属性列,所以表中会出现重复列*/
FROM Student,SC
WHERE Student.Sno = SC.Sno;
<自然连接>
SELECT Student.Sno,Sname,Ssex,Sage,Sdept,Cno,Grade /*虽然表中不会出现重复列,但是代码变长了_(:з」∠)_*/
FROM Student,SC
WHERE Student.Sno = SC.Sno;
//查询选修2号课程,且成绩在90分以上的所有学生姓名和学号:
<与选择条件复合>
SELECT Student.Sno,Sname
FROM Student,SC
WHERE Student.Sno = SC.Sno /*连接条件*/
AND Cno = '2' /*选择条件*/
AND Grade >= 90;
2. 自身连接
连接操作不仅可以在不同表上进行,还可以是表与自身的连接。
为了实现自身连接,同时保证逻辑的正确性,我们需要给表起若干个别名。
//查询每一门选修课(Cno)的先修课程(Cpno):
<思路>选修课和先修课同位于表Course,我们需要起两个别名,分别表示选修课和先修课所在的表。
SELECT FIRST.Cno,SECOND.Cpno /*FIRST表是选修课所在表,SECOND表是先修课所在表*/
FROM Course FIRST,Course SECOND /*给Course起两个别名*/
WHERE FIRST.Cpno = SECOND.Cno; /*在FIRST表中找先修课,再在SECOND表中找课程号*/
3. 外连接
采用外连接可以使被舍弃的悬浮元组显示在查询结果中。(可回顾“专门的关系运算”)
//查询每个学生及其选修课的情况
SELECT Student.Sno,Sname,Ssex,Sage,Sdept,Cno,Grade
FROM Student LEFT OUTER JOIN SC USING(Sno); /*USING(Sno)等价于Student.Sno = SC.Sno*/
细心的同学可以注意到,外连接里我们没有用到WHERE子句来连接两个表,这是因为例子中使用了JOIN短语。在FROM子句中使用JOIN就可以实现连接,而且
FROM Student INNER JOIN SC ON Student.Sno = SC.Sno; 等价于 WHERE Student.Sno = SC.Sno;
NATURAL JOIN 表示自然连接
LEFT OUTER JOIN 表示左外连接(同理可推导右外连接)
连接查询时使用WHERE还是JOIN就根据大家的编程习惯而定喽。
4. 多表连接
//查询每个学生的姓名、选修课程及成绩:
SELECT Sname,Cno,Grade
FROM Student,Course,SC
WHERE Student.Sno = SC.Sno AND Course.Cno = SC.Cno;
嵌套查询
在SQL语言中,我们司空见惯的SELECT-FROM-WHERE语句被称为一个查询块。将一个查询块嵌套在另一个查询块的WHERE子句或HAVING短语中的查询,我们定义为嵌套查询。
嵌套查询使用户可以用多个简单的查询构成复杂的查询,增强SQL的查询能力,这种查询正是SQL(结构化查询语言)中“结构化”的含义所在。
1.带有IN谓词的子查询
在嵌套查询中,子查询的结果往往是一个集合,IN正是我们之前提到的“确定集合”的查询谓词。
//查询选修了课程名为“信息系统”的学生学号和名字:
<嵌套查询>
SELECT Sno,Sname
FROM Student
WHERE Sno IN /*得到学生学号*/
(SELECT Sno
FROM SC /*用SC表将Student表和Course表联系起来
WHERE Cno IN /*得到课程号*/
(SELECT Cno
FROM Course
WHERE Cname = '信息系统'
)
);
<连接查询>
*有些嵌套查询可以用连接替代,有些却不可以。但在实际应用中,能够用连接查询的尽量用连接查询!
SELECT Student.Sno,Sname
FROM Student,Course,SC
WHERE Student.Sno = SC.Sno
AND SC.Cno = Course.Cno
AND Course.Cname = '信息系统';
*在上面的嵌套查询中,子查询的查询条件不依赖于父查询,子查询的查询结果用于建立父查询的查询条件,这样的查询叫做不相关子查询。下面要介绍的嵌套查询都属于相关子查询,即子查询的查询条件依赖于父查询。求解相关子查询不能像求解不相关子查询那样一次将子查询解出来,然后求解父查询。由于子查询的查询条件与父查询相关,所以需要反复求值。
2. 带有比较运算符的子查询
这种查询用比较运算符来连接父查询和子查询,即用比较运算符取代IN短语。
//找出每个学生超过自己选修课平均成绩的课程号:
SELECT Sno,Cno
FROM SC x /*给SC起两个别名*/
WHERE Grade >=
(SELECT AVG(Grade)
FROM SC y
WHERE x.Sno = y.Sno
);
3. 带有ANY(SOME)或ALL谓词的子查询
子查询返回单值时可以用比较运算符,但返回多值时要加上ANY(有的系统用SOME)或ALL谓词修饰符。
短语 | 语义 |
>ANY | 大于最小值 |
<ANY | 小于最大值 |
>ALL | 大于最大值 |
<ALL | 小于最小值 |
//查询非计算机科学系中比计算机科学系任意一个学生年龄小的学生姓名和年龄:
/*要注意,这里的“任意一个”并不是我们日常理解的“所有”。前面说过相关子查询的嵌套查询时反复求值的,因此这里的“任意一个”是指每一次求值时的“所有”,题干要表达“所有”的时候一般会说“全部”。但其实用词也没那么死板.......具体问题具体分析吧。*/
<使用ANY谓词>
SELECT Sname,Sage
FROM Student
WHERE Sage <ANY
(SELECT Sage
FROM Student
WHERE Sdept = 'CS')
AND Sdept != 'CS'; /*不等于也可以用<>,其实从敲键盘的角度来说,<>还舒服一点*/
/*而且要注意,最后一行AND语句是父查询的选择条件之一*/
<使用聚集函数>
*在实际应用中,用聚集函数的查询效率更高!
SELECT Sname,Sage
FROM Student
WHERE Sage <
(SELECT MAX(Sage)
FROM Student
WHERE Sdept = 'CS')
AND Sdept != 'CS';
4. 带有EXISTS谓词的子查询
EXISTS谓词只关心子查询有无返回值,有为真,无为假,而不需要查询具体值,所以有时候EXISTS是高效的查询方法。
若WHERE子句返回真,则在父查询中将正在被查询的元组的对应属性值放入结果表,然后再查询下一个元组。若返回假,则跳过。
(1)存在量词EXISTS
//查询所有选修了1号课程的学生姓名:
SELECT Sname
FROM Student
WHERE EXISTS
(SELECT * /*由EXISTS引出的子查询,目标列表达式通常用 * ,因为EXISTS只关心有无返回值,给列名无实际意义*/
FROM SC
WHERE Sno = Student.Sno AND Cno = '1'
);
//查询没有选修1号课程的学生姓名
<NOT EXISTS>
...
WHERE NOT EXISTS
... /*其余部分不用变*/
*遇到否定句式的问题时,先转换为肯定句式来写出代码,再添加NOT,这样就清晰很多了。
(2)全称量词NOT EXISTS-NOT EXISTS
SQL不提供全称量词,但我们可以用离散数学的知识,用EXISTS表示出来,即NOT EXISTS-NOT EXISTS(没有不...的)。
//查询选修了全部课程的学生姓名:
<思路> 没有一门是他不选修的 => 存在课程是他有选修的
第一步:从Student表中选择一名学生开始查询:
SELECT Sname
FROM Student
WHERE
第二步:存在一门课程:
EXISTS
(SELECT *
FROM Course
WHERE
第三步:存在正在查询的学生选修的课程:
EXISTS
(SELECT *
FROM SC
WHERE Sno = Student.Sno /*在SC表中对比正在查询的学生*/
AND Cno = Course.Cno
)
第四步:作思路的逆转换,存在课程是他有选修的 => 没有一门是他不选修的,即在EXISTS前加上NOT,并将前面的三部分连接起来:
SELECT Sname
FROM Student
WHERE NOT EXISTS
(SELECT *
FROM Course
WHERE NOT EXISTS
(SELECT *
FROM SC
WHERE Sno = Student.Sno
AND Cno = Course.Cno
)
); /*恭喜,答案出来了!*/
(3)蕴含(→)
SQL亦不提供蕴含的逻辑运算,但我们可以运用离散数学的知识表示出来,p→q ≡ 非p∨q ,即NOT EXISTS-OR。(但是在实际问题中,往往还需要进行若干次转化)
//查询至少选修了学生20170330选修的全部课程的学生学号:
<思路> 问题转换。学生20170330选修了课程y,记为事件;其余学生x选修了课程y,记为事件q。所以问题转换为,对于所有课程y,p都能推出(蕴含)q 。更进一步推导,只用存在或不存在来描述,不存在课程y,学生20170330选修了,而且不存在于学生x的选修课中。
从上面的推论可以得出,我们需要三个SC表(使用别名),一个用来选择学生x,一个用来确定学生20170330选择的课程,一个用来建立两者新的联系。
第一步:从SC表中选择一名学生开始查询,因为学生和他相应的课程信息都在该表中:
SELECT Sname
FROM SC X
WHERE
第二步:存在学生20170330选修了的课程y:
EXISTS
(SELECT *
FROM SC Y
WHERE Sno = '20170330'
第三步:而且存在学生x选修课程y:
AND EXISTS
(SELECT *
FROM SC Z
WHERE Z.Sno = X.Sno
AND Z.Cno = Y.Cno
)
第四步:反转,在EXISTS前加上NOT连接上面的三个部分:
SELECT Sname
FROM SC X
WHERE NOT EXISTS
(SELECT *
FROM SC Y
WHERE Y.Sno = '20170330'
AND NOT EXISTS
(SELECT *
FROM SC Z
WHERE Z.Sno = X.Sno
AND Z.Cno = Y.Cno
)
); /*恭喜,答案出来了!*/
集合查询
每一个查询块的查询结果都是元组的集合,而对于集合,我们可以进行并(UNION)、交(INTERSECT)和差(EXCEPT)操作。(可回顾“传统的集合运算”)
需要注意的是,参加集合操作的查询结果必须列数相同,而且对应项的数据类型也必须相同。
使用形式:
(查询块1)
[ UNION | INTERSECT | EXCEPT]
(查询块2)
基于派生表的查询
子查询不仅可以如嵌套查询那样,出现在WHERE子句中,它还可以出现在FROM子句中,这时子查询又被称为临时派生表。
//找出每个学生超过自己的选修课平均成绩的课程号:
SELECT
FROM SC,( SELECT Sno,Avg(Grade)
FROM SC
GROUP BY Sno)
AS Avg_sc(Avg_sno,Avg_grade) /*AS短语用于给临时表命名,并指明属性列*/
WHERE SC.Sno = Avg_sc.Avg_sno AND SC.Grade >= Avg_sc.Avg_grade;
/*如果子查询没有使用如AVG()这样的聚集函数,派生表可以不指明属性列,因为属性列没有被改变。*/
路过的圈毛君:“这篇博文是我忙里偷闲花了三天才爆肝出来的,有木有感觉超级良心呀~希望能给大家对数据查询的理解带来帮助_(:з」∠)_”