第3章 关系数据库标准语言 SQL
第3章 关系数据库标准语言 SQL
3.1 本章总览
这一章是数据库课程里最重要的一章之一,因为它把前面学过的关系模型真正落到了“如何写数据库语言”上。
前两章主要回答的是:
- 数据库里的数据应该怎样组织;
- 关系模型中的表、属性、元组、主码、完整性等概念是什么意思。
而本章开始回答的是:
- 如何用 SQL 定义数据库对象;
- 如何用 SQL 查询数据;
- 如何修改数据;
- 如何处理空值;
- 如何使用视图。
从知识结构上看,本章可以分成六个部分:
- SQL 概述:了解 SQL 是什么、有什么特点、在数据库系统中处于什么位置。
- 数据定义(DDL):学习怎样创建、修改、删除模式、表、索引等数据库对象。
- 数据查询(SELECT):学习 SQL 中最核心、最复杂的查询语句。
- 数据更新(INSERT / UPDATE / DELETE):学习如何插入、修改、删除表中的数据。
- 空值处理:理解 NULL 的含义、判断方式以及三值逻辑。
- 视图:理解视图的定义、查询、更新以及作用。
可以把本章概括为一句话:
前两章解决“数据长什么样”,本章解决“怎样用标准语言操作这些数据”。
3.2 SQL 概述
3.2.1 什么是 SQL
SQL 是 Structured Query Language,中文通常译为结构化查询语言。
它是关系数据库的标准语言,用来完成以下工作:
- 定义数据库对象;
- 查询数据;
- 更新数据;
- 控制访问权限;
- 配合完整性约束进行数据管理。
SQL 的地位非常高,但必须注意:
- SQL 是标准,不是某一个数据库产品本身;
- 各个数据库管理系统都以 SQL 为基础;
- 但没有任何一个 DBMS 能完整支持 SQL 标准中的所有特性。
这意味着:
- 你学的是通用思想;
- 真正上机时要结合具体系统(如 MySQL、PostgreSQL、SQL Server、Oracle)看语法差异。
3.2.2 SQL 标准与具体 DBMS 的差异
课件特别强调了一个实践中的常见问题:
- 在 MySQL 中,
CREATE SCHEMA和CREATE DATABASE功能几乎等价,可以互换使用; - 但在 Oracle、SQL Server、PostgreSQL 等系统中,
schema和database往往不是一个层级的概念。
因此学习 SQL 时必须形成一个观念:
先掌握标准 SQL 的基本思想,再理解具体 DBMS 的实现差异。
考试通常偏向标准概念; 工程实践则必须关注具体产品语法。
3.2.3 SQL 的主要功能分类
SQL 不是只会查询,它实际上覆盖了数据库工作的多个方面。
| 类别 | 含义 | 常见命令 |
|---|---|---|
| DDL | Data Definition Language,数据定义语言 | CREATE、DROP、ALTER |
| DML | Data Manipulation Language,数据操纵语言 | SELECT、INSERT、UPDATE、DELETE |
| DCL | Data Control Language,数据控制语言 | GRANT、REVOKE |
本章重点讲的是:
- DDL:定义表、视图、索引;
- DML:查询与更新;
- DCL 只做概念性提及,安全性与权限控制通常放到后续章节展开。
3.3 SQL 的特点
SQL 之所以能成为关系数据库的标准语言,是因为它有一组很重要的特征。
3.3.1 功能综合且风格统一
SQL 把多种数据库操作统一在一套语言体系中。
也就是说,用户不必分别学习:
- 一套建表语言;
- 一套查询语言;
- 一套权限语言;
- 一套更新语言。
而是都在 SQL 这个统一框架下完成。
这体现出 SQL 的一个核心优势:
同一种语言,贯穿数据库对象定义、数据查询、数据修改、权限控制。
3.3.2 数据操纵高度非过程化
SQL 最典型的特点是:
- 它强调做什么;
- 不强调怎么一步一步做。
例如:
SELECT Sname
FROM Student
WHERE Smajor = '计算机科学与技术';
你只是在告诉系统:
- 从
Student表中取出姓名; - 条件是主修专业等于“计算机科学与技术”。
你并没有告诉系统:
- 先扫描哪张表;
- 用什么索引;
- 先筛选还是先排序;
- 怎样访问磁盘。
这些都由数据库管理系统优化器决定。
所以 SQL 是一种声明式语言,而不是过程式语言。
3.3.3 面向集合的操作方式
SQL 的操作对象通常不是单个元组,而是元组集合。
例如:
- 一次可以查询出满足条件的一批记录;
- 一次可以更新一批记录;
- 一次可以删除一批记录;
- 聚集函数和分组操作本质上也是对集合进行计算。
这和关系模型建立在集合论基础上是一致的。
3.3.4 使用方式统一
SQL 使用一套相对统一的语法结构,就能支持多种场景:
- 交互式查询;
- 嵌入到高级语言中;
- 用在应用程序里;
- 通过视图、权限机制给不同用户提供不同的数据访问方式。
3.3.5 简洁且易学易用
SQL 的关键字数量不算特别多,但表达能力很强。
例如:
CREATE TABLE就能定义表;SELECT就能完成大量复杂查询;INSERT / UPDATE / DELETE就能完成常见更新操作。
不过“易学”不等于“简单”。
真正的难点主要在:
- 多表连接;
- 子查询;
- 分组与聚集;
- 空值逻辑;
- 视图更新条件;
- 负向查询(尤其是“没有选某门课”这类题)。
3.4 SQL 的基本概念
3.4.1 SQL 支持三级模式结构
从课件图示可以看出,SQL 支持数据库的三级模式体系:
- 外模式:面向用户的局部视图,通常通过视图体现;
- 模式:数据库的整体逻辑结构,通常通过基本表体现;
- 内模式:数据的物理存储结构,通常体现为存储文件及其组织方式。
它们之间的典型对应关系可以理解为:
- 用户看到的是视图;
- 数据库逻辑上管理的是基本表;
- 系统底层实际处理的是存储文件。
3.4.2 基本表、视图、存储文件
1. 基本表
基本表是数据库中本身独立存在的表。
特点:
- 一个关系通常对应一个基本表;
- 一个或多个基本表对应一个存储文件;
- 一个表可以建立若干索引;
- 基本表是数据库逻辑结构中的核心对象。
2. 视图
视图是从一个或多个基本表(或其他视图)中导出的表。
特点:
- 视图只保存定义,不保存实际数据;
- 视图中的数据来自基表;
- 基表变了,视图查询结果也会随之改变;
- 视图是一个虚表;
- 用户还可以在视图之上再定义视图。
3. 存储文件
存储文件对应的是数据库的内模式部分。
需要理解两点:
- 存储文件属于底层实现层;
- 物理结构由 DBMS 决定,而不是由普通用户决定。
所以站在数据库学习的角度,逻辑层最重要的是表和视图; 站在系统实现层,文件与索引结构更关键。
3.5 数据定义(DDL)
数据定义解决的问题是:
数据库对象如何建立、修改、删除。
本章涉及四类对象:
- 模式(Schema)
- 基本表(Table)
- 索引(Index)
- 数据字典(Data Dictionary)
3.5.1 数据库对象的层次关系
课件给出了一个很重要的层次结构:
- 一个 DBMS 实例中可以有多个数据库;
- 一个数据库中可以有多个模式;
- 一个模式下通常包含多个表、视图、索引等对象。
可概括为:
数据库 → 模式 → 表 / 视图 / 索引等对象
这个层次很重要,因为它决定了对象命名、对象归属和管理范围。
3.6 模式的定义与删除
3.6.1 模式是什么
模式可以理解为一个命名空间。
在这个命名空间里,可以定义:
- 基本表;
- 视图;
- 索引;
- 约束等数据库对象。
3.6.2 定义模式
基本形式:
CREATE SCHEMA <模式名> AUTHORIZATION <用户名>;
例如:
CREATE SCHEMA "S-T" AUTHORIZATION WANG;
这表示为用户 WANG 定义一个模式 S-T。
3.6.3 理解模式的本质
定义模式,本质上不是往里面立即放数据,而是先定义了一个管理对象的空间。
也就是说:
- 模式本身更像“逻辑容器”;
- 后续表、视图、索引等对象都会归属到某个模式中。
3.6.4 删除模式
课件把重点更多放在“定义模式”,而删除模式与删除其他数据库对象一样,也属于 DDL 的一部分。实际系统中删除模式时往往需要考虑其内部是否已有依赖对象。
理解重点是:
模式不是数据表本身,而是数据库对象的组织单位。
3.7 基本表的定义、修改与删除
3.7.1 定义基本表
基本语法:
CREATE TABLE <表名>
(
<列名> <数据类型> [列级约束],
<列名> <数据类型> [列级约束],
...,
[表级约束]
);
其中:
- 列级约束:只约束某一列;
- 表级约束:涉及多个属性列时必须写在表级。
3.7.2 学生选课数据库示例
课件围绕一个经典的学生选课模式 S-C-SC 展开:
Student(Sno, Sname, Ssex, Sbirthdate, Smajor)Course(Cno, Cname, Ccredit, Cpno)SC(Sno, Cno, Grade, Semester, Teachingclass)
这是整章所有 SQL 示例的基础。
3.7.3 创建 Student 表
典型语句:
CREATE TABLE Student
(
Sno CHAR(8) PRIMARY KEY,
Sname VARCHAR(20) UNIQUE,
Ssex CHAR(6),
Sbirthdate DATE,
Smajor VARCHAR(40)
);
这里体现出几个重要约束:
Sno是主码,不能重复且通常不能为空;Sname加了UNIQUE,表示姓名列中的值不能重复;- 其他列未额外限制时,可以允许空值(具体依赖定义)。
3.7.4 创建 Course 表
典型语句:
CREATE TABLE Course
(
Cno CHAR(5) PRIMARY KEY,
Cname VARCHAR(40) NOT NULL,
Ccredit SMALLINT,
Cpno CHAR(5),
FOREIGN KEY (Cpno) REFERENCES Course(Cno)
);
这个表特别值得注意的地方是:
Cpno引用了Course(Cno);- 这是一种自引用外键;
- 它表示某门课程的先修课也是课程表中的一门课。
3.7.5 创建 SC 表
典型语句:
CREATE TABLE SC
(
Sno CHAR(8),
Cno CHAR(5),
Grade SMALLINT,
Semester CHAR(5),
Teachingclass CHAR(8),
PRIMARY KEY (Sno, Cno),
FOREIGN KEY (Sno) REFERENCES Student(Sno),
FOREIGN KEY (Cno) REFERENCES Course(Cno)
);
这里的重点是:
SC是学生和课程之间的联系表;- 主码是复合主码
(Sno, Cno); - 它同时引用
Student和Course,体现了参照完整性。
3.7.6 列级约束与表级约束的区别
这是建表时的一个高频考点。
列级约束
只作用于一列,例如:
NOT NULLUNIQUE- 某些单列
PRIMARY KEY
表级约束
作用于多列,例如:
- 复合主码
PRIMARY KEY (Sno, Cno) - 复合唯一约束
- 涉及多列的其他约束
理解口诀:
单列问题可列级,多列问题必须表级。
3.8 数据类型
SQL 中“域”的概念,在实现上主要靠数据类型体现。
定义表的属性时必须指定:
- 属性名;
- 数据类型;
- 必要时指定长度、精度、小数位等。
3.8.1 常见字符串类型
| 类型 | 含义 |
|---|---|
CHAR(n) | 长度为 n 的定长字符串 |
VARCHAR(n) | 最大长度为 n 的变长字符串 |
CLOB | 字符大对象 |
BLOB | 二进制大对象 |
理解:
CHAR适合长度固定的数据,如编号;VARCHAR适合长度不固定的数据,如姓名、地址;CLOB/BLOB适合大对象数据。
3.8.2 常见数值类型
| 类型 | 含义 |
|---|---|
SMALLINT | 短整数 |
INT / INTEGER | 整数 |
BIGINT | 大整数 |
NUMERIC(p,d) | 定点数 |
DECIMAL(p,d) | 定点数,与 NUMERIC 类似 |
REAL | 单精度浮点数 |
DOUBLE PRECISION | 双精度浮点数 |
FLOAT(n) | 可选精度浮点数 |
理解要点:
- 金额、分数等对精度要求高的数据更适合
NUMERIC / DECIMAL; - 科学计算更常用浮点数;
- 学号、课程号虽然由数字组成,但通常应定义成字符型,而不是整数型。
3.8.3 逻辑与时间类型
| 类型 | 含义 |
|---|---|
BOOLEAN | 布尔量 |
DATE | 日期 |
TIME | 时间 |
TIMESTAMP | 时间戳 |
INTERVAL | 时间间隔 |
课件特别提示:
TIMESTAMP的使用要结合具体 DBMS;- 实践中还要注意具体系统版本和实现细节。
3.8.4 数据类型选择的原则
选类型时不要只看“能不能存下”,还要看:
- 是否符合语义;
- 是否便于比较与计算;
- 是否有利于约束与索引;
- 是否兼顾存储空间和精度。
例如:
- 学号应选
CHAR或VARCHAR; - 出生日期应选
DATE; - 学分可选
SMALLINT; - 平均成绩通常可用数值型。
3.9 基本表的修改
修改表使用 ALTER TABLE。
基本形式如下:
ALTER TABLE <表名>
[ ADD [COLUMN] <新列名> <数据类型> [完整性约束] ]
[ ADD <表级完整性约束> ]
[ DROP [COLUMN] <列名> [CASCADE | RESTRICT] ]
[ DROP CONSTRAINT <约束名> [RESTRICT | CASCADE] ]
[ ALTER COLUMN <列名> <数据类型> ];
3.9.1 增加新列
例如:
ALTER TABLE Student ADD Semail VARCHAR(30);
理解重点:
- 不管原表中是否已有数据;
- 新增列在原有元组上通常都取空值;
- 除非定义了默认值或不允许为空等约束。
3.9.2 修改列的数据类型
例如:
ALTER TABLE Student ALTER COLUMN Sbirthdate TYPE VARCHAR(20);
这表示把 Sbirthdate 从日期型改为字符串型。
但要注意:
- 修改类型可能影响已有数据;
- 也可能影响程序逻辑、比较运算、日期函数等。
所以实际工程中修改字段类型必须谨慎。
3.9.3 增加约束
例如:
ALTER TABLE Course ADD UNIQUE(Cname);
这表示课程名必须唯一。
3.9.4 模式与表的关系
课件强调:
- 每一个基本表都属于某一个模式;
- 一个模式可以包含多个基本表。
所以表不是孤立存在的,它一定属于某个逻辑命名空间。
3.10 基本表的删除
删除表使用 DROP TABLE。
语法:
DROP TABLE <表名> [RESTRICT | CASCADE];
3.10.1 RESTRICT 与 CASCADE
1. RESTRICT
表示受限删除。
含义是:
- 如果该表被其他对象引用;
- 或者存在依赖于它的视图、约束等;
- 则该表不能删除。
2. CASCADE
表示级联删除。
含义是:
- 删除该表时,同时删除依赖对象;
- 表上的索引、视图、触发器等通常也会一起失效或被删除。
3.10.2 删除表的本质后果
删除表不仅仅是“删数据”,而是:
- 表定义被删除;
- 表中的数据被删除;
- 与之相关的依赖对象也可能被影响。
所以 DROP TABLE 比 DELETE FROM 表名 更彻底。
3.10.3 视图依赖带来的影响
课件用 Student 表和 CS_Student 视图说明:
- 如果某个视图依赖一张表;
- 用
RESTRICT删除表时会失败; - 用
CASCADE则可能连依赖视图一起删除。
因此删除数据库对象时,一定要先考虑依赖关系。
3.11 索引的建立、修改与删除
3.11.1 为什么要建立索引
索引的主要目的只有一个:
加快查询速度。
数据库管理系统常见的索引类型包括:
- 顺序文件上的索引;
- B+ 树索引;
- 散列(Hash)索引;
- 位图索引。
课件强调的特点:
- B+ 树索引适合动态平衡,适用范围广;
- Hash 索引查找速度快,但通常更适合等值查找场景。
3.11.2 谁建立索引,谁维护索引
- 建立索引的人:数据库管理员或表的属主;
- 维护索引的人:数据库管理系统自动完成;
- 使用哪个索引:通常由 DBMS 自动选择,用户一般不必手工指定。
这说明索引是一个性能优化机制,不是用户日常查询必须写出来的内容。
3.11.3 建立索引
基本语法:
CREATE [UNIQUE] [CLUSTER] INDEX <索引名>
ON <表名>(<列名>[次序][, <列名>[次序]] ...);
3.11.4 唯一索引与排序方向
唯一索引
UNIQUE 表示索引列值不能重复。
排序方向
可指定:
ASC:升序;DESC:降序。
多列索引中,列的次序和排序方式都会影响查询性能。
3.11.5 课件中的典型索引示例
例如:
- Student 表按学生姓名升序建立唯一索引;
- Course 表按课程名升序建立唯一索引;
- SC 表按学号升序、课程号降序建立唯一索引。
这类题的重点不是死记语句,而是理解:
- 哪些列经常作为查询条件;
- 哪些列需要唯一性;
- 多列索引的先后顺序有意义。
3.11.6 修改索引名
例如:
ALTER INDEX Idx_SCCno RENAME TO Idx_SCSnoCno;
3.11.7 删除索引
删除索引使用:
DROP INDEX <索引名>;
3.11.8 对索引的正确认识
索引不是越多越好。
原因是:
- 建索引会占空间;
- 插入、删除、更新时还要维护索引;
- 过多索引会降低更新效率。
所以索引是以空间和维护代价换取查询速度。
3.12 数据字典
3.12.1 什么是数据字典
数据字典是 DBMS 内部的一组系统表,用来记录数据库中各种定义信息。
例如:
- 关系模式定义;
- 视图定义;
- 索引定义;
- 完整性约束定义;
- 用户权限;
- 统计信息等。
3.12.2 数据字典的作用
当执行 SQL 的数据定义语句时,系统实际上是在更新数据字典。
也就是说:
- 你建表,并不只是“看到一张表”;
- 系统还会把这张表的结构、字段、约束等信息写入内部目录;
- 后续查询、优化、权限检查都依赖这些系统信息。
因此可以把数据字典看成:
数据库的“元数据仓库”。
3.13 数据查询(SELECT)总述
查询是本章最核心的部分。
3.13.1 SELECT 的一般格式
课件给出的通用格式可整理为:
SELECT [ALL | DISTINCT] <目标列表达式> [别名], ...
FROM <表名或视图名> [别名], ...
| (<SELECT语句>) [AS] <别名>
[WHERE <条件表达式>]
[GROUP BY <列名> [HAVING <条件表达式>]]
[ORDER BY <列名> [ASC | DESC]]
[LIMIT <行数1> [OFFSET <行数2>]];
3.13.2 查询的学习主线
课件把数据查询分成五块:
- 单表查询
- 连接查询
- 嵌套查询
- 集合查询
- 基于派生表的查询
这五类查询是递进关系:
- 单表查询是基础;
- 连接查询解决多表联合;
- 嵌套查询解决“先查一个结果,再用这个结果继续查”;
- 集合查询强调结果集运算;
- 派生表则把中间结果显式放到
FROM子句中。
3.13.3 建议掌握的“查询执行思维”
写查询不能只背语法,更要形成逻辑顺序意识。
一个查询通常是在做这几件事:
- 先确定数据来自哪些表;
- 再确定如何连接;
- 再筛选满足条件的元组;
- 再分组;
- 再对组进行筛选;
- 再决定输出哪些列;
- 最后排序、截取前几行。
这也是你分析复杂查询题时最稳的思路。
3.14 单表查询
单表查询是所有查询的基础。
课件把单表查询分成六类:
- 选择表中的若干列
- 选择表中的若干元组
ORDER BY- 聚集函数
GROUP BYLIMIT
3.14.1 选择若干列
查询指定列
例如:
SELECT Sno, Sname
FROM Student;
这表示只取学号和姓名两列。
查询全部列
例如:
SELECT *
FROM Student;
* 表示所有属性列。
3.14.2 查询经过计算的列
SELECT 后面不一定只能写属性名,也可以写:
- 算术表达式;
- 字符串函数;
- 日期函数;
- 常量;
- 别名。
例如:
SELECT Sname, Year(curdate()) - Year(Sbirthdate) AS 年龄
FROM Student;
这表示输出姓名和计算得到的年龄。
再如:
SELECT RIGHT(Sname, 1)
FROM Student;
这表示取姓名最后一个字符。
理解重点:
查询结果中的列,不一定是原表中现成的列,也可以是“算出来的列”。
3.14.3 消除重复行:DISTINCT
例如:
SELECT Sno
FROM SC;
如果一个学生选了多门课,那么学号会重复出现。
要消除重复值,可以写:
SELECT DISTINCT Sno
FROM SC;
要点:
ALL是默认值;DISTINCT作用于整个目标列组合,而不是单独某一列。
3.14.4 条件查询:WHERE
WHERE 用于从表中筛选满足条件的元组。
常见条件包括:
- 比较大小:
=, >, <, >=, <=, <> - 范围:
BETWEEN ... AND ... - 集合:
IN (...) - 字符匹配:
LIKE - 空值判断:
IS NULL / IS NOT NULL - 逻辑组合:
AND / OR / NOT
1. 比较查询
例如:
SELECT Sname
FROM Student
WHERE Smajor = '计算机科学与技术';
2. 范围查询
例如:
SELECT Sname, Sbirthdate, Smajor
FROM Student
WHERE Year(curdate()) - Year(Sbirthdate) BETWEEN 20 AND 23;
表示查询年龄在 20 到 23 岁之间的学生。
3. 集合查询
例如用 IN 判断属性值是否属于某个集合。
3.14.5 字符匹配:LIKE
基本形式:
WHERE 列名 LIKE '匹配串'
通配符:
%:任意长度字符串;_:任意单个字符。
例如:
SELECT Sname, Sno, Ssex
FROM Student
WHERE Sname LIKE '刘%';
表示查询姓刘的学生。
再如:
SELECT Cname, Cno
FROM Course
WHERE Cno LIKE '81__6';
这里两个下划线表示两个任意字符。
3.14.6 转义通配符:ESCAPE
如果要把 _ 当作普通字符,而不是通配符,就要转义。
例如:
SELECT Cno, Ccredit
FROM Course
WHERE Cname LIKE 'DB\_Design' ESCAPE '\';
这里 \ 是换码字符,表示后面的 _ 是普通下划线。
3.14.7 MySQL 中的正则匹配:REGEXP
课件还补充了 MySQL 常用写法:
SELECT *
FROM Student
WHERE Sname REGEXP '^赵';
表示姓赵的学生。
例如:
SELECT *
FROM Student
WHERE Sno REGEXP '5$';
表示学号末尾为 5。
再如:
SELECT *
FROM Student
WHERE Sno REGEXP '2018|2019';
表示学号中包含 2018 或 2019。
注意:
LIKE是 SQL 层面更基础的模糊匹配;REGEXP是更强的正则匹配,但常带有 DBMS 方言特征。
3.14.8 空值相关查询
空值不能用 = 判断,而必须写:
IS NULLIS NOT NULL
例如:
SELECT Sno, Cno
FROM SC
WHERE Grade IS NULL;
表示查询缺少成绩的选课记录。
3.14.9 多重条件查询
多个条件可以用逻辑运算组合:
ANDORNOT
例如:
SELECT Sno, Sname, Ssex
FROM Student
WHERE Smajor = '计算机科学与技术'
AND Year(curdate()) - Year(Sbirthdate) <= 24;
理解要点:
AND的优先级高于OR;- 有歧义时要加括号。
3.15 排序、聚集、分组与分页
3.15.1 ORDER BY 排序
基本形式:
ORDER BY <列名> [ASC | DESC]
例如:
SELECT Sno, Grade
FROM SC
WHERE Cno = '81003'
ORDER BY Grade DESC;
表示按成绩从高到低排序。
理解要点:
ASC升序是默认;DESC表示降序;- 可以按多列排序;
- 空值排序位置由具体 DBMS 决定。
3.15.2 聚集函数
常见聚集函数包括:
| 函数 | 含义 |
|---|---|
COUNT(*) | 统计元组个数 |
COUNT(列名) | 统计该列中非空值的个数 |
SUM(列名) | 求和 |
AVG(列名) | 求平均值 |
MAX(列名) | 求最大值 |
MIN(列名) | 求最小值 |
例如:
SELECT COUNT(*)
FROM Student;
表示统计学生总人数。
例如:
SELECT COUNT(DISTINCT Sno)
FROM SC;
表示统计选修过课程的学生人数。
3.15.3 COUNT(*) 与 COUNT(列名) 的区别
这是特别容易错的点。
COUNT(*):统计行数,不管该行中某列是否为空;COUNT(Grade):只统计Grade非空的行数。
所以当存在空值时,这两个结果可能不同。
3.15.4 聚集函数对空值的处理
课件强调:
- 除
COUNT(*)外; - 其他聚集函数在计算时一般都会自动跳过空值。
例如:
SELECT AVG(Grade)
FROM SC
WHERE Cno = '81001';
如果某些成绩是 NULL,这些元组不会参与平均值计算。
3.15.5 GROUP BY 分组
GROUP BY 的作用是:
- 按指定列进行分组;
- 值相等的元组划为一组;
- 聚集函数对每个组分别计算。
例如:
SELECT Cno, COUNT(Sno)
FROM SC
GROUP BY Cno;
表示统计每门课的选课人数。
3.15.6 HAVING 与 WHERE 的区别
这是 SQL 查询中的关键点。
WHERE
- 作用对象:元组
- 作用时机:分组前
- 不能直接对聚集函数结果筛选
HAVING
- 作用对象:组
- 作用时机:分组后
- 常用于筛选聚集结果
例如:
SELECT Sno
FROM SC
WHERE Semester = '20192'
GROUP BY Sno
HAVING COUNT(*) > 10;
表示查询 2019 年第 2 学期选修了 10 门以上课程的学生学号。
3.15.7 为什么聚集条件不能写在 WHERE 中
因为 WHERE 执行时,分组还没完成;
此时 COUNT(*)、AVG() 等组级结果还不存在。
所以:
- 行条件放
WHERE; - 组条件放
HAVING。
这是必须形成的思维习惯。
3.15.8 LIMIT 与 OFFSET
LIMIT 用于限制返回行数,OFFSET 用于跳过前若干行。
例如:
SELECT Sno
FROM SC, Course
WHERE Course.Cname = '数据库系统概论'
AND SC.Cno = Course.Cno
ORDER BY Grade DESC
LIMIT 10;
表示取前 10 名。
再如:
SELECT Sno, AVG(Grade)
FROM SC
GROUP BY Sno
ORDER BY AVG(Grade) DESC
LIMIT 5 OFFSET 2;
表示:
- 先按平均成绩降序排序;
- 跳过前 2 行;
- 再取 5 行。
这就是“第 3 到第 7 名”的典型写法。
3.16 连接查询
3.16.1 什么是连接查询
连接查询是指:
同时涉及两个或两个以上表的查询。
连接的核心在于:
- 先把有关表关联起来;
- 再从关联结果中筛选想要的数据。
连接条件也称为连接谓词。
3.16.2 等值连接
例如:
SELECT Student.*, SC.*
FROM Student, SC
WHERE Student.Sno = SC.Sno;
这表示按学生学号把 Student 与 SC 连接起来。
3.16.3 自然连接思想
如果在等值连接中把重复列去掉,就形成了“自然连接”的效果。
例如:
SELECT Student.Sno, Sname, Ssex, Sbirthdate, Smajor, Cno, Grade
FROM Student, SC
WHERE Student.Sno = SC.Sno;
也可以写成更现代的语法:
SELECT Student.Sno, Sname, Ssex, Sbirthdate, Smajor, Cno, Grade
FROM Student JOIN SC ON Student.Sno = SC.Sno;
3.16.4 复合条件连接
连接查询中的 WHERE 可以同时包含:
- 连接条件;
- 普通筛选条件。
例如:
SELECT Student.Sno, Sname
FROM Student, SC
WHERE Student.Sno = SC.Sno
AND SC.Cno = '81002'
AND SC.Grade > 90;
这表示:
- 先连接学生与选课表;
- 再筛出选修 81002 且成绩大于 90 的学生。
3.16.5 自身连接
自身连接是同一张表和自己连接。
典型场景:
- 查间接先修课;
- 查上下级关系;
- 查同一实体内部的递归关系。
例如课程表中:
FIRST.Cpno = SECOND.Cno
可以找出某门课的先修课的先修课。
自身连接必须起别名,否则无法区分“同一张表的两个角色”。
3.16.6 外连接
外连接与普通连接的最大区别在于:
- 普通内连接只保留匹配成功的元组;
- 外连接会把“主体表中没有匹配成功的元组”也保留下来。
课件重点讲的是左外连接。
例如:
SELECT Student.Sno, Sname, Ssex, Sbirthdate, Smajor, Cno, Grade
FROM Student LEFT OUTER JOIN SC
ON Student.Sno = SC.Sno;
含义是:
- 以
Student为主体; - 即使学生没有选课,也要显示学生基本信息;
- 选课相关列填
NULL。
3.16.7 外连接中的计数陷阱
课件有个非常重要的点:
如果想统计每个学生的选课门数,包括没选课的学生,应该写:
SELECT Student.Sno, COUNT(Cno)
FROM Student LEFT OUTER JOIN SC
ON Student.Sno = SC.Sno
GROUP BY Student.Sno;
原因是:
- 没选课的学生在外连接后,
Cno为NULL; COUNT(Cno)不统计空值,因此结果会是 0;- 如果写成
COUNT(*),则外连接生成的那一行也会被计数,可能得到 1,语义就错了。
这个点很高频:
外连接统计“数量”时,要想清楚是统计行,还是统计某个非空字段。
3.16.8 多表连接
例如:
SELECT Student.Sno, Sname, Cname, Grade
FROM Student, SC, Course
WHERE Student.Sno = SC.Sno
AND SC.Cno = Course.Cno;
表示查询学生、课程和成绩三者联合信息。
多表连接的本质是:
- 逐步建立表之间的连接关系;
- 最终得到想要的综合结果。
3.16.9 连接查询中的 DBMS 差异
课件还提醒了一个实践问题:
- 不同数据库对
GROUP BY的语法严格程度不同; - 有的系统允许“目标列没有全部出现在
GROUP BY中”; - 有的系统会直接报错。
例如:
- MySQL 某些模式下可执行;
- SQL Server 更严格,可能报错。
所以在工程实践中,最好遵循更规范的写法:
凡是既不在聚集函数中、又不在
GROUP BY中的列,尽量不要直接出现在SELECT中。
3.17 嵌套查询
3.17.1 什么是嵌套查询
在一个 SELECT-FROM-WHERE 查询块中,再嵌入另一个查询块,就构成嵌套查询。
例如:
SELECT Sname
FROM Student
WHERE Sno IN (
SELECT Sno
FROM SC
WHERE Cno = '81001'
);
其中:
- 外层查询叫父查询;
- 内层查询叫子查询。
3.17.2 不相关子查询与相关子查询
不相关子查询
子查询条件不依赖外层查询。
处理方式:
- 先执行子查询;
- 得到结果;
- 再把结果给外层查询使用。
相关子查询
子查询条件依赖外层查询中的某些值。
处理方式:
- 取外层表的一行;
- 把该行值代入内层查询;
- 判断真假;
- 再处理下一行。
例如:
SELECT Cno, Sno, Grade
FROM SC sc1
WHERE Grade < (
SELECT AVG(Grade)
FROM SC sc2
WHERE sc1.Cno = sc2.Cno
);
表示查询每门课中低于该课程平均成绩的学生。
这里子查询依赖外层的 sc1.Cno,所以是相关子查询。
3.18 带 IN 的子查询
3.18.1 基本含义
IN 表示:某个值属于子查询结果集合。
例如:
SELECT Sno, Sname, Smajor
FROM Student
WHERE Smajor IN (
SELECT Smajor
FROM Student
WHERE Sname = '刘晨'
);
表示查询与刘晨在同一专业的学生。
3.18.2 排除本人
如果不想把刘晨自己查出来,可以再加条件:
SELECT Sno, Sname, Smajor
FROM Student
WHERE Sname <> '刘晨'
AND Smajor IN (
SELECT Smajor
FROM Student
WHERE Sname = '刘晨'
);
3.18.3 多层 IN 嵌套
例如查询选修“信息系统概论”的学生姓名时,可以分三步:
- 在
Course中找到课程号; - 在
SC中找到选了这门课的学生号; - 在
Student中找对应学生姓名。
这就是多层嵌套查询的典型逻辑。
3.19 带比较运算符的子查询
当子查询结果确定为单值时,可以直接和比较运算符配合。
例如:
SELECT Sno, Cno
FROM SC x
WHERE Grade >= (
SELECT AVG(Grade)
FROM SC y
WHERE y.Sno = x.Sno
);
表示找出每个学生中“成绩大于等于自己平均成绩”的课程号。
这里内层查询返回的是某个学生的平均成绩,是单值,所以能直接和 >= 配合。
理解重点:
- 如果内层只会返回一个值,用比较运算符最直接;
- 如果会返回多个值,就要考虑
IN、ANY、ALL、EXISTS等。
3.20 带 ANY / ALL 的子查询
3.20.1 基本语义
ANY 和 ALL 必须与比较运算符一起使用。
其含义如下:
> ANY:大于子查询结果中的某个值> ALL:大于子查询结果中的所有值< ANY:小于子查询结果中的某个值< ALL:小于子查询结果中的所有值= ANY:等价于IN<> ALL:常对应NOT IN的语义
3.20.2 与聚集函数的等价理解
课件给出了一组很有用的转换关系,可以记成:
> ANY (子查询)等价于> MIN(子查询结果)> ALL (子查询)等价于> MAX(子查询结果)< ANY (子查询)等价于< MAX(子查询结果)< ALL (子查询)等价于< MIN(子查询结果)= ANY (子查询)等价于IN<> ALL (子查询)常对应NOT IN
这些转换能帮你把 ANY / ALL 题目化简成更容易理解的聚集函数题。
3.20.3 典型例子
比计算机专业任意一个学生都年轻
“年龄小”如果用出生日期表示,就是“出生更晚”。
例如:
SELECT Sname, Sbirthdate, Smajor
FROM Student
WHERE Smajor <> '计算机科学与技术'
AND Sbirthdate > ANY (
SELECT Sbirthdate
FROM Student
WHERE Smajor = '计算机科学与技术'
);
可转化理解为:
- 这里只要求“比其中某一个计算机专业学生年轻”即可;
- 因为
> ANY等价于> MIN(...); - 所以也可以改写为“出生日期晚于计算机专业学生中最早的那个出生日期”。
比计算机专业所有学生都年轻
例如:
SELECT Sname, Sbirthdate
FROM Student
WHERE Sbirthdate > ALL (
SELECT Sbirthdate
FROM Student
WHERE Smajor = '计算机科学与技术'
);
这表示出生日期比该集合中的每一个都晚,也就是比所有这些学生都年轻。
3.21 带 EXISTS 的子查询
3.21.1 EXISTS 的本质
EXISTS 不关心子查询返回什么具体值,只关心:
- 子查询结果是否为空。
规则:
- 若子查询结果非空,则
EXISTS为真; - 若子查询结果为空,则
EXISTS为假。
因此 EXISTS 本质是在做“存在性判断”。
3.21.2 查询选修某门课的学生
例如:
SELECT Sname
FROM Student
WHERE EXISTS (
SELECT *
FROM SC
WHERE Sno = Student.Sno
AND Cno = '81001'
);
含义是:
- 对每个学生,检查 SC 中是否存在一条记录表明他选了 81001;
- 如果存在,就输出该学生姓名。
3.21.3 NOT EXISTS
NOT EXISTS 就是“子查询结果不存在”。
这是写否定型查询时非常重要的工具。
例如:
- 查询没有选修 81001 的学生;
- 查询没有不满足某条件的情况;
- 查询满足“所有……”这类全称量词问题。
3.21.4 否定查询的常见错误
课件专门强调了一个高频陷阱:
错误写法:
SELECT Sname
FROM Student
WHERE EXISTS (
SELECT *
FROM SC
WHERE Sno = Student.Sno
AND Cno <> '81001'
);
这并不表示“没有选 81001”,而只是表示:
- 存在一门他选过的课不是 81001。
一个同时选了 81001 和 81002 的学生,照样会被选出来。
这说明:
“不存在选过 81001 的记录” 和 “存在一条不是 81001 的记录” 完全不是一回事。
正确思路应该是:
SELECT Sname
FROM Student
WHERE NOT EXISTS (
SELECT *
FROM SC
WHERE Sno = Student.Sno
AND Cno = '81001'
);
3.21.5 全称量词问题
“选修了全部课程的学生”这类题,本质是全称量词问题,常用双重 NOT EXISTS 来表达。
核心思路是:
- 找这样的学生:
- 不存在一门课,是他没有选过的。
也就是:
没有一门课他不选。
这种写法虽然绕,但逻辑非常严密,是数据库理论题中的经典形式。
3.21.6 至少选修了某人全部课程
类似地,“至少选修了学生 20180002 所选全部课程的学生”,也是全称逻辑。
思路仍然是:
- 不存在这样一门 20180002 选过的课,而某个候选学生没选。
只要抓住“全称 = 否定存在反例”这个思想,就不容易乱。
3.22 集合查询
集合查询是把多个查询结果当作集合来做运算。
3.22.1 三类集合运算
UNION:并集INTERSECT:交集EXCEPT:差集
3.22.2 使用条件
参加集合运算的查询结果必须满足:
- 列数相同;
- 对应列的数据类型相同或兼容;
- 对应列含义也应一致。
3.22.3 并集:UNION
例如:
- 查询计算机专业学生;
- 或年龄不大于 19 岁的学生。
可以用 UNION。
本质上也可以改写成 OR 条件。
3.22.4 交集:INTERSECT
例如:
- 查询既是计算机专业;
- 又年龄不大于 19 岁的学生。
可以用 INTERSECT。
也可以改写成 AND 条件。
3.22.5 差集:EXCEPT
例如:
- 查询计算机专业学生中,去掉年龄不大于 19 岁的学生。
可以用 EXCEPT。
也可以改写成:
专业 = 计算机且年龄 > 19。
3.22.6 理解集合查询的意义
集合查询的优点是:
- 语义直观;
- 更贴近集合论思想;
- 对某些题目表达更自然。
但在一些 DBMS 中,不同集合运算的支持程度可能不同,工程中常常会改写成 AND / OR / NOT、子查询或连接形式。
3.23 基于派生表的查询
3.23.1 什么是派生表
派生表是指:
- 把一个子查询放在
FROM子句里; - 将子查询结果临时看作一张表;
- 再在外层查询中继续使用。
例如:
SELECT Sno, Cno
FROM SC,
(
SELECT Sno, AVG(Grade)
FROM SC
GROUP BY Sno
) AS Avg_sc(avg_sno, avg_grade)
WHERE SC.Sno = Avg_sc.avg_sno
AND SC.Grade >= Avg_sc.avg_grade;
这就是把“每个学生的平均成绩”先算成一张临时表,再与原表连接。
3.23.2 派生表与子查询的关系
派生表与子查询本质相近,但位置不同:
- 普通子查询常写在
WHERE中; - 派生表写在
FROM中。
3.23.3 派生表的特点
- 必须为派生表指定别名;
- 有时还要显式指定派生列名;
- 它只是查询过程中的中间结果;
- 查询结束后,派生表定义自动消失。
3.23.4 派生表和视图的区别
课件特别强调:
- 视图定义后会保存在数据字典中,可以反复使用;
- 派生表只是当前语句执行时临时定义,执行结束就没了。
因此:
- 临时一次性中间结果,用派生表;
- 多次复用、希望保存逻辑定义,用视图。
3.24 数据更新总述
数据更新包括三类操作:
- 插入数据
INSERT - 修改数据
UPDATE - 删除数据
DELETE
它们共同解决的问题是:
如何改变表中的数据内容。
需要特别记住:
- 更新数据时系统会检查完整性约束;
- 因此并不是语法写对了就一定能成功执行。
3.25 插入数据(INSERT)
3.25.1 插入元组
基本语法:
INSERT INTO <表名> [(<属性列1>, <属性列2>, ...)]
VALUES (<常量1>, <常量2>, ...);
3.25.2 指定属性列插入
例如:
INSERT INTO Student (Sno, Sname, Ssex, Smajor, Sbirthdate)
VALUES ('20180009', '陈冬', '男', '信息管理与信息系统', '2000-5-22');
这种写法的优点是:
- 列顺序可以不完全按建表顺序写;
- 语义更清楚;
- 漏掉的列会取空值或默认值。
3.25.3 不指定属性列插入
例如:
INSERT INTO Student
VALUES ('20180008', '张成民', '男', '2000-4-15', '计算机科学与技术');
这时要求:
- 值的个数必须与表的列数匹配;
- 顺序必须和建表时一致。
3.25.4 缺失列与空值
例如:
INSERT INTO SC(Sno, Cno, Semester, Teachingclass)
VALUES ('20180005', '81004', '20202', '81004-01');
由于没有给 Grade 提供值,系统会让它取 NULL。
这说明:
- 没出现的属性列,不一定报错;
- 但前提是它允许为空或有默认值;
- 若该列是
NOT NULL且无默认值,则会失败。
3.25.5 插入子查询结果
语法:
INSERT INTO <表名> [(列名列表)]
<子查询>
例如:
先建表:
CREATE TABLE Smajor_age
(
Smajor VARCHAR(20),
Avg_age SMALLINT
);
再插入统计结果:
INSERT INTO Smajor_age(Smajor, Avg_age)
SELECT Smajor, AVG(Year(curdate()) - Year(Sbirthdate))
FROM Student
GROUP BY Smajor;
这表示把查询结果整批插入新表。
3.25.6 插入时的完整性检查
系统会检查:
- 实体完整性;
- 参照完整性;
- 用户定义完整性,如
NOT NULL、UNIQUE、值域约束等。
所以插入失败常常不是语法错,而是约束不满足。
3.26 修改数据(UPDATE)
3.26.1 基本语法
UPDATE <表名>
SET <列名> = <表达式>[, <列名> = <表达式>] ...
[WHERE <条件>];
含义:
- 对满足
WHERE条件的元组进行修改; - 若省略
WHERE,则修改全表所有元组。
3.26.2 修改单个元组
例如:
UPDATE Student
SET Sbirthdate = '2001-3-18'
WHERE Sno = '20180001';
3.26.3 修改多个元组
例如:
UPDATE SC
SET Grade = Grade - 5
WHERE Semester = '20201' AND Cno = '81002';
表示对一批记录统一减分。
3.26.4 带子查询的修改
例如:
UPDATE SC
SET Grade = 0
WHERE Sno IN (
SELECT Sno
FROM Student
WHERE Smajor = '计算机科学与技术'
);
表示把计算机专业所有学生的成绩置零。
3.26.5 更新操作的风险意识
更新语句尤其要注意两点:
WHERE一旦漏写,可能修改整张表;- 更新后的值也必须满足完整性约束。
例如成绩加分、减分时,要考虑会不会超出允许范围。
课件也特别引导思考:
- 把成绩整体加 2 分,是否可能触发用户定义完整性条件?
答案是:有可能。
如果系统定义了“成绩必须在 0 到 100 之间”,那么把 99 分加到 101 分就会出错。
3.27 删除数据(DELETE)
3.27.1 基本语法
DELETE FROM <表名>
[WHERE <条件>];
含义:
- 删除满足条件的元组;
- 若省略
WHERE,则删除表中所有元组; - 但表结构仍然保留。
这和 DROP TABLE 不同。
3.27.2 删除单个元组
例如:
DELETE FROM Student
WHERE Sno = '20180007';
3.27.3 删除多个元组
例如:
DELETE FROM SC;
表示删除所有选课记录,但保留 SC 表本身。
3.27.4 带子查询的删除
例如:
DELETE FROM SC
WHERE Sno IN (
SELECT Sno
FROM Student
WHERE Smajor = '计算机科学与技术'
);
表示删除计算机专业学生的全部选课记录。
3.27.5 删除时的参照完整性问题
课件给了一个很重要的思考题:
如果要删除“计算机科学与技术”专业所有学生的信息记录,不能只删 Student,因为 SC 中可能还有这些学生的选课记录引用它们。
因此通常要分两步:
- 先删依赖表中的记录;
- 再删主表中的记录。
这本质上体现了参照完整性约束。
3.28 空值的处理
NULL 是 SQL 中一个极其重要、也极其容易混淆的概念。
3.28.1 空值的含义
空值不等于 0,也不等于空字符串。
NULL 表示的可能是:
- 该属性应该有值,但目前不知道;
- 该属性不应该有值;
- 由于某种原因暂时不便填写。
所以 NULL 的本质是:
不确定、未知、不存在或无意义。
3.28.2 空值的产生
例如:
INSERT INTO SC(Sno, Cno, Grade, Semester, Teachingclass)
VALUES('20180006', '81004', NULL, '20211', NULL);
表示:
- 成绩还没有;
- 选课班还没定。
再如:
UPDATE Student
SET Smajor = NULL
WHERE Sno = '20180006';
表示把主修专业改成未知。
3.28.3 空值的判断
判断是否为空,必须使用:
IS NULLIS NOT NULL
例如:
SELECT *
FROM Student
WHERE Sname IS NULL
OR Ssex IS NULL
OR Sbirthdate IS NULL
OR Smajor IS NULL;
3.28.4 空值约束
如果列被定义为 NOT NULL,则不能取空值。
另外:
- 主码属性不能取空值;
- 复合主码中的每一列都不能取空值。
例如:
Student的主码是Sno,不能为 NULL;SC的主码是(Sno, Cno),这两列都不能为 NULL。
3.28.5 空值参与运算的结果
算术运算
空值与其他值做算术运算,结果通常仍为空值。
比较运算
空值与其他值比较,结果不是 TRUE 或 FALSE,而是 UNKNOWN。
例如:
NULL = 5不是假,而是未知;NULL < 60也不是假,而是未知。
3.28.6 三值逻辑
因为有了 UNKNOWN,SQL 的逻辑不再只有:
TRUEFALSE
而是变成:
TRUEFALSEUNKNOWN
这正是很多查询“看起来应该查出来,结果却没查出来”的根源。
3.28.7 空值对条件查询的影响
例如:
SELECT Sno
FROM SC
WHERE Grade < 60 AND Cno = '81001';
这里不会把缺考(Grade IS NULL)的学生查出来。
因为:
NULL < 60的结果不是TRUE;- 而是
UNKNOWN; WHERE只保留结果为TRUE的元组。
如果要把缺考学生也算进去,应写:
SELECT Sno
FROM SC
WHERE Cno = '81001'
AND (Grade < 60 OR Grade IS NULL);
3.28.8 聚集函数与空值
课件又一次强调:
COUNT(*)统计所有元组;- 其他聚集函数通常跳过空值。
例如统计平均分时,NULL 不参与平均;
统计人数时,COUNT(grade) 也不会统计成绩为 NULL 的元组。
这个点在分析成绩、缺考、空记录时非常关键。
3.29 视图
视图是本章最后一个大模块,也是 SQL 中非常重要的逻辑抽象机制。
3.29.1 视图的特点
视图有四个核心特点:
- 视图是从一个或多个基本表(或视图)导出的表;
- 数据库中只存视图定义,不存视图数据;
- 基表数据变化时,视图查询结果会变化;
- 视图是虚表。
所以视图的本质可以概括为:
视图是对查询逻辑的封装。
3.30 定义视图
3.30.1 建立视图的语法
CREATE VIEW <视图名> [(列名列表)]
AS <子查询>
[WITH CHECK OPTION];
3.30.2 什么时候必须显式指定列名
课件指出,下列情况应显式指定视图列名:
- 目标列不是简单属性名,而是表达式或聚集函数;
- 多表连接选出了同名列;
- 希望给列改成更合适的新名字。
3.30.3 为什么不建议在视图中用 SELECT *
例如:
CREATE VIEW F_Student(Fsno, Fname, Fsex, Fbirthdate, Fmajor)
AS
SELECT *
FROM Student
WHERE Ssex = '女';
课件指出这种写法有缺点:
- 一旦基表
Student的结构变化; - 视图与基表的映射关系就可能被破坏;
- 导致视图工作异常。
因此更稳妥的做法是:
在视图定义中尽量显式写出字段名,而不是简单
SELECT *。
3.30.4 典型视图示例
1. 行列子集视图
CREATE VIEW IS_Student AS
SELECT Sno, Sname, Ssex, Sbirthdate, Smajor
FROM Student
WHERE Smajor = '信息管理与信息系统';
这表示只保留:
- 指定专业的学生;
- 指定的几个属性列。
这种视图叫做行列子集视图。
2. 带表达式的视图
CREATE VIEW S_AGE(Sno, Sname, Sage)
AS
SELECT Sno, Sname, Year(curdate()) - Year(Sbirthdate)
FROM Student;
表示把年龄作为计算列放进视图。
3. 分组视图
CREATE VIEW S_GradeAVG(Sno, Gavg)
AS
SELECT Sno, AVG(Grade)
FROM SC
GROUP BY Sno;
表示存储“每个学生平均成绩”这一逻辑定义。
4. 建立在多个表上的视图
CREATE VIEW IS_C1(Sno, Sname, Grade)
AS
SELECT Student.Sno, Sname, Grade
FROM Student, SC
WHERE Smajor = '信息管理与信息系统'
AND Student.Sno = SC.Sno
AND SC.Cno = '81001';
表示“信息管理专业选修 81001 的学生及成绩”。
视图还可以建立在已有视图上,例如再定义一个 90 分以上的子视图。
3.30.5 WITH CHECK OPTION
这是视图中非常重要的选项。
例如:
CREATE VIEW IS_Student AS
SELECT Sno, Sname, Ssex, Sbirthdate, Smajor
FROM Student
WHERE Smajor = '信息管理与信息系统'
WITH CHECK OPTION;
其含义是:
- 通过该视图执行
INSERT、UPDATE、DELETE时; - 系统会检查结果是否仍满足视图定义中的条件;
- 不满足则拒绝执行。
例如向该视图中插入一个“计算机科学与技术”专业学生,就会失败。
理解为:
用
WITH CHECK OPTION给视图加一层“边界保护”。
3.31 删除视图
语法:
DROP VIEW <视图名> [CASCADE];
3.31.1 删除视图的规则
- 删除视图时,是从数据字典中删除其定义;
- 如果该视图上还派生了其他视图,可能需要
CASCADE; - 不同 DBMS 对级联删除的支持细节可能不完全一致。
3.31.2 视图与基表删除的关系
- 删除基表时,由它导出的视图通常无法继续使用;
- 但视图定义不一定会自动合理清理;
- 因此视图往往需要显式处理。
3.32 查询视图
3.32.1 用户角度:和查表一样
从用户使用的角度看:
- 查询视图与查询基本表几乎没有区别;
- 仍然用
SELECT ... FROM 视图名 ...即可。
例如:
SELECT Sno, Sbirthdate
FROM IS_Student
WHERE Year(curdate()) - Year(Sbirthdate) <= 20;
3.32.2 系统角度:视图消解
DBMS 并不是直接“从视图里拿数据”,因为视图本身不存数据。
系统会做一件事:
把对视图的查询转换成对基本表的等价查询。
这叫做视图消解(View Resolution)。
例如:
SELECT Sno, Sbirthdate
FROM IS_Student
WHERE Year(curdate()) - Year(Sbirthdate) <= 20;
会被系统理解为:
- 先把
IS_Student替换成它的定义; - 再加上原查询中的条件;
- 最后变成一条对
Student表的查询。
3.32.3 视图消解的局限
课件给出了一个重要例子:
若查询分组视图 S_GradeAVG 中平均分大于等于 90 的学生,如果系统错误地把 HAVING 条件改写到 WHERE 中,就会出错。
错误改写:
SELECT Sno, AVG(Grade)
FROM SC
WHERE AVG(Grade) >= 90
GROUP BY Sno;
正确写法应是:
SELECT Sno, AVG(Grade)
FROM SC
GROUP BY Sno
HAVING AVG(Grade) >= 90;
这说明:
- 视图消解不是简单替换字符串;
- 还必须遵循 SQL 语义规则;
- 特别是涉及聚集、分组、派生表时更复杂。
3.33 更新视图
3.33.1 什么叫更新视图
更新视图是指通过视图执行:
INSERTUPDATEDELETE
系统再把这些操作转换成对基本表的更新。
3.33.2 可更新视图的基本思想
如果一个视图足够“简单”,系统通常能把对视图的操作还原成对基表的操作。
例如:
UPDATE IS_Student
SET Sname = '刘新奇'
WHERE Sno = '20180005';
本质上可以转成:
UPDATE Student
SET Sname = '刘新奇'
WHERE Sno = '20180005'
AND Smajor = '信息管理与信息系统';
3.33.3 通过视图插入数据
例如:
INSERT INTO IS_Student
VALUES('20180207', '赵新', '男', '2001-7-19', '信息管理与信息系统');
本质上是往 Student 中插入一条满足该视图条件的记录。
3.33.4 通过视图删除数据
例如:
DELETE FROM IS_Student
WHERE Sno = '20180207';
本质上会转成对 Student 表的条件删除。
3.33.5 哪些视图不可更新
不是所有视图都能更新。
课件重点说明:
CREATE VIEW S_GradeAVG(Sno, Gavg)
AS
SELECT Sno, AVG(Grade)
FROM SC
GROUP BY Sno;
这个视图就不可更新。
原因是:
- 视图中的
Gavg是由一组原始成绩计算出来的平均值; - 如果你说“把平均成绩改成 90”,系统并不知道应该把原始多门课成绩改成什么;
- 因此它无法唯一地转换回基表更新。
所以:
带聚集、分组、复杂表达式的视图,往往不可更新。
一般来说:
- 行列子集视图通常是可更新的;
- 聚集视图、复杂连接视图则要谨慎判断。
3.34 视图的作用
课件总结了视图的四大作用:
3.34.1 提供安全保护
可以把敏感字段隐藏起来,只通过视图向特定用户开放必要列。
例如:
- 学生表中可能有全部信息;
- 但给某类用户只开放学号、姓名、专业视图。
3.34.2 提供一定程度的逻辑独立性
如果基表结构调整,但可以通过视图维持原有接口,那么应用程序不必全部修改。
这相当于给用户提供了一个较稳定的逻辑外壳。
3.34.3 简化用户操作
一些复杂查询可以先封装成视图,后续直接查询视图即可。
例如“每个学生的最高成绩”这种经常用到的中间逻辑,就很适合做成视图。
3.34.4 提供多角度看待同一数据的方式
同一张基表,可以根据不同用户需求建立不同视图:
- 按专业看;
- 按成绩看;
- 按课程看;
- 按安全权限看。
这体现了视图在数据库逻辑抽象中的价值。
3.35 本章高频易错点总结
这一章最容易丢分的地方,不是不会写最简单的 SQL,而是细节理解不到位。
3.35.1 WHERE 与 HAVING 混淆
记法:
WHERE筛行;HAVING筛组。
凡是涉及 COUNT、AVG、SUM 等聚集结果做条件,优先考虑 HAVING。
3.35.2 COUNT(*) 与 COUNT(列名) 混淆
记法:
COUNT(*)数行;COUNT(列名)数非空值。
外连接统计数量时尤其要小心。
3.35.3 NULL 不能用 = 判断
错误:
WHERE Grade = NULL
正确:
WHERE Grade IS NULL
3.35.4 “没有选某门课”的错误写法
很多人会误写成:
WHERE Cno <> '81001'
这只表示“存在不是 81001 的选课记录”,不表示“没有选 81001”。
正确思路一般是:
NOT EXISTS,或者NOT IN(但还要注意 NULL 风险)。
3.35.5 外连接后误用 COUNT(*)
如果题目要求“包括没有选课的学生”,通常应考虑:
COUNT(Cno)
而不是:
COUNT(*)
3.35.6 更新或删除时忘写 WHERE
这是工程实践中的高危错误。
- 少写一个
WHERE,就可能全表更新或全表删除; - 在实际系统中后果非常严重。
3.35.7 把 DROP TABLE 和 DELETE FROM 混淆
区别:
DELETE FROM:删数据,不删表结构;DROP TABLE:表定义和数据都删掉。
3.35.8 视图中使用 SELECT *
短期方便,长期危险。
基表结构一改,视图可能失效。
3.35.9 把“能运行”误当成“规范正确”
不同 DBMS 宽松程度不同。
有些 SQL 在 MySQL 中可以执行,在 SQL Server 中可能报错。
所以学习阶段应尽量写:
- 语义清楚;
- 标准性更强;
- 依赖歧义更少的 SQL。
3.36 本章知识框架总结
这一章的核心可以整理成下面这张逻辑图:
一、SQL 是什么
- 关系数据库标准语言
- 统一支持 DDL、DML、DCL
- 非过程化、面向集合
二、SQL 管什么对象
- 模式
- 表
- 索引
- 视图
- 数据字典中的元数据
三、SQL 怎么查数据
- 单表查询
- 多表连接
- 子查询
- 集合查询
- 派生表查询
四、SQL 怎么改数据
- 插入
INSERT - 修改
UPDATE - 删除
DELETE
五、SQL 如何处理不确定数据
- 用 NULL 表示未知、无意义或不存在
- 用
IS NULL / IS NOT NULL判断 - 引入三值逻辑
六、SQL 如何进行逻辑封装
- 用视图保存查询定义
- 简化操作
- 增强安全性
- 提供逻辑独立性
3.37 学完本章后你应该达到的水平
如果这一章真正学明白了,你应该能做到:
- 能看懂并书写基本的
CREATE TABLE、ALTER TABLE、DROP TABLE语句; - 能写出单表查询,并正确使用
WHERE、ORDER BY、GROUP BY、HAVING、LIMIT; - 能区分内连接、外连接、自连接、多表连接;
- 能根据题意选择
IN、比较子查询、ANY/ALL、EXISTS; - 能正确理解“否定型查询”和“全称量词型查询”;
- 能写出基本的
INSERT、UPDATE、DELETE; - 能解释
NULL与三值逻辑; - 能理解视图的定义、查询、更新条件与作用。
如果再进一步,你还应形成一种更成熟的 SQL 思维:
先想清楚数据来自哪里、怎样筛选、是否需要分组、是否需要连接、结果究竟是“行级”还是“组级”,再动手写 SQL。
这比死背语句重要得多。
3.38 结语
第 3 章是后续学习 SQL 实践、数据库编程、事务、安全性、完整性、查询优化的基础。
这一章真正困难的地方,不是关键字本身,而是:
- 关系逻辑是否清楚;
- 条件语义是否严密;
- 能不能把自然语言题目准确翻译成 SQL。
所以学这一章时最有效的方法不是反复抄语法,而是:
- 先理解每种语句解决什么问题;
- 再分析例题中每个条件为什么这么写;
- 最后自己尝试把题目改写成不同形式,例如:
- 连接写法;
- 子查询写法;
- EXISTS 写法;
- 派生表写法。
当你能在多种写法之间自由转换,并且知道它们语义是否等价时,这一章才算真正掌握。