【実務・中級編】【上級】DAO.Recordsetの「BatchUpdate」を模倣する:トランザクション管理による一括更新の高速化 – Access VBA解析バイブル

スポンサーリンク

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開発者にとって必須の武器となる。
一件ずつノロノロと更新するコードとは今日で決別し、洗練された高速なアーキテクチャを君のプロジェクトに実装してほしい。

タイトルとURLをコピーしました