主题
第三章 SQL 语言详解
📖 ⏱️ 预计阅读时长 1 分钟 👑 会员专属
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 根据功能可分为以下几类:
- 数据定义语言 (DDL):用于定义和管理数据库对象的结构。
- 主要语句:
CREATE、ALTER、DROP、TRUNCATE。
- 主要语句:
- 数据操纵语言 (DML):用于对表中的数据进行增删改查。
- 主要语句:
INSERT、UPDATE、DELETE、SELECT。
- 主要语句:
- 数据控制语言 (DCL):用于权限管理。
- 主要语句:
GRANT(授权)、REVOKE(撤销权限)。
- 主要语句:
- 事务控制语言 (TCL):用于管理事务的提交和回滚。
- 主要语句:
COMMIT、ROLLBACK、SAVEPOINT。
- 主要语句:
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); -- 唯一索引- 单列索引与联合索引:联合索引的使用需遵循最左前缀匹配原则——查询条件必须从联合索引的最左列开始,才能有效利用该索引。
- 索引的代价:虽然索引能加速查询,但每次
INSERT、UPDATE、DELETE操作时都需要同步维护索引,因此不宜对频繁修改的列建立过多索引。
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 锁),因此可能引起锁等待甚至死锁。
在实际工程中,修改性语句的要求远高于“语法正确”。尤其是 UPDATE、DELETE、ALTER 这类语句,最稳妥的习惯是在正式执行前先写一条完全相同条件的 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]];各子句的逻辑执行顺序为:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER 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 NULL 或 IS 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;WHERE 与 HAVING 的区别:
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 语句,通常出现在 WHERE、FROM 或 SELECT 子句中。
标量子查询
返回单行单列的值:
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 视图的作用
- 简化查询:将复杂的多表连接查询封装为视图,供用户直接查询。
- 安全性:通过视图限制用户只能看到特定的行或列,实现数据的行级和列级权限控制。
- 逻辑独立性:当基本表结构变化时,可以通过调整视图定义保持应用接口不变。
视图特别像数据库里的“受控窗口”。它并不改变底层事实的存储方式,却能决定用户以什么角度看待这些事实。一个设计良好的视图,既可以降低查询复杂度,也能防止用户直接接触过宽、过敏感或语义过于底层的基表。也正因为如此,视图不是简单的语法包装,而是一种数据接口设计手段。把视图用好,数据库就不仅在保存数据,还在主动组织数据被理解和被消费的方式。
5.3 视图的更新限制
并非所有视图都支持 INSERT、UPDATE、DELETE 操作。以下情况的视图通常不可更新:
- 视图定义中包含聚合函数(如
SUM、AVG)。 - 包含
GROUP BY或DISTINCT。 - 由多个表连接生成。
- 包含
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)
触发器是一种特殊的存储过程,由数据库中特定事件(INSERT、UPDATE、DELETE)自动触发执行,无需手动调用。
- 执行时机:
BEFORE(操作执行之前触发)或AFTER(操作执行之后触发)。 - 伪表 NEW/OLD:触发器内可通过
NEW表访问即将写入的新值,通过OLD表访问修改前的旧值。 - 应用场景:审计日志记录、复杂的完整性约束校验、级联数据同步等。
- 注意事项:
CREATE VIEW等 DDL 操作不会触发针对 DML 事件的触发器——因为 DDL 并不实际修改表中的数据。
触发器最容易被误用的地方,在于它“自动执行”这一特性既是优点,也是风险。优点在于很多必须跟随数据变化发生的动作可以被强制绑定,例如审计日志、衍生字段维护、复杂约束校验;风险在于一旦逻辑过重、依赖过多或层层级联,系统行为会变得隐蔽,排查问题时很难一眼看出是哪一步自动触发了额外写入。因此,使用触发器时应坚持一个原则:只放那些与数据一致性强相关、且离开数据库层就难以保证的逻辑,而不要把大段业务流程悄悄藏在触发器里。
8.3 游标 (Cursors)
SQL 的查询结果是一个集合,而宿主语言(如 C、Java)通常一次只能处理一条记录。游标提供了一种逐行遍历结果集的机制。
游标的使用遵循固定的生命周期:
DECLARE:声明游标及其关联的SELECT语句。OPEN:执行关联的查询,生成结果集。FETCH:从结果集中逐行取出数据。CLOSE:关闭游标,释放资源。
WARNING
必须在 OPEN 之后才能执行 FETCH。如果在未执行 OPEN 的情况下直接 FETCH,系统将报"游标未打开"的错误。
游标的存在提醒我们,SQL 虽然擅长集合处理,但有些场景仍需要按行遍历结果集。不过,从教材角度看,游标更像一种必要但应谨慎使用的补充工具,而不是常规首选方案。因为一旦开始逐行处理,很多本可由集合运算一次完成的任务会退化成“循环 + 单行操作”,性能和表达简洁性都可能下降。因此,面对一个问题时,最好先问能不能用集合方式直接完成,只有当逻辑确实依赖逐行交互时,再考虑游标。
8.4 嵌入式 SQL (Embedded SQL)
在 C 等宿主语言中直接嵌入 SQL 语句时,需要遵循以下通信机制:
- SQLCA (SQL Communication Area):宿主程序通过 SQLCA 获取 SQL 执行的状态信息(如错误码)。
- 主变量 (Host Variables):在 SQL 语句中使用冒号前缀的变量(如
:v_sno)实现宿主语言与 SQL 之间的数据传递。 - 动态 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 不仅要会查数据,还要学会建立清晰的安全边界。
🔒 会员专属内容
检查登录状态中...
