【上級】DAO.Recordsetの「BatchUpdate」を自作する:トランザクション管理による一括更新の高速化
レガシーシステムと呼ばれる領域において、Microsoft Accessはその強烈なライフサイクルと手軽さ故に、今なお多くの現場で延命され、あるいは基幹の末端として稼働し続けている。
だが、Access VBAにおける最大のボトルネック、それは「ディスクI/Oの非効率性」と「暗黙のトランザクションのオーバーヘッド」に他ならない。
数万件規模のレコードに対し、ループを回して一件ずつ `MoveNext` と `Update` を実行するコードを見たことはないだろうか。もし君がシニアエンジニアであるなら、その設計がどれほどのパフォーマンス泥棒であるか、身をもって知っているはずだ。
今回は、ADOのようなネイティブなバッチ更新機能を持たないDAO(Data Access Objects)において、明示的なトランザクション制御とメモリ最適化によって「自作BatchUpdate」を実装し、処理速度を極限まで引き上げる手法を解説する。
—
1. なぜ「一件ずつの更新」は死刑宣告なのか?
Accessのバックエンド(Jet / ACEエンジン)は、デフォルトの状態では「メソッド呼び出しごとのトランザクション自動コミット」を行う。
つまり、以下のようなコードを書いた場合、
‘ 【アンチパターン】絶対にやってはならない実装
Dim rs As DAO.Recordset
Set rs = CurrentDb.OpenRecordset(“T_HeavyData”, dbOpenDynaset)
Do While Not rs.EOF
rs.Edit
rs!ProcessedFlag = True
rs.Update ‘ ←ここで毎回ディスクI/Oとトランザクション確定が発生する
rs.MoveNext
Loop
rs.Close
1万件のレコードがあれば、1万回の書き込み処理とロック制御、ログフラッシュがディスクに対して発生する。ネットワーク越しの共有フォルダ(LUN)上にACCDBがある環境などであれば、このアプローチはシステムを完全に沈黙させる。
DAOにおける真の高速化の方程式
パフォーマンスを極限まで高めるための要件は以下の3点だ。
1. トランザクションのスコープを極限まで広げる(一括コミット)
ループの外側で `BeginTrans` を宣言し、全処理が完了した瞬間のみ `CommitTrans` を叩く。これにより、Jetエンジンはログのフラッシュを最小限に抑える。
2. 適切なレコードセットタイプの選択
オーバーヘッドの大きい `dbOpenDynaset` ではなく、ローカルのメモリ上で完結しやすい操作、あるいは適切なキャッシュ戦略を持つストリームを構築する。
3. オブジェクトのライフサイクルの完全な支配
VBAのガベージコレクタは信用するな。メモリリークはパフォーマンス低下の隠れた致命傷となる。
—
2. 実装:トランザクション制御による「自作BatchUpdate」エンジン
以下に、実務の現場でそのまま投入可能な、堅牢かつ超高速な一括更新プロシージャの全貌を示す。エラーハンドリングとトランザクションのロールバック機構を完備した、チーフアーキテクトクオリティのコードだ。
Option Compare Database
Option Explicit
‘ =========================================================================
‘ 固有のエラー定義
‘ =========================================================================
Private Const ERR_BATCH_FAILED As Long = vbObjectError + 1000
Public Sub ExecuteBatchUpdateOptimized()
Dim ws As DAO.Workspace
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim lngCounter As Long
Dim dblStartTime As Double
dblStartTime = Timer
‘ デフォルトワークスペースを取得(トランザクション制御の必須要件)
Set ws = DBEngine.Workspaces(0)
Set db = CurrentDb
‘ レコードロックを最適化してオープン(ペシミスティックロックを排除)
Set rs = db.OpenRecordset(“T_TargetTable”, dbOpenDynaset, dbDenyWrite, dbOptimistic)
If rs.EOF And rs.BOF Then
MsgBox “処理対象のレコードが存在しません。”, vbInformation
GoTo Cleanup
End If
‘ =====================================================================
‘ トランザクションの開始(ここから一括処理の領域)
‘ =====================================================================
ws.BeginTrans
On Error GoTo TransactionError
lngCounter = 0
With rs
Do While Not .EOF
.Edit
‘ — 【ビジネスロジック・データ更新】 —
!ProcessedFlag = True
!LastUpdatedDate = Now
‘ —————————————-
.Update
lngCounter = lngCounter + 1
‘ 進捗管理やメモリフラッシュのためのインターバル制御(必要に応じて)
‘ 5000件ごとにバッファを解放する感覚でストリームを維持
If lngCounter Mod 5000 = 0 Then
‘ Jetエンジンの内部キャッシュを適正化するため、
‘ 必要であればここでDoEventsを挟むが、パフォーマンス優先なら削る
End If
.MoveNext
Loop
End With
‘ トランザクションの確定
ws.CommitTrans
Debug.Print “BatchUpdate Complete: ” & lngCounter & ” records processed in ” & Format(Timer – dblStartTime, “0.00”) & ” seconds.”
MsgBox “一括更新が完了しました。” & vbCrLf & “処理件数: ” & lngCounter & “件”, vbInformation
GoTo Cleanup
TransactionError:
‘ 障害発生時は即座にロールバックし、整合性を担保する
ws.Rollback
MsgBox “トランザクション中に致命的なエラーが発生しました。変更は破棄されます。” & vbCrLf & _
“Error ” & Err.Number & “: ” & Err.Description, vbCritical
Cleanup:
‘ =====================================================================
明的オブジェクト解放(メモリリークの完全阻止)
‘ =====================================================================
On Error Resume Next
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
Set db = Nothing
‘ WorkspaceはDBEngineが管理するため原則としてSet Nothingのみ、Closeはしない
Set ws = Nothing
On Error GoTo 0
End Sub
—
3. チーフアーキテクトが教える、現場の「地雷」と最適化の極意
上記のコードをさらに極限まで最適化し、レガシー環境のトラブルを未然に防ぐための実践知見を共有する。
① `DBEngine.Workspaces(0)` の重要性
DAOでトランザクションを張る際、`CurrentDb.BeginTrans` ではなく、必ず `DBEngine.Workspaces(0).BeginTrans` を使用するべきだ。
`CurrentDb` は呼び出すたびに一時的なデータベースオブジェクトのインスタンスを生成するため、トランザクションのスコープが意図せず曖昧になるリスクがある。データベースのセッションを完全に掌握するには、Workspaceを明示的に変数に格納して制御しなければならない。
② インデックスとトランザクションサイズのバランス
数百万件規模のテーブルに対して単一のトランザクションで `BeginTrans` ~ `CommitTrans` を囲むと、Jet/ACEエンジンのロックコンテナ(Locksプロパティの制限)が溢れ、「トランザクションが多すぎます (Too many simultaneous transactions)」というエラーが爆誕する。
もし処理対象が数十万件を超える場合は、以下のように「チャンク(分割)単位のコミット」をハイブリッドで実装するのがプロの選択だ。
‘ 5万件ごとにコミットを挟むハイブリッドバッチの概念
If lngCounter Mod 50000 = 0 Then
ws.CommitTrans
ws.BeginTrans
End If
これにより、メモリ溢れを防ぎつつ、一件ずつ更新するよりも圧倒的な速度(通常の10倍〜50倍)を維持することが可能になる。
③ オブジェクトの明示的解放(Destructorの概念)
VBAにはガベージコレクタの確定的実行タイミングが存在しない。`rs.Close` を行わずにプロシージャを抜けると、DAOの内部ロックやメモリ空間が解放されず、Accessの動作が徐々に重くなる(いわゆるメモリ肥大化現象)。
エラーハンドラ(`Cleanup:` ラベル)を必ず経由させる構造化プログラミングを徹底し、インスタンスの生存期間を完全にプロシージャのスコープ内に閉じ込めよ。
—
総括
Access VBAは、しばしば「おもちゃの言語」と揶揄される。しかし、その背後にあるDAOとJetエンジンの挙動を深く理解し、トランザクションのライフサイクルとメモリ管理を極限までチューニングすれば、エンタープライズ領域に匹敵するスループットを引き出すことが可能だ。
泥臭いレガシーシステムであっても、アーキテクトの知見と手腕次第で、現役の高速な戦闘機へと生まれ変わらせることができる。
コードの行数に酔うな。支配すべきは「リソースの呼吸」である。
