SQL捕获异常
Transact-SQL 代碼中的錯(cuò)誤可使用 TRY…CATCH 構(gòu)造處理,此功能類似于 Microsoft Visual C++ 和 Microsoft Visual C# 語(yǔ)言的異常處理功能。TRY…CATCH 構(gòu)造包括兩部分:一個(gè) TRY 塊和一個(gè) CATCH 塊。如果在 TRY 塊內(nèi)的 Transact-SQL 語(yǔ)句中檢測(cè)到錯(cuò)誤條件,則控制將被傳遞到 CATCH 塊(可在此塊中處理此錯(cuò)誤)。
CATCH 塊處理該異常錯(cuò)誤后,控制將被傳遞到 END CATCH 語(yǔ)句后面的第一個(gè) Transact-SQL 語(yǔ)句。如果 END CATCH 語(yǔ)句是存儲(chǔ)過(guò)程或觸發(fā)器中的最后一條語(yǔ)句,則控制將返回到調(diào)用該存儲(chǔ)過(guò)程或觸發(fā)器的代碼。將不執(zhí)行 TRY 塊中生成錯(cuò)誤的語(yǔ)句后面的 Transact-SQL 語(yǔ)句。
如果 TRY 塊中沒(méi)有錯(cuò)誤,控制將傳遞到關(guān)聯(lián)的 END CATCH 語(yǔ)句后緊跟的語(yǔ)句。如果 END CATCH 語(yǔ)句是存儲(chǔ)過(guò)程或觸發(fā)器中的最后一條語(yǔ)句,控制將傳遞到調(diào)用該存儲(chǔ)過(guò)程或觸發(fā)器的語(yǔ)句。
TRY 塊以 BEGIN TRY 語(yǔ)句開頭,以 END TRY 語(yǔ)句結(jié)尾。在 BEGIN TRY 和 END TRY 語(yǔ)句之間可以指定一個(gè)或多個(gè) Transact-SQL 語(yǔ)句。
CATCH 塊必須緊跟 TRY 塊。CATCH 塊以 BEGIN CATCH 語(yǔ)句開頭,以 END CATCH 語(yǔ)句結(jié)尾。在 Transact-SQL 中,每個(gè) TRY 塊僅與一個(gè) CATCH 塊相關(guān)聯(lián)。
使用 TRY...CATCH
使用 TRY...CATCH 構(gòu)造時(shí),請(qǐng)遵循下列規(guī)則和建議:
-
每個(gè) TRY...CATCH 構(gòu)造都必須位于一個(gè)批處理、存儲(chǔ)過(guò)程或觸發(fā)器中。例如,不能將 TRY 塊放置在一個(gè)批處理中而將關(guān)聯(lián)的 CATCH 塊放置在另一個(gè)批處理中。下面的腳本將生成一個(gè)錯(cuò)誤:
BEGIN TRYSELECT *FROM sys.messagesWHERE message_id = 21; END TRY GO -- The previous GO breaks the script into two batches, -- generating syntax errors. The script runs if this GO -- is removed. BEGIN CATCHSELECT ERROR_NUMBER() AS ErrorNumber; END CATCH; GO -
CATCH 塊必須緊跟 TRY 塊。
-
TRY…CATCH 構(gòu)造可以是嵌套式的。這意味著可以將 TRY…CATCH 構(gòu)造放置在其他 TRY 塊和 CATCH 塊內(nèi)。當(dāng)嵌套的 TRY 塊中出現(xiàn)錯(cuò)誤時(shí),程序控制將傳遞到與嵌套的 TRY 塊關(guān)聯(lián)的 CATCH 塊。
-
若要處理給定的 CATCH 塊中出現(xiàn)的錯(cuò)誤,請(qǐng)?jiān)谥付ǖ?CATCH 塊中編寫 TRY...CATCH 塊。
-
TRY...CATCH 塊不處理導(dǎo)致數(shù)據(jù)庫(kù)引擎關(guān)閉連接的嚴(yán)重性為 20 或更高的錯(cuò)誤。但是,只要連接不關(guān)閉,TRY...CATCH 就會(huì)處理嚴(yán)重性為 20 或更高的錯(cuò)誤。
-
嚴(yán)重性為 10 或更低的錯(cuò)誤被視為警告或信息性消息,TRY...CATCH 塊不處理此類錯(cuò)誤。
-
即使批處理位于 TRY...CATCH 構(gòu)造的作用域內(nèi),關(guān)注消息仍將終止該批處理。分布式事務(wù)失敗時(shí),Microsoft 分布式事務(wù)處理協(xié)調(diào)器 (MS DTC) 將發(fā)送關(guān)注消息。MS DTC 用于管理分布式事務(wù)。
注意 如 果在 TRY 塊的作用域內(nèi)執(zhí)行分布式事務(wù)且發(fā)生錯(cuò)誤,執(zhí)行將傳遞到關(guān)聯(lián)的 CATCH 塊。分布式事務(wù)進(jìn)入不可提交狀態(tài)。CATCH 塊中的執(zhí)行可能由管理分布式事務(wù)的 Microsoft 分布式事務(wù)處理協(xié)調(diào)器中斷。發(fā)生錯(cuò)誤時(shí),MS DTC 將異步通知參與分布式事務(wù)的所有服務(wù)器,并終止分布式事務(wù)中涉及的所有任務(wù)。此類通知以關(guān)注消息的形式發(fā)送(TRY...CATCH 構(gòu)造不處理此類通知),批處理將被終止。當(dāng)批處理完成運(yùn)行時(shí),數(shù)據(jù)庫(kù)引擎將回滾所有不可提交的活動(dòng)事務(wù)。如果事務(wù)進(jìn)入不可提交狀態(tài)時(shí)未發(fā)送錯(cuò)誤消息,則當(dāng) 批處理完成時(shí),將向客戶端應(yīng)用程序發(fā)送錯(cuò)誤消息,該消息指示檢測(cè)到或回滾了一個(gè)不可提交的事務(wù)。有關(guān)分布式事務(wù)的詳細(xì)信息,請(qǐng)參閱 分布式事務(wù)(數(shù)據(jù)庫(kù)引擎) 。
錯(cuò)誤函數(shù)
TRY...CATCH 使用下列錯(cuò)誤函數(shù)來(lái)捕獲錯(cuò)誤信息:
-
ERROR_NUMBER() 返回錯(cuò)誤號(hào)。
-
ERROR_MESSAGE() 返回錯(cuò)誤消息的完整文本。此文本包括為任何可替換參數(shù)(如長(zhǎng)度、對(duì)象名或時(shí)間)提供的值。
-
ERROR_SEVERITY() 返回錯(cuò)誤嚴(yán)重性。
-
ERROR_STATE() 返回錯(cuò)誤狀態(tài)號(hào)。
-
ERROR_LINE() 返回導(dǎo)致錯(cuò)誤的例程中的行號(hào)。
-
ERROR_PROCEDURE() 返回出現(xiàn)錯(cuò)誤的存儲(chǔ)過(guò)程或觸發(fā)器的名稱。
可 以使用這些函數(shù)從 TRY...CATCH 構(gòu)造的 CATCH 塊的作用域中的任何位置檢索錯(cuò)誤信息。如果在 CATCH 塊的作用域之外調(diào)用錯(cuò)誤函數(shù),錯(cuò)誤函數(shù)將返回 NULL。在 CATCH 塊中執(zhí)行存儲(chǔ)過(guò)程時(shí),可以在存儲(chǔ)過(guò)程中引用錯(cuò)誤函數(shù)并將其用于檢索錯(cuò)誤信息。如果這樣做,則不必在每個(gè) CATCH 塊中重復(fù)錯(cuò)誤處理代碼。在下面的代碼示例中,TRY 塊中的 SELECT 語(yǔ)句將生成一個(gè)被零除錯(cuò)誤。此錯(cuò)誤將由 CATCH 塊處理,它將使用存儲(chǔ)過(guò)程返回錯(cuò)誤信息。
USE AdventureWorks2008R2; GO-- Verify that the stored procedure does not exist. IF OBJECT_ID ('usp_GetErrorInfo', 'P') IS NOT NULLDROP PROCEDURE usp_GetErrorInfo; GO-- Create a procedure to retrieve error information. CREATE PROCEDURE usp_GetErrorInfo ASSELECT ERROR_NUMBER() AS ErrorNumber,ERROR_SEVERITY() AS ErrorSeverity,ERROR_STATE() as ErrorState,ERROR_PROCEDURE() as ErrorProcedure,ERROR_LINE() as ErrorLine,ERROR_MESSAGE() as ErrorMessage; GOBEGIN TRY-- Generate divide-by-zero error.SELECT 1/0; END TRY BEGIN CATCH-- Execute the error retrieval routine.EXECUTE usp_GetErrorInfo; END CATCH; GO編譯錯(cuò)誤和語(yǔ)句級(jí)重新編譯錯(cuò)誤
對(duì)于與 TRY...CATCH 構(gòu)造在同一執(zhí)行級(jí)別發(fā)生的錯(cuò)誤,TRY...CATCH 將不處理以下兩類錯(cuò)誤:
-
編譯錯(cuò)誤,例如阻止批處理執(zhí)行的語(yǔ)法錯(cuò)誤。
-
語(yǔ)句級(jí)重新編譯過(guò)程中出現(xiàn)的錯(cuò)誤,例如由于名稱解析延遲而造成在編譯后出現(xiàn)對(duì)象名解析錯(cuò)誤。
當(dāng)包含 TRY...CATCH 構(gòu)造的批處理、存儲(chǔ)過(guò)程或觸發(fā)器生成其中一種錯(cuò)誤時(shí),TRY...CATCH 構(gòu)造將不處理這些錯(cuò)誤。這些錯(cuò)誤將返回到調(diào)用生成錯(cuò)誤的例程的應(yīng)用程序或批處理。例如,下面的代碼示例顯示導(dǎo)致語(yǔ)法錯(cuò)誤的 SELECT 語(yǔ)句。如果在 SQL Server Management Studio 查詢編輯器中執(zhí)行此代碼,則由于批處理無(wú)法編譯,執(zhí)行將不啟動(dòng)。錯(cuò)誤將返回到查詢編輯器,將不會(huì)由 TRY...CATCH 捕獲。
USE AdventureWorks2008R2; GOBEGIN TRY-- This PRINT statement will not run because the batch-- does not begin execution.PRINT N'Starting execution';-- This SELECT statement contains a syntax error that-- stops the batch from compiling successfully.SELECT ** FROM HumanResources.Employee; END TRY BEGIN CATCHSELECT ERROR_NUMBER() AS ErrorNumber,ERROR_MESSAGE() AS ErrorMessage; END CATCH; GO與上述示例中的語(yǔ)法錯(cuò)誤不同,語(yǔ)句級(jí)重新編譯過(guò)程中發(fā)生的錯(cuò)誤不會(huì)阻礙批處理進(jìn)行編譯,但是一旦語(yǔ)句重新編譯失敗,它會(huì)立即終止批處理。例如,如果 批處理含有兩條語(yǔ)句并且第二條語(yǔ)句引用的表不存在,則延遲的名稱解析會(huì)使該批處理成功進(jìn)行編譯并開始執(zhí)行(無(wú)需將缺少的表綁定到查詢計(jì)劃),直到重新編譯 該語(yǔ)句為止。批處理到達(dá)引用缺失表的語(yǔ)句時(shí)將停止運(yùn)行,并返回一個(gè)錯(cuò)誤。在發(fā)生錯(cuò)誤的執(zhí)行級(jí)別,TRY...CATCH 構(gòu)造將不處理此類錯(cuò)誤。以下示例對(duì)此行為進(jìn)行了說(shuō)明。
USE AdventureWorks2008R2; GOBEGIN TRY-- This PRINT statement will run because the error-- occurs at the SELECT statement.PRINT N'Starting execution';-- This SELECT statement will generate an object name-- resolution error because the table does not exist.SELECT * FROM NonExistentTable; END TRY BEGIN CATCHSELECT ERROR_NUMBER() AS ErrorNumber,ERROR_MESSAGE() AS ErrorMessage; END CATCH; GO通過(guò)執(zhí)行 TRY 塊內(nèi)單獨(dú)批處理中的錯(cuò)誤生成代碼,可以使用 TRY...CATCH 來(lái)處理編譯或語(yǔ)句級(jí)重新編譯過(guò)程中發(fā)生的錯(cuò)誤。例如,這可以通過(guò)在存儲(chǔ)過(guò)程中放置代碼或使用 sp_executesql 執(zhí)行動(dòng)態(tài) Transact-SQL 語(yǔ)句來(lái)實(shí)現(xiàn)。這使 TRY...CATCH 能夠在比錯(cuò)誤發(fā)生的執(zhí)行級(jí)別更高的執(zhí)行級(jí)別捕獲錯(cuò)誤。例如,下面的代碼顯示一個(gè)生成對(duì)象名解析錯(cuò)誤的存儲(chǔ)過(guò)程。包含 TRY...CATCH 構(gòu)造的批處理在比存儲(chǔ)過(guò)程更高的級(jí)別執(zhí)行,并捕獲在更低級(jí)別發(fā)生的錯(cuò)誤。
USE AdventureWorks2008R2; GO-- Verify that the stored procedure does not already exist. IF OBJECT_ID ('usp_MyError', 'P') IS NOT NULLDROP PROCEDURE usp_MyError; GOCREATE PROCEDURE usp_MyError AS-- This SELECT statement will generate-- an object name resolution error.SELECT * FROM NonExistentTable; GOBEGIN TRY-- Run the stored procedure.EXECUTE usp_MyError; END TRY BEGIN CATCHSELECT ERROR_NUMBER() AS ErrorNumber,ERROR_MESSAGE() AS ErrorMessage; END CATCH; GO下面是結(jié)果集:
ErrorNumber ErrorMessage ----------- --------------------------------------- 208 Invalid object name 'NonExistentTable'.有關(guān)詳細(xì)信息,請(qǐng)參閱 延遲名稱解析和編譯 以及 執(zhí)行計(jì)劃的緩存和重新使用 中的“重新編譯執(zhí)行計(jì)劃”一節(jié)。
不可提交的事務(wù)
在 TRY...CATCH 構(gòu)造中,事務(wù)可以進(jìn)入一種狀態(tài):事務(wù)保持打開但無(wú)法提交。事務(wù)無(wú)法執(zhí)行寫事務(wù)日志的任何操作,例如修改數(shù)據(jù)或嘗試回滾到保存點(diǎn)。但是,在此狀態(tài)下,事務(wù)獲 取的鎖將被維護(hù),并且連接也保持打開。發(fā)出 ROLLBACK 語(yǔ)句之前,或批處理結(jié)束并且數(shù)據(jù)庫(kù)引擎自動(dòng)回滾事務(wù)之前,不會(huì)逆轉(zhuǎn)事務(wù)效果。如果事務(wù)進(jìn)入不可提交狀態(tài)時(shí)未發(fā)送錯(cuò)誤信息,則當(dāng)批處理完成時(shí),將向客戶端應(yīng) 用程序發(fā)送錯(cuò)誤消息,該消息指示檢測(cè)到或回滾了一個(gè)不可提交的事務(wù)。
發(fā)生錯(cuò)誤時(shí),事務(wù)在 TRY 塊內(nèi)進(jìn)入無(wú)法提交狀態(tài),否則此錯(cuò)誤將終止該事務(wù)。例如,數(shù)據(jù)定義語(yǔ)言 (DDL) 語(yǔ)句(如 CREATE TABLE)中的大多數(shù)錯(cuò)誤或 SET XACT_ABORT 設(shè)置為 ON 時(shí)出現(xiàn)的大多數(shù)錯(cuò)誤都在 TRY 塊外終止事務(wù),而在 TRY 塊內(nèi)使事務(wù)無(wú)法提交。
CATCH 塊中的代碼可以通過(guò)使用 XACT_STATE 函數(shù)來(lái)測(cè)試事務(wù)的狀態(tài)。如果會(huì)話中包含無(wú)法提交的事務(wù),XACT_STATE 將返回 -1。如果 XACT_STATE 返回 -1,則 CATCH 塊將不能執(zhí)行寫日志的任何操作。下面的代碼示例生成 DDL 語(yǔ)句錯(cuò)誤,并使用 XACT_STATE 測(cè)試事務(wù)的狀態(tài),以便執(zhí)行最合適的操作。
USE AdventureWorks2008R2; GO-- Verify that the table does not exist. IF OBJECT_ID (N'my_books', N'U') IS NOT NULLDROP TABLE my_books; GO-- Create table my_books. CREATE TABLE my_books(Isbn int PRIMARY KEY,Title NVARCHAR(100)); GOBEGIN TRYBEGIN TRANSACTION;-- This statement will generate an error because the -- column author does not exist in the table.ALTER TABLE my_booksDROP COLUMN author;-- If the DDL statement succeeds, commit the transaction.COMMIT TRANSACTION; END TRY BEGIN CATCHSELECTERROR_NUMBER() as ErrorNumber,ERROR_MESSAGE() as ErrorMessage;-- Test XACT_STATE for 1 or -1.-- XACT_STATE = 0 means there is no transaction and-- a commit or rollback operation would generate an error.-- Test whether the transaction is uncommittable.IF (XACT_STATE()) = -1BEGINPRINTN'The transaction is in an uncommittable state. ' +'Rolling back transaction.'ROLLBACK TRANSACTION;END;-- Test whether the transaction is active and valid.IF (XACT_STATE()) = 1BEGINPRINTN'The transaction is committable. ' +'Committing transaction.'COMMIT TRANSACTION; END; END CATCH; GO處理死鎖
TRY...CATCH 可用于處理死鎖。CATCH 塊可以捕獲 1205 死鎖犧牲品錯(cuò)誤,并且事務(wù)可以回滾,直至線程解鎖。有關(guān)死鎖的詳細(xì)信息,請(qǐng)參閱 死鎖 。
下面的示例顯示如何使用 TRY...CATCH 處理死鎖。第一部分創(chuàng)建用于說(shuō)明死鎖狀態(tài)的表和用于打印錯(cuò)誤信息的存儲(chǔ)過(guò)程。
USE AdventureWorks2008R2; GO-- Verify that the table does not exist. IF OBJECT_ID (N'my_sales',N'U') IS NOT NULLDROP TABLE my_sales; GO-- Create and populate the table for deadlock simulation. CREATE TABLE my_sales (Itemid INT PRIMARY KEY,Sales INT not null); GOINSERT my_sales (itemid, sales) VALUES (1, 1); INSERT my_sales (itemid, sales) VALUES (2, 1); GO-- Verify that the stored procedure for error printing -- does not exist. IF OBJECT_ID (N'usp_MyErrorLog',N'P') IS NOT NULLDROP PROCEDURE usp_MyErrorLog; GO-- Create a stored procedure for printing error information. CREATE PROCEDURE usp_MyErrorLog ASPRINT 'Error ' + CONVERT(VARCHAR(50), ERROR_NUMBER()) +', Severity ' + CONVERT(VARCHAR(5), ERROR_SEVERITY()) +', State ' + CONVERT(VARCHAR(5), ERROR_STATE()) + ', Line ' + CONVERT(VARCHAR(5), ERROR_LINE());PRINT ERROR_MESSAGE(); GO下面的會(huì)話 1 和會(huì)話 2 的代碼腳本在兩個(gè)單獨(dú)的 SQL Server Management Studio 連接下同時(shí)運(yùn)行。兩個(gè)會(huì)話都嘗試更新表中的相同行。在第一次嘗試過(guò)程中,其中一個(gè)會(huì)話將成功完成更新操作,而另一個(gè)會(huì)話將被選擇為死鎖犧牲品。死鎖犧牲品 錯(cuò)誤將使執(zhí)行跳至 CATCH 塊,事務(wù)將進(jìn)入無(wú)法提交狀態(tài)。在 CATCH 塊中,死鎖犧牲品會(huì)回滾事務(wù)并重試更新此表,直到更新成功或達(dá)到了重試限制(以先發(fā)生者為準(zhǔn))。
| USE AdventureWorks2008R2; GO-- Declare and set variable -- to track number of retries -- to try before exiting. DECLARE @retry INT; SET @retry = 5;-- Keep trying to update -- table if this task is -- selected as the deadlock -- victim. WHILE (@retry > 0) BEGINBEGIN TRYBEGIN TRANSACTION;UPDATE my_salesSET sales = sales + 1WHERE itemid = 1;WAITFOR DELAY '00:00:13';UPDATE my_salesSET sales = sales + 1WHERE itemid = 2;SET @retry = 0;COMMIT TRANSACTION;END TRYBEGIN CATCH -- Check error number.-- If deadlock victim error,-- then reduce retry count-- for next update retry. -- If some other error-- occurred, then exit-- retry WHILE loop.IF (ERROR_NUMBER() = 1205)SET @retry = @retry - 1;ELSESET @retry = -1;-- Print error information.EXECUTE usp_MyErrorLog;IF XACT_STATE() <> 0ROLLBACK TRANSACTION;END CATCH; END; -- End WHILE loop. GO | USE AdventureWorks2008R2; GO-- Declare and set variable -- to track number of retries -- to try before exiting. DECLARE @retry INT; SET @retry = 5;--Keep trying to update -- table if this task is -- selected as the deadlock -- victim. WHILE (@retry > 0) BEGINBEGIN TRYBEGIN TRANSACTION;UPDATE my_salesSET sales = sales + 1WHERE itemid = 2;WAITFOR DELAY '00:00:07';UPDATE my_salesSET sales = sales + 1WHERE itemid = 1;SET @retry = 0;COMMIT TRANSACTION;END TRYBEGIN CATCH -- Check error number.-- If deadlock victim error,-- then reduce retry count-- for next update retry. -- If some other error-- occurred, then exit-- retry WHILE loop.IF (ERROR_NUMBER() = 1205)SET @retry = @retry - 1;ELSESET @retry = -1;-- Print error information.EXECUTE usp_MyErrorLog;IF XACT_STATE() <> 0ROLLBACK TRANSACTION;END CATCH; END; -- End WHILE loop. GO |
使用 RAISERROR 的 TRY...CATCH
RAISERROR 可用在 TRY...CATCH 構(gòu)造的 TRY 或 CATCH 塊中影響錯(cuò)誤處理行為。
在 TRY 塊內(nèi)執(zhí)行的嚴(yán)重性為 11 至 19 的 RAISERROR 會(huì)使控制傳遞到關(guān)聯(lián)的 CATCH 塊。在 CATCH 塊內(nèi)執(zhí)行的嚴(yán)重性為 11 至 19 的 RAISERROR 將錯(cuò)誤返回到調(diào)用應(yīng)用程序或批處理。這樣,RAISERROR 可用于返回有關(guān)導(dǎo)致 CATCH 塊執(zhí)行的錯(cuò)誤的調(diào)用方信息。TRY...CATCH 錯(cuò)誤函數(shù)提供的錯(cuò)誤信息(包括原始錯(cuò)誤號(hào))可在 RAISERROR 消息中捕獲;但是,RAISERROR 的錯(cuò)誤號(hào)必須 >= 50000。
嚴(yán)重性為 10 或更低的 RAISERROR 在不調(diào)用 CATCH 塊的情況下將信息性消息返回到調(diào)用批處理或應(yīng)用程序。
嚴(yán)重性為 20 或更高的 RAISERROR 在不調(diào)用 CATCH 塊的情況下關(guān)閉數(shù)據(jù)庫(kù)連接。
下面的代碼示例顯示如何在 CATCH 塊內(nèi)使用 RAISERROR 將原始錯(cuò)誤信息返回到調(diào)用應(yīng)用程序或批處理。存儲(chǔ)過(guò)程 usp_GenerateError 在 TRY 塊內(nèi)執(zhí)行 DELETE 語(yǔ)句,該語(yǔ)句生成違反約束錯(cuò)誤。此錯(cuò)誤使執(zhí)行傳遞到 usp_GenerateError 內(nèi)關(guān)聯(lián)的 CATCH 塊,存儲(chǔ)過(guò)程 usp_RethrowError 在此塊內(nèi)執(zhí)行以使用 RAISERROR 生成違反約束錯(cuò)誤。RAISERROR 生成的此錯(cuò)誤將返回到調(diào)用批處理(usp_GenerateError 在其中執(zhí)行)并使執(zhí)行傳遞到調(diào)用批處理中關(guān)聯(lián)的 CATCH 塊。
| RAISERROR 僅能生成狀態(tài)從 1 到 127 的錯(cuò)誤。由于數(shù)據(jù)庫(kù)引擎可能引發(fā)狀態(tài)為 0 的錯(cuò)誤,因此,建議您先檢查由 ERROR_STATE 返回的錯(cuò)誤狀態(tài),然后將它作為一個(gè)值傳遞給狀態(tài)參數(shù) RAISERROR。 |
更改執(zhí)行流
轉(zhuǎn)載于:https://www.cnblogs.com/sxmny/archive/2012/11/01/2749286.html
總結(jié)
- 上一篇: 埋线多少钱一次啊?
- 下一篇: PL/SQL详细介绍