Skip to content

第三章 SQL 语言详解

📖 ⏱️ 预计阅读时长

1. SQL 概述

结构化查询语言 (SQL, Structured Query Language) 是关系型数据库的标准语言。它不同于 C、Java 等过程式编程语言——SQL 是一种声明式 (Declarative) 语言,用户只需描述"需要什么样的数据",而无需指定"如何获取数据"的具体步骤。底层的查询路径选择由 DBMS 的查询优化器自动完成。

也正因为 SQL 是声明式语言,学习 SQL 时最容易出现两种偏差。第一种偏差是把它当成关键字清单,只记语法模板,不理解每个子句在结果语义中承担什么作用;第二种偏差是只追求“语句能跑出来”,却忽略结果口径、执行代价和协作可维护性。真正成熟的 SQL 能力,既包括把业务问题准确翻译成查询结构,也包括判断这条语句在真实系统中是否稳定、安全、可读。换句话说,SQL 既是一门语言,也是一种工程接口。

1.1 SQL 的标准化历程

SQL 经过多次标准化修订,主要版本包括:

  • SQL-86 / SQL-89:最初的工业标准,确立了 SELECT-FROM-WHERE 的基本框架。
  • SQL-92:目前兼容性最广泛的版本,引入了外连接语法(LEFT/RIGHT JOIN)、子查询等特性。
  • SQL:1999:引入了公用表表达式 (CTE)、递归查询、触发器标准和布尔类型。
  • SQL:2003:引入了窗口函数 (Window Functions),极大增强了分析查询能力。
  • 后续版本:陆续增加了对 JSON、XML、时序数据等的支持。

1.2 SQL 语言的功能分类

SQL 根据功能可分为以下几类:

  1. 数据定义语言 (DDL):用于定义和管理数据库对象的结构。
    • 主要语句:CREATEALTERDROPTRUNCATE
  2. 数据操纵语言 (DML):用于对表中的数据进行增删改查。
    • 主要语句:INSERTUPDATEDELETESELECT
  3. 数据控制语言 (DCL):用于权限管理。
    • 主要语句:GRANT(授权)、REVOKE(撤销权限)。
  4. 事务控制语言 (TCL):用于管理事务的提交和回滚。
    • 主要语句:COMMITROLLBACKSAVEPOINT

NOTE

SELECT 有时被单独称为数据查询语言 (DQL),但在广义的 DML 分类中通常将其包含在内。


2. 数据定义 (DDL)

2.1 创建表

使用 CREATE TABLE 语句定义表的结构,包括列名、数据类型和各种约束条件。

sql
CREATE TABLE Student (
    Sno    CHAR(9)    PRIMARY KEY,     -- 主键约束
    Sname  CHAR(20)   NOT NULL,        -- 非空约束
    Ssex   CHAR(2),
    Sage   SMALLINT   CHECK (Sage >= 0 AND Sage <= 150),  -- CHECK 约束
    Sdept  CHAR(20)
);

常见的列级约束包括:

  • PRIMARY KEY:主键约束,保证实体完整性。主键值不能为 NULL 且不能重复。
  • NOT NULL:非空约束。
  • UNIQUE:唯一性约束,允许存在一个 NULL 值(取决于具体 DBMS 实现)。
  • CHECK:检查约束,限定列值的合法范围。
  • DEFAULT:为列指定默认值。

表级约束可以在列定义之后统一声明,适用于联合主键或外键:

sql
CREATE TABLE SC (
    Sno    CHAR(9),
    Cno    CHAR(4),
    Grade  SMALLINT,
    PRIMARY KEY (Sno, Cno),                      -- 联合主键
    FOREIGN KEY (Sno) REFERENCES Student(Sno)     -- 外键约束
        ON DELETE CASCADE                         -- 级联删除
        ON UPDATE CASCADE                         -- 级联更新
);

建表语句之所以在 SQL 学习中格外重要,是因为它决定了后续所有增删改查的语义边界。很多初学者更喜欢先学查询,觉得建表只是“把字段列出来”,其实恰恰相反。表结构一旦设计粗糙,后面再复杂的查询也只能在模糊结构上打补丁。主键决定对象如何被唯一识别,数据类型决定值能以什么形式进入系统,约束决定哪些非法状态在入口就被挡住。数据库系统之所以可靠,首先不是因为查询写得漂亮,而是因为从第一条 CREATE TABLE 开始就把边界定义清楚了。

选择数据类型时,也不能只看“能不能存进去”,而要看它是否与业务语义一致。年龄字段用整数、金额字段用精确小数、状态字段用受限枚举或字典引用、时间字段明确时区语义,这些都属于设计阶段必须提前想清楚的问题。若图省事把大量字段都做成字符串,短期内可能觉得灵活,长期就会在排序错误、比较失真、索引失效和统计口径混乱中付出更高代价。SQL 教材之所以反复强调类型,不是形式主义,而是在帮助学习者建立“结构先于操作”的思维。

2.2 修改表结构

使用 ALTER TABLE 语句对已有的表结构进行调整:

  • 添加新列:ALTER TABLE Student ADD S_entrance DATE;
  • 修改列的数据类型:ALTER TABLE Student ALTER COLUMN Sage INT;
  • 添加约束:ALTER TABLE Student ADD CONSTRAINT ck_age CHECK (Sage >= 0);
  • 删除约束:ALTER TABLE Student DROP CONSTRAINT ck_age;

ALTER TABLE 之所以值得单独强调,是因为它代表数据库进入持续演进阶段。真实系统几乎不可能永远保持第一次建表时的结构不变,字段会新增,含义会扩展,约束会收紧,索引也会跟着访问模式调整。也正因为结构会变化,修改表结构时不能只盯着语法是否正确,还要考虑旧数据如何迁移、默认值如何补齐、是否会触发大表锁定、上层应用是否已经适配。数据库结构变更不是普通文本编辑,而是一次对线上数据契约的正式修改。

2.3 删除表

DROP TABLE Student; 会同时删除表的结构定义和所有数据,该操作不可回滚(因为它属于 DDL 操作)。

需区分三个相关操作:

  • DROP TABLE:删除表结构及数据。
  • DELETE FROM table:删除表中的数据,表结构保留,操作可回滚,会触发触发器。
  • TRUNCATE TABLE:清空表数据,表结构保留,不可回滚,速度比 DELETE 快,不触发行级触发器。

2.4 索引的创建与管理

索引是提升查询性能的核心手段。其作用在于通过建立额外的数据结构(如 B+ 树、哈希表),使 DBMS 无需逐行扫描整张表即可快速定位目标数据。

sql
CREATE INDEX idx_sname ON Student(Sname);          -- 普通索引
CREATE UNIQUE INDEX idx_sno ON Student(Sno);       -- 唯一索引
  • 单列索引与联合索引:联合索引的使用需遵循最左前缀匹配原则——查询条件必须从联合索引的最左列开始,才能有效利用该索引。
  • 索引的代价:虽然索引能加速查询,但每次 INSERTUPDATEDELETE 操作时都需要同步维护索引,因此不宜对频繁修改的列建立过多索引。

3. 数据操纵 (DML) 基础

3.1 插入数据 (INSERT)

sql
-- 插入单行数据
INSERT INTO Student (Sno, Sname, Sage) VALUES ('001', '张明', 20);

-- 省略列名(按表定义顺序插入全部列)
INSERT INTO Student VALUES ('002', '李华', '男', 21, '计算机系');

-- 批量插入
INSERT INTO Student (Sno, Sname, Sage)
VALUES ('003', '王芳', 19), ('004', '赵强', 22);

-- 从查询结果中插入
INSERT INTO Dept_Age (Sdept, Avg_age)
SELECT Sdept, AVG(Sage) FROM Student GROUP BY Sdept;

INSERT 的本质不是“往表里加一行”,而是让某个新事实正式进入数据库。也因此,插入动作最值得注意的通常不是语法本身,而是数据来源是否合法、键是否冲突、默认值是否合理、是否会与现有约束发生矛盾。批量插入与查询结果插入尤其能体现 SQL 的集合处理能力,它们强调的不是一条条手工录入,而是把一批满足共同条件的事实整体纳入数据库。这种“面向集合而非面向单行”的思维,是 SQL 与普通编程语言的重要差别之一。

3.2 更新数据 (UPDATE)

sql
UPDATE Student SET Sage = Sage + 1 WHERE Sdept = '计算机系';

注意事项:

  • 如果省略 WHERE 子句,将更新表中所有行——这是生产环境中最常见的事故之一。
  • 在并发环境中,UPDATE 操作会对涉及的行加排他锁 (X 锁),因此可能引起锁等待甚至死锁。

在实际工程中,修改性语句的要求远高于“语法正确”。尤其是 UPDATEDELETEALTER 这类语句,最稳妥的习惯是在正式执行前先写一条完全相同条件的 SELECT 用来核对命中范围,再在事务中试运行并确认影响行数。对于关键业务表,还应记录操作原因、时间范围和回滚预案。真正成熟的 SQL 使用者,不只是会写出答案,更会考虑怎样安全地把答案作用到真实数据上。

3.3 删除数据 (DELETE)

sql
DELETE FROM Student WHERE Sno = '001';

UPDATE 类似,省略 WHERE 将删除表中全部数据。

删除操作在教材里往往被写成最简单的一条语句,但在真实系统中却常常最危险。因为删除不仅意味着记录消失,还可能影响外键关系、统计口径、审计追踪和历史复盘。很多业务并不适合物理删除,而更适合使用逻辑删除、状态置位、归档表迁移等方式保留痕迹。是否真的执行 DELETE,本质上是在回答一个业务问题:这个事实是应该彻底不存在,还是只是不再参与当前业务流程。只有把这个问题想清楚,删除策略才不会误伤系统长期可追溯性。


4. 数据查询 (SELECT) 详解

SELECT 是 SQL 中功能最丰富、使用频率最高的语句。其完整语法结构如下:

sql
SELECT [ALL | DISTINCT] <目标列表达式> [, <目标列表达式>] ...
FROM <表名或视图名> [, <表名或视图名>] ...
[WHERE <条件表达式>]
[GROUP BY <列名> [HAVING <条件表达式>]]
[ORDER BY <列名> [ASC | DESC]];

各子句的逻辑执行顺序为:FROMWHEREGROUP BYHAVINGSELECTORDER BY

SELECT 难学的根源,往往并不是关键字太多,而是它把一个业务问题拆成了多个逻辑阶段。初学者看到语句从上往下写,就容易误以为数据库也从上往下执行;真正理解执行顺序后,很多问题才会豁然开朗。例如,为什么 WHERE 不能直接使用聚合函数,因为分组还没发生;为什么 HAVING 能筛选聚合结果,因为它是在分组之后工作;为什么 ORDER BY 看上去写在最后,却经常决定最终输出形态。把查询视为一条“从原始表到目标结果”的加工链,比死记子句更容易形成稳定理解。

4.1 基本查询

sql
-- 查询全部列
SELECT * FROM Student;

-- 查询指定列并去重
SELECT DISTINCT Sdept FROM Student;

-- 使用表达式和别名
SELECT Sname, 2026 - Sage AS Birth_year FROM Student;

4.2 条件查询 (WHERE)

WHERE 子句用于在分组之前对行进行过滤。常见的条件运算符包括:

运算符含义示例
=, <>, <, >, <=, >=比较运算Sage > 20
BETWEEN ... AND ...范围Sage BETWEEN 18 AND 25
IN (值列表)集合成员测试Sdept IN ('CS', 'MA')
LIKE模式匹配Sname LIKE '张%'
IS NULL / IS NOT NULL空值判断Grade IS NULL
AND, OR, NOT逻辑运算Sage > 20 AND Sdept = 'CS'

关于 NULL 的特殊性:在 SQL 的三值逻辑 (True, False, Unknown) 中,任何值与 NULL 的比较运算结果都是 Unknown(包括 NULL = NULL)。WHERE 子句只保留条件结果为 True 的行,因此涉及 NULL 的行在使用 =<> 比较时通常不会出现在结果集中。判断空值必须使用 IS NULLIS NOT NULL

这里需要特别建立一个观念:NULL 不是零,也不是空字符串,更不是某种普通默认值。它表示的是“当前未知”或“当前缺失”。一旦把 NULL 错当成普通值,很多判断都会失真。例如 COUNT(*) 统计的是行数,而 COUNT(列名) 只统计该列非空值;布尔表达式中只要有 NULL 参与,结果就可能变成 Unknown。教学中最好始终追问一句:当前语句是在处理一个真实存在的值,还是在处理一个未知状态。这个问题一旦问清楚,很多空值相关错误都会明显减少。

4.3 聚合函数与分组查询 (GROUP BY, HAVING)

SQL 提供了以下常用聚合函数:

函数功能
COUNT(*)统计行数(含 NULL 行)
COUNT(列名)统计该列非空值的个数
SUM(列名)求和
AVG(列名)求平均值
MAX(列名) / MIN(列名)求最大值 / 最小值

GROUP BY 子句将查询结果按指定列的值分组,然后对每个分组分别应用聚合函数。

sql
SELECT Sdept, COUNT(*) AS 人数, AVG(Sage) AS 平均年龄
FROM Student
GROUP BY Sdept
HAVING COUNT(*) > 5;

WHEREHAVING 的区别:

  • WHERE 在分组之前过滤行,不能使用聚合函数。
  • HAVING 在分组之后过滤分组,可以使用聚合函数。

统计类 SQL 的真正难点,往往不在于会不会写 GROUP BY,而在于指标口径是否一致。一个看似简单的“日活用户”“有效订单数”“平均成绩”,都至少包含三层定义:统计对象是谁,时间范围怎么算,聚合方式按什么口径去重。如果这三层定义在不同报表里被随意改动,即使每条 SQL 语法都正确,最终也会形成多个彼此冲突的“版本真相”。因此,聚合查询一定要养成注重口径治理的习惯:先明确对象范围,再明确时间边界,最后明确分组与去重依据。

4.4 排序 (ORDER BY)

sql
SELECT * FROM Student ORDER BY Sage DESC, Sno ASC;

ORDER BY 在逻辑上是最后执行的子句,支持按多列排序。ASC 表示升序(默认),DESC 表示降序。

4.5 连接查询 (JOIN)

连接查询用于从多张表中获取关联数据,是关系型数据库区别于文件系统的核心能力。

内连接 (INNER JOIN)

只返回两张表中满足连接条件的行:

sql
SELECT Student.Sno, Sname, Cno, Grade
FROM Student INNER JOIN SC ON Student.Sno = SC.Sno;

左外连接 (LEFT OUTER JOIN)

在内连接的基础上,保留左表中所有行。当左表某行在右表中没有匹配时,右表对应的列用 NULL 填充:

sql
SELECT Student.Sno, Sname, Cno, Grade
FROM Student LEFT JOIN SC ON Student.Sno = SC.Sno;

右外连接与全外连接同理,分别保留右表或两表中的全部行。

自连接 (Self Join)

同一张表与自身进行连接,通常用于表示同一实体间的层次或对比关系:

sql
-- 查找同一部门中年龄大于平均年龄的学生
SELECT a.Sname, a.Sage
FROM Student a, (SELECT Sdept, AVG(Sage) AS avg_age FROM Student GROUP BY Sdept) b
WHERE a.Sdept = b.Sdept AND a.Sage > b.avg_age;

IMPORTANT

在外连接中,过滤条件放在 ON 子句和 WHERE 子句中的效果不同。放在 ON 中的条件只影响连接匹配过程;放在 WHERE 中的条件在连接完成后对结果集进行过滤,可能导致外连接退化为内连接的效果。

学习连接时,还必须理解 SQL 结果默认采用的是袋语义,也就是允许重复行存在。很多“连接后数据翻倍”的现象并不是数据库出错,而是连接基数本来就会产生多条匹配记录。若不先分析一对一、一对多还是多对多关系,而是直接套用 DISTINCT 去重,虽然结果看起来“恢复正常”,却可能掩盖了模型设计或查询逻辑上的真实问题。更稳妥的做法是先确认当前结果在业务上究竟应不应该唯一,再决定通过主键、分组、窗口函数筛选还是显式去重来约束结果集。

4.6 子查询 (Subqueries)

子查询是嵌套在其他查询内部的 SELECT 语句,通常出现在 WHEREFROMSELECT 子句中。

标量子查询

返回单行单列的值:

sql
SELECT Sname FROM Student
WHERE Sage > (SELECT AVG(Sage) FROM Student);

使用 IN 的子查询

sql
SELECT Sname FROM Student
WHERE Sno IN (SELECT Sno FROM SC WHERE Cno = 'C001');

使用 EXISTS 的相关子查询

EXISTS 检查子查询是否返回至少一行。相关子查询对外层的每一行都执行一次,逻辑上效率较低,但当内层表有合适索引时,优化器通常可以高效处理:

sql
-- 查找至少选修了一门课程的学生
SELECT Sname FROM Student
WHERE EXISTS (SELECT 1 FROM SC WHERE SC.Sno = Student.Sno);

IN 与 EXISTS 的选择

  • 当内层查询结果集较小时,IN 通常更直观高效。
  • 当外层表较小且内层表有索引时,EXISTS 可能更优。
  • 现代优化器在很多场景下可以自动将两者进行等价转换。

无论使用连接、子查询还是 CTE,真正要训练的是“从业务问题推导查询结构”的能力。一个复杂需求通常至少要先拆解四步:第一,明确最终输出的对象是明细、汇总还是排名;第二,明确涉及哪些表以及它们通过什么键关联;第三,明确时间、状态、范围等筛选边界;第四,明确结果是否需要分组、排序、分页或窗口分析。只要先把这四步想清楚,再写 SQL,复杂查询就会变得更像结构化推导,而不是语法拼装。

4.7 集合操作

SQL 提供了基于集合的操作,用于合并多个查询的结果:

  • UNION:取并集并去重。
  • UNION ALL:取并集但保留重复行。
  • INTERSECT:取交集。
  • EXCEPT (或 MINUS):取差集。
sql
SELECT Sno FROM SC WHERE Cno = 'C001'
UNION
SELECT Sno FROM SC WHERE Cno = 'C002';

5. 视图 (Views)

视图是从一个或多个基本表导出的虚拟表。数据库中只存储视图的定义(即 SELECT 语句),不存储视图对应的数据。

5.1 视图的创建与使用

sql
CREATE VIEW CS_Student AS
SELECT Sno, Sname, Sage FROM Student WHERE Sdept = 'CS';

对视图进行查询时,DBMS 会将视图的定义与用户的查询合并,转换为对基本表的查询(称为视图消解)。

5.2 视图的作用

  1. 简化查询:将复杂的多表连接查询封装为视图,供用户直接查询。
  2. 安全性:通过视图限制用户只能看到特定的行或列,实现数据的行级和列级权限控制。
  3. 逻辑独立性:当基本表结构变化时,可以通过调整视图定义保持应用接口不变。

视图特别像数据库里的“受控窗口”。它并不改变底层事实的存储方式,却能决定用户以什么角度看待这些事实。一个设计良好的视图,既可以降低查询复杂度,也能防止用户直接接触过宽、过敏感或语义过于底层的基表。也正因为如此,视图不是简单的语法包装,而是一种数据接口设计手段。把视图用好,数据库就不仅在保存数据,还在主动组织数据被理解和被消费的方式。

5.3 视图的更新限制

并非所有视图都支持 INSERTUPDATEDELETE 操作。以下情况的视图通常不可更新

  • 视图定义中包含聚合函数(如 SUMAVG)。
  • 包含 GROUP BYDISTINCT
  • 由多个表连接生成。
  • 包含 UNION 等集合操作。

6. 窗口函数 (Window Functions)

窗口函数是 SQL:2003 标准引入的分析特性,允许在不合并行的情况下对数据执行聚合或排序计算。

6.1 基本语法

sql
<窗口函数>() OVER (
    [PARTITION BY <分区列>]
    [ORDER BY <排序列>]
    [ROWS/RANGE BETWEEN ... AND ...]
)
  • PARTITION BY:将数据按指定列分区,类似 GROUP BY,但不会折叠行。
  • ORDER BY:指定分区内的排序规则。
  • ROWS/RANGE BETWEEN:定义窗口帧的范围(如当前行及前后各 N 行)。

6.2 常用窗口函数

  • 排名函数
    • ROW_NUMBER():为每行分配唯一的连续序号。
    • RANK():允许并列排名,但排名不连续(如 1, 1, 3)。
    • DENSE_RANK():允许并列排名,且排名连续(如 1, 1, 2)。
  • 聚合窗口函数SUM(), AVG(), COUNT(), MAX(), MIN() 均可作为窗口函数使用。
sql
-- 查询每个部门薪资排名前 3 的员工
SELECT * FROM (
    SELECT name, dept, salary,
           DENSE_RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS rnk
    FROM Employee
) tmp
WHERE rnk <= 3;

窗口函数的价值,在于它让“明细与汇总同时出现”成为可能。传统聚合会把多行压缩成一行,而窗口函数保留原始明细,只是在每一行旁边附加排名、累计和分区统计结果。这种能力特别适合经营分析、风控监控和行为序列分析。使用窗口函数时要格外看清两件事:PARTITION BY 决定比较范围,ORDER BY 决定顺序语义。分区选错,比较对象就会错;排序不稳定,累计结果就会漂移。教学中若能先画清“分区边界”和“窗口帧范围”,再写语句,逻辑会清楚很多。


7. 公用表表达式 (CTE) 与递归查询

WITH 子句(CTE)允许在查询的开头定义临时的命名结果集,提升复杂查询的可读性和可维护性。

sql
WITH HighEarners AS (
    SELECT emp_id, name, salary, dept_id FROM Employee WHERE salary > 50000
),
BigDepts AS (
    SELECT dept_id FROM HighEarners GROUP BY dept_id HAVING COUNT(*) > 10
)
SELECT name FROM HighEarners h JOIN BigDepts b ON h.dept_id = b.dept_id;

递归 CTE 可用于查询树形或层次结构数据(如组织架构、分类目录):

sql
WITH RECURSIVE OrgTree AS (
    -- 初始查询:找到根节点
    SELECT id, name, parent_id, 1 AS level FROM Department WHERE parent_id IS NULL
    UNION ALL
    -- 递归部分:不断向下展开
    SELECT d.id, d.name, d.parent_id, o.level + 1
    FROM Department d JOIN OrgTree o ON d.parent_id = o.id
)
SELECT * FROM OrgTree ORDER BY level, id;

CTE 的教学价值,不只是“让 SQL 写得好看”,而是帮助学习者把复杂问题拆成层次清晰的中间语义。很多长 SQL 之所以难读,并不是逻辑真的太难,而是把所有过滤、聚合、去重和连接一次性挤进一条语句里,导致人脑难以同时跟踪。使用 CTE 后,可以先定义“高收入员工是谁”,再定义“大部门是什么”,最后再组合结果。也就是说,CTE 本质上是在为查询过程命名中间概念。对复杂业务问题而言,这种能力非常接近写论文时先定义术语再做推导,因此特别适合教材式训练。


8. 数据库编程

8.1 存储过程 (Stored Procedures)

存储过程是存储在数据库中的经过预编译的 SQL 语句集合,可以包含流程控制逻辑(如条件分支和循环)。

  • 优点:减少网络通信开销(只需传递调用指令而非大量 SQL 文本);可以实现复杂的业务逻辑;支持权限封装(可授予用户执行存储过程的权限而不授予直接操作基本表的权限)。
  • 缺点:调试和版本管理不如应用层代码方便;过多使用会增加数据库服务器的计算压力。

如何看待存储过程,关键在于明确数据库层与应用层的边界。若某段逻辑本质上与数据约束强绑定,例如批量校验、审计记录、权限封装、就地计算统计结果,放在数据库层往往更自然;若逻辑高度依赖外部服务、复杂流程编排和频繁版本迭代,放在应用层通常更易维护。存储过程既不是应该被完全避免的“旧技术”,也不是所有业务逻辑都该塞进去的“万能容器”。它是一种把部分逻辑下沉到数据附近执行的手段,是否使用,取决于边界是否清晰。

8.2 触发器 (Triggers)

触发器是一种特殊的存储过程,由数据库中特定事件(INSERTUPDATEDELETE)自动触发执行,无需手动调用。

  • 执行时机BEFORE(操作执行之前触发)或 AFTER(操作执行之后触发)。
  • 伪表 NEW/OLD:触发器内可通过 NEW 表访问即将写入的新值,通过 OLD 表访问修改前的旧值。
  • 应用场景:审计日志记录、复杂的完整性约束校验、级联数据同步等。
  • 注意事项CREATE VIEW 等 DDL 操作不会触发针对 DML 事件的触发器——因为 DDL 并不实际修改表中的数据。

触发器最容易被误用的地方,在于它“自动执行”这一特性既是优点,也是风险。优点在于很多必须跟随数据变化发生的动作可以被强制绑定,例如审计日志、衍生字段维护、复杂约束校验;风险在于一旦逻辑过重、依赖过多或层层级联,系统行为会变得隐蔽,排查问题时很难一眼看出是哪一步自动触发了额外写入。因此,使用触发器时应坚持一个原则:只放那些与数据一致性强相关、且离开数据库层就难以保证的逻辑,而不要把大段业务流程悄悄藏在触发器里。

8.3 游标 (Cursors)

SQL 的查询结果是一个集合,而宿主语言(如 C、Java)通常一次只能处理一条记录。游标提供了一种逐行遍历结果集的机制。

游标的使用遵循固定的生命周期:

  1. DECLARE:声明游标及其关联的 SELECT 语句。
  2. OPEN:执行关联的查询,生成结果集。
  3. FETCH:从结果集中逐行取出数据。
  4. CLOSE:关闭游标,释放资源。

WARNING

必须在 OPEN 之后才能执行 FETCH。如果在未执行 OPEN 的情况下直接 FETCH,系统将报"游标未打开"的错误。

游标的存在提醒我们,SQL 虽然擅长集合处理,但有些场景仍需要按行遍历结果集。不过,从教材角度看,游标更像一种必要但应谨慎使用的补充工具,而不是常规首选方案。因为一旦开始逐行处理,很多本可由集合运算一次完成的任务会退化成“循环 + 单行操作”,性能和表达简洁性都可能下降。因此,面对一个问题时,最好先问能不能用集合方式直接完成,只有当逻辑确实依赖逐行交互时,再考虑游标。

8.4 嵌入式 SQL (Embedded SQL)

在 C 等宿主语言中直接嵌入 SQL 语句时,需要遵循以下通信机制:

  1. SQLCA (SQL Communication Area):宿主程序通过 SQLCA 获取 SQL 执行的状态信息(如错误码)。
  2. 主变量 (Host Variables):在 SQL 语句中使用冒号前缀的变量(如 :v_sno)实现宿主语言与 SQL 之间的数据传递。
  3. 动态 SQL (Dynamic SQL):当 SQL 语句的具体内容在编译时无法确定,需要在运行时动态构造并执行时,可使用 EXECUTE IMMEDIATE 等机制。

9. 数据控制 (DCL)

9.1 权限管理

sql
-- 将 Student 表的查询权限授予用户 user1
GRANT SELECT ON Student TO user1;

-- 将 Student 表的全部权限授予用户 user1,并允许其继续转授
GRANT ALL PRIVILEGES ON Student TO user1 WITH GRANT OPTION;

-- 撤销权限
REVOKE SELECT ON Student FROM user1;

WITH GRANT OPTION 允许被授权者将获得的权限继续授予其他用户,形成了一条权限传播链。使用 REVOKE 撤销权限时,系统可以选择级联收回 (CASCADE) 或受限收回 (RESTRICT)。

视图、权限和授权链并不是数据库附属功能,它们共同决定谁能看到什么数据、能在什么范围内修改数据。生产环境中最常见的问题不是“不会授权”,而是临时授权长期遗留、账号权限持续膨胀、责任边界越来越模糊。因此,权限管理应遵循三个基本原则:默认拒绝、按需授权、定期回收。尤其在多人协作环境中,SQL 不仅要会查数据,还要学会建立清晰的安全边界。

🔒 会员专属内容

检查登录状态中...