Access VBAを掌握する極限の知見:DAO.Recordsetの「BatchUpdate」を模倣する高速一括更新術
開発現場でよく見聞きする悲劇がある。
「Accessで数万件のデータをループ処理で更新したら、終わるまでにコーヒーが何杯も飲める」
「画面がフリーズして、Windowsに『応答なし』と冷たく宣告された」
もし君が、`CurrentDb.Execute` による一括SQLではなく、UIや複雑なビジネスロジックの都合で `Recordset` を使わざるを得ない状況にいるなら――そして、1件処理するたびに `MoveNext` と `Update` を繰り返しているなら、今すぐその手を止めてほしい。
ADOには `BatchUpdate` という強力な一括同期メカニズムが存在するが、Accessの真の支配者である DAO (Data Access Objects) には、クライアントサイドでの純粋なバッチ更新メソッドはない。
ならば、どうするか?
「トランザクションの明示的制御」と「適切なキャッシュ戦略」 を組み合わせ、DAOのポテンシャルを極限まで引き出して `BatchUpdate` を模倣するのだ。
今回は、数万件規模のレコード更新を劇的に高速化し、かつ堅牢性を担保するプロダクションコードの設計思想を伝授する。
—
なぜ「1件ずつの更新」は遅いのか?
DAOの `Recordset` で `.Edit` から `.Update` を呼ぶ時、裏側では何が起きているか。
Jet/ACEデータベースエンジンは、コードが `.Update` を実行するたびに、トランザクションの開始・書き込み・インデックスの再構築・ディスクI/O を実行しようとする。
1万件のレコードがあれば、これが1万回発生する。ディスクとメモリの往復によるオーバーヘッドが、処理を極限まで重くしている真犯人だ。
解決策:トランザクションの境界を意図的に支配する
アプローチはシンプルである。
「データベースエンジンに対する書き込みのトランザクションを、ループの外側に張り巡らせる」 のだ。
これにより、Jetエンジンはディスクへの物理書き込みをバッファリングし、コミット(`CommitTrans`)が呼ばれる瞬間までメモリ上でトランザクションを保持する。I/Oの回数が激減するため、パフォーマンスは文字通り「桁違い(10倍〜50倍以上)」に跳ね上がる。
ただし、これを実装するには「エラーハンドリングの徹底」と「メモリリークの防止」が絶対条件となる。中途半端なトランザクションは、データベースの破損(.accdbの肥大化・壊れ)を招くからだ。
—
【プロダクションコード】堅牢かつ高速な一括更新実装
以下に、実務の現場でそのまま使える、堅牢性と速度を極めたコードを示す。
数万件の受発注データやステータス一括変更を想定したモジュールだ。
Option Compare Database
Option Explicit
‘ =========================================================================
‘ 処理名 : 模擬BatchUpdateによる高速レコード更新
‘ 概要 : トランザクションを明示的に制御し、大量データの更新を高速化する
‘ 備考 : DAO.Workspaceを利用したトランザクション管理の模範実装
‘ =========================================================================
Public Sub ExecuteSimulatedBatchUpdate()
Dim ws As DAO.Workspace
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim startTime As Double
startTime = Timer
‘ デフォルトワークスペースを取得(トランザクション制御の要)
Set ws = DBEngine.Workspaces(0)
Set db = CurrentDb()
‘ 悲観的ロックを避け、共有モードかつ読込/書込可能なダイナセットを開く
‘ ※パフォーマンス向上のため、必要な列のみ、または条件を絞ったSQLを推奨
Set rs = db.OpenRecordset(“SELECT ID, Status, UpdatedDate FROM T_OrderData WHERE Status = ‘Pending'”, dbOpenDynaset)
‘ 対象データがゼロ件なら即座に抜ける
If rs.EOF Then
MsgBox “更新対象のデータが存在しません。”, vbInformation, “処理終了”
GoTo Cleanup
End If
‘ 【極限の知見】ここでトランザクションを開始する
‘ これにより、ループ内の.UpdateがディスクI/Oを発生させずメモリ上で処理される
ws.BeginTrans
On Error GoTo TransError
Dim updateCount As Long
updateCount = 0
Do While Not rs.EOF
‘ 編集モードへ移行
rs.Edit
‘ — ここにビジネスロジックを記述 —
rs!Status.Value = “Processed”
rs!UpdatedDate.Value = Now
‘ ———————————-
‘ バッファへ反映(ここではまだディスクに書き込まれない)
rs.Update
updateCount = updateCount + 1
rs.MoveNext
Loop
‘ 【極限の知見】一括コミット(ここで初めて物理ストレージへ書き込みが走る)
ws.CommitTrans
MsgBox “一括更新が完了しました。” & vbCrLf & _
“処理件数: ” & updateCount & ” 件” & vbCrLf & _
“処理時間: ” & Format(Timer – Timer, “0.00秒”) & ” (目安)”, vbInformation, “高速化完了”
GoTo Cleanup
TransError:
‘ 異常発生時は必ずロールバックし、データベースの整合性を守る
ws.Rollback
MsgBox “予期せぬエラーが発生したため、変更をロールバックしました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“エラー内容: ” & Err.Description, vbCritical, “致命的エラー”
Cleanup:
‘ オブジェクトの解放順序(メモリリークの防止)
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
Set db = Nothing
Set ws = Nothing
Debug.Print “トータル処理時間: ” & Timer – startTime & ” 秒”
End Sub
—
アーキテクトが教える、現場でハマる「3つの罠」と対策
このコードを実務に導入する際、以下のポイントを押さえておかないと、思わぬバグやパフォーマンス低下を引き起こす。
1. ワークスペース(`DBEngine.Workspaces(0)`)の明示的指定
トランザクションは、単なる `CurrentDb.BeginTrans` ではなく、必ず `Workspace` レベルで制御すること。Accessの仕様上、複数のデータベース接続や複雑なクエリが絡む場合、ワークスペースを明示することでトランザクションのスコープが明確になり、ロック競合を防げる。
2. レコードセットのスコープとメモリ管理
数百万件のテーブルに対して `SELECT ` でレコードセットを開いてはならない。メモリが枯渇する。
必ず必要なフィールド、かつ必要なレコード(`WHERE`句)に絞ったSQLで開くこと。また、処理終了後は必ず `rs.Close` と `Set rs = Nothing` を対で行い、Jetエンジンのメモリキャッシュを解放させること。
3. ロールバックの重要性
トランザクション中にエラー(例えば、桁あふれやバリデーション違反)が発生した場合、`ws.Rollback` が走るように `On Error GoTo` を必ず配置すること。これを怠ると、不完全なデータが中途半端に書き込まれ、データベースファイルが破損するリスク(破損したレコード構造の残留)を抱えることになる。
—
総括
Access VBAにおいて、パフォーマンスチューニングとは「ハードウェアの性能を頼ること」ではなく、「データベースエンジンの挙動(I/Oとトランザクションのライフサイクル)をハックすること」に他ならない。
今回紹介した「トランザクションによるBatchUpdateの模倣」は、DAOを扱うすべてのAccess開発者にとって必須の武器となる。
一件ずつノロノロと更新するコードとは今日で決別し、洗練された高速なアーキテクチャを君のプロジェクトに実装してほしい。
