1. 程式人生 > >三種批量刪除PLSQL寫法效率的比對

三種批量刪除PLSQL寫法效率的比對

我們有一個重要的舊系統,最近夜維出現了一些問題,夜間執行5小時未完成,為了不影響業務,只能早上高峰期之前,DBA手工kill夜維程序。

這一個夜維程式採用了PLSQL寫的儲存過程,通過資料庫job定時啟動執行。儲存過程我很少使用,藉著這次機會,補習了下,這個儲存過程中的邏輯比較簡單,依次刪除若干張業務表,每張表刪除的邏輯相同,為了便於說明,模擬了下刪除一張表的邏輯,示例如下,
TBL_CUSS表三個欄位,第一個欄位是NUMBER型別,第二個欄位是VARCHAR2型別,第三個欄位是DATE型別,
這裡寫圖片描述
這個儲存過程接受一個引數,表示刪除幾天前的資料,刪除DELETE語句按照一個時間欄位和這個引數比較得出符合條件的記錄集,同時使用rownum限制每次執行DELETE-COMMIT事務的條數,迴圈執行,直至出現ORA-1403無記錄的提示,退出此邏輯。這個儲存過程最優的地方,是使用了批量提交,不是執行一次DELETE刪除所有記錄COMMIT,如果這麼執行,可能會佔用大量的UNDO回滾段,進而可能出現回滾段空間不足的報錯,也可能出現ORA-1555的錯誤。畢竟UNDO中記錄的是SQL語句的逆向,對於DELETE語句,逆向就是INSERT,即會儲存刪除的整條記錄。

可能有朋友一眼就看出這個儲存過程的邏輯有一些問題,比如對於這種批量刪除,未使用遊標,相當於每次要檢索tbl_cuss表符合insert_time < trunc(SYSDATE)-:1條件的記錄,可每次僅刪除其中的rownum限制的條數,如果使用遊標,檢索只需要一次執行,不考慮是否有索引,執行語句次數的降低,可以帶來效能的一定提升。

針對此問題,寫了第二個儲存過程,
這裡寫圖片描述
接受刪除天數的引數,使用了遊標,執行一次SELECT,讀取出的則是符合insert_time < trunc(SYSDATE)-:1條件的所有結果集記錄的rowid資訊,遍歷遊標的時候使用BULK批量的方式,設定了一次性執行的條數限制MAX_ROW_SIZE,並且刪除語句是根據上面遊標獲取的rowid為條件進行的DELETE,如果各位瞭解rowid,可以知道他代表了這條記錄的物理位置,通過換算可以得出這條記錄儲存於的檔案、塊和行上,即可以快速定位這條記錄的物理位置,在RBO模式下,他的成本優先順序是最優的,高於索引。

繼續寫了第三個儲存過程,
這裡寫圖片描述
這和第二個儲存過程,基本一致,唯一不同就是第二個儲存過程中使用了for迴圈,第三個儲存過程則用forall迴圈。for迴圈會執行其中的每條SQL語句,forall則會將其中所有SQL批量傳送SQL引擎執行。當然可能有其他的寫法,比如使用遊標,但不使用BULK,按照rowid刪除,這種寫法執行SQL語句的次數和結果集資料量一致,效率可能還不如原始procedure。

從原理上說,使用BULK比單條語句執行,減少PLSQL和SQL引擎之間的切換頻率,也可以減少redo和undo的產生量。針對迴圈內執行的DELETE,適合於使用集合,放入forall。

delete和insert都可以從forall上面得到巨大的效能提升。但是對於update來說opcode沒有相關操作,提升應該不會那麼明顯。

接下來我們會對這三個儲存過程進行一些比對實驗,通過一些資料,說明各自的適用場景,首先建立測試資料集,製造了1300萬測試資料,
這裡寫圖片描述
每天50萬資料,一共26天,
這裡寫圖片描述

第一個儲存過程名稱為clear_without_fetch。
第二個儲存過程名稱為clear_all_fetch。
第三個儲存過程名稱為clear_fetch。

實驗有以下幾類,(以下執行基本採用第二次執行的結果避免第一次刷快取)
(1) 一次性刪除1萬條記錄,insert_time不是索引,刪除兩天的資料(即100萬),
clear_without_fetch用時01:16.31
clear_all_fetch用時00:40.50
clear_fetch用時00:21.73
clear_fetch勝出,clear_without_fetch最慢,說明TABLE ACCESS FULL下的SQL語句,一次性刪除1萬條記錄,使用遊標和BULK效率要高些,使用forall比for效率要高些。

(2) 一次性刪除5萬條記錄,insert_time不是索引,刪除兩天的資料(即100萬),
clear_without_fetch用時00:26.98
clear_all_fetch用時00:39.80
clear_fetch用時00:22.24
clear_fetch勝出,但一次性刪除5萬條記錄,TABLE ACCESS FULL下的SQL語句由於執行次數減小為原來的5倍,效率上有提升,clear_all_fetch基本一致,是因為無論什麼方式,其執行SQL語句的次數,和結果集一致,即100萬次SQL。

(3) 一次性刪除100萬條記錄,insert_time不是索引,刪除兩天的資料(即100萬),
clear_without_fetch用時00:21.92
clear_all_fetch用時01:22.00
clear_fetch用時00:25.95
clear_without_fetch勝出,TABLE ACCESS FULL下的SQL語句執行一次,clear_fetch雖然仍是批量一次傳送SQL,效能上的優勢不很明顯。

(4) 一次性刪除1萬條記錄,insert_time是索引,刪除兩天的資料(即100萬),
clear_without_fetch用時00:31.01
clear_all_fetch用時01:09.02
clear_fetch用時00:37.39
clear_without_fetch勝出,索引掃描執行效率提升,相比TABLE ACCESS FULL要明顯一些。

(5) 一次性刪除5萬條記錄,insert_time是索引,刪除兩天的資料(即100萬),
clear_without_fetch用時00:35.35
clear_all_fetch用時01:03.26
clear_fetch用時00:37.40
clear_without_fetch勝出,clear_all_fetch和clear_fetch基本保持和1萬一致。

(6) 一次性刪除100萬條記錄,insert_time是索引,刪除兩天的資料(即100萬),
clear_without_fetch用時00:23.33
clear_all_fetch用時01:27.80
clear_fetch用時00:33.68
clear_without_fetch勝出,相比1萬和5萬,效率提升一些,我理解主要是SQL執行次數從100次(1萬)->20次(5萬)->1次(100萬)。

上面的實驗中,資料接近的未必是絕對,和環境因素(例如機器配置、資料質量等)可能有關,因此大體方向上可以參考,不同次的實驗可能會略有不同,但方向應該比較接近,畢竟原理擺著。

如下是上面六個實驗,三個儲存過程SQL,各自執行的10046的trace,從中可以看出一些端倪,

clear_without_fetch儲存過程各場景trace,
(1) 一次性刪除1萬條記錄,insert_time不是索引,刪除兩天的資料(即100萬),
這裡寫圖片描述
可以看出由於使用了繫結變數,解析一次,由於迴圈邏輯的問題,執行了100+1次。

(2) 一次性刪除5萬條記錄,insert_time不是索引,刪除兩天的資料(即100萬),
這裡寫圖片描述

(3) 一次性刪除100萬條記錄,insert_time不是索引,刪除兩天的資料(即100萬),
這裡寫圖片描述
從elapsed以及query可以比較(1)-(3)。

(4) 一次性刪除1萬條記錄,insert_time是索引,刪除兩天的資料(即100萬),
這裡寫圖片描述

(5) 一次性刪除5萬條記錄,insert_time是索引,刪除兩天的資料(即100萬),
這裡寫圖片描述

(6) 一次性刪除100萬條記錄,insert_time是索引,刪除兩天的資料(即100萬),
這裡寫圖片描述
由於需要維護索引,相比TABLE ACCESS FULL,會有些消耗。

clear_all_fetch儲存過程各場景trace,
(1) 一次性刪除1萬條記錄,insert_time不是索引,刪除兩天的資料(即100萬),
這裡寫圖片描述

這裡寫圖片描述

(2) 一次性刪除5萬條記錄,insert_time不是索引,刪除兩天的資料(即100萬),
這裡寫圖片描述

這裡寫圖片描述

(3) 一次性刪除100萬條記錄,insert_time不是索引,刪除兩天的資料(即100萬),
這裡寫圖片描述

這裡寫圖片描述
無論(1)、(2)、(3),DELETE語句均執行了100萬次,唯一的區別就是SELECT語句和執行的次數。

(4) 一次性刪除1萬條記錄,insert_time是索引,刪除兩天的資料(即100萬),
這裡寫圖片描述

這裡寫圖片描述

(5) 一次性刪除5萬條記錄,insert_time是索引,刪除兩天的資料(即100萬),
這裡寫圖片描述

這裡寫圖片描述

(6) 一次性刪除100萬條記錄,insert_time是索引,刪除兩天的資料(即100萬),
這裡寫圖片描述

這裡寫圖片描述
有索引和無索引相比,有一些需要維護索引的消耗。

clear_fetch儲存過程各場景trace,
(SELECT和clear_all_fetch儲存過程相近,此處忽略)
(1) 一次性刪除1萬條記錄,insert_time不是索引,刪除兩天的資料(即100萬),
這裡寫圖片描述

(2) 一次性刪除5萬條記錄,insert_time不是索引,刪除兩天的資料(即100萬),
這裡寫圖片描述

(3) 一次性刪除100萬條記錄,insert_time不是索引,刪除兩天的資料(即100萬),
這裡寫圖片描述

(4) 一次性刪除1萬條記錄,insert_time是索引,刪除兩天的資料(即100萬),
這裡寫圖片描述

(5) 一次性刪除5萬條記錄,insert_time是索引,刪除兩天的資料(即100萬),
這裡寫圖片描述

(6) 一次性刪除100萬條記錄,insert_time是索引,刪除兩天的資料(即100萬),
這裡寫圖片描述
可以看見clear_fetch和clear_all_fetch唯一區別就是DELETE語句執行次數,clear_fetch中執行次數和迴圈次數一樣,說明是批量傳送的,單條DELETE相同,但執行次數的不同,影響了資源消耗和執行時間。

從實驗中可以得出的結論,
(1) SQL使用TABLE ACCESS FULL的執行計劃,若SQL執行次數較多時,則BULK+forall的方式,效率較高;若SQL執行次數較少時,很可能使用TABLE ACCESS FULL的執行計劃的SQL,效率和BULK+forall接近,甚至有更優的可能。
(2) SQL使用INDEX RANGE SCAN的執行計劃,效率會比BULK+forall略高,若SQL執行次數較少時,使用INDEX RANGE SCAN的執行計劃的SQL,效率較高;SQL執行次數對於BULK+forall的方式基本一致。
(3) 無論是否用索引,BULK+forall的方式均優於BULK+for。可以使用索引,則用遊標和不用遊標,效率比較接近,從實驗上看,不用遊標反而可能略高一些,這和使用遊標需要一些解析類的消耗可能有關,但遊標可以帶來便捷性,比如方便控制結果集,可以更靈活地編輯邏輯,既然效率比較接近,若時間均是可接受範圍內,可以根據實際來考慮,選擇什麼方式。無論什麼方式,大表資料的批量刪除,這是首要原則。