MYSQL速通(4/5)
十一、视图
视图是一张虚拟表,其内容由一条 SELECT 语句定义。它只保存 SQL 逻辑,不存储实际数据(除非是有物化视图),对数据的操作其实是对基表的数据进行操作。调用视图时,数据库会把视图定义中的 SQL 与外部查询合并,再对基表(base table)执行
视图的优点
逻辑解耦:对外暴露统一接口,底层表结构变化对应用透明。
权限隔离:只授予视图权限,而不暴露基表。
简化查询:把常用 JOIN/聚合封装成视图,SQL 更简洁。
兼容老系统:老代码用旧字段名,可在视图中做列别名/计算列。
视图的缺点
性能损耗:每次查询都要实时执行视图 SQL,复杂视图可能导致全表扫描/重复计算。
维护成本:基表变化(列改名、删列)可能导致视图失效。
更新限制:复杂视图无法直接 DML,需要额外触发器或存储过程。
1、基本语法
-- 创建CREATE [OR REPLACE] VIEW <视图名称> ASSELECT <要封装的字段>FROM <封装语句所处的表> <可加条件>;-- 查询视图SELECT * FROM <视图名称> <后面可加条件>-- 查询建视图语句SHOW CREATE VIEW <视图名称>; # 包含默认参数-- 修改视图# 使用创建语句替换CREATE OR REPLACE VIEW <视图名称> ASSELECT <要封装的字段>FROM <封装语句所处的表> <可加条件>;# 或使用alter修改ALTER VIEW <视图名称> ASSELECT <要封装的字段>FROM <封装语句所处的表> <可加条件>;-- 删除DROP VIEW [IF EXISTS] <视图名称>;
2、检查选项
2.1cascaded
当我们通过视图插入或更新数据时,如果没有使用WITH CHECK OPTION
:
MySQL只会检查数据是否符合基表的约束(如主键、NOT NULL等)
不会检查数据是否符合视图的WHERE条件
数据会成功插入基表,但可能无法通过该视图查询到
当我们使用了检查语句:
WITH CASCADED CHECK OPTION
:检查所有底层视图的条件(默认行为)
-- 给出测试案例即预测结果create table view_test(id int primary key not null unique comment '学号',name varchar(20) comment '姓名',age smallint comment '年龄',gender varchar(1) comment '性别')comment '视图测试表';INSERT INTO view_test (id, name, age, gender) VALUES(1, '张三', 12, '男'),(2, '李四', 22, '女'),(3, '王五', 15, '男'),(4, '赵六', 18, '女'),(5, '孙七', 25, '男');SELECT * FROM view_test;-- 基于基表创建 view_1 视图CREATE OR REPLACE VIEW view_1 AS SELECT id, name FROM view_test WHERE id <= 20;# 预计成功,因为插入id=11 <= 20,满足 view_1 条件INSERT INTO view_1 VALUES (11,'id是11');# 预计成功,因为没有使用检查选项,故不检查是否id满足条件,数据实际插入到了基表 view_test 中,但通过view_1 查询不到这条记录INSERT INTO view_1 VALUES (21,'id是21');SELECT * FROM view_1;-- 基于 view_1 视图创建 view_2 视图CREATE OR REPLACE VIEW view_2 AS SELECT id, name FROM view_1 WHERE id >= 10 WITH CASCADED CHECK OPTION ;# 预计失败,因为使用了检查选项,插入id=6 < 10,不满足view_2条件INSERT INTO view_2 VALUES (6,'id是6');# 预计成功,因为使用了检查选项,插入id=12 > 10,满足view_2条件————>且id=12 < 20,满足view_1条件INSERT INTO view_2 VALUES (12,'id是12');# 预计失败,因为使用了检查选项,插入id=22 > 10,满足view_2条件————>且id=22 > 20,不满足view_1条件INSERT INTO view_2 VALUES (22,'id是22');SELECT * FROM view_2;-- 基于 view_2 视图创建 view_3 视图CREATE OR REPLACE VIEW view_3 AS SELECT id, name FROM view_2 WHERE id <= 15;# 预计失败,没有使用检查选项故不检查view_3条件————>因为插入id=7 < 10,不满足view_2条件INSERT INTO view_3 VALUES (7,'id是7');# 预计成功,没有使用检查选项故不检查view_3条件————>因为插入id=13 > 10,满足view_2条件————>且id=13 < 20,满足view_1条件INSERT INTO view_3 VALUES (13,'id是13');# 预计成功,没有使用检查选项故不检查view_3条件————>因为插入id=17 > 10,满足view_2条件————>且id=17 < 20,满足view_1条件# 数据实际插入到了基表view_test中,但通过view_3查询不到这条记录,可通过view_2和view_1查看到此记录INSERT INTO view_3 VALUES (17,'id是17');# 预计失败,没有使用检查选项故不检查view_3条件————>因为插入id=23 > 10,满足view_2条件————>且id=23 > 20,不满足view_1条件INSERT INTO view_3 VALUES (23,'id是23');SELECT * FROM view_3;
最终基表、view_1视图、view_2视图、view_3视图结果如图:
2.2local
当我们使用了检查语句:
WITH LOCAL CHECK OPTION
:只检查当前视图的条件
-- 删除上面的案例,将cascaded改为loacl给出测试案例即预测结果create table view_test(id int primary key not null unique comment '学号',name varchar(20) comment '姓名',age smallint comment '年龄',gender varchar(1) comment '性别')comment '视图测试表';INSERT INTO view_test (id, name, age, gender) VALUES(1, '张三', 12, '男'),(2, '李四', 22, '女'),(3, '王五', 15, '男'),(4, '赵六', 18, '女'),(5, '孙七', 25, '男');SELECT * FROM view_test;-- 基于基表创建 view_1 视图CREATE OR REPLACE VIEW view_1 AS SELECT id, name FROM view_test WHERE id <= 20;# 预计成功,因为插入id=11 <= 20,满足 view_1 条件INSERT INTO view_1 VALUES (11,'id是11');# 预计成功,因为没有使用检查选项,故不检查是否id满足条件,数据实际插入到了基表 view_test 中,但通过view_1 查询不到这条记录INSERT INTO view_1 VALUES (21,'id是21');SELECT * FROM view_1;-- 基于 view_1 视图创建 view_2 视图CREATE OR REPLACE VIEW view_2 AS SELECT id, name FROM view_1 WHERE id >= 10 WITH LOCAL CHECK OPTION ;# 预计失败,因为使用了检查选项,插入id=6 < 10,不满足view_2条件INSERT INTO view_2 VALUES (6,'id是6');# 预计成功,因为使用了检查选项,插入id=12 > 10,满足view_2条件————>且 view_1 没有使用检查选项故不检查view_1条件INSERT INTO view_2 VALUES (12,'id是12');# 预计成功,因为使用了检查选项,插入id=22 > 10,满足view_2条件————>且 view_1 没有使用检查选项故不检查view_1条件INSERT INTO view_2 VALUES (22,'id是22');SELECT * FROM view_2;-- 基于 view_2 视图创建 view_3 视图CREATE OR REPLACE VIEW view_3 AS SELECT id, name FROM view_2 WHERE id <= 15;# 预计失败,没有使用检查选项故不检查view_3条件————>因为插入id=7 < 10,不满足view_2条件INSERT INTO view_3 VALUES (7,'id是7');# 预计成功,没有使用检查选项故不检查view_3条件————>因为插入id=13 > 10,满足view_2条件————>且 view_1 没有使用检查选项故不检查view_1条件INSERT INTO view_3 VALUES (13,'id是13');# 预计成功,没有使用检查选项故不检查view_3条件————>因为插入id=17 > 10,满足view_2条件————>且 view_1 没有使用检查选项故不检查view_1条件# 数据实际插入到了基表view_test中,但通过view_3查询不到这条记录,可通过view_2和view_1查看到此记录INSERT INTO view_3 VALUES (17,'id是17');# 预计成功,没有使用检查选项故不检查view_3条件————>因为插入id=23 > 10,满足view_2条件————>且 view_1 没有使用检查选项故不检查view_1条件# 数据实际插入到了基表view_test中,但通过view_3、view_2和view_1查询不到这条记录INSERT INTO view_3 VALUES (23,'id是23');SELECT * FROM view_3;
最终基表、view_1视图、view_2视图、view_3视图结果如图:
3、视图更新
在MySQL中,视图(View)的更新操作(INSERT/UPDATE/DELETE)有以下基本规则:
视图必须基于单表:不能更新基于多表JOIN的视图
不能包含聚合函数:如COUNT(), SUM(), AVG()等
不能包含DISTINCT、GROUP BY、HAVING子句
不能包含子查询在SELECT列表中
不能包含某些运算符:如UNION, UNION ALL
必须包含基表的所有NOT NULL列:除非这些列有默认值
十二、存储过程
存储过程是MySQL中一组预编译的SQL语句集合,存储在数据库中,可以通过调用来执行。
优点:
提高性能:预编译执行,减少网络传输
代码复用:一次编写,多次调用
安全性:可以限制对基表的直接访问
简化复杂操作:封装复杂业务逻辑
存储过程支持三种参数类型:
IN
(输入参数,默认)OUT
(输出参数)INOUT
(输入输出参数)
1、基本语法
# 创建存储过程-- 当使用命令行创建存储过程时,会默认以;为结束符-- 当存储过程体中包含含;的SQL语句,会提示存储过程创建不成功-- 可使用delimiter <你定义的结束符>来指定结束符不以;结束,而以自定义结束符结束CREATE PROCEDURE 存储过程名([参数列表])BEGIN-- 存储过程体-- 包含SQL语句和流程控制语句END ;-- 或DELIMITER // # 自定义结束符为 //CREATE PROCEDURE 过程名([参数列表])BEGIN-- 存储过程体-- 包含SQL语句和流程控制语句END //DELIMITER ; # 自定义结束符为 ;# 使用参数类型CREATE PROCEDURE example_proc(IN|OUT|INOUT 参数名 参数类型)BEGIN-- 过程体END;-- 举例CREATE PROCEDURE example_proc(IN p_id INT,OUT p_name VARCHAR(20),INOUT p_count INT)BEGIN-- 过程体END;# 查看所有存储过程SHOW PROCEDURE STATUS;# 查看特定存储过程定义SHOW CREATE PROCEDURE 存储过程名;# 修改存储过程-- 先删除再重建DROP PROCEDURE IF EXISTS 过程名;CREATE PROCEDURE 过程名() ...-- 或者使用ALTER(但功能有限)ALTER PROCEDURE 过程名 [特征...]# 删除存储过程DROP PROCEDURE [IF EXISTS] 存储过程名;# 调用存储过程CALL 存储过程名([参数列表]);
2、变量
①、系统变量
系统变量是MySQL服务器提供的,又可分为全局变量和会话变量
# 查看系统变量-- 查看所有系统变量SHOW VARIABLES;-- 查看会话(默认)/全局变量SHOW [SESSION | GLOBAL] VARIABLES;-- 查看特定系统变量(模糊匹配)SHOW VARIABLES LIKE '变量名';-- 查看会话/全局指定变量SELECT @@[SESSION | GLOBAL] .<系统变量名>;# 设置变量-- 设置全局变量(需要管理员权限)SET GLOBAL 变量名 = 值;-- 设置会话变量(仅影响当前连接)SET SESSION 变量名 = 值;-- 或简写为SET @变量名 = 值;
②、用户自定义变量
用户变量以@
开头,仅在当前会话中有效
不区分数据类型
会话结束时自动销毁
不需要预先声明
# 赋值SET @变量名 = 表达式;-- 或SELECT @变量名 := 表达式;-- 或SELECT 表达式 INTO @变量名;# 使用用户变量SET @变量名;
③、局部变量
局部变量仅在存储程序(存储过程、函数、触发器)中使用,需要先声明后使用。
必须在使用前声明
只在BEGIN...END块中有效
有明确的数据类型
# 声明局部变量DECLARE 变量名 数据类型 [DEFAULT 默认值];# 赋值SET 变量名 = 表达式;-- 或SET 变量名 := 表达式;
3、流程控制
IF语句
CASE语句
循环(WHILE【满足条件进入循环】, REPEAT【满足条件退出循环】, LOOP)
# IF 语句IF <条件1> THEN<要执行的语句1>ELSEIF <条件2> THEN<要执行的语句2>ELSE<要执行的语句3>END IF;-- IF 举例DELIMITER // -- 定义一个存储过程CREATE PROCEDURE check_age_status(IN s_id INT)BEGINDECLARE s_age INT; -- 定义变量DECLARE status VARCHAR(20);SELECT age INTO s_age FROM view_test WHERE id = s_id;IF s_age < 18 THENSET status = '未成年';ELSEIF s_age >= 18 AND s_age < 60 THENSET status = '成年';ELSESET status = '老年';END IF;SELECT CONCAT('ID:', s_id, ' 状态:', status) AS result;END //DELIMITER ;CALL check_age_status(1); -- 调用上面创建的存储过程# CASE 语句CASE <case 值>WHEN <条件1> THEN <执行语句1>WHEN <条件2> THEN <执行语句2>WHEN <条件3> THEN <执行语句3>。。。。ELSE <执行语句>END CASE;# WHILE 语句WHILE <条件> DO <执行语句>END WHILE;-- WHILE循环示例CREATE PROCEDURE while_example(IN max_num INT)BEGINDECLARE i INT DEFAULT 1; -- 定义变量WHILE i <= max_num DOINSERT INTO numbers VALUES (i);SET i = i + 1;END WHILE;END;# REPEAT 语句REPEAT<执行语句>UNTIL <退出条件>END REPEAT;-- REPEAT循环示例CREATE PROCEDURE repeat_example(IN max_num INT)BEGINDECLARE i INT DEFAULT 1;REPEATINSERT INTO numbers VALUES (i);SET i = i + 1;UNTIL i > max_num END REPEAT;END;# 不使用 LEAVE/ITERATE 的LOOP 语句(死循环无退出条件)<标记1:>LOOP <执行语句>END LOOP;# 使用了 LEAVE 的 LOOP 语句<标记1:>LOOP <执行语句>LEAVE <标记1>; -- 相当于breakEND LOOP <标记1>;# 使用了 ITERATE 的 LOOP 语句<标记1:>LOOP <执行语句>ITERATE <标记1>; -- 相当于continueEND LOOP <标记1>;
4、游标(cursor)
游标类似变量,只不过变量只可以存储单一数据,游标是存储数据集,是MySQL中用于遍历结果集的一种数据库对象,特别适用于在存储过程和函数中处理多行数据。
①、游标的工作流程:
声明游标:定义要遍历的结果集
打开游标:执行查询并填充结果集
获取数据:逐行读取结果
关闭游标:释放资源
②、基本语法
# 声明游标DECLARE 游标名称 CURSOR FOR SELECT语句;# 打开游标OPEN 游标名称;# 获取数据FETCH 游标名称 INTO 变量列表;# 关闭游标CLOSE 游标名称;# 游标异常处理(条件处理程序)DECLARE handler_type HANDLER FOR condition_value [, condition_value] ... handler_statements# 处理游标结束(NOT FOUND)DECLARE CONTINUE HANDLER FOR NOT FOUND SET 结束标志 = TRUE;-- 举例DELIMITER // # 自定义结束符为 //CREATE PROCEDURE update_student_scores() # 创建一个存储过程 update_student_scoresBEGINDECLARE done INT DEFAULT FALSE; # 定义整形变量 done ,初始值为FALSE(0)DECLARE s_id INT; # 定义整形变量 s_idDECLARE s_score DECIMAL(5,2); # 定义整形变量 s_scoreDECLARE new_score DECIMAL(5,2);-- 声明游标:获取需要更新的学生DECLARE cur CURSOR FOR # 声明一个叫 cur 的游标# 获取students表中所有分数低于60分的学生ID和分数SELECT id, score FROM students WHERE score < 60;# 声明一个异常处理器,当游标读取不到更多数据时(NOT FOUND),将done变量设为TRUE(1)DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;OPEN cur; # 打开游标update_loop: LOOP # 给循环加入标签 update_loopFETCH cur INTO s_id, s_score; # 获取 s_id 和 s_score 的数据并赋给游标 curIF done THENLEAVE update_loop; # 如果读取数据完毕就跳出 LOOP 循环END IF;-- 计算新分数(示例:给不及格学生加10分,但不超过60)SET new_score = LEAST(s_score + 10, 60);-- 更新记录UPDATE students SET score = new_score WHERE id = s_id;# 返回更新完成的提示信息SELECT CONCAT('已更新ID:', s_id, ' 原分数:', s_score, ' 新分数:', new_score) AS 更新日志;END LOOP;CLOSE cur; # 关闭游标,释放相关资源SELECT '分数更新完成' AS 结果; # 返回操作完成的提示信息END //DELIMITER ; # 自定义结束符为 ;
③、条件处理程序
# 游标异常处理(条件处理程序)DECLARE handler_type HANDLER FOR condition_value [, condition_value] ... handler_statements
handler_type:处理程序类型
CONTINUE
:执行处理语句后继续程序EXIT
:执行处理语句后退出当前BEGIN...END块UNDO
:执行处理语句后回滚操作(MySQL中不支持)
condition_value:可捕获的条件
SQLSTATE [VALUE] sqlstate_value
condition_name
:用户定义的命名条件SQLWARNING
:以'01'开头的SQLSTATE代码NOT FOUND
:以'02'开头的SQLSTATE代码(常用于游标)SQLEXCEPTION
:不以'00'、'01'、'02'开头的SQLSTATE代码
handler_statements:触发条件时要执行的语句
④、游标的特性
只读:游标不能用于修改数据
单向:只能向前移动,不能后退
十三、触发器
触发器(Trigger)是MySQL中的一种特殊存储过程,它会在特定数据库事件(INSERT、UPDATE、DELETE)发生时自动执行。
1、基本语法
# 创建触发器CREATE TRIGGER 触发器名称触发时机(BEFORE|AFTER) 触发事件(INSERT|UPDATE|DELETE)ON 表名 FOR EACH ROW -- 行级触发器BEGIN-- 触发器逻辑END;# 查看触发器SHOW TRIGGERS;# 删除触发器-- 若没指定数据库名称,默认是当前数据库DROP TRIGGERS <数据库名称>.<触发器名称>;
2、组成部分:
①、触发时机:
BEFORE
:在操作执行前触发AFTER
:在操作执行后触发
②、触发事件:
INSERT
:插入数据时触发UPDATE
:更新数据时触发DELETE
:删除数据时触发
③、FOR EACH ROW:
表示行级触发器(MySQL只支持行级触发器)
3、特殊变量
在触发器内部,可以使用两个特殊变量访问受影响的数据:
NEW
:引用新数据(用于INSERT和UPDATE)OLD
:引用旧数据(用于UPDATE和DELETE)
4、举例
# BEFORE INSERTDELIMITER //CREATE TRIGGER before_student_insert # 创建一个叫 before_student_insert 的触发器BEFORE INSERT ON students # 在对表 students 进行插入操作之前触发FOR EACH ROW # 行级触发器BEGIN-- 自动设置创建时间为当前时间SET NEW.created_at = NOW();-- 验证数据IF NEW.age < 0 THENSIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '年龄不能为负数';END IF;END //DELIMITER ;# AFTER UPDATEDELIMITER //CREATE TRIGGER after_student_update # 创建一个叫 after_student_update 的触发器AFTER UPDATE ON students # 在对表 students 进行更新操作之后触发FOR EACH ROWBEGIN-- 记录变更历史IF NEW.name != OLD.name THENINSERT INTO student_change_log(student_id, changed_field, old_value, new_value)VALUES(OLD.id, 'name', OLD.name, NEW.name);END IF;IF NEW.score != OLD.score THENINSERT INTO student_change_log(student_id, changed_field, old_value, new_value)VALUES(OLD.id, 'score', OLD.score, NEW.score);END IF;END //DELIMITER ;# BEFORE DELETEDELIMITER //CREATE TRIGGER before_student_deleteBEFORE DELETE ON studentsFOR EACH ROWBEGIN-- 归档被删除的学生记录INSERT INTO students_archive(id, name, age, score, deleted_at)VALUES(OLD.id, OLD.name, OLD.age, OLD.score, NOW());-- 删除相关记录DELETE FROM student_courses WHERE student_id = OLD.id;END //DELIMITER ;