目录
十一、视图
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 语句暴露在网上的风险,也提高了数据查询的安全性
5、相较于函数,存储过程是没有返回值的
分类
存储过程的参数类型可以是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'),
('邓紫棋','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后面一定加的是标签
#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;