DAO.Recordsetの「BatchUpdate」を模倣する:トランザクション管理による一括更新の極限高速化
Access VBAのパフォーマンスチューニングにおいて、数万件以上のレコード処理に直面したとき、多くの開発者が絶望的な遅さに直面する。1件ごとの`Update`メソッド発行、そして背後で暗黙的に走るトランザクションとディスクI/Oの嵐。これをそのまま放置すれば、社内システムはただの「動く要塞」ではなく「動かない粗大ゴミ」と化す。
ADOには `BatchUpdate` という一括処理の機構が存在するが、AccessのネイティブエンジンであるJET/ACEの真価を極限まで引き出すには、DAO(Data Access Objects)のトランザクション管理とレコードセットの組み合わせによる「自前バッチ更新」を実装するのが最も確実かつ高速である。
今回は、長年のレガシーシステム保守と極限の最適化をくぐり抜けてきたアーキテクトの視点から、Jet/ACEのキャッシュ機構を支配し、オーバーヘッドを最小化する極限のデータ処理手法を解説する。
—
なぜ通常のレコード更新は遅いのか?
Access VBAでテーブルをループ処理し、値を書き換えていくコードを書いていないだろうか。
‘ 悪夢のアンチパターン:毎回の暗黙的トランザクションとディスク書き込み
Dim db As DAO.Database
Dim rs As DAO.Recordset
Set db = CurrentDb
Set rs = db.OpenRecordset(“T_HeavyData”, dbOpenDynaset)
Do Until rs.EOF
rs.Edit
rs!Status = “Processed”
rs.Update ‘ ← ここで毎回トランザクションが確定し、ディスクI/Oが発生する
rs.MoveNext
Loop
このコードの何が問題か。Jet/ACEエンジンは、明示的なトランザクション(`BeginTrans`)が宣言されていない場合、`Update`メソッドが実行されるたびに自動的に単一のトランザクションを開始・コミットし、さらにインデックスの更新とディスクへのフラッシュを試みる。数万回これが繰り返されれば、OSのファイルシステムとHDD/SSDのヘッド(またはキャッシュ)は悲鳴を上げる。
これに対抗する唯一の手段が、「トランザクションのスコープを広げ、ディスクI/Oの回数を物理的に激減させる」ことだ。
—
限界突破のアーキテクチャ:トランザクション制御とメモリ最適化
DAOの `Workspace` レベルでトランザクションを明示的に制御し、さらに適切な型(`dbOpenTable`)とメモリ管理を行うことで、処理速度を最大数十倍〜数百倍へと跳ね上げることができる。
以下に、実務で即座に使える極限最適化された一括更新プロシージャを示す。
実装コード:DAOバッチ模倣パターン
Option Compare Database
Option Explicit
”’
”’
Public Sub ExecuteBatchUpdateSimulation()
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
‘ 【重要】dbOpenTableはインデックス順での最速アクセスを保証する(テーブルソース限定)
‘ ※クエリやリンクテーブルの場合は dbOpenDynaset を使用すること
Set rs = db.OpenRecordset(“T_HeavyData”, dbOpenTable)
‘ トランザクションの開始(ディスク書き込みの遅延と一括確定)
ws.BeginTrans
On Error GoTo TransactionError
If Not (rs.BOF And rs.EOF) Then
rs.MoveFirst
Do Until rs.EOF
‘ 編集モード突入
rs.Edit
‘ — ビジネスロジック(値の更新) —
rs!Status = “Processed”
rs!ProcessedDate = Now
‘ ———————————-
‘ メモリ上のバッファを更新(この時点ではディスクに書き込まれない)
rs.Update
lngCounter = lngCounter + 1
‘ 5000件ごとにコミットとトランザクション再開を挟む
‘ (※トランザクションログの溢れを防ぎ、メモリを解放するための安全弁)
If lngCounter Mod 5000 = 0 Then
ws.CommitTrans
ws.BeginTrans
‘ トランザクション再開時は、レコードセットの位置を再同期する必要はないが
‘ 長大すぎるトランザクションによるロック競合を防ぐために分割する
End If
rs.MoveNext
Loop
End If
‘ 残りの変更をコミット
ws.CommitTrans
‘ クリーンアップ
rs.Close
Set rs = Nothing
Set db = Nothing
MsgBox “一括更新完了: ” & lngCounter & ” 件 / 処理時間: ” & Format(Timer – dblStartTime, “0.00秒”), vbInformation
Exit Sub
TransactionError:
‘ 障害発生時はロールバックしてデータベースの整合性を守る
ws.Rollback
‘ オブジェクトの明示的解放(メモリリークの根絶)
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
Set db = Nothing
MsgBox “致命的なエラーが発生しました。ロールバックを実行します。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“エラー内容: ” & Err.Description, vbCritical
End Sub
—
チーフアーキテクトが教える「実装の急所」
上記のコードには、単なる「動くコード」を超えた、現場の修羅場を潜り抜けてきた知見が凝縮されている。
1. `dbOpenTable` の選択と制約の理解
ローカルの物理テーブルに対して処理を行う場合、`dbOpenDynaset` や `dbOpenSnapshot` ではなく `dbOpenTable` を選択すべきだ。`dbOpenTable` はインデックス直接アクセス(ISAMレベル)を利用するため、データアクセスのオーバーヘッドが極限まで削ぎ落とされる。
ただし、これはリンクテーブルやSQLクエリには使えないため、その場合は `dbOpenDynaset` と適切な `dbSeeChanges` フラグの併用を検討すること。
2. トランザクションの「分割(チャンク処理)」の哲学
「トランザクションは一括がいいなら、100万件一気に `BeginTrans` から `CommitTrans` まで囲めば最速だろ?」というのは素人の発想だ。
Jet/Accessのエンジンは、トランザクションログをメモリ(およびテンポラリファイル)に蓄積する。これを無限にやると、システムリソースを食い潰すか、最悪の場合「ロックの競合」や「リソース不足」でクラッシュする。
そのため、コード内にあるように 5,000件〜10,000件程度のチャンク(区切り)で `CommitTrans` と `BeginTrans` をループ内で挟む のが、パフォーマンスと安定性の黄金律である。
3. 容赦なきメモリ管理(オブジェクトの明示的解放)
Access VBAにおいて、`Set rs = Nothing` や `Set db = Nothing` をサボるプログラマは、エンジニアを名乗る資格がない。
特に大量データを扱うループの上下では、COMコンポーネントの参照カウントを確実にデクリメントさせなければ、Access特有の「メモリ肥大化現象(Bloat)」を引き起こし、デスクトップアプリ全体の動作が不安定になる。エラーハンドラー内も含めた二重のクリーンアップがプロの作法である。
—
さらに極限を求めるエンジニアへ:SQLパススルー・外部連携への布石
もし対象のデータがAccessのローカルJet/ACEではなく、SQL ServerやPostgreSQLなどのRDBMSであれば、上記のようなVBA側でのレコードセット走査自体が「時代遅れ」となる。
その場合は、VBAから直接サーバーサイドのストアドプロシージャを叩くか、以下のような一括更新SQL(T-SQL等)を `CurrentProject.Connection.Execute` で一撃で流し込むべきだ。
‘ RDBMS連携時の真の最適化(VBAは指示を出すだけで、処理はサーバーに丸投げする)
Dim strSQL As String
strSQL = “UPDATE T_HeavyData SET Status = ‘Processed’, ProcessedDate = GETDATE() WHERE Status = ‘Pending'”
CurrentProject.Connection.Execute strSQL, , adExecuteNoRecords
しかし、クライアントサイドの制約でどうしてもAccessのVBAエンジン上で完結させなければならない環境、あるいはレガシーなMDBファイルを叩かざるを得ないインフラの現場においては、今回解説した 「DAOトランザクション + チャンク分割 + 適切なレコードセット型」 の組み合わせこそが、業務システムを救う唯一無二の武器となる。
コードの構造を理解し、ボトルネックを物理的に排除せよ。それが、システムを支配する者の流儀である。
