返回 Database
Database 2026-05-19

第3章 关系数据库标准语言 SQL

第3章 关系数据库标准语言 SQL

3.1 本章总览

这一章是数据库课程里最重要的一章之一,因为它把前面学过的关系模型真正落到了“如何写数据库语言”上。

前两章主要回答的是:

  • 数据库里的数据应该怎样组织;
  • 关系模型中的表、属性、元组、主码、完整性等概念是什么意思。

而本章开始回答的是:

  • 如何用 SQL 定义数据库对象;
  • 如何用 SQL 查询数据;
  • 如何修改数据;
  • 如何处理空值;
  • 如何使用视图。

从知识结构上看,本章可以分成六个部分:

  1. SQL 概述:了解 SQL 是什么、有什么特点、在数据库系统中处于什么位置。
  2. 数据定义(DDL):学习怎样创建、修改、删除模式、表、索引等数据库对象。
  3. 数据查询(SELECT):学习 SQL 中最核心、最复杂的查询语句。
  4. 数据更新(INSERT / UPDATE / DELETE):学习如何插入、修改、删除表中的数据。
  5. 空值处理:理解 NULL 的含义、判断方式以及三值逻辑。
  6. 视图:理解视图的定义、查询、更新以及作用。

可以把本章概括为一句话:

前两章解决“数据长什么样”,本章解决“怎样用标准语言操作这些数据”。


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 SCHEMACREATE DATABASE 功能几乎等价,可以互换使用;
  • 但在 Oracle、SQL Server、PostgreSQL 等系统中,schemadatabase 往往不是一个层级的概念。

因此学习 SQL 时必须形成一个观念:

先掌握标准 SQL 的基本思想,再理解具体 DBMS 的实现差异。

考试通常偏向标准概念; 工程实践则必须关注具体产品语法。

3.2.3 SQL 的主要功能分类

SQL 不是只会查询,它实际上覆盖了数据库工作的多个方面。

类别含义常见命令
DDLData Definition Language,数据定义语言CREATEDROPALTER
DMLData Manipulation Language,数据操纵语言SELECTINSERTUPDATEDELETE
DCLData Control Language,数据控制语言GRANTREVOKE

本章重点讲的是:

  • 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)
  • 它同时引用 StudentCourse,体现了参照完整性。

3.7.6 列级约束与表级约束的区别

这是建表时的一个高频考点。

列级约束

只作用于一列,例如:

  • NOT NULL
  • UNIQUE
  • 某些单列 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 数据类型选择的原则

选类型时不要只看“能不能存下”,还要看:

  • 是否符合语义;
  • 是否便于比较与计算;
  • 是否有利于约束与索引;
  • 是否兼顾存储空间和精度。

例如:

  • 学号应选 CHARVARCHAR
  • 出生日期应选 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 TABLEDELETE 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 查询的学习主线

课件把数据查询分成五块:

  1. 单表查询
  2. 连接查询
  3. 嵌套查询
  4. 集合查询
  5. 基于派生表的查询

这五类查询是递进关系:

  • 单表查询是基础;
  • 连接查询解决多表联合;
  • 嵌套查询解决“先查一个结果,再用这个结果继续查”;
  • 集合查询强调结果集运算;
  • 派生表则把中间结果显式放到 FROM 子句中。

3.13.3 建议掌握的“查询执行思维”

写查询不能只背语法,更要形成逻辑顺序意识。

一个查询通常是在做这几件事:

  1. 先确定数据来自哪些表;
  2. 再确定如何连接;
  3. 再筛选满足条件的元组;
  4. 再分组;
  5. 再对组进行筛选;
  6. 再决定输出哪些列;
  7. 最后排序、截取前几行。

这也是你分析复杂查询题时最稳的思路。


3.14 单表查询

单表查询是所有查询的基础。

课件把单表查询分成六类:

  1. 选择表中的若干列
  2. 选择表中的若干元组
  3. ORDER BY
  4. 聚集函数
  5. GROUP BY
  6. LIMIT

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 NULL
  • IS NOT NULL

例如:

SELECT Sno, Cno
FROM SC
WHERE Grade IS NULL;

表示查询缺少成绩的选课记录。

3.14.9 多重条件查询

多个条件可以用逻辑运算组合:

  • AND
  • OR
  • NOT

例如:

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;

原因是:

  • 没选课的学生在外连接后,CnoNULL
  • 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 嵌套

例如查询选修“信息系统概论”的学生姓名时,可以分三步:

  1. Course 中找到课程号;
  2. SC 中找到选了这门课的学生号;
  3. Student 中找对应学生姓名。

这就是多层嵌套查询的典型逻辑。


3.19 带比较运算符的子查询

当子查询结果确定为单值时,可以直接和比较运算符配合。

例如:

SELECT Sno, Cno
FROM SC x
WHERE Grade >= (
    SELECT AVG(Grade)
    FROM SC y
    WHERE y.Sno = x.Sno
);

表示找出每个学生中“成绩大于等于自己平均成绩”的课程号。

这里内层查询返回的是某个学生的平均成绩,是单值,所以能直接和 >= 配合。

理解重点:

  • 如果内层只会返回一个值,用比较运算符最直接;
  • 如果会返回多个值,就要考虑 INANYALLEXISTS 等。

3.20 带 ANY / ALL 的子查询

3.20.1 基本语义

ANYALL 必须与比较运算符一起使用。

其含义如下:

  • > 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 NULLUNIQUE、值域约束等。

所以插入失败常常不是语法错,而是约束不满足


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 更新操作的风险意识

更新语句尤其要注意两点:

  1. WHERE 一旦漏写,可能修改整张表;
  2. 更新后的值也必须满足完整性约束。

例如成绩加分、减分时,要考虑会不会超出允许范围。

课件也特别引导思考:

  • 把成绩整体加 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 中可能还有这些学生的选课记录引用它们。

因此通常要分两步:

  1. 先删依赖表中的记录;
  2. 再删主表中的记录。

这本质上体现了参照完整性约束


3.28 空值的处理

NULL 是 SQL 中一个极其重要、也极其容易混淆的概念。

3.28.1 空值的含义

空值不等于 0,也不等于空字符串。

NULL 表示的可能是:

  1. 该属性应该有值,但目前不知道;
  2. 该属性不应该有值;
  3. 由于某种原因暂时不便填写。

所以 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 NULL
  • IS 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 空值参与运算的结果

算术运算

空值与其他值做算术运算,结果通常仍为空值。

比较运算

空值与其他值比较,结果不是 TRUEFALSE,而是 UNKNOWN

例如:

  • NULL = 5 不是假,而是未知;
  • NULL < 60 也不是假,而是未知。

3.28.6 三值逻辑

因为有了 UNKNOWN,SQL 的逻辑不再只有:

  • TRUE
  • FALSE

而是变成:

  • TRUE
  • FALSE
  • UNKNOWN

这正是很多查询“看起来应该查出来,结果却没查出来”的根源。

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 视图的特点

视图有四个核心特点:

  1. 视图是从一个或多个基本表(或视图)导出的表;
  2. 数据库中只存视图定义,不存视图数据;
  3. 基表数据变化时,视图查询结果会变化;
  4. 视图是虚表。

所以视图的本质可以概括为:

视图是对查询逻辑的封装。


3.30 定义视图

3.30.1 建立视图的语法

CREATE VIEW <视图名> [(列名列表)]
AS <子查询>
[WITH CHECK OPTION];

3.30.2 什么时候必须显式指定列名

课件指出,下列情况应显式指定视图列名:

  1. 目标列不是简单属性名,而是表达式或聚集函数;
  2. 多表连接选出了同名列;
  3. 希望给列改成更合适的新名字。

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;

其含义是:

  • 通过该视图执行 INSERTUPDATEDELETE 时;
  • 系统会检查结果是否仍满足视图定义中的条件;
  • 不满足则拒绝执行。

例如向该视图中插入一个“计算机科学与技术”专业学生,就会失败。

理解为:

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 什么叫更新视图

更新视图是指通过视图执行:

  • INSERT
  • UPDATE
  • DELETE

系统再把这些操作转换成对基本表的更新。

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 WHEREHAVING 混淆

记法:

  • WHERE 筛行;
  • HAVING 筛组。

凡是涉及 COUNTAVGSUM 等聚集结果做条件,优先考虑 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 TABLEDELETE 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 学完本章后你应该达到的水平

如果这一章真正学明白了,你应该能做到:

  1. 能看懂并书写基本的 CREATE TABLEALTER TABLEDROP TABLE 语句;
  2. 能写出单表查询,并正确使用 WHEREORDER BYGROUP BYHAVINGLIMIT
  3. 能区分内连接、外连接、自连接、多表连接;
  4. 能根据题意选择 IN、比较子查询、ANY/ALLEXISTS
  5. 能正确理解“否定型查询”和“全称量词型查询”;
  6. 能写出基本的 INSERTUPDATEDELETE
  7. 能解释 NULL 与三值逻辑;
  8. 能理解视图的定义、查询、更新条件与作用。

如果再进一步,你还应形成一种更成熟的 SQL 思维:

先想清楚数据来自哪里、怎样筛选、是否需要分组、是否需要连接、结果究竟是“行级”还是“组级”,再动手写 SQL。

这比死背语句重要得多。


3.38 结语

第 3 章是后续学习 SQL 实践、数据库编程、事务、安全性、完整性、查询优化的基础。

这一章真正困难的地方,不是关键字本身,而是:

  • 关系逻辑是否清楚;
  • 条件语义是否严密;
  • 能不能把自然语言题目准确翻译成 SQL。

所以学这一章时最有效的方法不是反复抄语法,而是:

  • 先理解每种语句解决什么问题;
  • 再分析例题中每个条件为什么这么写;
  • 最后自己尝试把题目改写成不同形式,例如:
    • 连接写法;
    • 子查询写法;
    • EXISTS 写法;
    • 派生表写法。

当你能在多种写法之间自由转换,并且知道它们语义是否等价时,这一章才算真正掌握。