DAO.Recordsetの「BatchUpdate」を模倣する:トランザクション管理による一括更新の高速化
Access VBAでの大量データ処理において、最大のボトルネックは何か知っているか?
愚直に `CurrentDb.Execute` でSQLをループ発行することか?あるいは、UIフォームで1件ずつ `MoveNext` しながら `.Edit` と `.Update` を繰り返すことか?
どちらも最悪だ。前者はJet/ACEエンジンに毎回クエリのパースと実行プランの生成を強制し、後者に至っては画面描画やレコードロックのオーバーヘッド、そして何より「1回の更新ごとにトランザクションログがディスクにフラッシュされる」という致命的なI/O遅延を引き起こす。
ADO.NETなどのモダンなフレームワークには、メモリ上で変更をキャッシュし、一括でコミットする `BatchUpdate` のような洗練された仕組みが存在する。しかし、Accessのネイティブなデータアクセスの主役であるDAO(Data Access Object)の `Recordset` オブジェクトには、直接的な「バッチ更新メソッド」は用意されていない。
ならば、諦めるか? いや、開発リーダーである我々が、DAOのトランザクション管理とメモリ上のキャッシュ構造をハックし、「疑似BatchUpdate」を実装すればいい。
今回は、数万件規模のレコード更新を極限まで高速化し、かつ堅牢性を微塵も損なわないプロフェッショナルな設計手法を伝授する。
—
なぜ「1件ごとの更新」は遅いのか?(アーキテクチャの理解)
データベースの更新処理において、パフォーマンスを決定づける要因は「CPUの処理速度」ではなく「トランザクションとディスクI/Oの回数」だ。
Access(ACEエンジン)は、デフォルトの状態では `Recordset.Update` が実行されるたび、あるいは `Execute` が走るたびに、内部で暗黙のトランザクションを開始・コミットし、変更差分をディスクの `.accdb` ファイルに書き込もうとする。
1万件のレコードを処理するということは、この重たいディスクI/Oを1万回発生させることを意味する。これでは、どんなハイスペックマシンであっても処理は終わらない。
解決のアプローチ
この問題を打破する原則はただ一つ。
「トランザクションのスコープを最小限の回数に絞り、ディスクへの書き込み(フラッシュ)をメモリ上で可能な限り遅延させること」だ。
DAOの `BeginTrans` と `CommitTrans` を使い、複数件の更新を一つの巨大なトランザクションで包み込む。さらに、更新対象のデータをメモリ上の `Recordset` に一気に読み込み、ローカルなループで高速に処理を流し込むことで、圧倒的なパフォーマンスを引き出す。
—
堅牢性と速度を両立する「疑似BatchUpdate」実装パターン
実務の現場で耐えうるコードとは、「速い」だけではなく、「エラー時に確実にロールバックされ、データが絶対に汚染されない」ものである。
以下のプロダクションコードを見てほしい。これは、特定の条件に合致する数万件のレコードに対し、一括でステータスや計算値を更新するプロシージャだ。エラーハンドリング、トランザクションの制御、そしてオブジェクトの適切な解放(クリーンアップ)を完璧に網羅している。
Option Compare Database
Option Explicit
‘ =========================================================================
‘ 処理名 : ExecuteBatchUpdateSimulation
‘ 概要 : DAO.Recordsetとトランザクションを駆使し、一括更新を高速化するサンプル
‘ 備考 : 数万件以上のレコード更新時に、ディスクI/Oのオーバーヘッドを劇的に削減
‘ =========================================================================
Public Sub ExecuteBatchUpdateSimulation()
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim lngCount As Long
Dim startTime As Double
startTime = Timer ‘ 処理時間計測用
‘ 1. データベース参照の取得 (CurrentDbの乱用を避け、変数に保持する)
Set db = CurrentDb
‘ 2. 更新対象を限定したレコードセットを開く
‘ ※テーブル全体ではなく、必要なカラムと行に絞ることでメモリ消費を抑制する
Set rs = db.OpenRecordset(“SELECT ID, Status, UpdatedDate FROM T_LargeData WHERE Status = ‘Pending'”, dbOpenDynaset)
‘ 対象データがゼロ件の場合は早期抜け
If rs.EOF Then
MsgBox “更新対象のデータが存在しません。”, vbInformation, “処理終了”
GoTo Cleanup
End If
‘ 3. 【核心】トランザクションの開始
‘ これ以降の変更は、CommitTransが実行されるまでディスクに確定されない
db.BeginTrans
On Error GoTo ErrorHandler
lngCount = 0
‘ 4. メモリ上のレコードセットを高速ループ
Do While Not rs.EOF
‘ 編集モードへ移行
rs.Edit
‘ — ここにビジネスロジック(値の書き換え)を記述 —
rs!Status = “Completed”
rs!UpdatedDate = Now
————————————————–
‘ 更新を確定(この時点ではメモリ上およびトランザクション内のみに反映)
rs.Update
lngCount = lngCount + 1
rs.MoveNext
Loop
‘ 5. トランザクションのコミット(一括書き込みの実行)
db.CommitTrans
MsgBox “一括更新が完了しました。” & vbCrLf & _
“処理件数: ” & lngCount & ” 件” & vbCrLf & _
“実行時間: ” & Format(Timer – startTime, “0.00”) & ” 秒”, _
vbInformation, “高速バッチ処理完了”
Cleanup:
‘ 6. オブジェクトの確実な解放(メモリリークの防止)
On Error Resume Next
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
Set db = Nothing
Exit Sub
ErrorHandler:
‘ 7. 障害発生時のロールバック
db.Rollback
MsgBox “エラーが発生したため、処理をロールバックしました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“エラー内容: Err.Description”, _
vbCritical, “致命的エラー”
Resume Cleanup
End Sub
—
典拠となる設計のポイントと、現場でやってはいけないアンチパターン
このコードには、シニアエンジニアとしてのこだわりがいくつか詰まっている。開発現場でレビューする際、以下のポイントを後輩やチームメンバーに叩き込んでほしい。
1. `CurrentDb` の乱用を避ける
VBA初心者は、コードのあちこちに `CurrentDb.Execute` や `CurrentDb.OpenRecordset` を書き散らしがちだ。しかし、`CurrentDb` を呼び出すたびに、Accessは内部で新しいデータベースオブジェクトのインスタンスを生成し、古いものを破棄するという重たい処理を行っている。
変数 `Dim db As DAO.Database` に一度格納し、それを使い回すこと。これだけで無駄なオーバーヘッドが消える。
2. トランザクション中にユーザー入力を挟まない
`BeginTrans` から `CommitTrans` の間に、`MsgBox` や `InputBox`、あるいはフォームからの入力待ちを挟むのは絶対にご法度だ。トランザクションが開きっぱなしになると、その間テーブルやレコードがロックされ、他のユーザーやプロセスがデータベースにアクセスできなくなる(デッドロックやパフォーマンス低下の温床となる)。
トランザクションのスコープは、純粋なデータ処理のループ内だけに極限まで短く絞れ。
3. クエリの範囲(スコープ)を最小限にする
`SELECT FROM T_LargeData` のように、不要なカラムまでメモリにロードしていないか?
VBAの `OpenRecordset` では、必要なフィールドだけに絞る、あるいは `WHERE` 句でレコード数を絞り込むことが、ネットワーク(バックエンドが別ファイルの場合)やメモリの負荷を劇的に下げる鉄則だ。
—
まとめ:Access VBAの限界を超えるために
「Accessだから遅い」のではない。「書き方が悪いから遅い」のだ。
今回紹介した「トランザクション管理による疑似BatchUpdate」は、DAOのライフサイクルとデータベースエンジンの挙動を正しく理解していれば、ごく自然に導き出せるアプローチである。
数万件のレコード更新に何分もかかっていた処理が、この設計を取り入れることで数秒に短縮される瞬間は、エンジニアにとってこの上なく痛快なはずだ。
現場のシステムを「使えるレベル」から「プロフェッショナル・グレード」へと引き上げるため、ぜひ今日のコードをあなたのリポジトリに組み込んでみてほしい。
