草庐IT

如何使用 SQL 存储过程简化复杂的操作

Vin_c° 2023-03-28 原文

本文介绍什么是 SQL 存储过程,为什么要使用存储过程,如何使用存储过程,以及创建和使用存储过程的基本语法。

一、存储过程

迄今为止,我们使用的大多数 SQL 语句都是针对一个或多个表的单条语句。并非所有操作都这么简单,经常会有一些复杂的操作需要多条语句才能完成,例如以下的情形。

  • 为了处理订单,必须核对以保证库存中有相应的物品。
  • 如果物品有库存,需要预定,不再出售给别的人,并且减少物品数据以反映正确的库存量。
  • 库存中没有的物品需要订购,这需要与供应商进行某种交互。
  • 关于哪些物品入库(并且可以立即发货)和哪些物品退订,需要通知相应的顾客。

这显然不是一个完整的例子,它甚至超出了本文的范围,但足以表达我们的意思了。执行这个处理需要针对许多表的多条 SQL 语句。

此外,需要执行的具体 SQL 语句及其次序也不是固定的,它们可能会根据物品是否在库存中而变化。

那么,怎样编写代码呢?可以单独编写每条 SQL 语句,并根据结果有条件地执行其他语句。在每次需要这个处理时(以及每个需要它的应用中),都必须做这些工作。

可以创建存储过程。简单来说,存储过程就是为以后使用而保存的一条或多条 SQL 语句。可将其视为批文件,虽然它们的作用不仅限于批处理。

说明:不适用于 SQLite

SQLite 不支持存储过程。

说明:还有更多内容

存储过程很复杂,全面介绍它需要很大篇幅。

市面上有专门讲存储过程的书。

本文不打算讲解存储过程的所有内容,只给出简单介绍,让读者对它们的功能有所了解。

因此,这里给出的例子只提供 Oracle 和 SQL Server 的语法。

二、为什么要使用存储过程

我们知道了什么是存储过程,那么为什么要使用它们呢?理由很多,下面列出一些主要的。

  • 通过把处理封装在一个易用的单元中,可以简化复杂的操作(如前面例子所述)。
  • 由于不要求反复建立一系列处理步骤,因而保证了数据的一致性。如果所有开发人员和应用程序都使用同一存储过程,则所使用的代码都是相同的。
  • 上一点的延伸就是防止错误。需要执行的步骤越多,出错的可能性就越大。防止错误保证了数据的一致性。
  • 简化对变动的管理。如果表名、列名或业务逻辑(或别的内容)有变化,那么只需要更改存储过程的代码。使用它的人员甚至不需要知道这些变化。
  • 上一点的延伸就是安全性。通过存储过程限制对基础数据的访问,减少了数据讹误(无意识的或别的原因所导致的数据讹误)的机会。
  • 因为存储过程通常以编译过的形式存储,所以 DBMS 处理命令所需的工作量少,提高了性能。
  • 存在一些只能用在单个请求中的 SQL 元素和特性,存储过程可以使用它们来编写功能更强更灵活的代码。

换句话说,使用存储过程有三个主要的好处,即简单、安全、高性能。显然,它们都很重要。不过,在将 SQL 代码转换为存储过程前,也必须知道它的一些缺陷。

  • 不同 DBMS 中的存储过程语法有所不同。事实上,编写真正的可移植存储过程几乎是不可能的。

    不过,存储过程的自我调用(名字以及数据如何传递)可以相对保持可移植。

    因此,如果需要移植到别的 DBMS,至少客户端应用代码不需要变动。

  • 一般来说,编写存储过程比编写基本 SQL 语句复杂,需要更高的技能,更丰富的经验。

    因此,许多数据库管理员把限制存储过程的创建作为安全措施(主要受上一条缺陷的影响)。

尽管有这些缺陷,存储过程还是非常有用的,并且应该使用。事实上,多数 DBMS 都带有用于管理数据库和表的各种存储过程。更多信息请参阅具体的 DBMS 文档。

说明:不会写存储过程?你依然可以使用

大多数 DBMS 将编写存储过程所需的安全和访问权限与执行存储过程所需的安全和访问权限区分开来。

这是好事情,即使你不能(或不想)编写自己的存储过程,也仍然可以在适当的时候执行别的存储过程。

三、执行存储过程

存储过程的执行远比编写要频繁得多,因此我们先介绍存储过程的执行。

执行存储过程的 SQL 语句很简单,即 EXECUTE

EXECUTE 接受存储过程名和需要传递给它的任何参数。

请看下面的例子(你无法运行这个例子,因为 AddNewProduct 这个存储过程还不存在):

EXECUTE AddNewProduct('JTS01',
                      'Stuffed Eiffel Tower',
                      6.49,
                      'Plush stuffed toy with the text La Tour Eiffel in red white and blue');

这里执行一个名为 AddNewProduct 的存储过程,将一个新产品添加到 Products 表中。

AddNewProduct 有四个参数,分别是:供应商 ID(Vendors 表的主键)、产品名、价格和描述。

这 4 个参数匹配存储过程中 4 个预期变量(定义为存储过程自身的组成部分)。此存储过程将新行添加到 Products 表,并将传入的属性赋给相应的列。

我们注意到,在 Products 表中还有另一个需要值的列 prod_id 列,它是这个表的主键。

为什么这个值不作为属性传递给存储过程?要保证恰当地生成此 ID,最好是使生成此 ID 的过程自动化(而不是依赖于最终用户的输入)。

这也是这个例子使用存储过程的原因。以下是存储过程所完成的工作:

  • 验证传递的数据,保证所有 4 个参数都有值;
  • 生成用作主键的唯一 ID;
  • 将新产品插入 Products 表,在合适的列中存储生成的主键和传递的数据。

这就是存储过程执行的基本形式。对于具体的 DBMS,可能包括以下的执行选择。

  • 参数可选,具有不提供参数时的默认值。
  • 不按次序给出参数,以“参数=值”的方式给出参数值。
  • 输出参数,允许存储过程在正执行的应用程序中更新所用的参数。
  • SELECT 语句检索数据。
  • 返回代码,允许存储过程返回一个值到正在执行的应用程序。

四、创建存储过程

正如所述,存储过程的编写很重要。为了获得感性认识,我们来看一个简单的存储过程例子,它对邮件发送清单中具有邮件地址的顾客进行计数。

下面是该过程的 Oracle 版本:

CREATE PROCEDURE MailingListCount (
  ListCount OUT INTEGER
)
IS
v_rows INTEGER;
BEGIN
    SELECT COUNT(*) INTO v_rows
    FROM Customers
    WHERE NOT cust_email IS NULL;
    ListCount := v_rows;
END;

这个存储过程有一个名为 ListCount 的参数。此参数从存储过程返回一个值而不是传递一个值给存储过程。

关键字 OUT 用来指示这种行为。Oracle 支持 IN(传递值给存储过程)、OUT(从存储过程返回值,如这里)、INOUT(既传递值给存储过程也从存储过程传回值)类型的参数。

存储过程的代码括在 BEGINEND 语句中,这里执行一条简单的 SELECT 语句,它检索具有邮件地址的顾客。然后用检索出的行数设置 ListCount(要传递的输出参数)。

调用 Oracle 例子可以像下面这样:

var ReturnValue NUMBER
EXEC MailingListCount(:ReturnValue);
SELECT ReturnValue;

这段代码声明了一个变量来保存存储过程返回的任何值,然后执行存储过程,再使用 SELECT 语句显示返回的值。

下面是该过程的 SQL Server 版本。

CREATE PROCEDURE MailingListCount
AS
DECLARE @cnt INTEGER
SELECT @cnt = COUNT(*)
FROM Customers
WHERE NOT cust_email IS NULL;
RETURN @cnt;

此存储过程没有参数。调用程序检索 SQL Server 的返回代码提供的值。

其中用 DECLARE 语句声明了一个名为 @cnt 的局部变量(SQL Server 中所有局部变量名都以 @ 起头);

然后在 SELECT 语句中使用这个变量,让它包含 COUNT() 函数返回的值;最后,用 RETURN @cnt 语句将计数返回给调用程序。

调用 SQL Server 例子可以像下面这样:

DECLARE @ReturnValue INT
EXECUTE @ReturnValue=MailingListCount;
SELECT @ReturnValue;

这段代码声明了一个变量来保存存储过程返回的任何值,然后执行存储过程,再使用 SELECT 语句显示返回的值。

下面是另一个例子,这次在 Orders 表中插入一个新订单。此程序仅适用于 SQL Server,但它说明了存储过程的某些用途和技术:

CREATE PROCEDURE NewOrder @cust_id CHAR(10)
AS
-- 为订单号声明一个变量
DECLARE @order_num INTEGER
-- 获取当前最大订单号
SELECT @order_num=MAX(order_num)
FROM Orders
-- 决定下一个订单号
SELECT @order_num=@order_num+1
-- 插入新订单
INSERT INTO Orders(order_num, order_date, cust_id)
VALUES(@order_num, GETDATE(), @cust_id)
-- 返回订单号
RETURN @order_num;

此存储过程在 Orders 表中创建一个新订单。

它只有一个参数,即下订单顾客的 ID。订单号和订单日期这两列在存储过程中自动生成。

代码首先声明一个局部变量来存储订单号。接着,检索当前最大订单号(使用 MAX() 函数)并增加 1(使用 SELECT 语句)。

然后用 INSERT 语句插入由新生成的订单号、当前系统日期(用 GETDATE() 函数检索)和传递的顾客 ID 组成的订单。

最后,用 RETURN @order_num 返回订单号(处理订单物品需要它)。

请注意,此代码加了注释,在编写存储过程时应该多加注释。

说明:注释代码

应该注释所有代码,存储过程也不例外。增加注释不影响性能,因此不存在缺陷(除了增加编写时间外)。

注释代码的好处很多,包括使别人(以及你自己)更容易地理解和更安全地修改代码。

对代码进行注释的标准方式是在之前放置 --(两个连字符)。

有的 DBMS 还支持其他的注释语法,不过所有 DBMS 都支持 --,因此在注释代码时最好都使用这种语法。

下面是相同 SQL Server 代码的一个很不同的版本:

CREATE PROCEDURE NewOrder @cust_id CHAR(10)
AS
-- 插入新订单
INSERT INTO Orders(cust_id)
VALUES(@cust_id)
-- 返回订单号
SELECT order_num = @@IDENTITY;

此存储过程也在 Orders 表中创建一个新订单。这次由 DBMS 生成订单号。

大多数 DBMS 都支持这种功能;SQL Server 中称这些自动增量的列为标识字段(identity field),而其他 DBMS 称之为自动编号(auto number)或序列(sequence)。

传递给此过程的参数也是一个,即下订单的顾客 ID。

订单号和订单日期没有给出,DBMS 对日期使用默认值(GETDATE() 函数),订单号自动生成。

怎样才能得到这个自动生成的 ID?在 SQL Server 上可在全局变量 @@IDENTITY 中得到,它返回到调用程序(这里使用 SELECT 语句)。

可以看到,借助存储过程,可以有多种方法完成相同的工作。不过,所选择的方法受所用 DBMS 特性的制约。

五、小结

本文介绍了什么是存储过程,为什么使用存储过程。我们介绍了执行和创建存储过程的语法,使用存储过程的一些方法。

存储过程是个相当重要的主题,一篇文章无法全部涉及。

各种 DBMS 对存储过程的实现不一,你使用的 DBMS 可能提供了一些这里提到的功能,也有其他未提及的功能,更详细的介绍请参阅具体的 DBMS 文档。

原文链接:https://www.developerastrid.com/sql/sql-stored-procedures/

(完)

有关如何使用 SQL 存储过程简化复杂的操作的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  4. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  5. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  6. ruby - 在 Ruby 中使用匿名模块 - 2

    假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

  7. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

    我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

  8. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  9. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  10. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

随机推荐