概述
这一章开始,数据库课程从 关系模型的理论表达 走向 SQL 的实际使用。
如果说 Chapter 2 解决的是:
- 数据怎样抽象成关系
- 主键、外键、关系代数分别是什么
那么 Chapter 3 解决的就是:
- 这些关系到底怎样用 SQL 建出来
- 怎样把想问的问题写成查询
- 怎样做分组统计、子查询、增删改
这一章是数据库课程里非常关键的一章,因为后面几乎所有更复杂的内容,都是在这里的 SQL 基础上继续展开的。
目录
- 概述
- 目录
- SQL Overview
- Data Definition
- Basic Query Structure
- Additional Basic Operations
- Set Operations And Duplicate Semantics
- Null Values
- Aggregate Functions
- Nested Subqueries
- Modification Of The Database
SQL Overview
SQL 是 Structured Query Language,也就是结构化查询语言。
它的作用不是只查询而已,而是同时负责:
- 定义数据库中的表结构
- 查询数据
- 插入、删除、更新数据
- 表达约束和表之间的联系
你可以把 SQL 看成:
关系数据库的通用工作语言。
DDL 与 DML
1. DDL
DDL 是 Data Definition Language,数据定义语言。
它解决的问题是:
- 表长什么样
- 每一列是什么类型
- 哪些列不能为空
- 哪些列是主键、外键
DDL 不只是定义列名和类型,理论上还可以描述:
- relation 的 schema
- 每个属性对应的 domain
- integrity constraints
- 以及后续会见到的索引、安全授权、物理存储结构等信息
典型命令:
create tablealter tabledrop table
2. DML
DML 是 Data Manipulation Language,数据操作语言。
它解决的问题是:
- 怎么查
- 怎么插
- 怎么删
- 怎么改
典型命令:
selectinsertdeleteupdate
一个很重要的理解方式是:
DDL 决定表的规则,DML 决定表里的数据怎么动。
Data Definition
这一部分回答的是:关系在 SQL 里到底怎么定义出来。
数据类型
字符串类型
char(n):固定长度字符串char(5)更像固定占 5 个字符的位置
varchar(n):可变长度字符串,最大长度为nvarchar(20)更像最多 20 个字符,实际长度可以更短
ID char(5),name varchar(20)整数与数值类型
intsmallintnumeric(p, d)/decimal(p, d):定点数realdouble precisionfloat(n)
其中
numeric(p, d) 表示:
- 总共最多
p位 - 其中小数点右边有
d位
salary numeric(8, 2)表示工资总共最多 8 位,其中 2 位是小数位。
日期时间类型
datetimetimestampinterval
date '2005-7-27'time '09:00:30'time '09:00:30.75'timestamp '2005-7-27 09:00:30.75'interval '1' day还提到了常见日期时间函数:
current_date()current_time()year(x)month(x)day(x)hour(x)minute(x)second(x)
类型到底解决什么问题
类型本质上是在规定:
- 这一列允许存什么值
- 数据该如何解释
- 数据库后续能对它做什么运算
这和 Chapter 2 里的 domain(域) 是直接对应的。
也就是说:
在关系模型里说属性属于某个域,到了 SQL 里就具体落成这一列是什么数据类型。
create table 与常见约束
create table
SQL 用 create table 定义关系。
基本形式:
create table r ( A1 D1, A2 D2, ..., An Dn, integrity-constraint1, ..., integrity-constraintk);含义是:
r是表名Ai是属性名Di是属性的数据类型- 后面可以继续声明完整性约束
create table instructor ( ID char(5), name varchar(20) not null, dept_name varchar(20), salary numeric(8,2));这张表表达的是:
- 教师编号是固定长度字符串
- 姓名不能为空
- 系名是字符串
- 工资是带两位小数的数值
not null
not null 的意思是:
这一列必须有值,不能是
null。
它解决的问题是:
- 有些属性在业务上必须存在
- 不能允许空着
name varchar(20) not null表示教师姓名不能为空。
primary key
主键解决的是:
如何唯一标识表中的一条记录。
create table instructor ( ID char(5), name varchar(20) not null, dept_name varchar(20), salary numeric(8,2), primary key (ID));理解重点:
- 主键必须唯一
- 主键不能为
null - slides 特别强调:primary key declaration automatically ensures not null
也就是说,只要某列被声明成主键,它自动就不能是空值。
复合主键
有些表单靠一列不够唯一,就需要多列一起组成主键。
slides 中 takes 的例子:
create table takes ( ID varchar(5), course_id varchar(8), sec_id varchar(8), semester varchar(6), year numeric(4,0), grade varchar(2), primary key (ID, course_id, sec_id, semester, year));这说明:
- 一条选课记录不是靠单个属性唯一
- 而是学生、课程、班号、学期、年份一起才能唯一确定
foreign key
外键解决的是:
怎样把一张表和另一张表联系起来,并保证引用合法。
例如:
create table instructor ( ID char(5), name varchar(20) not null, dept_name varchar(20), salary numeric(8,2), primary key (ID), foreign key (dept_name) references department);这里表示:
instructor.dept_name必须引用department表中的合法系名
再比如:
create table course ( course_id varchar(8) primary key, title varchar(50), dept_name varchar(20), credits numeric(2,0), foreign key (dept_name) references department(dept_name));这体现了外键的本质:
让表与表之间的联系不仅能连起来,而且连得合法。
default
default 用来给列设置默认值。
create table student ( ID varchar(5), name varchar(20) not null, dept_name varchar(20), tot_cred numeric(3,0) default 0, primary key (ID), foreign key (dept_name) references department);意思是:
- 如果插入学生时没写
tot_cred - 那就默认记为
0
这个机制解决的是缺省值的问题。
参照动作:on delete / on update
外键后面可以跟参照动作,例如:
on delete cascadeon delete set nullon delete restricton delete no actionon delete set defaulton update cascadeon update set nullon update restricton update no actionon update set default
它们解决的是:
当被引用表中的记录被删除或修改时,引用它的表该怎么办。
最常见的直觉:
cascade:跟着改、跟着删set null:把外键列改成nullrestrict/no action:不允许你这么做
insert into
例如:
insert into instructorvalues ('10211', 'Smith', 'Biology', 66000);这说明:
- 表建好后,数据是按元组一行一行放进去的
一个插入 null 的例子:
insert into instructorvalues ('10211', null, 'Biology', 66000);如果某列声明了 not null,那这种插入就会出问题。
表结构修改语句
这三个命令非常容易混。
1. drop table
drop table student;作用:
- 删除整张表
- 也删除表里的内容
2. delete from
delete from student;作用:
- 删除表中的所有元组
- 但表结构还保留着
3. alter table
两种基本形式:
alter table student add resume varchar(256);表示给表增加一列。
新列加进去以后,已有元组在这列上的值默认是 null。
还有:
alter table r drop A;表示删除某个属性。
dropping of attributes not supported by many databases
也就是说,理论上能写,实际数据库支持程度可能不同。
Basic Query Structure
查询骨架
典型形式是:
select A1, A2, ..., Anfrom r1, r2, ..., rmwhere P;其中:
select:你想要哪些列from:数据从哪些表来where:你要满足什么条件
The result of an SQL query is a relation.
也就是说,查询的输出本身依然可以看成一张关系表,这也是为什么查询结果还能继续参与后续操作,比如再做子查询、再做集合运算、再放进 from 里。
可以把它理解成一个三步过程:
- 先确定数据来源
- 再筛掉不符合条件的行
- 最后只保留想看的列
这就是最基础的 SQL 思维。
select:我要看什么
例如找出所有教师的姓名:
select namefrom instructor;这和关系代数里的 投影 是对应的。
SQL 名字默认大小写不敏感
NameNAMEname
本质上都可以看成同一个标识符。
select *
如果你想看一张表的所有列,可以写:
select *from instructor;* 的含义是所有属性。
适合:
- 快速查看表内容
- 调试查询
但在正式查询中,通常还是更推荐只写需要的列。
select 中可以放表达式
select 不只能写列名,也可以写算术表达式。
例如:
select ID, name, salary / 12from instructor;它解决的问题是:
- 不是只能把原始数据拿出来
- 还可以在查询时顺手做计算
这里的意思是把年薪换算成月薪。
如果想让结果更清楚,通常会配合 as 起别名,这一点在后面讲。
from:数据从哪里来
from 指定参与查询的关系。
例如:
select namefrom instructor;表示数据来自 instructor 表。
如果 from 中列出多张表,例如:
select *from instructor, teaches;它的本质是先形成:
instructor × teaches
也就是 笛卡尔积。
这一步本身通常没什么实际意义,但它是多表查询的出发点。
where:我要什么条件
例如找出计算机系中工资高于 80000 的教师:
select namefrom instructorwhere dept_name = 'Comp. Sci.' and salary > 80000;where 对应关系代数中的 选择。
它解决的问题是:
- 不是所有元组都要
- 只保留满足条件的那些
条件、去重与范围判断
and / or / not
SQL 中条件可以用逻辑连接词组合:
andornot
例如:
select namefrom instructorwhere dept_name = 'Comp. Sci.' and salary > 80000;其中:
and表示两个条件都要满足
以后条件复杂起来时,你会发现 SQL 本质上就是在写逻辑表达式。
distinct 与 all
SQL 默认允许重复。
例如:
select dept_namefrom instructor;如果多个教师来自同一个系,结果里这个系名可能会重复出现。
如果想去重,就写:
select distinct dept_namefrom instructor;如果明确表示不去重,可以写:
select all dept_namefrom instructor;虽然 all 很少手写,但
SQL 默认不是集合语义,而是允许重复的多重集语义。
between
例如找工资在 90000 到 100000 之间的教师:
select namefrom instructorwhere salary between 90000 and 100000;它等价于:
salary >= 90000- 并且
salary <= 100000
理解上可以把 between a and b 看成闭区间。
元组比较
SQL 不只是单个属性能比较,元组也可以比较。
select name, course_idfrom instructor, teacheswhere (instructor.ID, dept_name) = (teaches.ID, 'Biology');等价于:
where instructor.ID = teaches.ID and dept_name = 'Biology'所以整条查询最终是在找:Biology 系教师所教授的课程(姓名 + course_id)。
这说明 SQL 可以把多个值打包成一个元组来比较。
多表查询的基本直觉
比如查询所有授过课的教师姓名和所授课程号:
select name, course_idfrom instructor, teacheswhere instructor.ID = teaches.ID;这条语句非常重要,因为它代表了最常见的多表查询逻辑:
from instructor, teaches先把两张表放在一起where instructor.ID = teaches.ID再把真正有关联的行筛出来select name, course_id取出需要的列
多表查询是先组合,再按关联条件筛掉无关组合。
Additional Basic Operations
连接操作
为什么会出现 join
前面我们已经看到:
from instructor, teacheswhere instructor.ID = teaches.ID这种写法其实已经能做连接。
后来 SQL 又提供了更明确的连接写法,例如
natural joinjoin ... using(...)
它们本质上仍然是在解决同一个问题:
把本来分散在不同表中的相关信息拼起来。
natural join
例如:
select *from instructor natural join teaches;natural join 的含义是:
- 自动找出两张表中 同名属性
- 要求这些同名属性值相等
- 结果中这些公共列只保留一份
所以它解决的是显式写等值条件太麻烦的问题。
natural join 的典型正确用法
select name, course_idfrom instructor natural join teaches;之所以成立,是因为:
instructor和teaches里共同有ID- 而这个共同属性刚好就是它们真实的连接键
所以自然连接在同名属性就是同一个含义时很好用。
natural join 的风险
Beware of unrelated attributes with same name which get equated incorrectly
也就是说:
- 只要两张表里有同名列
natural join就会自动拿它们去相等比较- 即使这两个列同名,但业务含义根本不该拿来连接
select name, titlefrom instructor natural join teaches natural join course;这是错误版本。
因为它会错误地把:
course.dept_nameinstructor.dept_name
也拿去相等比较。
结果会把本来不该筛掉的数据筛掉。
正确处理方式:显式指定连接列
第一种:
select name, titlefrom instructor natural join teaches, coursewhere teaches.course_id = course.course_id;第二种:
select name, titlefrom (instructor natural join teaches) join course using (course_id);第三种:
select name, titlefrom instructor, teaches, coursewhere instructor.ID = teaches.ID and teaches.course_id = course.course_id;这三种写法的共同点是:
- 只按真正需要的列建立关联
- 不让同名但无关的列偷偷参与连接
natural join 和显式连接条件的区别
natural join
- 自动使用所有同名列
- 写法短
- 风险是自动做太多
显式连接条件
- 你自己决定按哪一列连接
- 写法稍长
- 但语义更清楚、更安全
join ... using(...)
join course using (course_id)它的含义是:
- 指定只用
course_id作为连接列 - 并且结果中公共列只保留一份
这可以看成:
介于
natural join和完全手写连接条件之间的一种折中写法。
适合我知道就是按这个同名列连,但不想让别的同名列也参与的场景。
一个典型连接例子
select distinct student.IDfrom (student natural join takes) join course using (course_id)where student.dept_name <> course.dept_name;它找的是:
选修了本系之外课程的学生。
这个例子很好,因为它同时体现了:
- 先把学生和选课记录连起来
- 再把课程信息连进来
- 最后比较学生所在系和课程所属系
重命名、模式匹配与排序
as:重命名
SQL 允许给关系或属性起别名。
基本形式:
old_name as new_name例如:
select ID, name, salary / 12 as monthly_salaryfrom instructor;这解决的是:
- 计算结果默认没名字,不好读
- 多表查询中表名太长,不好写
给表起别名
一个自连接的经典例子:
select distinct T.namefrom instructor as T, instructor as Swhere T.salary > S.salary and S.dept_name = 'Comp. Sci.';这个查询的意思是:
- 把
instructor当成两份不同的表来用 T代表当前教师S代表计算机系中的某位教师
如果不给别名,就根本分不清两个 instructor 分别是谁。
as关键字可以省略- 例如
instructor as T等价于instructor T
like:字符串模式匹配
like 用来解决:
不是要精确相等,而是要按某种模式匹配字符串。
slides 指出两个特殊字符:
%:匹配任意子串_:匹配任意单个字符
例如找名字里含有 dar 的教师:
select namefrom instructorwhere name like '%dar%';escape
如果你想匹配的字符串里本身就有 %,那 % 会和通配符含义冲突。
这时可以用 escape。
like '100 \%' escape '\'like '100 #%' escape '#'它的意思是:
- 把
\或#指定成转义字符 - 后面的
%不再表示通配符,而表示字符%本身
like 的典型模式
'Intro%':匹配所有以Intro开头的字符串'%Comp%':匹配包含Comp的字符串'___':匹配恰好 3 个字符的字符串'___%':匹配长度至少 3 的字符串
patterns are case sensitive
也就是说,模式匹配默认区分大小写。
字符串操作 一些常见字符串操作:
- 字符串拼接
|| - 大小写转换
- 求长度
- 截取子串
order by
默认情况下,关系是无序的。
所以如果你想明确按某种顺序显示结果,就要用 order by。
例如按字母顺序列出教师姓名:
select distinct namefrom instructororder by name;asc / desc 与多列排序
排序方向:
asc:升序,默认desc:降序
例如:
select distinct namefrom instructororder by name desc;还可以按多个属性排序:
select *from instructororder by dept_name, name;它表示:
- 先按
dept_name排 - 若系名相同,再按
name排
limit
limit 子句,用于限制返回行数。
例如找工资最高的前三位教师:
select namefrom instructororder by salary desclimit 3;也可以写成:
limit 0, 3即:
- 从偏移量 0 开始
- 取 3 行
这类写法在实际系统里很常见,不过不同数据库的细节语法可能略有差异。
Set Operations And Duplicate Semantics
集合运算
SQL 默认为什么允许重复
这是 SQL 和纯数学集合很不一样的一点。
- SQL 允许关系中有重复
- 查询结果里也允许重复
因此 SQL 更接近 multiset / bag(多重集),而不是严格的集合。
本章讲了三个标准集合运算:
unionintersectexcept
union
例如找 2009 Fall 或 2010 Spring 开过的课程:
(select course_id from section where sem = 'Fall' and year = 2009)union(select course_id from section where sem = 'Spring' and year = 2010);表示并集。
intersect
例如找两个学期都开过的课程:
(select course_id from section where sem = 'Fall' and year = 2009)intersect(select course_id from section where sem = 'Spring' and year = 2010);表示交集。
except
例如找 2009 Fall 开过但 2010 Spring 没开过的课程:
(select course_id from section where sem = 'Fall' and year = 2009)except(select course_id from section where sem = 'Spring' and year = 2010);表示差集。
这些集合运算默认会去重
union、intersect、except会自动消除重复。
所以它们更接近集合语义。
all 版本
如果你想保留重复,就用:
union allintersect allexcept all
多重集计数规则是:
r union all s:某元组出现m + n次r intersect all s:某元组出现min(m, n)次r except all s:某元组出现max(0, m - n)次
SQL 对重复是有精确定义的,不是随便处理。
Null Values
null 的含义与影响
null 是什么
null 表示:
- 值未知
- 或者值不存在
这不是普通的数据值,而是一种缺失或未知状态。
这点非常重要,因为:
null不是 0,不是空字符串,也不是 false。
null 会带来什么影响
任何涉及
null的算术表达式结果仍然是null
例如:
5 + null结果是:
null因为有一个值未知,整个结果也就无法确定。
is null
检查空值时要用:
is null例如:
select namefrom instructorwhere salary is null;为什么不能用 = 判断 null
原因不是语法不推荐,而是因为 SQL 里:
- 与
null比较 - 不会得到
true或false - 而会得到第三种逻辑值:
unknown
所以像:
salary = null不会正确判断空值。
正确写法必须是:
salary is null三值逻辑
因为有 null,SQL 不是普通二值逻辑,而是三值逻辑。
OR
unknown or true = trueunknown or false = unknownunknown or unknown = unknown
AND
true and unknown = unknownfalse and unknown = falseunknown and unknown = unknown
NOT
not unknown = unknown
unknown 在 where 中的后果
在
select的条件判断中,如果谓词结果是unknown,会被当成 false 对待。
意思是:
where只留下结果为true的行- 如果是
false,不要 - 如果是
unknown,也不要
所以很多为什么这条记录没查出来的问题,本质上都是 null 导致条件变成了 unknown。
Aggregate Functions
如果我不只是想看单条记录,而是想做统计,该怎么办?
常见聚合函数
5 个最基本的聚合函数:
avgminmaxsumcount
它们操作的对象不是单个元组,而是:
某一列值构成的一个集合(更准确说是多重集)。
基本例子
求平均工资
select avg(salary)from instructorwhere dept_name = 'Comp. Sci.';它的意思是:
- 先选出计算机系教师
- 再看这些人的
salary - 对这一列求平均
统计开课教师人数
select count(distinct ID)from teacheswhere semester = 'Spring' and year = 2010;这里的重点是 count(distinct ID):
- 先去重
- 再计数
统计 course 表的元组数
select count(*)from course;count(*) 表示统计行数。
group by / having
group by:先分组,再统计
如果你要按系分别统计平均工资,那就不能把所有教师混在一起算一个平均值,而要先分组。
例如:
select dept_name, avg(salary)from instructorgroup by dept_name;这里的过程是:
- 按
dept_name把教师分组 - 每组内部各自计算
avg(salary)
所以 group by 解决的是:
统计不是对整张表做,而是对每一类分别做。
group by 和 select 的关系
select子句中,凡是不在聚合函数里的属性,都必须出现在group by列表中。
例如下面是错误写法:
select dept_name, ID, avg(salary)from instructorgroup by dept_name;原因是:
- 你按系分组了
- 每组里可能有多个
ID - 那结果里这个
ID到底该显示哪一个?说不清
所以这是不合法的。
having:对分组后的结果再筛选
例如找平均工资大于 42000 的院系:
select dept_name, avg(salary)from instructorgroup by dept_namehaving avg(salary) > 42000;过程是:
- 先按系分组
- 对每组算平均工资
- 再只保留平均工资大于 42000 的组
where 和 having 的区别
where
- 在分组前过滤行
- 作用对象是单条记录
having
- 在分组后过滤组
- 作用对象是一个分组的统计结果
having在 groups formed 之后判断,where在 forming groups 之前判断。
例如:
select dept_name, count(*) as cntfrom instructorwhere salary >= 100000group by dept_namehaving count(*) > 10order by cnt;这里:
where salary >= 100000:先只保留工资至少 10 万的教师group by dept_name:对这些人按系分组having count(*) > 10:只保留人数超过 10 的组
聚合函数与 null
1. sum(salary) 会忽略 null
select sum(salary)from instructor;如果某些工资是 null,它们不会参与求和。
2. 除 count(*) 外,其他聚合函数都忽略聚合列中的 null
也就是说:
avgminmaxsum
都不会把 null 当普通值参与计算。
3. 如果一整组全是 null
count返回0- 其他聚合返回
null
这点非常重要。
一个检查重复的例子
一个很好的 group by + having 练习:
找出没有重名学生的院系:
select dept_namefrom studentgroup by dept_namehaving count(distinct name) = count(ID);它背后的逻辑是:
- 如果重名不存在
- 那么不同姓名个数就等于学生总人数
Nested Subqueries
子查询的作用
子查询是嵌套在另一个查询中的
select-from-where表达式。
它的常见用途包括:
- 集合成员测试
- 集合比较
- 集合大小/存在性测试
理解子查询最好的方式是:
先用一个查询算出一个中间结果,再让外层查询拿这个结果继续判断。
集合成员测试与集合比较
in:属于某个集合
例如找 2009 Fall 开课且 2010 Spring 也开课的课程:
select distinct course_idfrom sectionwhere semester = 'Fall' and year = 2009 and course_id in ( select course_id from section where semester = 'Spring' and year = 2010 );这条语句的逻辑是:
- 外层先看 Fall 2009 的课程
- 再要求这个
course_id同时出现在 Spring 2010 的课程集合里
所以:
in本质上是在问:这个值是否属于子查询返回的集合。
not in:不属于某个集合
例如找 2009 Fall 开过但 2010 Spring 没开过的课程:
select distinct course_idfrom sectionwhere semester = 'Fall' and year = 2009 and course_id not in ( select course_id from section where semester = 'Spring' and year = 2010 );这和上面的 in 正好相反。
元组 in
in 不只是能比较单列,也能比较元组。
select count(distinct ID)from takeswhere (course_id, sec_id, semester, year) in ( select course_id, sec_id, semester, year from teaches where teaches.ID = '10101');意思是:
- 先找出教师
10101教过的那些具体开课班次 - 再找修过这些班次的学生人数
some:和集合中的至少一个值比较
slides 给出的例子:
select namefrom instructorwhere salary > some ( select salary from instructor where dept_name = 'Biology');它表示:
- 只要某位教师的工资
- 大于 Biology 系里 至少一位 教师的工资
- 就满足条件
F <comp> some r等价于:存在r中某个值,使得比较成立。
all:和集合中的所有值比较
例如:
select namefrom instructorwhere salary > all ( select salary from instructor where dept_name = 'Biology');表示:
- 该教师工资大于 Biology 系中 每一位 教师的工资
F <comp> all r等价于:对r中每个值,比较都成立。
some 和 in 的关系
(= some) ≡ in(<> all) ≡ not in
这说明:
in可以看成= somenot in可以看成某种对所有都不相等的表达
不过最好还是按常规写法来记:
- 集合成员测试用
in / not in - 集合比较用
some / all
标量子查询与存在性测试
标量子查询
标量子查询指的是:
这个子查询应该只返回一个值。
select namefrom instructorwhere salary * 10 > (select budget from department where department.dept_name = instructor.dept_name);这里子查询返回的是某个院系的 budget,即一个单值。
如果标量子查询返回多行,会产生运行时错误。
所以你必须确保它真的只会返回一个值。
exists:只关心有没有
exists 不关心子查询具体返回什么值,只关心:
子查询结果是否非空。
exists r:r ≠ Ønot exists r:r = Ø
例如找在 2009 Fall 和 2010 Spring 都开过的课程:
select course_idfrom section as Swhere semester = 'Fall' and year = 2009 and exists ( select * from section as T where semester = 'Spring' and year = 2010 and S.course_id = T.course_id );相关子查询 correlated subquery
上面这个 exists 例子就是相关子查询。
因为内层子查询里用到了外层的:
S.course_id这表示:
- 内层查询不是独立执行一次就完
- 而是会随着外层当前元组不同而变化
所以相关子查询的本质是:
内层查询依赖外层当前这一行。
not exists + except:表达全都修过
这是经典的一题。
找出修过 Biology 系所有课程的学生:
select distinct S.ID, S.namefrom student as Swhere not exists ( (select course_id from course where dept_name = 'Biology') except (select T.course_id from takes as T where S.ID = T.ID));这个写法的关键思想是:
- 先取 Biology 系所有课程这个集合
X - 再取该学生修过的课程这个集合
Y - 如果
X - Y为空 - 就说明
X ⊆ Y - 也就是该学生把 Biology 的课程全修了
unique
它测试的是:
子查询结果中是否没有重复元组。 对空集它也会返回 true。
例如找 2009 年至多开过一次的课程:
select T.course_idfrom course as Twhere unique ( select R.course_id from section as R where T.course_id = R.course_id and R.year = 2009);如果再加上 exists,就能把至多一次改成恰好一次。
from 子查询、lateral 与 with
from 子查询
SQL 允许把子查询结果直接当作一张临时表放进 from 中。
例如先算每个系的平均工资,再从中筛出大于 42000 的系:
select dept_name, avg_salaryfrom ( select dept_name, avg(salary) as avg_salary from instructor group by dept_name)where avg_salary > 42000;这种写法的意义是:
- 把复杂查询分成两步
- 先构造中间结果
- 再对中间结果继续查询
给 from 子查询起名字
select dept_name, avg_salaryfrom ( select dept_name, avg(salary) from instructor group by dept_name) as dept_avg(dept_name, avg_salary)where avg_salary > 42000;这里:
dept_avg是子查询得到的临时关系名- 后面的
(dept_name, avg_salary)是它的列名
lateral
例子:
select name, salary, avg_salaryfrom instructor I1, lateral ( select avg(salary) as avg_salary from instructor I2 where I2.dept_name = I1.dept_name );它的含义是:
from后面较晚出现的那部分- 可以引用前面已经出现的关系变量
所以可以把 lateral 理解成:
from子句里的可引用前文的子查询。
with:临时视图 temporary view
with子句定义一个临时视图,它只在当前查询中可用。
例如找预算最大的院系:
with max_budget(value) as ( select max(budget) from department)select dept_namefrom department, max_budgetwhere department.budget = max_budget.value;这条语句的思路是:
- 先把最大预算命名成一个临时结果
max_budget - 外层查询再拿它去和
department比较
为什么 with 重要
因为复杂查询如果全写在一层里,会很难读。
with dept_total(dept_name, value) as ( select dept_name, sum(salary) from instructor group by dept_name),dept_total_avg(value) as ( select avg(value) from dept_total)select dept_namefrom dept_total, dept_total_avgwhere dept_total.value >= dept_total_avg.value;这个例子说明:
with很适合把复杂逻辑分层命名- 让每一步都更清楚
你可以把 with 理解成:
给中间结果起名字,像搭积木一样组织复杂查询。
Modification Of The Database
前面主要是查,这一部分开始讲改。
delete
删除整张表中的所有元组
delete from instructor;表示:
- 删掉
instructor表中的所有记录 - 但表结构还在
删除满足条件的元组
例如删除 Finance 系所有教师:
delete from instructorwhere dept_name = 'Finance';delete 中也可以用子查询
例如删除所在院系位于 Watson 楼的教师:
delete from instructorwhere dept_name in ( select dept_name from department where building = 'Watson');delete 中的一个细节
delete from instructorwhere salary < (select avg(salary) from instructor);如果边删边重新计算平均值,逻辑会混乱。
SQL 采用的语义是:
- 先计算需要删除哪些元组
- 再统一删除
而不是删一条就重新算一次平均值。
这说明 SQL 在修改语句中也很强调语义稳定。
insert
insert ... values
最简单的插入方式是:
insert into coursevalues ('CS-437', 'Database Systems', 'Comp. Sci.', 4);也可以显式写列名:
insert into course(course_id, title, dept_name, credits)values ('CS-437', 'Database Systems', 'Comp. Sci.', 4);写列名的好处是更清晰,也更不容易因为列顺序变化而出错。
插入 null
例如:
insert into studentvalues ('3003', 'Green', 'Finance', null);这表示把 tot_cred 设成空值。
insert ... select
SQL 还支持把一个查询结果整体插入另一张表。
例如把所有教师加入学生表,并把 tot_creds 设为 0:
insert into studentselect ID, name, dept_name, 0from instructor;这说明:
insert的数据来源不一定是手写一条- 也可以来自另一条查询
为什么 insert ... select 会先完整计算再插入
select-from-where语句会先完整求值,再把结果插入目标关系。
这是为了避免像下面这种语句出问题:
insert into table1select *from table1;如果不是先算完再插,那就可能一边读一边写,逻辑会失控。
所以它的语义是:
先把源结果确定好,再统一插入。
update
update 用来修改已有元组的值。
例如:
update instructorset salary = salary * 1.03where salary > 100000;这表示:
- 对满足条件的元组
- 把
salary更新成原来的1.03倍
两条 update 的顺序问题
- 工资超过 100000 的教师涨 3%
- 其他教师涨 5%
如果写成两条语句:
update instructorset salary = salary * 1.03where salary > 100000;
update instructorset salary = salary * 1.05where salary <= 100000;顺序是重要的。
因为第一条执行后,某些元组的值已经变了,可能影响第二条的判断。
case:在一次 update 里表达分情况更新
更好的写法是:
update instructorset salary = case when salary <= 100000 then salary * 1.05 else salary * 1.03end;这条语句的优势是:
- 逻辑集中
- 不容易受多条语句先后顺序影响
所以 case 是实际 SQL 里非常常见的工具。
update + 子查询
一个很典型的重算字段例子:
update student Sset tot_cred = ( select sum(credits) from takes natural join course where S.ID = takes.ID and takes.grade <> 'F' and takes.grade is not null);这个查询表示:
- 对每个学生
- 重新计算其已获得学分
- 只统计成绩不是
F且不为null的课程
如果某学生没修过课,sum(credits) 会变成 null。
case when sum(credits) is not null then sum(credits) else 0end也就是说:
- 如果求和不是
null,就取这个和 - 否则设成
0