|
本文就如何組織 SQL 存儲過程中的邏輯以增強(qiáng)性能提供了一些具體建議。 有關(guān)如何調(diào)優(yōu)數(shù)據(jù)庫系統(tǒng)和應(yīng)用程序的可用建議來源有很多。諸如 OLTP 應(yīng)用程序的 DB2 調(diào)優(yōu)技巧(以前在 IBM? DB2? 開發(fā)者園地上發(fā)表)之類的文章通過使用事務(wù)和數(shù)據(jù)并行性以及分析查詢方案,給出了從表空間和索引設(shè)計到緩沖池的內(nèi)存分配等方面的建議。這些方面的內(nèi)容是性能調(diào)優(yōu)的基礎(chǔ)知識。 但是,有關(guān)如何組織存儲過程自身中的邏輯并著眼于其性能的專門建議卻并不多見。本文就提供了這樣一種建議。盡管本文著重于介紹 SQL 過程,但是這里所提供的大多數(shù)信息同樣適用于用其它語言編寫的在應(yīng)用程序中或存儲過程中嵌入的 SQL 邏輯。
在 深入研究詳細(xì)問題之前,讓我們先回顧 DB2 中有關(guān)過程化 SQL 的一些基本術(shù)語和概念。過程化 SQL 構(gòu)造(例如標(biāo)量變量、IF 語句和 WHILE 循環(huán))是在 DB2 Universal Database? (UDB) V7 發(fā)行版中引入 DB2 的。以前的 DB2 發(fā)行版支持 C 和 Java? 作為存儲過程的語言。V7 引入了 SQL 存儲過程,以及其它許多可以促進(jìn) OLTP 應(yīng)用程序開發(fā)的特性(例如臨時表、應(yīng)用程序保存點和標(biāo)識列)。 當(dāng)創(chuàng)建 SQL 過程時,DB2 將過程主體中的 SQL 查詢與過程邏輯區(qū)分開來。為了使性能最優(yōu),SQL 查詢被靜態(tài)地編譯成包中的節(jié)。(對于靜態(tài)編譯的查詢而言,節(jié)主要是由 DB2 優(yōu)化器為該查詢選擇的存取方案構(gòu)成的。包是節(jié)的集合。有關(guān)包和節(jié)的更多信息,請參閱 DB2 SQL 參考大全,第 1 卷。)另一方面,過程邏輯被編譯成 DLL(動態(tài)鏈接庫)。 在 過程的執(zhí)行期間,每當(dāng)控制從過程邏輯流向 SQL 語句時,在 DLL 和 DB2 引擎之間就存在“上下文切換”。(在 DB2 V8 中,SQL 過程是在“不受保護(hù)的方式”下運(yùn)行的,即與 DB2 引擎在相同的尋址空間中。因此我們這里談及的上下文切換并不是操作系統(tǒng)級別上的完全的上下文切換,而是指 DB2 中層的更換。)減少頻繁調(diào)用的過程(例如 OLTP 應(yīng)用程序中的過程)或者處理大量行的過程(例如執(zhí)行數(shù)據(jù)清理的過程)中的上下文切換次數(shù),對它們的性能有顯著的影響。本文中的幾個技巧恰好旨在減少這些上 下文切換。 剛開始的時候(DB2 通用數(shù)據(jù)庫 V7 GA),只允許在 SQL 過程中使用 SQL 過程語言(通常稱為 SQL PL)。后來(在 DB2 UDB V7.2 中),在 SQL 函數(shù)和觸發(fā)器主體中開始支持該語言的子集。SQL PL 的這個子集即所謂的 內(nèi)聯(lián)(inline)SQL PL?!皟?nèi) 聯(lián)”一詞突出顯示了它與完整語言的重要區(qū)別。SQL PL 過程是通過將其單獨的 SQL 查詢靜態(tài)地編譯成包中的節(jié)實現(xiàn)的,而內(nèi)聯(lián) SQL PL 函數(shù)就象其名稱所展示的,是通過將函數(shù)主體內(nèi)聯(lián)到使用它的查詢中實現(xiàn)的。稍后我們將再看一下內(nèi)聯(lián) SQL PL 及其用法的一些示例。 現(xiàn)在,讓我們研究在使用 SQL 過程語言時可用來提高性能的一些具體工作。
讓我們從一個簡單的編碼技巧開始。如下所示的單個 INSERT 行序列:
可以改寫成:
執(zhí)行這個多行 INSERT 語句所需時間大約是執(zhí)行原來三條語句的三分之一。孤立地看,這一改進(jìn)看起來似乎是微乎其微的,但是,如果這一代碼段是重復(fù)執(zhí)行的(例如該代碼段位于循環(huán)體或觸發(fā)器體中),那么改進(jìn)是非常顯著的。 類似地,如下所示的 SET 語句序列:
可以寫成一條 VALUES 語句:
如果任何兩條語句之間都沒有相關(guān)性,那么這一轉(zhuǎn)換保留了原始序列的語義。為了說明這一點,請考慮:
將上面兩條語句轉(zhuǎn)換成:
不會保留原始的語義,因為是以“并行”方式對 INTO 關(guān)鍵字之前的表達(dá)式進(jìn)行求值的。這意味著賦給 B 的值并不以賦給 A 的值為基礎(chǔ),這是原始語句預(yù)期的語義。
跟 其它編程語言一樣,SQL 語言提供了兩類條件構(gòu)造:過程型(IF 和 CASE 語句)和函數(shù)型(CASE 表達(dá)式)。在大多數(shù)環(huán)境中,可使用任何一種構(gòu)造來表達(dá)計算,到底使用哪一種只是喜好問題。但是,使用 CASE 表達(dá)式編寫的邏輯不但比使用 CASE 或 IF 語句編寫的邏輯更緊湊,而且更有效。 請考慮下面的 SQL PL 代碼片段:
IF 子句中的條件僅用于決定將什么值插入 tab_comp.Val 列中。為了避免過程層和數(shù)據(jù)流層之間的上下文切換,可利用 CASE 表達(dá)式將相同的邏輯表示成一個 INSERT 語句:
值得注意的是,CASE 表達(dá)式可在任何希望有標(biāo)量值的上下文中使用。特別地,可在賦值符號的右邊使用它們。例如:
可以改寫成:
實際上,這個特殊的示例有一個更好的解決方案:
諸 如循環(huán)、賦值和游標(biāo)之類的過程化構(gòu)造允許我們表達(dá)那些只使用 SQL DML 語句是不可能表達(dá)的計算。但是,當(dāng)我們擁有一些可以隨意使用的過程語句時,即使我們手頭的計算實際上僅使用 SQL DML 語句就可表達(dá),但轉(zhuǎn)換成過程語句還是有風(fēng)險的。正如我們以前提到的,過程計算的性能與使用 DML 語句表達(dá)的同一個計算的性能相比會慢幾個數(shù)量級。請考慮下面的代碼片段:
首先,通過應(yīng)用上一節(jié)討論的轉(zhuǎn)換可以改進(jìn)循環(huán)體:
但是通過進(jìn)一步觀察,我們發(fā)現(xiàn)整個代碼塊可以寫成一個帶有 SELECT 子句的 INSERT 語句:
在原始的表述中,SELECT 語句中每行的過程層和數(shù)據(jù)流層之間都有一個上下文切換。在最后一個表述中,根本沒有上下文切換,并且優(yōu)化器有機(jī)會對整個計算進(jìn)行全局優(yōu)化。另一方面,如果 每個 INSERT 語句針對的都是不同的表,那么這種引人注目的簡化是不可能的,如下所示。
但是,這里也可以利用 SQL 的一次處理一個集合(set-at-a-time)特性:
在研究改進(jìn)現(xiàn)有過程邏輯的性能時,為消除游標(biāo)循環(huán)而花費的任何時間都可能是值得的。
如果存儲過程中的邏輯確實需要游標(biāo),那么要使性能最優(yōu),請牢記下面這些內(nèi)容。 首 先,請確保不使用高于您所需的隔離級別。隔離級別決定了 DB2 對過程讀取或更新的行應(yīng)用的鎖定的數(shù)量。隔離級別越高,DB2 將執(zhí)行的鎖定越多,因此為同一資源而競爭的應(yīng)用程序之間的并發(fā)就越少。例如,使用可重復(fù)讀(Repeatable Read,RR)隔離級別的過程將形成對其讀取的任何行的共享鎖,而使用游標(biāo)穩(wěn)定性(Cursor Stability,CS)的過程只會鎖定任何可更新游標(biāo)的當(dāng)前行??梢允褂?DB2_SQLROUTINE_PREPOPTS 注冊表變量來指定 SQL 過程的隔離級別。例如,要將 SQL 過程的隔離級別設(shè)置為未提交的讀(Uncommitted Read)(最低的級別,用于訪問只讀數(shù)據(jù)的過程),請使用下面這條命令:
注:要使該設(shè)置生效,必須重新啟動 db2 實例。 DB2 中缺省的隔離級別是游標(biāo)穩(wěn)定性。但是,當(dāng)然了,為了保持應(yīng)用程序的正確性,有時需要使用可重復(fù)讀。還需記住一件重要的事情,一旦創(chuàng)建了需要可重復(fù)讀的過程,必須將 DB2_SQLROUTINE_PREPOPTS 重新設(shè)置回較低的隔離級別。 有關(guān)隔離級別還值得一提的是,DB2 允許我們在單獨的查詢中覆蓋缺省的隔離級別,如下所示:
上面的查詢將以隔離級別 UR 進(jìn)行執(zhí)行,而不管 DB2_SQLROUTINE_PREPOPTS 中指定的隔離級別。 在嘗試改進(jìn)游標(biāo)性能時需要牢記的一個相關(guān)問題是游標(biāo)的可更新能力。如果游標(biāo)涉及的行是可以使用 INSERT 或 DELETE 語句中的 WHERE CURRENT OF 子句進(jìn)行更新或刪除,那么它就是 可刪除的。當(dāng)游標(biāo)可刪除時,DB2 必須獲取行上的 互斥鎖(與 共享鎖相對),并且不能執(zhí)行行分塊。行上的互斥鎖甚至可以防止其它應(yīng)用程序讀取該行(在互斥鎖被釋放之前,這些應(yīng)用程序必須等待,除非它們的隔離級別是 UR),而行分塊通過在一個操作中檢索行塊,從而減少了用于游標(biāo)的數(shù)據(jù)庫管理器開銷。 只 有不可刪除的游標(biāo)才可以進(jìn)行行分塊。這就是為什么讓 DB2 了解將如何使用游標(biāo)是很重要的原因。通過在 SELECT 語句中指定 FOR READ ONLY 子句,可以將游標(biāo)顯式地聲明為不可刪除,或者通過在 SELECT 語句中使用 FOR UPDATE 子句將其聲明為可刪除。根據(jù)該信息(并且還根據(jù)下面描述的 BLOCKING 選項),DB2 將確定是否將行分塊用于給定的游標(biāo)。 缺 省情況下,對于那些使用 FOR READ ONLY 子句定義的游標(biāo),DB2 將始終使用行分塊,除非指定了 BLOCKING NO 綁定選項。另一方面,如果使用了 BLOCKING ALL 綁定選項,那么對于含混游標(biāo)(既不是定義成 FOR READ ONLY 也不是定義成 FOR UPDATE 的游標(biāo)),DB2 將使用行分塊。 簡而言之:如果可能,則在游標(biāo)定義中使用 FOR READ ONLY 子句;如果您的過程包含含混游標(biāo),那么請使用 BLOCKING ALL 綁定選項。要設(shè)置 BLOCKING 綁定選項的值,我們還可以使用 DB2_SQLROUTINE_PREPOPTS 注冊表變量。例如,要將 SQL 過程的隔離級別設(shè)置為未提交的讀,并將行分塊設(shè)置為 BLOCKING ALL,請使用下面這條命令:
對于返回大型結(jié)果集的過程而言,分塊特別重要。 通過使用 DB2_SQLROUTINE_PREPOPTS 注冊表,還可以為存儲過程指定其它綁定選項。請參閱 Application Development Guide: Building and Running Applications中有關(guān)“Setting Up the SQL Procedures Environment”方面的內(nèi)容以獲得更多信息。此外,有關(guān)隔離級別、鎖定和分塊的完整說明已超出了本文范圍。請參閱 DB2 Administration Guide: Performance和 SQL Reference中有關(guān) DECLARE CURSOR 的條目,以獲取完整的說明。
正如我們在簡介中提及的,SQL 過程和 SQL 函數(shù)是使用不同技術(shù)實現(xiàn)的。SQL 過程中的查詢是單獨編譯的,每個查詢都成為包中的一個節(jié)。編譯是在過程創(chuàng)建時進(jìn)行的,直到重新創(chuàng)建過程或者直到重新綁定其相關(guān)的包時才重新編譯這些查詢。 另一方面,SQL 函數(shù)中的查詢是一起編譯的,就好象函數(shù)體是一個查詢一樣。每當(dāng)編譯一條使用 SQL 函數(shù)的語句時,也會對 SQL 函數(shù)進(jìn)行編譯。 與 SQL 過程中所發(fā)生的情況不同,SQL 函數(shù)中的過程語句與數(shù)據(jù)流語句是在同一個層中執(zhí)行的。因此,每當(dāng)控制從過程語句流向數(shù)據(jù)流語句或相反時,并不發(fā)生上下文切換。 因 為存在這些區(qū)別,所以當(dāng)給定的過程代碼段作為函數(shù)實現(xiàn)時的執(zhí)行速度通常比作為過程實現(xiàn)時要快。但是,當(dāng)然了,有一個小問題。函數(shù)只能包含那些不會改變數(shù)據(jù) 庫狀態(tài)的語句(例如 INSERT、UPDATE 或 DELETE 語句是不允許的)。并且只允許完整 SQL PL 語言的子集出現(xiàn)在 SQL 函數(shù)中(不能是 CALL 語句、游標(biāo)和條件處理)。 盡管有這些限制,但大多數(shù) SQL 過程都可以在無副作用的情況下轉(zhuǎn)換成 SQL 函數(shù)。例如,下面的過程:
等同于下面的函數(shù):
請注意,盡管使用了 CALL 語句來調(diào)用過程,但還需要使用 VALUES 語句從命令行調(diào)用函數(shù):
另一方面,與過程不同的是,您可以在允許表達(dá)式的任何上下文中調(diào)用函數(shù):
因此,正如本節(jié)標(biāo)題所展示的,當(dāng)您只是從數(shù)據(jù)庫抽取數(shù)據(jù)而不執(zhí)行任何更改時,請考慮使用 SQL 函數(shù)而不是使用 SQL 過程。
在 V7 中,DB2 引入了臨時表。對臨時表的操作通常比對常規(guī)表的操作快。讓我們看一些原因:
在對 SQL 過程中的臨時表進(jìn)行任何應(yīng)用之前,表定義在編譯環(huán)境中必須是可用的。例如,在下面的 CLP 腳本(該腳本使用“%”作為語句的終結(jié)符)中,表定義的唯一目的就是能夠創(chuàng)建 SQL 過程:
在執(zhí)行了 CONNECT RESET 命令后,臨時表將不復(fù)存在。在運(yùn)行時,應(yīng)用程序必須確保在執(zhí)行使用臨時表的首個查詢之前該表是存在的。最后的這個觀察引出了一個我們從未提及的要點:引用 臨時表的任何查詢都將被動態(tài)地編譯,即使該查詢被寫成靜態(tài)的 SQL。跟其它任何動態(tài)查詢一樣,在編譯該查詢之后,它將以已編譯的形式保留在包高速緩存中。在下一次執(zhí)行相同的查詢時,僅當(dāng)無法在高速緩存發(fā)現(xiàn)它時, DB2 才重新編譯它。 如果您打算創(chuàng)建相對較大的臨時表,并對這些表運(yùn)行幾個查詢,請考慮定義索引并對它們運(yùn)行 runstats(顯然后者是填充了表后進(jìn)行的)。 下一節(jié)將介紹更多這方面的內(nèi)容。 有關(guān)在 SQL 過程中使用臨時表的最后一個說明是:如果需要根據(jù)在同一個過程中創(chuàng)建的臨時表返回結(jié)果集,那么必須在嵌套的復(fù)合語句中定義結(jié)果集,如下面的示例所示:
必須在嵌套的復(fù)合語句中定義結(jié)果集的理由是, DECLARE GLOBAL TEMPORARY TABLE 是一個可執(zhí)行語句,而可執(zhí)行語句只能在聲明語句(例如 DECLARE CURSOR)之后編寫。如果我們在游標(biāo)定義之后在外部作用域中聲明表,那么當(dāng)編譯 DECLARE CURSOR 語句時,該表在編譯環(huán)境中將不可用,因此編譯會失敗。
當(dāng) 創(chuàng)建了一個過程時,其單獨的 SQL 查詢被編譯成包中的節(jié)。其中,DB2 優(yōu)化器根據(jù)表的統(tǒng)計信息(例如,表大小或某列中數(shù)據(jù)值出現(xiàn)的相對頻率)以及編譯查詢時可用的索引來選擇查詢的執(zhí)行方案。當(dāng)表經(jīng)過了重大更改時,讓 DB2 再次收集有關(guān)這些表的統(tǒng)計信息可能是個好主意。當(dāng)更新了統(tǒng)計信息時,或者當(dāng)創(chuàng)建了新的索引時,重新綁定那些與使用表的 SQL 過程相關(guān)聯(lián)的包,以使 DB2 創(chuàng)建使用最新統(tǒng)計信息和索引的方案,這可能也是一個好主意。 可以使用 RUNSTATS 命令更新表的統(tǒng)計信息。要重新綁定與 SQL 過程關(guān)聯(lián)的包,可以使用 REBIND_ROUTINE_PACKAGE 內(nèi)置過程(在 DB2 V8 中可用)。例如,可以使用下面這條命令來重新綁定過程 MYSCHEMA.MYPROC 的包:
其中 ‘P‘ 表明該包對應(yīng)于一個過程,而 ‘ANY‘ 表明 SQL 路徑中的任何函數(shù)和類型都被當(dāng)作函數(shù)和類型解析。(請參閱 REBIND 命令的 Command Reference 條目,以獲取更多詳細(xì)信息。)
在本文中,我提供了一類可能有助于改進(jìn) SQL 過程的性能的提示和技巧(請查閱 Yip等 編寫的書籍,以獲取 SQL PL 的良好簡介)。作為一般規(guī)則,首先考慮系統(tǒng)性能(硬件和 OS)和數(shù)據(jù)庫管理器(緩沖池、容器和表空間等等)這些基本的問題。DB2 配置顧問程序?qū)τ诤笳叻浅S袔椭?。然后請確保應(yīng)用程序中一些關(guān)鍵查詢的方案是合適的。最后,利用本文提供的建議來研究改進(jìn)您的存儲過程和應(yīng)用程序。祝您在 調(diào)優(yōu)的工作中好運(yùn)!
感謝 Lee Johnson、Paul Yip、Drew Bradstock 和 Clara Liu 對本文草稿提供的寶貴意見。
|