INSERT 處理過程概述
首先讓我們快速地看看插入一行時的處理步驟。這些步驟中的每一步都有優化的潛力,對此我們在後面會一一討論。
在客戶機准備 語句。對於動態 SQL,在語句執行前就要做這一步,此處的性能是很重要的;對於靜態 SQL,這一步的性能實際上關系不大,因為語句的准備是事先完成的。 在客戶機,將要插入的行的各個 列值組裝起來,發送到 DB2 服務器。 DB2 服務器確定將這一行插入到哪一頁中。DB2 在 用於該頁的緩沖池中預留一個位置。如果 DB2 選定的是一個已有的頁,那麼就需要讀磁盤;如果使用一個新頁,則要在表空間(如果是SMS,也就是系統管理存儲的表空間)中為該頁物理地分配空間。插入了新行的每一頁最後都要從緩沖池寫入到磁盤。在目標頁中對該行進行格式化,並獲得該行上的一個X(exclusive,獨占的) 行鎖。將反映該 insert 的一條記錄寫入到日志緩沖區中。
最後提交包含該 insert 的事務,如果這時日志緩沖區中的記錄還沒有被寫入日志文件的話,則將這些記錄寫到日志文件中。此外,還可能發生很多類型的附加處理,這取決於數據庫配置,例如,索引或觸發器的存在。這種額外的處理對於性能來說也是意義重大的,我們在後面會討論到。
INSERT 的替代方案
在詳細討論 insert 的優化之前,讓我們先考慮一下 insert 的兩種替代方案:load 和 import。import 實用程序實際上是 SQL INSERT 的一個前端,但它的某些功能對於您來說也是有用的。load 也有一些有用的額外功能,但是我們使用 load 而不使用 insert 的主要原因是可以提高性能。
load 直接格式化數據頁,而避免了由於插入導致的對每一行進行處理的大部分開銷(例如,日志記錄在這裡實際上是消除了)。而且,load 可以更好地利用多處理器機器上的並行性。在 V8 load 中有兩個新功能,它們對於 load 成為 insert 的替代方案有著特別的功效,這兩個功能是:從游標裝載和從調用層接口(CLI)應用程序裝載。
從游標裝載
這種方法可用於應用程序的程序代碼(通過 db2Load API),或用於 DB2 腳本。下面是後一種情況的一個例子:
declare staffcursor cursor forselect * from staff;
load from staffcursor of cursor insert into myschema.new_staff;
這兩行可以用下面一行替代:
insert into myschema.new_staff select * from staff
同等效的 INSERT ... SELECT 語句相比,從游標裝載幾乎可以提高 20% 的性能。
從 CLI 裝載
這種方法顯然只限於調用層接口(CLI)應用程序,但是它非常快。這種技巧非常類似於數組插入,DB2 附帶了這樣的示例,使用 load 時的速度是使用經過完全優化的數組插入時的兩倍,幾乎要比未經優化的數組插入快 10 倍。
所有INSERT可以改進的地方
讓我們看看插入處理的一些必要步驟,以及我們可以用來優化這些步驟的技巧。
1. 語句准備
作為一條 SQL 語句,INSERT 語句在執行之前必須由 DB2 進行編譯。這一步驟可以自動發生(例如在 CLP 中,或者在一次 CLI SQLExecDirect 調用中),也可以顯式地進行(例如,通過一條 SQL Prepare、CLI SQLPrepare 或 JDBC prepareStatement 語句)。該編譯過程牽涉到授權檢查、優化,以及將語句轉化為可執行格式時所需的其他一些活動。在編譯語句時,語句的訪問計劃被存儲在包緩存中。
如果重復地執行相同的 INSERT 語句,則該語句的訪問計劃(通常)會進入到包緩存中,這樣就免除了編譯的開銷。然而,如果 insert 語句對於每一行有不同的值,那麼每一條語句都將被看成是惟一的,必須單獨地進行編譯。因此,將像下面這樣的重復語句:
insert into mytable values (1, 'abc')
insert into mytable values (2, 'def')
等等,換成帶有參數標記的語句,一次准備,重復執行,這樣做是十分可取的:
insert into mytable values (?, ?)
使用參數標記可以讓一系列的 insert 的運行速度提高數倍。(在靜態 SQL 程序中使用主機變量也可以獲得類似的好處。)
2. 發送列值到服務器
可以歸為這一類的優化技巧有好幾種。最重要的一種技巧是在每條 insert 語句中包括多行,這樣就可以避免對於每一行都進行客戶機-服務器通信,同時也減少了 DB2 開銷。可用於多行插入的技巧有:
在 VALUES 子句中包含多行的內容。例如,下面的語句將插入三行:INSERT INTO mytable VALUES (1, 'abc'), (2, 'def'), (3, 'ghi') 在 CLI 中使用數組插入(array insert)。這需要准備一條帶參數標記的 INSERT 語句,定義一個用於存儲要插入的值的數組,將該數組綁定到參數標記,以及對於每個數組中的一組內容執行一次 insert。而且,示例程序 sqllib/samples/cli/tbload.c 提供了數組插入的基本框架(但是執行的是 CLI LOAD)。從不使用數組改為使用包含 100 行的數組,可以將時間縮短大約 2.5 倍。所以應該盡可能地使用包含至少 100 行的數組。
在 JDBC 中使用批處理操作。這跟 CLI 中的數組插入一樣,基於相同的概念,但是實現細節有所不同。當通過 prepareStatement 方法准備了 insert 語句之後,剩下的步驟是針對每一列調用適當的 setXXXX 方法(例如,setString 或 setInt),然後是 addBatch。對於要插入的每一行,都要重復這些步驟,然後調用 executeBatch 來執行插入。要查看這方面的例子,請參閱“參考資料”一節中的 JDBC Tutorial。
使用 load 將數據快速地裝入到一個 staging 表中,然後使用 INSERT ... SELECT 填充主表。(通過這種方法節省下來的代價源於 load 的速度非常快,再加上 INSERT ... SELECT 是在 DB2 內(在服務器上)傳輸數據的,從而消除了通信上的代價。一般情況下我們不會使用這種方法,除非在 INSERT ... SELECT 中還要另外做 load 無法完成的處理。
如果不可能在一條 insert 語句中傳遞多行,那麼最好是將多條 insert 語句組成一組,將它們一起從客戶機傳遞到服務器。(不過,這意味著每條 insert 都包含不同的值,都需要准備,因而其性能實際上要比使用參數標記情況下的性能更差一些。)將多條語句組合成一條語句可以通過 Compound SQL 來實現。在 SQL 中,復合語句是通過 BEGIN ATOMIC 或 BEGIN COMPOUND 語句創建的。在 CLI 中,復合語句可以通過 SQLExecDirect 和 SQLExecute 調用來建立。對於 DB2 V8 FixPak 4,另一種生成復合語句的方法是在(對一條預處理語句)發出多個 SQLExecute 調用之前設置語句屬性 SQL_ATTR_CHAINING_BEGIN,並在調用之後設置語句屬性 SQL_ATTR_CHAINING_END。
下面是關於該話題的其他一些建議:
如果可能的話,讓客戶機與要存取的數據庫使用相同的代碼頁,以避免在服務器上的轉換代價。數據庫的代碼頁可以通過運行“get db cfg for ”來確定。
在某些情況下,CLI 會自動執行數據類型轉換,但是這樣同時也會帶來看不見的(小小的)性能損耗。因此,盡量使插入值直接處於與相應列對應的格式。
將應用程序中與插入相關的設置開銷最小化。例如,當在 CLI 中使用數組插入時,對於整個一組插入,應該盡量保證對於每一列只執行一次 SQLBindParameter,而不是對每一組數組內容都執行一次。對於個體來說,這些調用的代價並不高,但是這些代價是累積的。
3. 找到存儲行的地方
DB2 使用三種算法中的一種來確定將行插入到哪裡。(如果使用了多維群集(Multi-dimensional Clustering,MDC),則另當別論,我們在這裡不予討論。)
缺省模式是,DB2 搜索散布在表的各頁上的自由空間控制記錄(Free Space Control Records,FSCR),以找到有足夠自由空間存放新行的頁。顯然,如果每頁上的自由空間都比較少的話,就要浪費很多的搜索時間。為了應付這一點, DB2 提供了 DB2MAXFSCRSEARCH 注冊表變量,以便允許將搜索范圍限制為少於缺省的 5 頁。
當表是通過 ALTER TABLE 以 APPEND 模式放置時,就要使用第二種算法。這樣就完全避免了 FSCR 搜索,因為只需簡單地將行直接放到表的末尾。
當表有群集索引(clustering index)時,就要用到最後一種算法。在這種情況下,DB2 試圖將每一行插入到有相似鍵值的一頁中。如果那一頁沒有空間了,DB2 就會嘗試附近的頁,如果附近的頁也沒有空間,DB2 就進行 FSCR 搜索。
如果只考慮插入時間的優化,那麼使用 APPEND 模式對於批量插入是最快的一種方法,但是這種方法的效果遠不如我們這裡討論的很多其他方法那麼成效顯著。第二好的方法應該是采用缺省算法,但是,如果在最佳環境中,更改 DB2MAXFSCRSEARCH 的值影響很小,而在一個 I/O 約束較少的環境中,這種更改所造成的影響就比較可觀了。
如果有群集索引,則對 insert 的性能會有很大的負面影響,這一點也不驚奇,因為使用群集索引的目的就是通過在插入時做額外的工作來提高查詢(即 select)性能的。如果的確需要群集索引,那麼可以通過確保有足夠的自由空間來使其對插入的影響降至最小:使用 ALTER TABLE 增加 PCTFREE,然後使用 REORG 預留自由空間。不過,如果允許太多自由空間的存在,則可能導致查詢時需要讀取額外的頁,這反而大大違反了使用群集索引的本意。另一種選擇是,在批量插入之前先刪除群集索引,而後再重新創建群集索引,也許這是最優的方法(創建群集索引的開銷跟創建常規索引的開銷差不多,都不是很大,只是在插入時有額外的開銷)。
4. 緩沖池、I/O 和頁清除
每一條 insert 在執行時,都是先將新行存儲在一個頁中,並最終將那個頁寫到磁盤上。一旦像前面討論的那樣指定了頁,那麼在將行添加到該頁之前,該頁必須已經在緩沖池中。對於批量插入,大部分頁都是最新指派給表的,因此讓我們關注一下對新頁的處理。
如果表在系統管理存儲的(System Managed Storage,SMS)表空間中,當需要新頁時,缺省情況下是從文件系統中分別為每一頁分配空間。但是,如果對數據庫運行了 db2empfa 命令,那麼每個 SMS 表空間就會為新頁一次性分配一個區段。我們建議運行 db2empfa 命令,並使用 32 頁的區段。
對於數據庫管理的存儲(Database Managed Storage,DMS)表空間,空間是在創建表空間時就預先分配的,但是頁的區段則是在插入處理過程中指派給表的。與 SMS 相比,DMS 對空間的預分配可以提高大約 20% 的性能 -- 使用 DMS 時,更改區段大小並沒有明顯的效果。
如果表上有索引,則對於每個插入的行,都要添加一個條目到每條索引。這要求在緩沖池中存在適當的索引頁。晚些時候我們將討論索引的維護,但是現在只需記住,插入時對緩沖池和 I/O 的考慮也類似地適用於索引頁,對於數據頁也是一樣。
隨著插入的進行,越來越多的頁中將填入被插入的行,但是,DB2 不要求在 insert 或 Commit 後將任何新插入的或更新後的數據或索引寫入到磁盤。(這是由於 DB2 的 writeahead 日志記錄算法。但是有一個例外,這將在關於日志記錄的小節中論述到。)然而,這些頁需要在某一時刻寫到磁盤上,這個時刻可能會在數據庫關閉時才會輪到。
一般來說,對於批量插入,您會希望積極地進行 異步頁清除(asynchronous page cleaning),這樣在緩沖池中就總有可用於新頁的空余位置。頁清除率,或者說總缺頁率,可能導致計時上的很大不同,使得性能比較容易產生誤解。例如,如果使用 100,000 頁的緩沖池,並且不存在頁清除,則批量插入在結束前不會有任何新的或更改過的(“髒的”)頁寫到磁盤上,但是隨後的操作(例如選擇,甚至乎關閉數據庫)都將被大大推遲,因為這時有至多 100,000 個在插入時產生的髒頁要寫到磁盤上。另一方面,如果在同一情況下進行了積極的頁清除,則批量插入過程可能要花更長的時間,但是此後緩沖池中的髒頁要少一些,從而使得隨後的任務執行起來性能更佳。至於那些結果中到底哪個要更好些,我們並不是總能分得清,但是通常來說,將所有髒頁都存儲在緩沖池中是不可能的,所以為了取得最佳性能,采取有效的頁清除是有必要的。
為了盡可能好地進行頁清除:將 CHNGPGS_THRESH 數據庫配置參數的值從缺省的 60 減少到 5 這麼低。這個參數決定緩沖池中髒頁的阈值百分比,當髒頁達到這個百分比時,就會啟動頁清除。
嘗試啟用注冊表變量 DB2_USE_ALTERNATE_PAGE_CLEANING(在 DB2 V8 FixPak 4 中最新提供)。通過將這個變量設置成 ON,可以為頁清除提供一種比缺省方法(基於 CHNGPGS_THRESH 和 LSN 間隙觸發器)更積極的方法。我沒有評測過其效果。請參閱 FixPak 4 Release Notes 以了解這方面的信息。確保 NUM_IOCLEANERS 數據庫配置參數的值至少等於數據庫中物理存儲設備的數量。
至於 I/O 本身,當需要建立索引時,可以通過使用盡可能大的緩沖池來將 I/O 活動減至最少。如果不存在索引,則使用較大的緩沖池幫助不大,而只是推遲了 I/O。也就是說,它允許所有新頁暫時安放在緩沖池中,但是最終仍需要將這些頁寫到磁盤上。
當發生將頁寫到磁盤的 I/O 時,通過一些常規的 I/O 調優步驟可以加快這一過程,例如:
將表空間分布在多個容器(這些容器映射到不同磁盤)。
盡可能使用最快的硬件和存儲管理配置,這包括磁盤和通道速度、寫緩存以及並行寫等因素。
避免 RAID5(除非是與像 Shark 這樣有效的存儲設備一起使用)。
5. 鎖
缺省情況下,每一個插入的行之上都有一個 X 鎖,這個鎖是在該行創建時就開始有的,一直到 insert 被提交。有兩個跟 insert 和鎖相關的性能問題:
為獲得和釋放鎖而產生的 CPU 開銷。
可能由於鎖沖突而導致的並發問題。
對於經過良好優化的批量插入,由獲得每一行之上的一個 X 鎖以及後來釋放該鎖引起的 CPU 開銷是比較可觀的。對於每個新行之上的鎖,惟一可以替代的是表鎖(DB2 中沒有頁鎖)。當使用表鎖時,耗時減少了 3%。有 3 種情況可以導致表鎖的使用,在討論表鎖的缺點之前,我們先用一點時間看看這 3 種情況:
運行 ALTER TABLE LOCKSIZE TABLE。這將導致 DB2 為隨後使用該表的所有 SQL 語句使用一個表鎖,直到 locksize 參數改回到 ROW。
運行 LOCK TABLE IN EXCLUSIVE MODE。這將導致表上立即上了一個 X 鎖。注意,在下一次提交(或回滾)的時候,這個表將被釋放,因此,如果您要運行一個測試,測試中每 N 行提交一次,那麼就需要在每次提交之後重復執行 LOCK TABLE。
使用缺省鎖,但是讓 LOCKLIST 和 MAXLOCKS 數據庫配置參數的值比較小。當獲得少量的行鎖時,行鎖就會自動地逐漸升級為表鎖。
當然,所有這些的缺點就在於並發的影響:如果表上有一個 X 鎖,那麼其他應用程序除非使用了隔離級別 UR(未提交的讀),否則都不能訪問該表。如果知道獨占訪問不會導致問題,那麼就應該盡量使用表鎖。但是,即使您堅持使用行鎖,也應記住,在批量插入期間,表中可能存在數千個有 X 鎖的新行,所以就可能與其他使用該表的應用程序產生沖突。通過一些方法可以將這些沖突減至最少:
確保鎖的升級不會無故發生。您可能需要加大 LOCKLIST 和/或 MAXLOCKS 的值,以允許插入應用程序有足夠的鎖。對於其他的應用程序,使用隔離級別 UR。
對於 V8 FixPak 4,或許也可以通過 DB2_EVALUNCOMMITTED 注冊表變量來減少鎖沖突:如果將該變量設置為 YES,那麼在很多情況下,只能獲得那些符合某個謂詞的行上的鎖,而並不是獲得被檢查的所有行上的鎖。
發出一個 COMMIT 命令以釋放鎖,因此如果更頻繁地提交的話就足以減輕鎖沖突的負擔。
注意
在 V7 中,存在涉及 insert 和鍵鎖的並發問題,但是在 V8 中,由於提供了 type-2 索引,這些問題實際上已經不見了。如果要遷移到 V8 中來,那麼應該確保使用帶 CONVERT 關鍵字的 REORG INDEXES 命令,以便將索引從 type-1 轉換為 type-2。
如果有一條語句失敗,那麼這個表將被標記為不可訪問的,並且需要被刪除掉。這與其他恢復問題(請參閱 SQL Reference 關於 Create Table 的討論)一起使得 NLI 在很多情況下不能成為可行的方法。
在工作單元最後進行的提交,必須等到在此工作單元內涉及的所有髒頁都被寫到磁盤之後才能完成。這意味著這種提交要占用大量的時間。如果沒有積極地進行頁清除,那麼在使用 NLI 的情況下,Insert 加上提交所耗費的總時間要更長一些。將 NLI 與積極的頁清除一起使用的時候,可以大大減少耗時。如果使用 NLI,就要瞪大眼睛盯緊提交操作所耗費的時間。
至於提高日志寫的速度,有下面一些可能性:
將日志與新行所要插入到的表分別放在不同的磁盤上。
在操作系統層將日志分放到多個磁盤。
考慮為日志使用原始設備(raw device),但是要注意,這樣管理起來要更困難些。
避免使用 RAID 5,因為它不適合於寫密集型(write-intensive)活動。
7. 提交
提交迫使將日志記錄寫到磁盤上,以保證提交的插入肯定會存在於數據庫中,並且釋放新行上的鎖。這些都是有價值的活動,但是因為 Commit 總是要牽涉到同步 I/O(對於日志),而 insert 則不會,所以 Commit 的開銷很容易高於 insert 的開銷。因此,在進行批量插入時,每一行都提交一次的做法對於性能來說是很糟糕的,所以應確保不使用自動提交(對於 CLI 和 CLP 來說缺省情況正是如此)。建議大約每 1000 行提交一次:當每 1000 行而不是一兩行提交一次時,性能可以提高大概 10 倍。不過,一次提交多於 1000 行只能節省少量的時間,但是一旦出現失敗,恢復起來所花的時間要更多。
對上述方法的一種修正:如果 MINCOMMIT 數據庫配置參數的值大於 1 (缺省值),則 DB2 就不必對每次 commit 都進行一次同步 I/O,而是等待,並試圖與一組事件一起共享日志 I/O。對於某些環境來講,這樣做是有好處,但是對於批量插入常常沒有作用,甚至有負作用,因此,如果要執行的關鍵任務是批量插入,就應該讓 MINCOMMIT 的值保持為 1。