本章核心:SQL 不只是查询,还包括定义数据结构、修改数据、以及用程序化的方式操作数据库。
一、SQL 数据定义语言(DDL)
1.1 是什么?
DDL(Data Definition Language)= 定义和修改数据库对象的语言,负责"建骨架"。
下辖知识点
| 语句 | 作用 |
|---|---|
| CREATE | 创建数据库对象(数据库、表、视图、索引、约束等) |
| ALTER | 修改已有对象的结构 |
| DROP | 删除对象 |
| TRUNCATE | 清空表数据(保留结构) |
| 数据类型 | 定义列的存储类型(INT、VARCHAR、DATE、DECIMAL 等) |
| 完整性约束 | PRIMARY KEY、FOREIGN KEY、UNIQUE、NOT NULL、CHECK、DEFAULT |
| 索引 | INDEX,加速查询的数据结构 |
| 域(Domain) | 自定义数据类型(部分 DBMS 支持) |
| 模式(Schema) | 数据库对象的命名空间/容器 |
1.2 为什么要有 DDL?
没有 DDL,数据库就是一团混沌:
| 没有 DDL 的问题 | DDL 解决后 |
|---|---|
| 不知道数据存在哪、什么格式 | 用 CREATE TABLE 明确定义结构 |
| 程序代码和数据结构耦合 | 改表结构不用改程序(数据独立性) |
| 数据没有约束,想填什么填什么 | 用 CHECK、NOT NULL 等保证质量 |
| 查询慢得离谱 | 用 CREATE INDEX 建立索引加速 |
| 多人协作各自为政 | 用 Schema 做命名空间隔离 |
核心价值:
-
结构化管理 ------ 数据不是乱堆的,而是有明确定义的格式。
-
约束前置 ------ 在数据入库前就拦住脏数据。
-
性能基础 ------ 索引、分区等性能优化手段都靠 DDL 实现。
1.3 怎么用?
创建表(CREATE TABLE)
CREATE TABLE 学生 (
学号 CHAR(10) PRIMARY KEY, -- 主码约束
姓名 VARCHAR(20) NOT NULL, -- 非空约束
性别 CHAR(2) CHECK (性别 IN ('男','女')), -- CHECK约束
年龄 INT DEFAULT 18, -- 默认值
系号 CHAR(10),
身份证号 CHAR(18) UNIQUE, -- 唯一约束
-- 外码约束(表级定义)
CONSTRAINT FK_系号 FOREIGN KEY (系号)
REFERENCES 系(系号)
ON DELETE SET NULL -- 系被删,学生系号变NULL
ON UPDATE CASCADE -- 系号改了,学生跟着改
);
修改表(ALTER TABLE)
-- 加列
ALTER TABLE 学生 ADD 邮箱 VARCHAR(50);
-- 删列
ALTER TABLE 学生 DROP COLUMN 邮箱;
-- 改列类型
ALTER TABLE 学生 ALTER COLUMN 年龄 SMALLINT;
-- 加约束
ALTER TABLE 学生 ADD CONSTRAINT CHK_年龄 CHECK (年龄 BETWEEN 15 AND 50);
-- 删约束
ALTER TABLE 学生 DROP CONSTRAINT CHK_年龄;
删除表(DROP TABLE)
DROP TABLE 学生; -- 表结构和数据全删
DROP TABLE 学生 CASCADE; -- 级联删除关联对象(如外键引用的视图)
索引(CREATE INDEX)
-- 单列索引
CREATE INDEX IX_姓名 ON 学生(姓名);
-- 复合索引
CREATE INDEX IX_系名_年龄 ON 学生(系号, 年龄);
-- 唯一索引
CREATE UNIQUE INDEX IX_身份证 ON 学生(身份证号);
-- 删除索引
DROP INDEX IX_姓名;
索引的作用:加速 WHERE、ORDER BY、JOIN 的查询速度;代价是占用空间、减慢 INSERT/UPDATE/DELETE。
模式(Schema)
-- 创建模式(命名空间)
CREATE SCHEMA 教学管理;
-- 在指定模式下建表
CREATE TABLE 教学管理.学生 (...);
-- 授权
GRANT CREATE SCHEMA TO 用户名;
二、SQL 数据更新语言(DML)
2.1 是什么?
DML(Data Manipulation Language)= 操作表中数据的语言,负责"填内容"。
下辖知识点
| 语句 | 作用 |
|---|---|
| INSERT | 插入新数据 |
| UPDATE | 修改已有数据 |
| DELETE | 删除数据 |
| MERGE / UPSERT | 插入或更新(存在则更新,不存在则插入) |
| 批量插入 | 一次插入多行 |
| 子查询插入 | 从查询结果插入 |
2.2 为什么要有 DML?
数据不是静态的,需要持续维护:
| 场景 | DML 动作 |
|---|---|
| 新生入学 | INSERT 插入学生记录 |
| 学生转系 | UPDATE 修改系号 |
| 学生退学 | DELETE 删除记录 |
| 成绩录入 | INSERT / UPDATE 成绩 |
| 数据迁移 | INSERT + 子查询从旧表导入 |
核心价值:
-
数据生命周期管理 ------ 数据会增删改,DML 是日常运维的主力。
-
集合操作 ------ SQL 的 DML 操作的是集合(多行),不是逐行处理,效率高。
-
与查询结合 ------ INSERT/UPDATE/DELETE 可以嵌套子查询,实现复杂的数据维护。
2.3 怎么用?
插入(INSERT)
-- 插入单行
INSERT INTO 学生 (学号, 姓名, 性别, 年龄, 系号)
VALUES ('2024001', '张三', '男', 20, 'CS');
-- 插入多行
INSERT INTO 学生 (学号, 姓名, 性别, 年龄, 系号)
VALUES
('2024002', '李四', '女', 19, 'CS'),
('2024003', '王五', '男', 21, 'EE');
-- 从查询结果插入(批量导入)
INSERT INTO 优秀学生 (学号, 姓名, 平均分)
SELECT 学号, 姓名, AVG(成绩)
FROM 学生 JOIN 选课 ON 学生.学号 = 选课.学号
GROUP BY 学号, 姓名
HAVING AVG(成绩) > 85;
更新(UPDATE)
-- 单表更新
UPDATE 学生
SET 年龄 = 年龄 + 1
WHERE 系号 = 'CS';
-- 带子查询的更新
UPDATE 学生
SET 系号 = 'AI'
WHERE 学号 IN (
SELECT 学号 FROM 选课
WHERE 课程号 = 'AI101'
GROUP BY 学号
HAVING COUNT(*) >= 3
);
-- 多表关联更新(SQL Server语法)
UPDATE 学生
SET 学生.系号 = 新系表.新系号
FROM 学生 JOIN 新系表 ON 学生.系号 = 新系表.旧系号;
删除(DELETE)
-- 条件删除
DELETE FROM 学生 WHERE 系号 = 'CS';
-- 带子查询的删除
DELETE FROM 学生
WHERE 学号 NOT IN (SELECT DISTINCT 学号 FROM 选课);
-- 删除没选任何课的学生
-- 清空表(可回滚)
DELETE FROM 学生;
DELETE vs TRUNCATE 区别:
| 特性 | DELETE | TRUNCATE |
|---|---|---|
| 语句类型 | DML | DDL |
| 是否记录日志 | 逐行记录,日志量大 | 记录页级操作,日志量小 |
| 能否回滚 | ✅ 可以 | ✅ 可以(事务内) |
| 能否触发触发器 | ✅ 触发 DELETE 触发器 | ❌ 不触发 |
| 效率 | 慢 | 快 |
| 重置自增ID | ❌ 不重置 | ✅ 重置 |
三、视图(View)
3.1 是什么?
视图 = 从一个或多个基本表(或其他视图)导出的虚拟表,不存储实际数据,只保存查询定义。
下辖知识点
| 知识点 | 是什么 |
|---|---|
| 简单视图 | 基于单表、无函数、无 GROUP BY 的视图 |
| 复杂视图 | 多表连接、聚合函数、分组等 |
| 可更新视图 | 可以通过视图 INSERT/UPDATE/DELETE 基本表 |
| 不可更新视图 | 涉及聚合、DISTINCT、GROUP BY 等的视图,无法直接更新 |
| WITH CHECK OPTION | 限制通过视图修改的数据必须满足视图的 WHERE 条件 |
| 物化视图 | 实际存储查询结果的视图(有冗余,但查询快) |
| 级联删除/更新 | 视图定义中的级联操作 |
3.2 为什么要有视图?
直接操作基本表的困境:
| 问题 | 视图解决 |
|---|---|
| 表结构复杂,用户只需要看几列 | 视图只暴露需要的列,隐藏敏感/无关字段 |
| 同样的复杂查询要写无数次 | 视图把查询"封装"起来,用时像查表一样简单 |
| 不同用户应该看到不同数据 | 视图加权限控制,实现行级/列级安全 |
| 重构表结构后,应用程序要改 | 视图作为抽象层,底下表结构变了,视图不变 |
| 报表查询要实时计算聚合 | 物化视图预存结果,查询直接读 |
核心价值:
-
简化查询 ------ 复杂查询包成视图,用的时候
SELECT * FROM 视图名。 -
安全隔离 ------ 用户只能看到视图允许看到的列和行。
-
逻辑独立性 ------ 表结构调整时,通过视图兼容旧接口。
-
性能优化(物化视图) ------ 预计算 + 预存储,报表秒出。
3.3 怎么用?
创建视图(CREATE VIEW)
-- 简单视图:只暴露部分列
CREATE VIEW 学生基本信息 AS
SELECT 学号, 姓名, 性别, 系号
FROM 学生;
-- 复杂视图:多表连接 + 聚合
CREATE VIEW 系成绩统计 AS
SELECT
学生.系号,
COUNT(DISTINCT 学生.学号) AS 学生人数,
AVG(选课.成绩) AS 平均成绩,
MAX(选课.成绩) AS 最高分
FROM 学生 LEFT JOIN 选课 ON 学生.学号 = 选课.学号
GROUP BY 学生.系号;
-- 安全视图:只显示成绩及格的学生
CREATE VIEW 及格学生 AS
SELECT 学号, 姓名, 系号
FROM 学生
WHERE 学号 IN (SELECT 学号 FROM 选课 WHERE 成绩 >= 60)
WITH CHECK OPTION; -- 通过此视图插入的学生必须满足条件
使用视图
-- 像查表一样查视图
SELECT * FROM 系成绩统计 WHERE 平均成绩 > 80;
-- 更新视图(仅限可更新视图)
UPDATE 学生基本信息 SET 系号 = 'AI' WHERE 学号 = '2024001';
-- 实际修改的是底层"学生"表
删除视图
DROP VIEW 系成绩统计;
-- 只删视图定义,不影响基本表
物化视图(Materialized View)
-- Oracle / PostgreSQL 语法
CREATE MATERIALIZED VIEW 月销售统计 AS
SELECT 月份, SUM(金额) AS 总额
FROM 订单
GROUP BY 月份;
-- 手动刷新
REFRESH MATERIALIZED VIEW 月销售统计;
-- 或定时自动刷新
视图 vs 物化视图:
| 特性 | 普通视图 | 物化视图 |
|---|---|---|
| 是否存数据 | ❌ 不存,每次实时查 | ✅ 存了查询结果 |
| 查询速度 | 慢(要执行底层查询) | 快(直接读结果) |
| 数据实时性 | 实时 | 取决于刷新策略 |
| 占用空间 | 几乎不占用 | 占用存储 |
四、T-SQL 简介
4.1 是什么?
T-SQL(Transact-SQL)= SQL Server 的 SQL 方言扩展,是标准 SQL 加上微软扩展的过程化编程能力。
下辖知识点
| 知识点 | 是什么 |
|---|---|
| 变量声明 | DECLARE @变量名 类型 |
| 赋值 | SET @变量 = 值 或 SELECT @变量 = 列 FROM ... |
| 流程控制 | IF...ELSE、WHILE、CASE、GOTO、RETURN |
| 批处理 | GO 分隔批处理 |
| 注释 | -- 单行 和 /* 多行 */ |
| 系统函数 | GETDATE()、LEN()、CONVERT() 等 |
| TRY...CATCH | 异常处理 |
| 事务控制 | BEGIN TRAN、COMMIT、ROLLBACK、SAVEPOINT |
4.2 为什么要有 T-SQL?
标准 SQL 是声明式的,只描述要什么,不能描述"怎么做"的流程:
| 标准 SQL 的局限 | T-SQL 解决 |
|---|---|
| 没法写变量暂存中间结果 | 用 @变量 存储 |
| 没法按条件分支执行 | 用 IF...ELSE |
| 没法循环处理 | 用 WHILE |
| 出错只能返回错误码 | 用 TRY...CATCH 优雅处理 |
| 多条语句无法打包执行 | 用 BEGIN...END 和 GO |
| 无法调用复杂业务逻辑 | 用存储过程/函数封装 |
核心价值:
-
过程化编程 ------ 在数据库内部完成复杂逻辑,不用来回传数据到应用层。
-
减少网络往返 ------ 逻辑在数据库里执行,省去应用层和数据库的通信开销。
-
统一维护 ------ 业务逻辑写在数据库里,一处改到处生效。
4.3 怎么用?
变量与赋值
DECLARE @系号 CHAR(10);
DECLARE @人数 INT;
-- 直接赋值
SET @系号 = 'CS';
-- 从查询结果赋值
SELECT @人数 = COUNT(*) FROM 学生 WHERE 系号 = @系号;
PRINT '该系有 ' + CAST(@人数 AS VARCHAR) + ' 人';
流程控制
-- IF...ELSE
DECLARE @平均分 DECIMAL(5,2);
SELECT @平均分 = AVG(成绩) FROM 选课 WHERE 课程号 = 'DB';
IF @平均分 >= 80
PRINT '成绩优秀';
ELSE IF @平均分 >= 60
PRINT '成绩合格';
ELSE
PRINT '需要加强';
-- WHILE 循环
DECLARE @i INT = 1;
WHILE @i <= 10
BEGIN
INSERT INTO 测试表 (序号) VALUES (@i);
SET @i = @i + 1;
END;
-- CASE 表达式
SELECT 姓名,
CASE
WHEN 成绩 >= 90 THEN '优秀'
WHEN 成绩 >= 80 THEN '良好'
WHEN 成绩 >= 60 THEN '及格'
ELSE '不及格'
END AS 等级
FROM 学生 JOIN 选课 ON ...;
事务控制
BEGIN TRANSACTION;
BEGIN TRY
UPDATE 账户 SET 余额 = 余额 - 1000 WHERE 账户号 = 'A';
UPDATE 账户 SET 余额 = 余额 + 1000 WHERE 账户号 = 'B';
COMMIT; -- 成功提交
PRINT '转账成功';
END TRY
BEGIN CATCH
ROLLBACK; -- 出错回滚
PRINT '转账失败:' + ERROR_MESSAGE();
END CATCH;
常用系统函数
SELECT
GETDATE() AS 当前时间, -- 2025-01-15 10:30:00
LEN('数据库') AS 字符串长度, -- 3
CAST(123 AS VARCHAR) AS 转字符串, -- '123'
CONVERT(VARCHAR, GETDATE(), 120) AS 格式化日期 -- 120 = ODBC 规范
五、游标(Cursor)
5.1 是什么?
游标 = 一种逐行处理查询结果集的机制,把集合型的 SQL 结果转换成一行一行处理的方式。
下辖知识点
| 知识点 | 是什么 |
|---|---|
| 声明游标 | DECLARE 游标名 CURSOR FOR SELECT... |
| 打开游标 | OPEN |
| 取数据 | FETCH NEXT INTO @变量 |
| 循环遍历 | WHILE @@FETCH_STATUS = 0 |
| 关闭游标 | CLOSE |
| 释放游标 | DEALLOCATE |
| 游标类型 | 只进游标、动态游标、键集驱动游标、静态游标 |
| 游标属性 | @@FETCH_STATUS、@@CURSOR_ROWS |
5.2 为什么要有游标?
SQL 是集合操作的,但有些场景必须逐行处理:
| 场景 | 为什么需要游标 |
|---|---|
| 对每行数据做不同处理 | 比如根据成绩分段发送不同的通知 |
| 调用逐行处理的存储过程 | 每行都要触发一个复杂操作 |
| 结果集太大,内存放不下 | 用游标一次读一行,流式处理 |
| 与不支持集合操作的外部系统交互 | 逐行输出给旧系统 |
| 行与行之间有依赖关系 | 下一行的计算依赖上一行的结果 |
核心价值:
-
逐行控制能力 ------ 弥补 SQL 集合操作的不足。
-
内存友好 ------ 不用一次性加载整个结果集。
但注意 :游标是最后手段,能用集合操作解决的优先用集合操作,因为游标性能差。
5.3 怎么用?
游标的基本使用流程
-- 1. 声明游标
DECLARE cur_student CURSOR FOR
SELECT 学号, 姓名, 年龄 FROM 学生 WHERE 系号 = 'CS';
-- 2. 打开游标
OPEN cur_student;
-- 3. 取第一行
FETCH NEXT FROM cur_student INTO @学号, @姓名, @年龄;
-- 4. 循环处理
WHILE @@FETCH_STATUS = 0 -- 0 表示成功取到数据
BEGIN
-- 对当前行做处理
PRINT @姓名 + ' 的年龄是 ' + CAST(@年龄 AS VARCHAR);
-- 取下一行
FETCH NEXT FROM cur_student INTO @学号, @姓名, @年龄;
END;
-- 5. 关闭并释放
CLOSE cur_student;
DEALLOCATE cur_student;
一个完整的业务例子
-- 给每个学生发送通知(假设有发邮件的存储过程)
DECLARE @学号 CHAR(10), @姓名 VARCHAR(20), @成绩 INT;
DECLARE cur_grade CURSOR FOR
SELECT 学生.学号, 学生.姓名, 选课.成绩
FROM 学生 JOIN 选课 ON 学生.学号 = 选课.学号
WHERE 选课.课程号 = 'DB';
OPEN cur_grade;
FETCH NEXT FROM cur_grade INTO @学号, @姓名, @成绩;
WHILE @@FETCH_STATUS = 0
BEGIN
IF @成绩 >= 90
EXEC 发送通知 @学号, '恭喜' + @姓名 + ',你的数据库成绩优秀!';
ELSE IF @成绩 < 60
EXEC 发送通知 @学号, @姓名 + ',你的数据库课程需要补考。';
FETCH NEXT FROM cur_grade INTO @学号, @姓名, @成绩;
END;
CLOSE cur_grade;
DEALLOCATE cur_grade;
集合操作的替代方案(更推荐):
-- 用 CASE 一次性处理,性能更好
SELECT 学号, 姓名,
CASE
WHEN 成绩 >= 90 THEN '优秀'
WHEN 成绩 < 60 THEN '需补考'
ELSE '正常'
END AS 评价
FROM 学生 JOIN 选课 ON ...;
六、存储过程(Stored Procedure)
6.1 是什么?
存储过程 = 预编译并存储在数据库中的一组 SQL 语句,可以接收参数、执行逻辑、返回结果。
下辖知识点
| 知识点 | 是什么 |
|---|---|
| 创建存储过程 | CREATE PROCEDURE |
| 参数 | 输入参数(IN)、输出参数(OUT)、输入输出参数(INOUT) |
| 返回值 | RETURN 返回整数状态码 |
| 结果集 | SELECT 返回结果集 |
| 修改/删除 | ALTER PROCEDURE、DROP PROCEDURE |
| 执行 | EXEC / EXECUTE |
| 递归调用 | 存储过程调用自身 |
| 加密 | WITH ENCRYPTION 保护源码 |
| 重新编译 | WITH RECOMPILE 每次执行重新生成执行计划 |
6.2 为什么要有存储过程?
把 SQL 语句写在应用程序里的困境:
| 问题 | 存储过程解决 |
|---|---|
| 同样的 SQL 在多处重复写 | 封装一次,到处调用 |
| SQL 语句通过网络传来传去 | 只传过程名和参数,减少网络流量 |
| 数据库结构改了,所有应用代码要改 | 改存储过程即可,应用层无感知 |
| 复杂逻辑要在应用层和数据库间往返 | 逻辑在数据库内部完成,减少往返 |
| SQL 注入攻击风险 | 参数化存储过程天然防注入 |
| 权限控制粒度粗 | 用户只需有执行存储过程的权限,无需直接访问表 |
核心价值:
-
代码复用 ------ 一处写,到处用。
-
性能提升 ------ 预编译、减少网络传输。
-
安全增强 ------ 权限隔离、防 SQL 注入。
-
维护方便 ------ 改存储过程 = 改全局逻辑。
6.3 怎么用?
创建存储过程
-- 简单存储过程:查询某系学生
CREATE PROCEDURE GetStudentsByDept
@系号 VARCHAR(10)
AS
BEGIN
SELECT 学号, 姓名, 年龄
FROM 学生
WHERE 系号 = @系号;
END;
-- 带输出参数的存储过程:统计某系人数
CREATE PROCEDURE CountStudentsByDept
@系号 VARCHAR(10),
@人数 INT OUTPUT
AS
BEGIN
SELECT @人数 = COUNT(*) FROM 学生 WHERE 系号 = @系号;
RETURN 0; -- 返回状态码 0 表示成功
END;
-- 复杂存储过程:转账
CREATE PROCEDURE TransferMoney
@转出账户 CHAR(10),
@转入账户 CHAR(10),
@金额 DECIMAL(18,2)
AS
BEGIN
BEGIN TRANSACTION;
BEGIN TRY
-- 检查余额
DECLARE @余额 DECIMAL(18,2);
SELECT @余额 = 余额 FROM 账户 WHERE 账户号 = @转出账户;
IF @余额 < @金额
BEGIN
ROLLBACK;
RETURN 1; -- 余额不足
END;
-- 执行转账
UPDATE 账户 SET 余额 = 余额 - @金额 WHERE 账户号 = @转出账户;
UPDATE 账户 SET 余额 = 余额 + @金额 WHERE 账户号 = @转入账户;
COMMIT;
RETURN 0; -- 成功
END TRY
BEGIN CATCH
ROLLBACK;
RETURN -1; -- 系统错误
END CATCH;
END;
执行存储过程
-- 执行简单存储过程
EXEC GetStudentsByDept 'CS';
-- 执行带输出参数的存储过程
DECLARE @人数 INT;
EXEC CountStudentsByDept 'CS', @人数 OUTPUT;
PRINT '该系人数:' + CAST(@人数 AS VARCHAR);
-- 执行转账存储过程
DECLARE @结果 INT;
EXEC @结果 = TransferMoney 'A001', 'B002', 1000.00;
IF @结果 = 0
PRINT '转账成功';
ELSE IF @结果 = 1
PRINT '余额不足';
ELSE
PRINT '系统错误';
删除存储过程
DROP PROCEDURE GetStudentsByDept;
七、触发器(Trigger)
7.1 是什么?
触发器 = 一种特殊的存储过程 ,不由用户显式调用,而是在特定事件(INSERT/UPDATE/DELETE)发生时自动触发执行。
下辖知识点
| 知识点 | 是什么 |
|---|---|
| DML 触发器 | 由 INSERT/UPDATE/DELETE 触发 |
| DDL 触发器 | 由 CREATE/ALTER/DROP 等触发 |
| AFTER 触发器 | 在操作完成后触发(SQL Server 默认) |
| INSTEAD OF 触发器 | 替代原始操作执行(常用于视图更新) |
| BEFORE 触发器 | 在操作执行前触发(MySQL/Oracle 支持) |
| INSERTED 表 | 存放刚插入/更新后的新数据(内存中的虚拟表) |
| DELETED 表 | 存放刚删除/更新前的旧数据(内存中的虚拟表) |
| 行级触发器 | 每行触发一次 |
| 语句级触发器 | 每条语句触发一次 |
| 递归/嵌套触发器 | 触发器触发另一个触发器 |
| ENABLE/DISABLE | 启用/禁用触发器 |
7.2 为什么要有触发器?
有些操作必须在数据变更时自动发生,不能靠人工:
| 场景 | 触发器作用 |
|---|---|
| 插入订单时,自动扣减库存 | INSERT 触发器自动更新库存表 |
| 删除员工时,自动删除其家属记录 | DELETE 触发器级联删除 |
| 修改账户余额时,自动记录审计日志 | UPDATE 触发器写审计表 |
| 防止非法时间段的修改 | BEFORE 触发器检查并拒绝 |
| 复杂约束无法用 CHECK 表达 | 用触发器写任意逻辑判断 |
| 数据同步 | 主表修改时,触发器同步更新从表/缓存 |
核心价值:
-
自动化 ------ 事件驱动,不用手动调用。
-
数据一致性 ------ 关联表之间的级联操作自动完成。
-
审计追踪 ------ 谁在什么时候改了什么,自动记录。
-
复杂约束 ------ CHECK 不够用时,触发器可以写任意逻辑。
但注意 :触发器是双刃剑,过多或过于复杂会降低性能、增加维护难度。
7.3 怎么用?
创建触发器
-- 触发器1:插入学生时,自动初始化选课统计表
CREATE TRIGGER trg_InsertStudent
ON 学生
AFTER INSERT
AS
BEGIN
INSERT INTO 学生统计 (学号, 选课门数, 总学分)
SELECT 学号, 0, 0 FROM INSERTED;
END;
-- 触发器2:删除学生时,级联删除其选课记录
CREATE TRIGGER trg_DeleteStudent
ON 学生
INSTEAD OF DELETE
AS
BEGIN
-- 先删选课记录
DELETE FROM 选课 WHERE 学号 IN (SELECT 学号 FROM DELETED);
-- 再删学生
DELETE FROM 学生 WHERE 学号 IN (SELECT 学号 FROM DELETED);
END;
-- 触发器3:修改成绩时,记录审计日志
CREATE TRIGGER trg_AuditGrade
ON 选课
AFTER UPDATE
AS
BEGIN
IF UPDATE(成绩)
BEGIN
INSERT INTO 成绩变更日志 (学号, 课程号, 旧成绩, 新成绩, 修改时间, 操作人)
SELECT
DELETED.学号,
DELETED.课程号,
DELETED.成绩,
INSERTED.成绩,
GETDATE(),
SUSER_SNAME()
FROM DELETED JOIN INSERTED
ON DELETED.学号 = INSERTED.学号
AND DELETED.课程号 = INSERTED.课程号;
END;
END;
INSERTED 和 DELETED 虚拟表
| 操作 | INSERTED | DELETED |
|---|---|---|
| INSERT | 包含刚插入的新行 | 空 |
| DELETE | 空 | 包含刚删除的旧行 |
| UPDATE | 包含更新后的新行 | 包含更新前的旧行 |
禁用/启用触发器
-- 禁用触发器(批量导入数据时临时禁用)
DISABLE TRIGGER trg_AuditGrade ON 选课;
-- 批量操作...
-- 重新启用
ENABLE TRIGGER trg_AuditGrade ON 选课;
触发器 vs 约束 vs 存储过程 对比
| 特性 | 触发器 | 约束 | 存储过程 |
|---|---|---|---|
| 执行时机 | 自动(事件驱动) | 自动(数据变更时检查) | 手动调用 |
| 主要用途 | 级联操作、审计、复杂规则 | 保证数据完整性 | 封装业务逻辑 |
| 灵活性 | 最高(任意逻辑) | 低(固定规则) | 高(任意逻辑) |
| 性能影响 | 较大 | 小 | 可控 |
| 是否透明 | 对应用透明 | 对应用透明 | 应用需显式调用 |
八、知识脉络图
SQL 数据定义、更新及数据库编程
│
├── DDL(建骨架)
│ ├── 是什么:CREATE/ALTER/DROP/TRUNCATE
│ ├── 为什么:定义结构、加约束、建索引、保证质量
│ └── 怎么用:建表、改表、删表、加索引、建模式
│
├── DML(填内容)
│ ├── 是什么:INSERT/UPDATE/DELETE/MERGE
│ ├── 为什么:数据增删改、批量导入、集合操作
│ └── 怎么用:单行/多行插入、带子查询更新、条件删除
│
├── 视图(虚拟表)
│ ├── 是什么:从基本表导出的虚拟表
│ ├── 为什么:简化查询、安全隔离、逻辑独立、性能优化
│ └── 怎么用:CREATE VIEW、可更新视图、WITH CHECK OPTION、物化视图
│
├── T-SQL(过程化编程)
│ ├── 是什么:SQL Server 的过程化扩展
│ ├── 为什么:变量、分支、循环、异常处理、事务控制
│ └── 怎么用:DECLARE/SET、IF...ELSE/WHILE/CASE、TRY...CATCH
│
├── 游标(逐行处理)
│ ├── 是什么:逐行遍历结果集的机制
│ ├── 为什么:集合操作无法处理的逐行场景
│ └── 怎么用:DECLARE/OPEN/FETCH/CLOSE/DEALLOCATE
│
├── 存储过程(预编译代码块)
│ ├── 是什么:预编译并存储的 SQL 过程
│ ├── 为什么:复用、性能、安全、减少网络传输
│ └── 怎么用:CREATE PROCEDURE、参数、返回值、EXEC
│
└── 触发器(事件驱动自动执行)
├── 是什么:INSERT/UPDATE/DELETE 时自动触发的特殊过程
├── 为什么:自动化、级联操作、审计、复杂约束
└── 怎么用:AFTER/INSTEAD OF、INSERTED/DELETED、ENABLE/DISABLE
九、一句话记忆
| 概念 | 一句话 |
|---|---|
| DDL | 建骨架:CREATE 建、ALTER 改、DROP 删 |
| DML | 填内容:INSERT 插、UPDATE 改、DELETE 删 |
| 视图 | 虚拟表,不存数据,只存查询定义 |
| 可更新视图 | 简单的视图可以直接改底层表 |
| 物化视图 | 把视图结果存下来,查起来飞快 |
| T-SQL | SQL + 变量 + 分支 + 循环 + 异常处理 |
| 游标 | 一行一行啃结果集,性能差但有时候必须用 |
| 存储过程 | 把一堆 SQL 打包存数据库里,随叫随到 |
| 触发器 | 表一有动静就自动执行的"暗器" |
| INSERTED 表 | 触发器里看新数据的窗口 |
| DELETED 表 | 触发器里看旧数据的窗口 |
| AFTER 触发器 | 改完后再执行 |
| INSTEAD OF 触发器 | 把原来的操作替换掉 |
| 触发器双刃剑 | 用多了性能差,调试难,谨慎用 |
总结基于《数据库系统概论》第3章扩展及数据库编程知识体系整理