十一、视图
1.常见的数据库对象
对象 | 描述 |
---|---|
表(TABLE) | 表是存储数据的逻辑单元,以行和列的形式存在,列就是字段,行就是记录 |
数据字典 | 就是系统表,存放数据库相关信息的表。系统表的数据通常由数据库系统维护,程序员通常不应该修改,只可查看 |
约束(CONSTRAINT) | 执行数据校验的规则,用于保证数据完整性的规则 |
视图(VIEW) | 一个或者多个数据表里的数据的逻辑显示,视图并不存储数据 |
索引(INDEX) | 用于提高查询性能,相当于书的目录 |
存储过程(PROCEDURE) | 用于完成一次完整的业务处理,没有返回值,但可通过传出参数将多个值传给调用环境 |
存储函数(FUNCTION) | 用于完成一次特定的计算,具有一个返回值 |
触发器(TRIGGER) | 相当于一个事件监听器,当数据库发生特定事件后,触发器被触发,完成相应的处理 |
2.视图概述
为什么使用视图 ?
视图一方面可以帮我们使用表的一部分而不是所有的表,另一方面也可以针对不同的用户制定不同的查询视图。比如,针对一个公司的销售人员,我们只想给他看部分数据,而某些特殊的数据,比如采购的价格,则不会提供给他。再比如,人员薪酬是个敏感的字段,那么只给某个级别以上的人员开放,其他人的查询视图中则不提供这个字段。
视图的理解 :
-
视图是一种 虚拟表 ,本身是 不具有数据 的,占用很少的内存空间,它是 SQL 中的一个重要概念。
-
视图建立在已有表的基础上, 视图赖以建立的这些表称为基表。
-
视图的创建和删除只影响视图本身,不影响对应的基表。但是当对视图中的数据进行增加、删除和修改操作时,数据表中的数据会相应地发生变化,反之亦然。
-
向视图提供数据内容的语句为 SELECT 语句, 可以将视图理解为存储起来的 SELECT 语句。
-
在数据库中,视图不会保存数据,数据真正保存在数据表中。当对视图中的数据进行增加、删除和修改操作时,数据表中的数据会相应地发生变化;反之亦然。
-
-
视图,是向用户提供基表数据的另一种表现形式。通常情况下,小型项目的数据库可以不使用视图,但是在大型项目中,以及数据表比较复杂的情况下,视图的价值就凸显出来了,它可以帮助我们把经常查询的结果集放到虚拟表中,提升使用效率。理解和使用起来都非常方便。
3.创建视图
#第十四章_视图(VIEW) /* 1.视图的理解 1> 视图, 可以看做是一个虚拟表, 本身是不存储数据的 视图的本质, 就可以看做是存储起来的SELECT语句 2> 视图中SELECT语句中涉及到的表, 称为基表 3> 针对视图做DML操作, 会影响到对应的基表中的数据。反之亦然 4> 视图本身的删除不会导致基表数据的删除。 5> 视图的应用场景:针对小型项目, 不推荐使用视图, 针对于大型项目, 可以考虑使用视图。 6> 视图的优点 : 简化查询;控制数据的访问 */ # 2. 如何创建视图 CREATE DATABASE dbtest14; USE dbtest14; CREATE TABLE emps AS SELECT * FROM atguigudb.employees; CREATE TABLE depts AS SELECT * FROM atguigudb.departments; SELECT * FROM emps; SELECT * FROM depts; DESC emps;#CREATE TABLE AS 方式创建的表不会将主键约束复制过来 (除了非空约束都没有带过来) #针对于单表 CREATE VIEW vu_emp1 AS SELECT employee_id, last_name, salary FROM emps; SELECT * FROM vu_emp1; #确定视图中字段名的方式1 : CREATE VIEW vu_emp2 AS SELECT employee_id AS "emp_id", last_name AS "lname", salary FROM emps#查询语句中的字段别名会作为视图中的字段 WHERE salary > 8000; SELECT * FROM vu_emp2; #确定视图中字段名的方式2 : CREATE VIEW vu_emp3(emp_id, NAME, monthly_sal)#小括号内字段个数与select字段个数相同 AS SELECT employee_id, last_name, salary FROM emps WHERE salary > 8000; SELECT * FROM vu_emp3; #可以有原来表中不存在的数据(运算出来的) CREATE VIEW vu_emp_sal AS SELECT department_id, AVG(salary) AS "avg_sal" FROM emps WHERE department_id IS NOT NULL GROUP BY department_id; SELECT * FROM vu_emp_sal; #针对于多表 CREATE VIEW vu_emp_dept AS SELECT employee_id, e.department_id, department_name FROM emps e JOIN depts d ON e.department_id = d.department_id; SELECT * FROM vu_emp_dept; #利用视图对数据进行格式化 CREATE VIEW vu_emp_dept1 AS SELECT CONCAT(e.last_name, '(', d.department_name, ')') AS "emp_info" FROM emps e JOIN depts d ON e.department_id = d.department_id; SELECT * FROM vu_emp_dept1; #基于视图创建视图 CREATE VIEW vu_emp4 AS SELECT employee_id, last_name FROM vu_emp1; SELECT * FROM vu_emp4;
4.查看视图
# 3. 查看视图 #查看数据库的表对象和视图对象 SHOW TABLES; #查看视图的结构 DESC vu_emp1; #查看视图的属性信息 SHOW TABLE STATUS LIKE 'vu_emp1'; #查看视图的详细定义信息 SHOW CREATE VIEW vu_emp1;
5.更新视图
#4. 更新(包括增删改)视图中的数据 # 一般情况下可以更新数据 SELECT * FROM vu_emp1; SELECT employee_id, last_name, salary FROM emps; #更新数据 UPDATE vu_emp1 SET salary = 20000 WHERE employee_id = 101;#对应基表也改变了 UPDATE emps SET salary = 10000 WHERE employee_id = 101;#对应视图也改变了 #删除数据 DELETE FROM vu_emp1 WHERE employee_id = 101;#基表中的也被删了 #更新失败的情况 #运算出来的列 SELECT * FROM vu_emp_sal; #更新失败 UPDATE vu_emp_sal SET avg_sal = 5000 WHERE department_id = 30;#error : The target table vu_emp_sal of the UPDATE is not updatable #删除失败 DELETE FROM vu_emp_sal WHERE department_id = 30;#error : The target table vu_emp_sal of the DELETE is not updatable /* 总结 : 虽然视图可以更新数据, 但是视图作为虚拟表, 主要用于方便查询, 不建议更新视图数据。对视图 数据的修改都是通过实际数据表里数据的操作完成的。 */
6.修改、删除视图
#5. 修改删除视图 DESC vu_emp1; #修改 : #方式一 CREATE OR REPLACE VIEW vu_emp1#如果没有就创建 如果有就替换 AS SELECT employee_id, last_name, salary, email FROM emps WHERE salary > 7000; DESC vu_emp1; #方式二 : ALTER VIEW vu_emp1 AS SELECT employee_id, last_name, salary, email, hire_date FROM emps; DESC vu_emp1; #删除 SHOW TABLES; DROP VIEW vu_emp4; DROP VIEW IF EXISTS vu_emp2, vu_emp3; #注意 : 如果是基于两个VIEW造出的VIEW如果将基表删除 会导致造出的VIEW查询失败 必须要修改视图
7.总结
视图优点 :
-
操作简单
-
减少数据冗余
-
数据安全
-
适应灵活多变的需求
-
能够分解复杂的查询逻辑
视图不足 :
维护性的问题 : 如果基表的结构变更了,我们就需要及时对相关的视图进行相应的维护
如果视图过多,会导致数据库维护成本问题
如果覆盖的视图过多会导致数据不清晰
章节练习
#第十四章_章节练习 #练习一 : #1. 使用表employees创建视图employee_vu 其中包括姓名,员工号,部门号 CREATE OR REPLACE VIEW employee_vu AS SELECT last_name, employee_id, department_id FROM atguigudb.employees; #2. 显示视图的结构 DESC employee_vu; #3. 查询视图中的全部内容 SELECT * FROM employee_vu; #4. 将视图中的数据限定在部门号是80的范围内 ALTER VIEW employee_vu AS SELECT last_name, employee_id, department_id FROM atguigudb.employees WHERE department_id = 80; SELECT * FROM employee_vu; #练习二 : #1. 创建视图emp_v1,要求查询电话号码以‘011’开头的员工姓名和工资、邮箱 USE dbtest14; DESC emps; CREATE VIEW emp_v1 AS SELECT last_name, salary, email FROM emps WHERE phone_number LIKE '011%'; SELECT * FROM emp_v1; #2. 要求将视图 emp_v1 修改为查询电话号码以‘011’开头的并且邮箱中包含 e 字符的员工姓名和工资 邮箱、电话号码 CREATE OR REPLACE VIEW emp_v1 AS SELECT last_name, salary, email, phone_number FROM emps WHERE phone_number LIKE '011%' AND email REGEXP '[e]'; SELECT * FROM emp_v1; #3. 向 emp_v1 插入一条记录,是否可以? INSERT INTO emp_v1 VALUES ('Partners', 2300, 'AERRAZUR', '011.44.1344.467268'); #Field of view 'dbtest14.emp_v1' underlying table doesn't have a default value #说明 : 向视图中插入就相当于向基表中插入 但是由于字段不全 其他的视图没有的字段 #可能会有NOT NILL的约束 所以会插入失败 #4. 修改emp_v1中员工的工资,每人涨薪1000 UPDATE emp_v1 SET salary = salary + 1000; SELECT * FROM emp_v1; #5. 删除emp_v1中姓名为Olsen的员工 DELETE FROM emp_v1 WHERE last_name = 'Olsen'; #6. 创建视图emp_v2,要求查询部门的最高工资高于 12000 的部门id和其最高工资 CREATE OR REPLACE VIEW emp_v2(dept_id, max_sal) AS SELECT department_id, MAX(salary) FROM emps GROUP BY department_id HAVING MAX(salary) > 12000; SELECT * FROM emp_v2; #7. 向 emp_v2 中插入一条记录,是否可以? INSERT INTO emp_v2 VALUES (1000, 12345);#The target table emp_v2 of the INSERT is not insertable-into #模棱两可不能添加 #8. 删除刚才的emp_v2 和 emp_v1 DROP VIEW IF EXISTS emp_v2, emp_v1; SHOW TABLES;
十五、存储过程与函数
1.存储过程概述
用户自定义的函数。存储过程和函数, 可以将复杂的SQL逻辑封装在一起, 应用程序无需关注存储过程和函数内部复杂的SQL逻辑, 只需要简单的调用存储过程和函数即可。
好处
1、简化操作,提高了sql语句的重用性,减少了开发程序员的压力
2、减少操作过程中的失误,提高效率
3、减少网络传输量(客户端不需要把所有的 SQL 语句通过网络发给服务器)
4、减少了 SQL 语句暴露在网上的风险,也提高了数据查询的安全性
相较于函数, 存储过程是没有返回值的
分类
存储过程的参数类型可以是IN、OUT和INOUT。根据这点分类如下:
1、没有参数(无参数无返回)
2、仅仅带 IN 类型(有参数无返回)
3、仅仅带 OUT 类型(无参数有返回 )
4、既带 IN 又带 OUT(有参数有返回)
5、带 INOUT(有参数有返回)
注意:IN、OUT、INOUT 都可以在一个存储过程中带多个。
2.创建存储过程与调用
#0. 准备工作 CREATE DATABASE dbtest15; USE dbtest15; CREATE TABLE IF NOT EXISTS employees AS SELECT * FROM atguigudb.employees; CREATE TABLE IF NOT EXISTS departments AS SELECT * FROM atguigudb.departments; SELECT * FROM employees; SELECT * FROM departments; #1. 创建存储过程 #类型一 : 无参数 无返回值 #举例1:创建存储过程select_all_data(),查看 emps 表的所有数据 DELIMITER $ CREATE PROCEDURE select_all_data1() BEGIN SELECT * FROM employees; END $ DELIMITER ; #举例2:创建存储过程avg_employee_salary(),返回所有员工的平均工资 DELIMITER // CREATE PROCEDURE avg_employee_salary() BEGIN SELECT AVG(salary) FROM employees; END // DELIMITER ; #调用 CALL avg_employee_salary(); #举例3:创建存储过程show_max_salary(),用来查看“emps”表的最高薪资值。 DELIMITER // CREATE PROCEDURE show_max_salary() BEGIN SELECT MAX(salary) FROM employees; END // DELIMITER ; CALL show_max_salary(); #类型二 : 带OUT的 #举例4:创建存储过程show_min_salary(),查看“emps”表的最低薪资值。并将最低薪资通过OUT参数“ms” DELIMITER // CREATE PROCEDURE show_min_salary(OUT ms DOUBLE) BEGIN SELECT MIN(salary) INTO ms FROM employees; END // DELIMITER ; #调用 CALL show_min_salary(@ms);#加@表明是用户定义的变量 #查看变量值 SELECT @ms; #带IN的 #举例5:创建存储过程show_someone_salary(),查看“emps”表的某个员工的薪资,并用IN参数empname #输入员工姓名。 DELIMITER // CREATE PROCEDURE show_someone_salary(IN empname VARCHAR(20)) BEGIN SELECT salary FROM employees WHERE last_name = empname; END // DELIMITER ; #调用方式一 : CALL show_someone_salary('Abel'); #调用方式二 : SET @empname := 'Abel'; CALL show_someone_salary(@empname) #带IN 带OUT的 #举例6:创建存储过程show_someone_salary2(),查看“emps”表的某个员工的薪资,并用IN参数empname #输入员工姓名,用OUT参数empsalary输出员工薪资。 DELIMITER // CREATE PROCEDURE show_someone_salary2(IN empname VARCHAR(15), OUT empsalary DOUBLE) BEGIN SELECT salary INTO empsalary FROM employees WHERE last_name = empname; END // DELIMITER ; #调用 SET @empname = 'Abel'; CALL show_someone_salary2(@empname, @empsalary); SELECT @empsalary; #带INOUT的 #举例7:创建存储过程show_mgr_name(),查询某个员工领导的姓名,并用INOUT参数“empname”输入员 #工姓名,输出领导的姓名。 DELIMITER // CREATE PROCEDURE show_mgr_name(INOUT empname VARCHAR(25)) BEGIN SELECT last_name INTO empname FROM employees WHERE employee_id = ( SELECT manager_id FROM employees WHERE last_name = empname ); END // DELIMITER ; #调用 SET @empname := 'Abel'; CALL show_mgr_name(@empname); SELECT @empname;
3.存储函数概述
MySQL支持用户自定义的函数, 定义好以后, 调用方式与调用系统预定义的系统函数方式一样。
语法分析
CREATE FUNCTION 函数名(参数名 参数类型,...) RETURNS 返回值类型 [characteristics ...] BEGIN 函数体 #函数体中肯定有 RETURN 语句 END
说明:
1、参数列表:指定参数为IN、OUT或INOUT只对PROCEDURE是合法的,FUNCTION中总是默认为IN参数。
2、RETURNS type 语句表示函数返回数据的类型;
RETURNS子句只能对FUNCTION做指定,对函数而言这是 强制 的。它用来指定函数的返回类型,而且函数体必须包含一个 RETURN value 语句。
3、characteristic 创建函数时指定的对函数的约束。取值与创建存储过程时相同,这里不再赘述。
4、函数体也可以用BEGIN…END来表示SQL代码的开始和结束。如果函数体只有一条语句,也可以省略BEGIN…END。
4.创建存储函数与调用
#第十五章_存储过程与存储函数 USE dbtest15; #举例1:创建存储函数,名称为email_by_name(),参数定义为空,该函数查询Abel的email,并返回,数据类型为 #字符串型。 DELIMITER // CREATE FUNCTION email_by_name() RETURNS VARCHAR(25) #在别的可视化软件 sqlyog可能会报错 因为没加特征 BEGIN RETURN(SELECT email FROM employees WHERE last_name = 'Abel'); END // DELIMITER ; SELECT email_by_name() FROM DUAL; #举例2 : 创建存储函数,名称为email_by_id(),参数传入emp_id,该函数查询emp_id的email,并返回,数据类型 #为字符串型。 DELIMITER // CREATE FUNCTION email_by_id(emp_id INT)#不用加IN 默认全是IN RETURNS VARCHAR(25) BEGIN RETURN(SELECT email FROM employees WHERE employee_id = emp_id); END // DELIMITER ; #调用 SELECT email_by_id(101) FROM DUAL; SET @emp_id := 102; SELECT email_by_id(@emp_id) FROM DUAL; SELECT * FROM employees; #举例3 : 创建存储函数count_by_id(),参数传入dept_id,该函数查询dept_id部门的员工人数,并返回,数据类型 #为整型。 DELIMITER // CREATE FUNCTION count_by_id(dept_id INT) RETURNS INT BEGIN RETURN(SELECT COUNT(*) FROM employees WHERE department_id = dept_id); END // DELIMITER ; #调用 SET @dept_id := 30; SELECT count_by_id(@dept_id);
5.对比存储过程与存储函数
关键字 | 调用语法 | 返回值 | 应用场景 | |
---|---|---|---|---|
存储过程 | PROCEDURE | CALL 存储过程() | 理解为有0个或者多个返回值 | 一般用于更新 |
存储函数 | FUNCTION | SELECT 存储函数() | 只能是一个 | 一般用于查询 |
此外,存储函数可以放在查询语句中使用,存储过程不行。反之,存储过程的功能更加强大,包括能够执行对表的操作(比如创建表,删除表等)和事务操作,这些功能是存储函数不具备的。
6.存储过程和函数的查看、修改、删除
#3. 存储过程和函数的查看 #1. 使用SHOW CREATE 语句查看存储过程和函数的创建信息 SHOW CREATE PROCEDURE show_mgr_name; SHOW CREATE FUNCTION count_by_id; #2. 使用SHOW STATUS 语句查看存储过程和存储函数的状态信息 SHOW PROCEDURE STATUS;#查看的是全部的存储过程 SHOW PROCEDURE STATUS LIKE 'show_max_salary';#模糊查询 SHOW FUNCTION STATUS LIKE 'email_by_id'; #3. 从information_schema.Routines表中查看存储过程和函数的信息 SELECT * FROM information_schema.ROUTINES WHERE ROUTINE_NAME = 'email_by_id';#存储函数 SELECT * FROM information_schema.ROUTINES WHERE ROUTINE_NAME = 'email_by_id' AND ROUTINE_TYPE = 'function'; SELECT * FROM information_schema.ROUTINES WHERE ROUTINE_NAME = 'show_min_salary';#存储过程
#修改存储过程和函数,不影响存储过程和函数的功能,只是修改相关的特性。使用ALTER语句实现 SHOW CREATE PROCEDURE show_max_salary; ALTER PROCEDURE show_max_salary SQL SECURITY INVOKER COMMENT '查询最高工资';
#5. 删除 DROP FUNCTION IF EXISTS count_by_id; DROP PROCEDURE IF EXISTS show_min_salary;
7.关于存储过程使用的争议
优点 :
-
存储过程可以一次编译多次使用
-
可以减少开发工作量
-
存储过程的安全性强
-
可以减少网络传输量
-
良好的封装性
缺点 :
-
可移植性差
-
调试困难
-
存储过程的版本管理很难
-
不适合高并发的场景
章节练习
#15_存储过程与函数章节练习 CREATE DATABASE IF NOT EXISTS test15_pro_func CHARACTER SET 'utf8'; USE test15_pro_func; #存储过程 #1. 创建存储过程insert_user(),实现传入用户名和密码,插入到admin表中 CREATE TABLE admin( id INT PRIMARY KEY AUTO_INCREMENT, user_name VARCHAR(15) NOT NULL, pwd VARCHAR(25) NOT NULL ); DELIMITER // CREATE PROCEDURE insert_user(IN username VARCHAR(15), IN loginPwd VARCHAR(25)) BEGIN INSERT INTO admin(user_name, pwd) VALUES (username, loginPwd); END // DELIMITER ; #调用 CALL insert_user('Tom', 'abc123'); SELECT * FROM admin; #2. 创建存储过程get_phone(),实现传入女神编号,返回女神姓名和女神电话 CREATE TABLE beauty( id INT PRIMARY KEY AUTO_INCREMENT, NAME VARCHAR(15) NOT NULL, phone VARCHAR(15) UNIQUE, birth DATE ); INSERT INTO beauty(NAME,phone,birth) VALUES ('朱茵','13201233453','1982-02-12'), ('孙燕姿','13501233653','1980-12-09'), ('田馥甄','13651238755','1983-08-21'), ('邓紫棋','17843283452','1991-11-12'), ('刘若英','18635575464','1989-05-18'), ('杨超越','13761238755','1994-05-11'); SELECT * FROM beauty; DELIMITER // CREATE PROCEDURE get_phone(IN beauty_id INT, OUT phone VARCHAR(15), OUT name VARCHAR(15)) BEGIN SELECT name, phone INTO name, phone FROM beauty WHERE id = beauty_id; END // DELIMITER ; DROP PROCEDURE get_phone; CALL get_phone(3, @phone, @name); SELECT @name, @phone; #3. 创建存储过程date_diff(),实现传入两个女神生日,返回日期间隔大小 DELIMITER // CREATE PROCEDURE date_diff(IN beauty1_birth DATE, IN beauty2_birth DATE, OUT sum_date INT) BEGIN SELECT DATEDIFF(beauty1_birth, beauty2_birth) INTO sum_date FROM DUAL; END // DELIMITER ; CALL date_diff('1994-05-11', '1982-02-12', @sum_date); SELECT @sum_date; #4. 创建存储过程format_date(),实现传入一个日期,格式化成xx年xx月xx日并返回 DELIMITER // CREATE PROCEDURE format_date(IN my_date DATE, OUT str_date VARCHAR(25)) BEGIN SELECT DATE_FORMAT(my_date,'%y年%m月%d日') INTO str_date; END // DELIMITER ; CALL format_date('1994-05-11', @str_date); SELECT @str_date; #5. 创建存储过程beauty_limit(),根据传入的起始索引和条目数,查询女神表的记录 DELIMITER // CREATE PROCEDURE beauty_limit(IN `s_index` INT, IN size INT) BEGIN SELECT * FROM beauty LIMIT s_index, size; END // DELIMITER ; CALL beauty_limit(2, 2); #创建带inout模式参数的存储过程 #6. 传入a和b两个值,最终a和b都翻倍并返回 DELIMITER // CREATE PROCEDURE a_b_double(INOUT a INT, INOUT b INT) BEGIN SET a = a * 2; SET b = b * 2; END // DELIMITER ; SET @a := 5, @b := 6; CALL a_b_double(@a, @b); SELECT @a, @b; #7. 删除题目5的存储过程 DROP PROCEDURE beauty_limit(); #8. 查看题目6中存储过程的信息 SHOW CREATE PROCEDURE a_b_double; SHOW PROCEDURE STATUS LIKE 'a_b_%'; #存储函数 #0. 准备工作 USE test15_pro_func; CREATE TABLE employees AS SELECT * FROM atguigudb.`employees`; CREATE TABLE departments AS SELECT * FROM atguigudb.`departments`; #无参有返回 #1. 创建函数get_count(),返回公司的员工个数 DELIMITER // CREATE FUNCTION get_count() RETURNS INT BEGIN RETURN (SELECT COUNT(*) FROM employees); END // DELIMITER ; SELECT get_count() FROM DUAL; #有参有返回 DESC employees; DESC departments; #2. 创建函数ename_salary(),根据员工姓名,返回它的工资 DELIMITER // CREATE FUNCTION ename_salary(emp_name VARCHAR(15)) RETURNS DOUBLE(8, 2) BEGIN RETURN(SELECT salary FROM employees WHERE last_name = emp_name); END // DELIMITER ; SET @emp_name := 'Abel'; SELECT ename_salary(@emp_name) FROM DUAL; #3. 创建函数dept_sal() ,根据部门名,返回该部门的平均工资 DELIMITER // CREATE FUNCTION dept_sal(dept_name VARCHAR(30)) RETURNS DOUBLE(8, 2) BEGIN RETURN(SELECT AVG(salary) FROM employees GROUP BY department_id HAVING department_id = ( SELECT department_id FROM departments WHERE department_name = 'IT' )); END // DELIMITER ; SELECT dept_sal('IT') FROM DUAL; #多表查询 SELECT AVG(salary) FROM employees e JOIN departments d ON e.department_id = d.department_id WHERE department_name = 'IT'; #4. 创建函数add_float(),实现传入两个float,返回二者之和 DELIMITER // CREATE FUNCTION add_float(num1 FLOAT, num2 FLOAT) RETURNS FLOAT BEGIN RETURN (SELECT num1 + num2); END // DELIMITER ; SELECT add_float(5.12, 5.13);
十六、变量、流程控制与游标
1.变量
在MySQL数据库的存储过程与函数中,可以使用变量来存储查询或计算的中间结果数据,或者输出最终的结果数据。在MySQL数据库中,变量分为系统变量
和用户自定义变量
。
1.1系统变量
系统变量分类
全局系统变量 和 会话系统变量
全局系统变量是启动服务器之后赋值的系统变量
会话系统变量是建立连接之后(MySQL实例)赋值的系统变量
有的变量既是全局系统变量也是会话系统变量
#第十六章_变量_流程控制与游标 #1.变量: #系统变量(全局系统变量与会话系统变量) VS 用户自定义变量 # 1.1 查看系统变量 #查看全局的系统变量 SHOW GLOBAL VARIABLES;#617 #查看会话的系统变量 (可加模糊查询) SHOW SESSION VARIABLES; #或 SHOW VARIABLES;#640 #查看字符集的变量 SHOW VARIABLES LIKE 'character_%'; # 1.2查看指定系统变量 #@@global 或 @@session #如果没写全局或者是会话 那么就会先从会话中的变量找 没找到再去全局的找 SELECT @@global.变量名; SELECT @@global.MAX_CONNECTIONS; SELECT @@global.character_set_client;#global级别的变量 SELECT @@session.变量名; SELECT @@session.character_set_client;#session级别的变量 SELECT @@session.pseudo_thread_id;#用于标记当前会话的MySQL连接ID SELECT @@变量名; SELECT @@character_set_client;#查询的是会话的系统变量 因为先查会话的系统变量 #1.3修改系统变量的值 #全局系统变量 #方法一 : 直接修改配置文件 重启服务器 #方式二 : 在MySQL服务运行期间, 使用'SET'命令重新设置系统变量的值 #方式一 : SET @@global.MAX_CONNECTIONS = 161; SELECT @@global.max_connections; #方式二 : SET GLOBAL max_connections = 171; SELECT @@global.max_connections; #上述方式一旦重启服务器就会失效 #会话系统变量 #方式一 : SET @@session.character_set_client = 'gbk'; #方式二 : SET SESSION character_set_client = 'utf8mb3'; #换一个会话就会失效
1.2用户变量
用户变量是自己定义的变量, MySQL中的用户变量以一个 ' @ ' 开头, 根据作用范围不同, 分为会话用户变量和局部变量。
-
会话用户变量 : 作用域和会话变量一样, 只对当前连接的变量有效
-
局部变量 : 只在BEGIN 和 END 语句块中有效。局部变量只能在存储过程和函数中使用
#2. 用户变量 # 会话用户变量 VS 局部变量 #2.1 会话用户变量 #准备工作 CREATE DATABASE IF NOT EXISTS dbtest16 CHARACTER SET = 'utf8'; USE dbtest16; CREATE TABLE employees AS SELECT * FROM atguigudb.employees; CREATE TABLE departments AS SELECT * FROM atguigudb.departments; SELECT * FROM employees; SELECT * FROM employees; #变量的定义 #方式一 : "=" 或 ":=" SET @m1 = 1; SET @m2 := 2; SET @sum := @m1 + @m2; SELECT @sum; #方式二 : ":=" 或 INTO关键字 SELECT @count := COUNT(*) FROM employees; SELECT @count; SELECT AVG(salary) INTO @avg_sal FROM employees; SELECT @avg_sal; #2.2 局部变量 /* 局部变量必须满足 : 必须使用DECLARE声明 声明并使用在BEGIN...END中(存储过程和存储函数) DECLARE的方式声明的局部变量必须声明在BEGIN中的首行位置 定义方式 : DECLARE 变量名 类型 [default 值]; # 如果没有DEFAULT子句,初始值为NULL 变量赋值 : SET 变量名=值; SET 变量名:=值; SELECT 字段名或表达式 INTO 变量名 FROM 表; 使用变量 : SELECT 局部变量名; */ #举例 DELIMITER // CREATE PROCEDURE test_var() BEGIN #1.声明局部变量 DECLARE a INT DEFAULT 0; DECLARE b INT; DECLARE emp_name VARCHAR(25); #2.赋值 SET a = 1; SET b := 2; SELECT last_name INTO emp_name FROM employees WHERE employee_id = 101; #3.使用 SELECT a, b, emp_name; END // DELIMITER ; #调用 CALL test_var(); #举例3:创建存储过程“different_salary”查询某员工和他领导的薪资差距,并用IN参数emp_id接收员工 #id,用OUT参数dif_salary输出薪资差距结果。 DELIMITER // CREATE PROCEDURE different_salary(IN emp_id INT, OUT dif_salary DOUBLE(8, 2)) BEGIN DECLARE mar_id INT; DECLARE mar_sal, emp_sal DOUBLE(8, 2); SELECT manager_id INTO mar_id FROM employees WHERE employee_id = emp_id; SELECT salary INTO emp_sal FROM employees WHERE employee_id = emp_id; SELECT salary INTO mar_sal FROM employees WHERE employee_id = mar_id; SET dif_salary = mar_sal - emp_sal; END // DELIMITER ; SET @emp_id = 101; SET @dif_sal = 0; CALL different_salary(@emp_id, @dif_sal); SELECT @dif_sal FROM DUAL;
2.定义条件与处理程序
定义条件
是事先定义程序执行过程中可能遇到的问题,处理程序
定义了在遇到问题时应当采取的处理方式,并且保证存储过程或函数在遇到警告或错误时能继续执行。这样可以增强存储程序处理问题的能力,避免程序异常停止运行。
说明:定义条件和处理程序在存储过程、存储函数中都是支持的。
2.1案例分析
#2.1 错误演示 : USE dbtest16; #[Err] 1364 - Field 'email' doesn't have a default value INSERT INTO employees(last_name) VALUES ('Tom'); #2.2 DELIMITER // CREATE PROCEDURE UpdateDataNoCodition() BEGIN SET @x = 1; UPDATE employees SET email = NULL WHERE last_name = 'Abel'; SET @x = 2; UPDATE employees SET email = 'aabbel' WHERE last_name = 'Abel'; SET @x = 3; END // DELIMITER ; #调用存储过程 #错误代码 : [Err] 1048 - Column 'email' cannot be null CALL UpdateDataNoCodition(); SELECT @x;# res:1
2.2定义条件
定义条件就是给MySQL中的错误码命名,这有助于存储的程序代码更清晰。它将一个 错误名字 和 指定的 错误条件 关联起来。这个名字可以随后被用在定义处理程序的 DECLARE HANDLER 语句中。
DECLARE 错误名称 CONDITION FOR 错误码(或错误条件)
错误码的说明:
-
MySQL_error_code 和 sqlstate_value 都可以表示MySQL的错误。
-
MySQL_error_code是数值类型错误代码。
-
sqlstate_value是长度为5的字符串类型错误代码。
例如,在ERROR 1418 (HY000)中,1418是MySQL_error_code,'HY000'是sqlstate_value。
例如,在ERROR 1142(42000)中,1142是MySQL_error_code,'42000'是sqlstate_value。
#格式 : DECLARE 错误名称 CONDITION FOR 错误码(或错误条件) #举例1:定义“Field_Not_Be_NULL”错误名与MySQL中违反非空约束的错误类型是“ERROR 1048 (23000)”对应。 #方式一 : 使用MySQL_error_code DECLARE Field_Not_Be_NULL CONDITION FOR 1048; #方式二 : 使用 sqlstate_value DECLARE Field_Not_Be_NULL CONDITION FOR SQLSTATE '23000';
2.3定义处理程序
可以为SQL执行过程中发生的某种类型的错误定义特殊的处理程序。定义处理程序时,使用DECLARE语句的语法如下:
DECLARE 处理方式 HANDLER FOR 错误类型 处理语句
处理方式:处理方式有3个取值:CONTINUE、EXIT、UNDO。
-
CONTINUE :表示遇到错误不处理,继续执行。
-
EXIT :表示遇到错误马上退出。
-
UNDO :表示遇到错误后撤回之前的操作。MySQL中暂时不支持这样的操作。
错误类型(即条件)可以有如下取值:
-
SQLSTATE '字符串错误码' :表示长度为5的sqlstate_value类型的错误代码;
-
MySQL_error_code :匹配数值类型错误代码;
-
错误名称 :表示DECLARE ... CONDITION定义的错误条件名称。
-
SQLWARNING :匹配所有以01开头的SQLSTATE错误代码;
-
NOT FOUND :匹配所有以02开头的SQLSTATE错误代码;
-
SQLEXCEPTION :匹配所有没有被SQLWARNING或NOT FOUND捕获的SQLSTATE错误代码;
处理语句:如果出现上述条件之一,则采用对应的处理方式,并执行指定的处理语句。语句可以是
像“ SET 变量 = 值 ”这样的简单语句,也可以是使用 BEGIN ... END 编写的复合语句。
2.4案例解决
#2.4 案例的解决 DROP PROCEDURE UpdateDataNoCodition; #重新定义存储过程 DELIMITER // CREATE PROCEDURE UpdateDataNoCodition() BEGIN #声明处理程序 DECLARE CONTINUE HANDLER FOR 1048 SET @prc_value = -1; SET @x = 1; UPDATE employees SET email = NULL WHERE last_name = 'Abel'; SET @x = 2; UPDATE employees SET email = 'aabbel' WHERE last_name = 'Abel'; SET @x = 3; END // DELIMITER ; #调用 CALL UpdateDataNoCodition(); #查看变量 SELECT @prc_value, @x;#-1, 3
3.流程控制
解决复杂问题不可能通过一个 SQL 语句完成,我们需要执行多个 SQL 操作。流程控制语句的作用就是控制存储过程中 SQL 语句的执行顺序,是我们完成复杂操作必不可少的一部分。只要是执行的程序,流程就分为三大类:
-
顺序结构 :程序从上往下依次执行
-
分支结构 :程序按条件进行选择执行,从两条或多条路径中选择一条执行
-
循环结构 :程序满足一定条件下,重复执行一组语句
针对于MySQL 的流程控制语句主要有 3 类。注意:只能用于存储程序。
-
条件判断语句 :IF 语句和 CASE 语句
-
循环语句 :LOOP、WHILE 和 REPEAT 语句
-
跳转语句 :ITERATE 和 LEAVE 语句
3.1分支结构之IF
#3 流程控制 USE dbtest16; SHOW TABLES; #3.1 分支结构之IF #举例 1 DELIMITER // CREATE PROCEDURE test_if() BEGIN /*情况一 #声明局部变量 DECLARE stu_name VARCHAR(15); IF stu_name IS NULL THEN SELECT 'stu_name is null'; END IF;*/ /*情况二 : 二选一 DECLARE email VARCHAR(25) DEFAULT 'jchuan_Hou@163.com'; IF email IS NULL THEN SELECT 'email is null'; ELSE SELECT 'email is not null'; END IF;*/ DECLARE age INT DEFAULT 20; IF age > 40 THEN SELECT '中老年'; ELSEIF age > 18 THEN SELECT '青壮年'; ELSEIF age > 8 THEN SELECT '青少年'; ELSE SELECT '婴幼儿'; END IF; END // DELIMITER ; #调用 CALL test_if; DROP PROCEDURE test_if; #举例2:声明存储过程“update_salary_by_eid1”,定义IN参数emp_id,输入员工编号。判断该员工 #薪资如果低于8000元并且入职时间超过5年,就涨薪500元;否则就不变。 DELIMITER // CREATE PROCEDURE update_salary_by_eid1(IN emp_id INT) BEGIN DECLARE emp_salary DOUBLE; DECLARE hire_year DOUBLE; SELECT salary INTO emp_salary FROM employees WHERE emp_id = employee_id; SELECT DATEDIFF(CURRENT_DATE(), hire_date) / 365 INTO hire_year FROM employees WHERE emp_id = employee_id; IF emp_salary < 8000 && hire_year > 5.0 THEN UPDATE employees SET salary = salary + 500 WHERE emp_id = employee_id; END IF; END // DELIMITER ; #调用 CALL update_salary_by_eid1(104); SELECT last_name, salary FROM employees WHERE employee_id = 104; #举例4:声明存储过程“update_salary_by_eid3”,定义IN参数emp_id,输入员工编号。判断该员工 #薪资如果低于9000元,就更新薪资为9000元;薪资如果大于等于9000元且低于10000的,但是奖金 #比例为NULL的,就更新奖金比例为0.01;其他的涨薪100元。 DELIMITER // CREATE PROCEDURE update_salary_by_eid3(IN emp_id INT) BEGIN #声明变量 DECLARE emp_salary DOUBLE; DECLARE emp_commission_pct DOUBLE; #变量赋值 SELECT salary INTO emp_salary FROM employees WHERE emp_id = employee_id; SELECT commission_pct INTO emp_commission_pct FROM employees WHERE emp_id = employee_id; #执行语句 IF emp_salary < 9000 THEN UPDATE employees SET salary = 9000 WHERE emp_id = employee_id; ELSEIF emp_salary >= 9000 && emp_salary < 10000 && emp_commission_pct IS NULL THEN UPDATE employees SET commission_pct = 0.01 WHERE emp_id = employee_id; ELSE UPDATE employees SET salary = salary + 100 WHERE emp_id =employee_id; END IF; END // DELIMITER ; DESC employees; DROP PROCEDURE update_salary_by_eid3; SELECT * FROM employees WHERE employee_id = 103; #调用 CALL update_salary_by_eid3(104); CALL update_salary_by_eid3(102); CALL update_salary_by_eid3(103);
3.2分支结构之CASE
#3.2 分支结构之CASE #举例 DELIMITER // CREATE PROCEDURE test_case() BEGIN /*情况一 : 相当于 SWITCH DECLARE var INT DEFAULT 2; CASE var WHEN 1 THEN SELECT 'var is 1'; WHEN 2 THEN SELECT 'var is 2'; WHEN 3 THEN SELECT 'var is 3'; ELSE SELECT 'var is others'; END CASE;*/ #情况二 : case when ...then ... DECLARE var INT DEFAULT 10; CASE WHEN var >= 100 THEN SELECT '三位数'; WHEN var >= 10 THEN SELECT '两位数'; ELSE SELECT '一位数'; END CASE; END // DELIMITER ; #调用 CALL test_case(); DROP PROCEDURE test_case; #举例2:声明存储过程update_salary_by_eid5,定义IN参数emp_id,输入员工编号。判断该员工的 #入职年限,如果是0年,薪资涨50;如果是1年,薪资涨100;如果是2年,薪资涨200;如果是3年, #薪资涨300;如果是4年,薪资涨400;其他的涨薪500。 DELIMITER // CREATE PROCEDURE update_salary_by_eid5(IN emp_id INT) BEGIN #声明变量 DECLARE hire_year DOUBLE; #变量赋值 SELECT ROUND(DATEDIFF(CURRENT_DATE(), hire_date) / 365) INTO hire_year FROM employees WHERE emp_id = employee_id; #分支结构 CASE hire_year WHEN 0 THEN UPDATE employees SET salary = salary + 50 WHERE employee_id = emp_id; WHEN 1 THEN UPDATE employees SET salary = salary + 100 WHERE employee_id = emp_id; WHEN 2 THEN UPDATE employees SET salary = salary + 200 WHERE employee_id = emp_id; WHEN 3 THEN UPDATE employees SET salary = salary + 300 WHERE employee_id = emp_id; WHEN 4 THEN UPDATE employees SET salary = salary + 400 WHERE employee_id = emp_id; ELSE UPDATE employees SET salary = salary + 500 WHERE employee_id = emp_id; END CASE; END // DELIMITER ; DROP PROCEDURE update_salary_by_eid5; #调用 CALL update_salary_by_eid5(103); SELECT salary FROM employees WHERE employee_id = 103;
3.3循环结构之LOOP
#3.3 循环结构之LOOP DELIMITER // CREATE PROCEDURE test_loop() BEGIN #声明局部变量 DECLARE num INT DEFAULT 1; loop_label:LOOP #可以不加 loop_label: #重新赋值 SET num = num + 1; IF num >= 10 THEN LEAVE loop_label; END IF; END LOOP loop_label;#可以不加 loop_label #查看值 SELECT num FROM DUAL; END // DELIMITER ; #调用 CALL test_loop(); #举例2:当市场环境变好时,公司为了奖励大家,决定给大家涨工资。声明存储过程 #“update_salary_loop()”,声明OUT参数num,输出循环次数。存储过程中实现循环给大家涨薪,薪资涨为 #原来的1.1倍。直到全公司的平均薪资达到12000结束。并统计循环次数。 DELIMITER // CREATE PROCEDURE update_salary_loop(OUT num INT) BEGIN DECLARE avg_salary DOUBLE;#记录员工的平均工资 DECLARE loop_count INT DEFAULT 0;#记录循环次数 SELECT AVG(salary) INTO avg_salary FROM employees; label_loop : LOOP IF avg_salary >= 12000 THEN LEAVE label_loop; END IF; UPDATE employees SET salary = 1.2 * salary; SELECT AVG(salary) INTO avg_salary FROM employees; SET loop_count = loop_count + 1; END LOOP label_loop; SET num = loop_count; END // DELIMITER ; #调用 SET @num = 0; CALL update_salary_loop(@num); SELECT @num; SELECT AVG(salary) FROM employees;
3.4循环结构之WHILE
#3.4 循环结构之WHILE /* 凡是循环结构 : 一定具备四个结构 1. 初始化条件 2. 循环条件 3. 循环体 4. 迭代条件 */ #举例 DELIMITER // CREATE PROCEDURE test_while() BEGIN #初始化条件 DECLARE num INT DEFAULT 0; #循环条件 WHILE num <= 10 DO #循环体 (略) #迭代条件 SET num = num + 1; END WHILE; SELECT num; END // DELIMITER ; CALL test_while(); #举例2:市场环境不好时,公司为了渡过难关,决定暂时降低大家的薪资。声明存储过程 #“update_salary_while()”,声明OUT参数num,输出循环次数。存储过程中实现循环给大家降薪,薪资降 #为原来的90%。直到全公司的平均薪资达到5000结束。并统计循环次数。 DELIMITER // CREATE PROCEDURE update_salary_while(OUT cnt INT) BEGIN #初始化条件 DECLARE num INT DEFAULT 0; DECLARE avg_salary DOUBLE; SELECT AVG(salary) INTO avg_salary FROM employees; #循环条件 WHILE avg_salary >= 5000 DO #循环体 UPDATE employees SET salary = salary * 0.9; #迭代条件 SET num = num + 1; SELECT AVG(salary) INTO avg_salary FROM employees; END WHILE; SET cnt = num; END // DELIMITER ; #调用 SELECT AVG(salary) FROM employees; SET @num = 0; CALL update_salary_while(@num); SELECT @num;
3.5循环结构之REPEAT
#3.5 循环结构REPEAT #举例 DELIMITER // CREATE PROCEDURE test_repeat() BEGIN #声明变量 DECLARE num INT DEFAULT 1; REPEAT SET num = num + 1; UNTIL num >= 10#不能加分号 END REPEAT; #查看 SELECT num; END // DELIMITER ; #调用 DROP PROCEDURE test_repeat; CALL test_repeat; #举例2:当市场环境变好时,公司为了奖励大家,决定给大家涨工资。声明存储过程 #“update_salary_repeat()”,声明OUT参数num,输出循环次数。存储过程中实现循环给大家涨薪,薪资涨 #为原来的1.15倍。直到全公司的平均薪资达到13000结束。并统计循环次数。 DELIMITER // CREATE PROCEDURE update_salary_repeat(OUT num INT) BEGIN #初始化条件 DECLARE cnt INT DEFAULT 0; DECLARE avg_salary DOUBLE; SELECT AVG(salary) INTO avg_salary FROM employees; REPEAT #循环体 UPDATE employees SET salary = salary * 1.15; #迭代条件 SET cnt = cnt + 1; SELECT AVG(salary) INTO avg_salary FROM employees; UNTIL avg_salary >= 13000 END REPEAT; SET num = cnt; END // DELIMITER ; SELECT AVG(salary) FROM employees; SET @num = 0; CALL update_salary_repeat(@nun); SELECT @num;
对比三种循环结构:
这三种循环都可以省略名称,但如果循环中添加了循环控制语句(LEAVE或ITERATE)则必须添加名称。
-
LOOP:一般用于实现简单的"死"循环
-
WHILE:先判断后执行
-
REPEAT:先执行后判断,无条件至少执行一次
3.6跳转语句之LEAVE语句
相当于C语言中的BREAK
LOOP循环语句只能用LEAVE终止循环
LEAVE后面一定加的是标签
SELECT @num; #3.6 跳转语句之LEAVE语句 #举例 DELIMITER // CREATE PROCEDURE leave_begin(IN num INT) begin_label : BEGIN IF num <= 0 THEN LEAVE begin_label; ELSEIF num = 1 THEN SELECT AVG(salary) FROM employees; ELSEIF num = 2 THEN SELECT MIN(salary) FROM employees; ELSE SELECT MAX(salary) FROM employees; END IF; #查询总人数 SELECT COUNT(*) FROM employees; END // DELIMITER ; #调用 CALL leave_begin(1);#查平均工资 CALL leave_begin(0);#跳出
3.7跳转语句之ITERATE
相当于java中的continue
只能在循环中使用
#3.7 跳转语句之ITERATE /* 举例: 定义局部变量num,初始值为0。循环结构中执行num + 1操作。 如果num < 10,则继续执行循环; 如果num > 15,则退出循环结构; */ DELIMITER // CREATE PROCEDURE test_iterate() BEGIN #定义局部变量 DECLARE num INT DEFAULT 0; label_loop : LOOP #迭代 SET num = num + 1; IF num < 10 THEN ITERATE label_loop; ELSEIF num > 15 THEN LEAVE label_loop; END IF;#可以直接END IF 不用ELSE SELECT 'Jingchuan_Hou' FROM DUAL;#判断循环执行位置 END LOOP label_loop; SELECT num FROM DUAL; END // DELIMITER ; #调用 DROP PROCEDURE test_iterate; CALL test_iterate();
小结 :
-
LEAVE 可以使用在循环中和BEGIN END 中 含义为BREAK 跳出循环或存储函数存储过程
-
ITERATE 只能用在循环中 含义为CONTINUE 跳过本次循环
-
上述两种语法后面必须跟标签名称
4.游标
4.1什么是游标
虽然我们也可以通过筛选条件 WHERE 和 HAVING,或者是限定返回记录的关键字 LIMIT 返回一条记录,但是,却无法在结果集中像指针一样,向前定位一条记录、向后定位一条记录,或者是 随意定位到某一 条记录 ,并对记录的数据进行处理。
游标可以对结果集中的每一条记录进行定位, 游标让SQL这种面向集合的语言有了面向过程的开发能力
。
游标在SQL中充当了C语言中指针的作用,可以利用操作游标来对数据行进行操作。
4.2使用游标的步骤
#第十六章_变量、流程控制与游标 #游标 USE dbtest16; #2. 使用CURSOR的四步骤 /* 1. 声明 2. 打开 3. 使用 4. 关闭 */ #举例 : 创建存储过程“get_count_by_limit_total_salary()”,声明IN参数 limit_total_salary,DOUBLE类型;声明 #OUT参数total_count,INT类型。函数的功能可以实现累加薪资最高的几个员工的薪资值,直到薪资总和 #达到limit_total_salary参数的值,返回累加的人数给total_count。 DELIMITER // CREATE PROCEDURE get_count_by_limit_total_salary(IN limit_total_salary DOUBLE, OUT total_count INT) BEGIN #定义局部变量 DECLARE sum_sal DOUBLE DEFAULT 0.0; DECLARE emp_sal DOUBLE DEFAULT 0.0; DECLARE emp_count INT DEFAULT 0; #1. 声明游标 DECLARE emp_cursor CURSOR FOR SELECT salary FROM employees ORDER BY salary DESC; #2. 打开游标 : 游标指向第一条记录的前面 OPEN emp_cursor; WHILE sum_sal <= limit_total_salary DO #3. 使用游标 : 游标每使用一次自动跳转下一行记录 游标只读 不能跳记录 FETCH emp_cursor INTO emp_sal; SET sum_sal = sum_sal + emp_sal; SET emp_count = emp_count + 1; END WHILE; #4. 关闭游标 CLOSE emp_cursor; SET total_count = emp_count; END // DELIMITER ; #调用 CALL get_count_by_limit_total_salary(200000, @total_count); SELECT @total_count; SELECT SUM(salary) FROM employees ORDER BY salary DESC LIMIT 0, 4;
小结 :
游标是 MySQL 的一个重要的功能,为 逐条读取 结果集中的数据,提供了完美的解决方案。跟在应用层面实现相同的功能相比,游标可以在存储程序中使用,效率高,程序也更加简洁。但同时也会带来一些性能问题,比如在使用游标的过程中,会对数据行进行 加锁 ,这样在业务并发量大的时候,不仅会影响业务之间的效率,还会 消耗系统资源 ,造成内存不足,这是因为游标是在内存中进行的处理。
建议:养成用完之后就关闭的习惯,这样才能提高系统的整体效率。
十七、触发器
在实际开发中,我们经常会遇到这样的情况:有 2 个或者多个相互关联的表,如 商品信息 和 库存信息 分别存放在 2 个不同的数据表中,我们在添加一条新商品记录的时候,为了保证数据的完整性,必须同时在库存表中添加一条库存记录。
这样一来,我们就必须把这两个关联的操作步骤写到程序里面,而且要用 事务 包裹起来,确保这两个操作成为一个 原子操作 ,要么全部执行,要么全部不执行。要是遇到特殊情况,可能还需要对数据进行手动维护,这样就很 容易忘记其中的一步 ,导致数据缺失。
这个时候,咱们可以使用触发器。你可以创建一个触发器,让商品信息数据的插入操作自动触发库存数据的插入操作。这样一来,就不用担心因为忘记添加库存数据而导致的数据缺失了。
1.触发器概述
MySQL从 5.0.2 版本开始支持触发器。MySQL的触发器和存储过程一样,都是嵌入到MySQL服务器的一段程序。
触发器是由 事件来触发 某个操作,这些事件包括 INSERT 、 UPDATE 、 DELETE 事件。所谓事件就是指用户的动作或者触发某项行为。如果定义了触发程序,当数据库执行这些语句时候,就相当于事件发生了,就会 自动 激发触发器执行相应的操作。
当对数据表中的数据执行插入、更新和删除操作,需要自动执行一些数据库逻辑时,可以使用触发器来实现。
2.触发器的创建
语法结构
CREATE TRIGGER 触发器名称 {BEFORE|AFTER} {INSERT|UPDATE|DELETE} ON 表名 FOR EACH ROW 触发器执行的语句块;
说明:
-
表名 :表示触发器监控的对象。
-
BEFORE|AFTER :表示触发的时间。BEFORE 表示在事件之前触发;AFTER 表示在事件之后触发。
-
INSERT|UPDATE|DELETE :表示触发的事件。
-
INSERT 表示插入记录时触发;
-
UPDATE 表示更新记录时触发;
-
DELETE 表示删除记录时触发。
-
-
触发器执行的语句块 :可以是单条SQL语句,也可以是由BEGIN…END结构组成的复合语句块。
#第十七章_触发器 #准备工作 CREATE DATABASE dbtest17; USE dbtest17; #1. 创建触发器 #创建数据表 CREATE TABLE test_trigger( id INT PRIMARY KEY AUTO_INCREMENT, t_note VARCHAR(30) ); CREATE TABLE test_trigger_log( id INT PRIMARY KEY AUTO_INCREMENT, t_log VARCHAR(30) ); #查看数据 SELECT * FROM test_trigger; SELECT * FROM test_trigger_log; #创建触发器: #创建名称为before_insert_test_tri的触发器,向test_trigger数据表插入数据之前,向 #test_trigger_log数据表中插入before_insert的日志信息。 DELIMITER // CREATE TRIGGER before_insert_test_tri BEFORE INSERT ON test_trigger FOR EACH ROW BEGIN INSERT INTO test_trigger_log(t_log) VALUES ('before insert ...'); END // DELIMITER ; #测试 INSERT INTO test_trigger(t_note) VALUES ('Tom'); #查看数据 SELECT * FROM test_trigger; SELECT * FROM test_trigger_log;#触发器起作用 #举例:定义触发器“salary_check_trigger”,基于员工表“employees”的INSERT事件,在INSERT之前检查 #将要添加的新员工薪资是否大于他领导的薪资,如果大于领导薪资,则报sqlstate_value为'HY000'的错 #误,从而使得添加失败。 CREATE TABLE employees AS SELECT * FROM atguigudb.employees; CREATE TABLE departments AS SELECT * FROM atguigudb.departments; #创建触发器 DELIMITER // CREATE TRIGGER salary_check_trigger BEFORE INSERT ON employees FOR EACH ROW BEGIN #查询到要添加的数据的mar的薪资 DECLARE mar_sal DOUBLE; #使用NEW来表示要插入的一行记录 SELECT salary INTO mar_sal FROM employees WHERE employee_id = NEW.manager_id; IF NEW.salary > mar_sal THEN SIGNAL SQLSTATE 'HY000' SET MESSAGE_TEXT = '薪资高于领导薪资错误'; END IF; END // DELIMITER ; #上面触发器的声明过程中的NEW关键字代表INSERT添加语句的新记录 #测试 DESC employees; INSERT INTO employees(employee_id, last_name, email, hire_date, job_id, salary, manager_id) VALUES (300, 'Tom', 'tom@126.com', NOW(), 'AD_VP', 8000, 103); SELECT * FROM employees; INSERT INTO employees(employee_id, last_name, email, hire_date, job_id, salary, manager_id) VALUES (301, 'Tom1', 'tom1@126.com', NOW(), 'AD_VP', 10000, 103);
3.查看和删除触发器
#2. 查看和删除触发器 #查看当前数据库的所有触发器的定义 SHOW TRIGGERS; #查看当前数据库中摸个触发器的定义 SHOW CREATE TRIGGER salary_check_trigger; #从系统库中查看信息 SELECT * FROM information_schema.`TRIGGERS`; #3. 删除触发器 DROP TRIGGER salary_check_trigger; SHOW TRIGGERS;
4.触发器的优缺点
优点 :
-
触发器可以确保数据的完整性
-
触发器可以帮助我们记录操作日志
-
触发器还可以用在操作数据前, 对数据进行合法性的检查
缺点 :
-
触发器最大的一个问题就是可读性差
-
相关数据的更新, 可能会导致触发器出错
注意 :
注意,如果在子表中定义了外键约束,并且外键指定了ON UPDATE/DELETE CASCADE/SET NULL子句,此时修改父表被引用的键值或删除父表被引用的记录行时,也会引起子表的修改和删除操作,此时基于子表的UPDATE和DELETE语句定义的触发器并不会被激活。
例如:基于子表员工表(t_employee)的DELETE语句定义了触发器t1,而子表的部门编号(did)字段定义了外键约束引用了父表部门表(t_department)的主键列部门编号(did),并且该外键加了“ON DELETE SET NULL”子句,那么如果此时删除父表部门表(t_department)在子表员工表(t_employee)有匹配记录的部门记录时,会引起子表员工表(t_employee)匹配记录的部门编号(did)修改为NULL, mysql> update demo.membermaster set memberdeposit=20 where memberid = 2; ERROR 1054 (42S22): Unknown column 'aa' in 'field list'但是此时不会激活触发器t1。只有直接对子表员工表(t_employee)执行DELETE语句时才会激活触发器t1。
章节练习
#第十七章_触发器章节练习 #练习一 : #0. 准备工作 USE dbtest17; CREATE TABLE emps AS SELECT employee_id,last_name,salary FROM atguigudb.`employees`; #1. 复制一张emps表的空表emps_back,只有表结构,不包含任何数据 CREATE TABLE emps_back AS SELECT * FROM emps WHERE 1 = 2; #2. 查询emps_back表中的数据 SELECT * FROM emps_back; #3. 创建触发器emps_insert_trigger,每当向emps表中添加一条记录时,同步将这条记录添加到emps_back表 中 DELIMITER // CREATE TRIGGER emps_insert_trigger AFTER INSERT ON emps FOR EACH ROW BEGIN INSERT INTO emps_back VALUES(NEW.employee_id, NEW.last_name, NEW.salary); END // DELIMITER ; #4. 验证触发器是否起作用 INSERT INTO emps VALUES (300, 'Tom', 1600); SELECT * FROM emps_back; #练习二 : #0. 准备工作:使用练习1中的emps表 #1. 复制一张emps表的空表emps_back1,只有表结构,不包含任何数据 CREATE TABLE emps_back1 AS SELECT * FROM emps WHERE 1 = 2; #2. 查询emps_back1表中的数据 SELECT * FROM emps_back1; #3. 创建触发器emps_del_trigger,每当向emps表中删除一条记录时,同步将删除的这条记录添加到 emps_back1表中 DELIMITER // CREATE TRIGGER emps_del_trigger AFTER DELETE ON emps FOR EACH ROW BEGIN INSERT INTO emps_back1 VALUES(OLD.employee_id, OLD.last_name, OLD.salary); END // DELIMITER ; #4. 验证触发器是否起作用 DELETE FROM emps WHERE employee_id = 300; SELECT * FROM emps_back1; DELETE FROM emps; SELECT * FROM emps_back1;