MySQL必知必会15存储过程:提高性能和安全性

本文深入介绍了MySQL存储过程的背景、创建、参数、程序体、查看、调用、修改和删除等核心概念,并通过实例展示了如何创建一个用于计算单品销售统计的存储过程。在实践中,存储过程能提高效率,增强安全性,但开发和调试成本较高。文章还提供了存储过程的调试方法和注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

阅读整理自《MySQL 必知必会》- 朱晓峰,详细内容请登录 极客时间 官网购买专栏。

背景

在超市项目中,每天营业结束后,超市经营者都要计算当日的销量,核算成本和毛利等营业数据,这也就意味着每天都要做重复的数据统计工作。其实,这种数据量大,而且计算过程复杂的场景,就非常适合使用存储过程

简单说,存储过程就是把一系列 SQL 语句预先存储在 MySQL 服务器上,需要执行的时候,客户端只需要向服务器端发出调用存储过程的命令,服务器端就可以把预先存储好的这一系列 SQL 语句全部执行。

不仅执行效率非常高,而且客户端不需要把所有的 SQL 语句通过网络发给服务器,减少了 SQL 语句暴露在网上的风险,也提高了数据查询的安全性。


创建存储过程

create procedure 存储过程名 ([in | out | inout] 参数名称 类型) 程序体

数据准备:

mysql> select * from demo.transactiondetails;
+---------------+------------+----------+------------+------------+
| transactionid | itemnumber | quantity | salesprice | salesvalue |
+---------------+------------+----------+------------+------------+
|             1 |          1 |        2 |      89.00 |     178.00 |
|             1 |          2 |        5 |       5.00 |      25.00 |
|             2 |          1 |        3 |      89.00 |     267.00 |
|             3 |          2 |       10 |       5.00 |      50.00 |
|             3 |          3 |        3 |      15.00 |      45.00 |
+---------------+------------+----------+------------+------------+
5 rows in set (0.01 sec)

mysql> select * from demo.transactionhead;
+---------------+------------------+------------+---------------------+----------+-----------+
| transactionid | transactionno    | operatorid | transdate           | memberid | cashierid |
+---------------+------------------+------------+---------------------+----------+-----------+
|             1 | 0120201201000001 |          1 | 2020-12-01 00:00:00 |        1 |         1 |
|             2 | 0120201202000001 |          2 | 2020-12-02 00:00:00 |        2 |         1 |
|             3 | 0120201202000002 |          1 | 2020-12-01 01:00:00 |     NULL |         1 |
+---------------+------------------+------------+---------------------+----------+-----------+
3 rows in set (0.00 sec)

mysql> select * from demo.goodsmaster;
+------------+---------+-----------+---------------+------+-----------+----------------+
| itemnumber | barcode | goodsname | specification | unit | saleprice | avgimportprice |
+------------+---------+-----------+---------------+------+-----------+----------------+
|          1 | 0001    || 16||     90.00 |          33.50 |
|          2 | 0002    || NULL          ||      5.00 |           3.50 |
|          3 | 0003    | 胶水      | NULL          ||     10.00 |          11.00 |
+------------+---------+-----------+---------------+------+-----------+----------------+

存储过程会用刚刚的三个表中的数据进行计算,并且把计算的结果存储到下面的这个单品统计表中。

mysql> describe demo.dailystatistics;
+-------------+---------------+------+-----+---------+----------------+
| Field       | Type          | Null | Key | Default | Extra          |
+-------------+---------------+------+-----+---------+----------------+
| id          | int           | NO   | PRI | NULL    | auto_increment |
| itemnumber  | int           | YES  |     | NULL    |                |
| quantity    | decimal(10,3) | YES  |     | NULL    |                |
| actualvalue | decimal(10,2) | YES  |     | NULL    |                |
| cost        | decimal(10,2) | YES  |     | NULL    |                |
| profit      | decimal(10,2) | YES  |     | NULL    |                |
| profitratio | decimal(10,4) | YES  |     | NULL    |                |
| salesdate   | datetime      | YES  |     | NULL    |                |
+-------------+---------------+------+-----+---------+----------------+

创建一个存储过程,完成单品销售统计的计算:

  1. 把 SQL 语句的分隔符改为“//”。因为存储过程中包含很多 SQL 语句,如果不修改分隔符的话,MySQL 会在读到第一个 SQL 语句的分隔符“;”的时候,认为语句结束并且执行,这样就会导致错误。
  2. 创建存储过程,把要处理的日期作为一个参数传入。同时用 BEGIN 和 END 关键字把存储过程中的 SQL 语句包裹起来,形成存储过程的程序体。
  3. 在程序体中,先定义 2 个数据类型为 DATETIME 的变量,用来记录要计算数据的起始时间和截止时间。
  4. 删除保存结果数据的单品统计表中相同时间段的数据,目的是防止数据重复。
  5. 计算起始时间和截止时间内单品的销售数量合计、销售金额合计、成本合计、毛利和毛利率,并且把结果存储到单品统计表中。
delimiter //  -- 设置分割符为//
create procedure demo.dailyoperation(transdate text)
begin  -- 开始程序体
declare startdate, enddate datetime;  -- 定义变量
set startdate = date_format(transdate, '%Y-%m-%d');  -- 给起始时间赋值
set enddate = date_add(transdate, interval 1 day);  -- 截止时间赋值为1天以后
-- 删除原有数据
delete from demo.dailystatistics where salesdate = startdate;
-- 重新插入数据
insert into demo.dailystatistics
(
salesdate,
itemnumber,
quantity,
actualvalue,
cost,
profit,
profitratio
)
select 
left(b.transdate, 10), 
a.itemnumber, 
sum(a.quantity), 
sum(a.salesvalue), 
sum(a.quantity*c.avgimportprice),  -- 计算成本
sum(a.salesvalue-a.quantity*c.avgimportprice),  -- 计算毛利
case sum(a.salesvalue) when 0 then 0 else round(sum(a.salesvalue-a.quantity*c.avgimportprice)/sum(a.salesvalue), 4) end  -- 计算毛利率
from demo.transactiondetails as a
join demo.transactionhead as b on (a.transactionid = b.transactionid)
join demo.goodsmaster as c on (a.itemnumber = c.itemnumber)
where b.transdate > startdate and b.transdate < enddate
group by left(b.transdate, 10), a.itemnumber
order by left(b.transdate, 10), a.itemnumber;
end
//
delimiter ; -- 恢复分隔符为;

存储过程参数

存储过程可以有参数,也可以没有参数。一般来说,当我们通过客户端或者应用程序调用存储过程的时候,如果需要与存储过程进行数据交互,比如,存储过程需要根据输入的数值为基础进行某种数据处理和计算,或者需要把某个计算结果返回给调用它的客户端或者应用程序,就需要设置参数。否则,就不用设置参数。

参数有 3 种,分别是 IN、OUT 和 INOUT。

  • IN 表示输入的参数,存储过程只是读取这个参数的值。如果没有定义参数种类,默认就是 IN,表示输入参数。
  • OUT 表示输出的参数,存储过程在执行的过程中,把某个计算结果值赋给这个参数,执行完成之后,调用这个存储过程的客户端或者应用程序就可以读取这个参数返回的值了。
  • INOUT 表示这个参数既可以作为输入参数,又可以作为输出参数使用。

除了定义参数种类,还要对参数的数据类型进行定义。在这个存储过程中,定义了一个参数 transdate 的数据类型是 text。这个参数的用处是告诉存储过程,我要处理的是哪一天的数据。没有指定参数种类是 IN、OUT 或者 INOUT,这是因为在 MySQL 中,如果不指定参数的种类,默认就是 IN,表示输入参数。

存储过程程序体

在程序体的开始部分,定义了 2 个变量,分别是 startdate 和 enddate。它们都是 datetime 类型,作用是根据输入参数 transdate,计算出需要筛选的数据的时间区间。

后面的代码分 3 步完成起始时间和截止时间的计算,并且分别赋值给变量 startdate 和 enddate。

  • 用 date_format() 函数,把输入的参数,按照 YYYY 年 MM 月 DD 日的格式转换成了日期时间类型数据,比如输入参数是“2020-12-01”,那么,转换成的日期时间值是“2020-12-01 00:00:00”,表示 2020 年 12 月 01 日 00 点 00 分 00 秒。
  • 把第一步中计算出的值,作为起始时间赋值给变量 startdate。
  • 把第一步中计算出的值,通过 date_add() 函数,计算出 1 天以后的时间赋值给变量 enddate。

计算出了起始时间和截止时间之后,先删除需要计算日期的单品统计数据,以防止数据重复。接着,我们重新计算单品的销售统计,并且把计算的结果插入到单品统计表。

需要注意的是,这里使用 case 函数来解决销售金额为 0 时计算毛利的问题。这是为了防止计算出现被 0 除而报错的情况。不要以为销售金额就一定大于 0,在实际项目运行的过程中,会出现因为优惠而导致实际销售金额为 0 的情况。在实际工作中,把这些极端情况都考虑在内,提前进行防范,这样你的代码才能稳定可靠。


查看存储过程

show create procedure demo.dailyoperation \G
mysql> show create procedure demo.dailyoperation \G
*************************** 1. row ***************************
           Procedure: dailyoperation
            sql_mode: STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION
    Create Procedure: CREATE DEFINER=`root`@`localhost` PROCEDURE `dailyoperation`(transdate text)
begin
declare startdate, enddate datetime;
set startdate = date_format(transdate, '%Y-%m-%d');
set enddate = date_add(transdate, interval 1 day);
-- 删除原有数据
delete from demo.dailystatistics where salesdate = startdate;
-- 重新插入数据
insert into demo.dailystatistics
(
salesdate,
itemnumber,
quantity,
actualvalue,
cost,
profit,
profitratio
)
select
left(b.transdate, 10),
a.itemnumber,
sum(a.quantity),
sum(a.salesvalue),
sum(a.quantity*c.avgimportprice),
sum(a.salesvalue-a.quantity*c.avgimportprice),
case sum(a.salesvalue) when 0 then 0 else round(sum(a.salesvalue-a.quantity*c.avgimportprice)/sum(a.salesvalue), 4) end
from demo.transactiondetails as a
join demo.transactionhead as b on (a.transactionid = b.transactionid)
join demo.goodsmaster as c on (a.itemnumber = c.itemnumber)
where b.transdate > startdate and b.transdate < enddate
group by left(b.transdate, 10), a.itemnumber
order by left(b.transdate, 10), a.itemnumber;
end
character_set_client: utf8mb4
collation_connection: utf8mb4_0900_ai_ci
  Database Collation: utf8mb4_0900_ai_ci
1 row in set (0.00 sec)

调用存储过程

调用一下这个存储过程,并且给它传递一个参数“2020-12-01”,也就是计算 2020 年 12 月 01 日的单品统计数据:

mysql> update demo.transactiondetails set quant^Chere transactionid=3 and itemnumber=3;
mysql> CALL demo.dailyoperation('2020-12-01');
Query OK, 2 rows affected (0.01 sec)

存储过程执行结果提示“Query OK”,表示执行成功了。“2 rows affected”表示执行的结果影响了 2 条数据记录。

mysql> select * from demo.dailystatistics;
+----+------------+----------+-------------+-------+--------+-------------+---------------------+
| id | itemnumber | quantity | actualvalue | cost  | profit | profitratio | salesdate           |
+----+------------+----------+-------------+-------+--------+-------------+---------------------+
|  4 |          2 |   10.000 |       50.00 | 35.00 |  15.00 |      0.3000 | 2020-12-01 00:00:00 |
|  5 |          3 |    3.000 |       45.00 | 33.00 |  12.00 |      0.2667 | 2020-12-01 00:00:00 |
+----+------------+----------+-------------+-------+--------+-------------+---------------------+
2 rows in set (0.00 sec)

可以看到,存储过程被执行了,它计算出了我们需要的单品统计结果,并且把统计结果存入了单品统计表中。

如果不使用存储过程的语句,查询所有:

mysql> select  left(b.transdate, 10),  a.itemnumber,  sum(a.quantity),
    -> sum(a.salesvalue),  sum(a.quantity*c.avgimportprice) as 成本,
    -> sum(a.salesvalue-a.quantity*c.avgimportprice) as 毛利,
    -> case sum(a.salesvalue) when 0 then 0 else round(sum(a.salesvalue- a.quantity*c.avgimportprice)/sum(a.salesvalue), 4) end as 毛利率
    -> from demo.transactiondetails as a
    -> join demo.transactionhead as b on (a.transactionid = b.transactionid)
    -> join demo.goodsmaster as c on (a.itemnumber = c.itemnumber)
    -> group by left(b.transdate, 10), a.itemnumber
    -> order by left(b.transdate, 10), a.itemnumber;
+-----------------------+------------+-----------------+-------------------+--------+--------+--------+
| left(b.transdate, 10) | itemnumber | sum(a.quantity) | sum(a.salesvalue) | 成本   | 毛利   | 毛利率 |
+-----------------------+------------+-----------------+-------------------+--------+--------+--------+
| 2020-12-01            |          1 |               2 |            178.00 |  67.00 | 111.00 | 0.6236 |
| 2020-12-01            |          2 |              15 |             75.00 |  52.50 |  22.50 | 0.3000 |
| 2020-12-01            |          3 |               3 |             45.00 |  33.00 |  12.00 | 0.2667 |
| 2020-12-02            |          1 |               3 |            267.00 | 100.50 | 166.50 | 0.6236 |
+-----------------------+------------+-----------------+-------------------+--------+--------+--------+
4 rows in set (0.01 sec)

修改存储过程

修改存储过程的内容,建议在 Workbench 中操作。这是因为可以在里面直接修改存储过程,而如果用 SQL 命令来修改存储过程,就必须删除存储过程再重新创建,相比之下,在 Workbench 中修改比较简单。

在左边的导航栏,找到数据库 demo,展开之后,找到存储过程 stored procedure,然后找到刚刚创建的 dailyoperation,点击右边的设计按钮,就可以在右边的工作区进行修改了。修改完成之后,点击工作区右下方的按钮“Apply”,保存修改。

请添加图片描述

在 MySQL 中,存储过程不像普通的编程语言(比如 VC++、Java 等)那样有专门的集成开发环境。因此,可以通过 SELECT 语句,把程序执行的中间结果查询出来,来调试一个 SQL 语句的正确性。调试成功之后,把 SELECT 语句后移到下一个 SQL 语句之后,再调试下一个 SQL 语句。这样逐步推进,就可以完成对存储过程中所有操作的调试了。当然,也可以把存储过程中的 SQL 语句复制出来,逐段单独调试。


删除存储过程

drop procedure 存储过程名称;

小结

存储过程的优点就是执行效率高,而且更加安全,不过,它也有着自身的缺点,那就是开发和调试的成本比较高,而且不太容易维护。

在存储过程开发的过程中,虽然也有一些第三方工具可以对存储过程进行调试,但要收费。建议通过 SELECT 语句输出变量值的办法进行调试,虽然有点麻烦,但是成本低,而且简单可靠。如果存储过程需要随产品一起分发,可以考虑把脚本放在安装程序中,在产品安装的过程中创建需要的存储过程。

测试题:写一个简单的存储过程,要求是定义 2 个参数,一个输入参数 a,数据类型是 INT;另一个输出参数是 b,类型是 INT。程序体完成的操作是:b = a + 1

delimiter //
create procedure demo.test_add(in a int, out b int)
begin
set b = a + 1;
end
//
delimiter ;
-- 调用方法
mysql> call demo.test_add(1, @a);
Query OK, 0 rows affected (0.02 sec)

mysql> select @a;
+------+
| @a   |
+------+
|    2 |
+------+
1 row in set (0.00 sec)
存储过程的安全及性能优化 存储过程分类  系统存储过程  自定义存储过程  SQL Server使用者编写的存储过程  扩展存储过程  动态链接库(DLL)函数的调用看,主要用于客户端服务器端之间进行通信  exec master..xp_cmdshell 'dir *.exe' -- 执行目录命令查询[sql2005\sql2008]  exec master..xp_fixeddrives --列出硬盘分区各自可用空间  xp_regwrite根键,子键,值名,值类型,值【sql2008拒绝访问】  写入注册表,例如:  exec master..db.xp_regwrite 'HKEY_LOCAL_MACHINE','SOFTWARE\Microsoft\Windows\CurrentVersion\run','TestValueName','reg_sz','hello'  xp_regdeletevalue 根键,子键,值名【sql2008拒绝访问】  删除注册表某个值  xp_regdeletekey键,值【sql2008拒绝访问】  删除该键下包括的所有值 xp_cmdshell语法  xp_cmdshell {'command_string'} [,no_output]  command_string是在操作系统命令行解释器上执行的命令字符串。command_string数据类型为varchar(255)或者nvarchar(4000),没有默认值  no_output为可选参数,可以控制是否想客户端返回信息  该存储过程一般情况下被禁用的,需要手动开启使用,如下:  exec sp_configure 'show advanced options',1  go   reconfigure  go   exec sp_configure 'xp_cmdshell',1—1表示启用,0表示禁用  go   reconfigure  go 删除xp_cmdshell SQL SERVER200删除xp_cmdshell use master exec sp_dropextendedproc 'xp_cmdshell' go SQL SERVER2005以上禁用xp_cmdshell,但不能删除掉 exec sp_configure 'xp_cmdshell',0 —1表示启用,0表示禁用  go reconfigure --让sp_configurre立即生效  go  exec sp_configure 'show advanced options',0  go   reconfigure  go --注意:SQL SERVER2008考虑安全性很多存储过程直接被拒绝访问 恢复/启用扩展存储过程 SQLServer2000 use master exec sp_addextendedproc xp_cmdshell,'xplog70.dll' go SQL Server2005或SQL Server2008启用xp_cmdshell  exec sp_configure 'show advanced options',1  go   reconfigure  go   exec sp_configure 'xp_cmdshell',1—1表示启用,0表示禁用  go   reconfigure  go 扩展存储过程的定义  扩展存储过程是SQL Server中的另一类存储过程,它是以其它语言编写的外部程序,是以动态链接库(DLL)形式存储在服务器上,最终SQLServer就可以动态加载并执行它们  编写好后使用SQLServer的固定角色sysadmin注册该扩展存储过程,并将执行权限授予其它用户,这个扩展存储过程只能添加到master数据库。  在编写扩展存储过程中可能要用到某些系统存储过程,这些系统存储过程如下: 利用OLE自动化存储过程调用dll 1.创建类库程序集 namespace PB_ExtendProcedure { public class ExtendProcedure { public string SayHi() { return "hello world"; } } } 2.生成动态链接库并注册到系统中 2.1.生成动态链接库使用VS2010命令行工具 使用sn命令生成一个强命名文件: sn -k helpkey.snk 使用csc生成dll csc /t:library /keyfile:helperkey.snk ExtendProcedure.cs 向系统注册这个dll regasm /tlb:ExtendProcedure.tlb ExtendProcedure.dll /codebase 2.2.在SQL Server中编写扩展存储过程 --sp_OACreate --sp_OAMethod --sp_OADestroy --sp_OAGetErrorInfo 流程: DECLARE @object int --返回创建的对象 DECLARE @hr int --过程返回值 DECLARE @return varchar(255) --dll方法的返回值 DECLARE @src varchar(255),@desc varchar(255) ---过程的错误原因、描述 --1.创建对象 EXEC @hr = sp_OACreate 'PB_ExtendProcedure.ExtendProcedure',@object out IF @hr0 BEGIN EXEC sp_OAGetErrorInfo @object,@src,@desc out select hr = convert(varchar(4),@hr),Source=@src,Description=@desc return END --2.调用方法 EXEC @hr = sp_OAMethod @object,'SayHi',@return out IF @hr0 BEGIN EXEC sp_OAGetErrorInfo @object,@src,@desc out select hr = convert(varchar(4),@hr),Source=@src,Description=@desc return END print @return --3.销毁对象实例 EXEC @hr = sp_OADestroy @object IF @hr0 BEGIN EXEC sp_OAGetErrorInfo @object,@src,@desc out select hr = convert(varchar(4),@hr),Source=@src,Description=@desc return END 注意:默认情况sqlserver2008是禁止调用ole自动化存储过程的,解决方法如下: sp_configure 'show advanced options',1 go reconfigure go sp_configure 'ole automation procedures',1 go reconfiugre go 加密存储过程 创建加密存储过程语法 CREATE PROCEDURE WITH ENCRYPTION AS 存储过程执行机制 语法阶段:指创建存储过程时,系统检查其创建语句语法正确性的过程。若语法检查通过则系统将该存储过程存储在当前数据库的sys.sql_modules目录视图当中 解析阶段:指某个存储过程首次执行时,查询处理器从sys.sql_modules目录视图中读取该存储过程的文本并且检查该过程引用的对象是否存在的过程。 编译阶段:指分析存储过程生成存储过程执行计划的过程。执行计划是来描述存储过程执行过程的。查询优化器是在分析完存储过程之后将生成的执行计划存储在存储过程高速缓冲存储器中,此后每次调用已经创建的存储过程时将直接执行不再需要编译,这样就可以提高程序的运行性能。 执行阶段:指执行驻留在存储过程高速缓冲存储区中的存储过程执行计划的过程。 了解SQL Server Profiler SQL Server Profiler是图形化实时监视工具 能帮助系统管理员监视数据库服务器行为,比如死锁数量、致命错误 跟踪T-SQL语句存储过程 通常使用Profiler监视重要事件: 登录连接的失败 成功或断开连接 delete、insert、update命令 存储过程开始或结束 存储过程中的每一条语句 写入sql server错误日志的错误 打开游标 向数据对象添加或释放锁 Profiler事件 SQL Server Profiler里Standard模板的事件类 数据库引擎优化顾问介绍 引擎优化顾问提供数据库系统的优化建议,经过管理员修改达到优化目的 引擎优化顾问分析数据库的工作负荷【Sql Server Profiler的跟踪文件或者表】物理实现 引擎优化顾问提出合理的物理设计结构以降工作负荷的开销 数据库引擎优化顾问介绍 引擎优化顾问提供数据库系统的优化建议,经过管理员修改达到优化目的 引擎优化顾问分析数据库的工作负荷【Sql Server Profiler的跟踪文件或者表】物理实现 引擎优化顾问提出合理的物理设计结构以降工作负荷的开销 提高存储过程性能方法 存储过程里面包含很多语句,我们真正要提高性能的是存储过程里面这些语句  默认情况存储过程会返回每条语句执行后的影响行数,如果不需要这些信息的话,可以使用SET NOCOUNT ON来终止这种返回行为,这样可以避免客户端服务器之间多次进行信息传递的过程。  减少可选参数:频繁使用可选参数是很容易降性能的。  优化SQL语句  避免频繁访问同一张或多张表,尤其是数据量大的表  尽量避免大事务操作  尽量避免使用游标,游标效率很差,如果使用游标就不要在游标的循环中使用表连接操作  注意where语句的写法,应该根据索引的顺序、范围的顺序、范围的大小来确定条件子句的前后顺序,尽量让字段顺序与索引顺序保持一致  尽量使用exists代替select count()判断是否存在记录  注意表之间连接的数据类型  先写DDL,再写DML: 当DML先于DDL执行的时候,SQL Server会重新编译存储过程,因为DML引用了DDL中的一些内容,也就是所谓的一些表。这个时候SqlServer需要统计由DDL定义的一些对象的变化。以此来创建DML的执行计划,如果将DDL放在前面那么这种编译只需要以此就可以了。  合理使用索引: 根据实际查询需求来创建索引 尽量使用索引字段做查询条件 尽量避免对大数据量表进行全表扫描,可考虑新建索引  合理使用tempdb系统表 尽量避免使用distinct、order by、group by、having、join语句--这些语句加重tempdb的负担  避免频繁创建删除临时表  临时表中插入数据过大,可使用select into 代替 create table  使用了临时表,要在存储过程最后显式删除  避免使用大临时表与其他大数据量表的连接查询修改
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值