【実務・中級編】DAO.RecordsetのOpenRecordsetでハマる「カーソルタイプ」の最適解 – Access VBA解析バイブル

スポンサーリンク

DAO.Recordsetの呪縛を解く:`dbOpenDynaset`と`dbOpenSnapshot`の最適解

現場でAccess VBAを使った大規模な業務システムを構築していると、ある日突然、こんな壁にぶつかる。

「数万件のレコードを処理し始めた途端、Accessがフリーズしたように固まる」
「タスクマネージャーを見ると、メモリがみるみるうちに喰いつぶされている」

原因の多くは、`CurrentDb.OpenRecordset`の第一引数、すなわちカーソルタイプの選定ミスにある。
「とりあえず動くから」と、何も考えずにデフォルト、あるいは安易に `dbOpenDynaset` を使っていないだろうか? その選択が、ネットワークの帯域とクライアントのメモリを焼き尽くしている可能性が高い。

今回は、DAO(Data Access Objects)のカーソルモデルの本質を紐解き、数万〜数百万件のデータを扱うプロダクション環境で「絶対に事故らない」ための最適解をロジカルに伝授する。

1. なぜAccessは重くなるのか? `dbOpenDynaset` の裏側

まず、DAOにおける主要なカーソルタイプである `dbOpenDynaset` と `dbOpenSnapshot` の「物理的な挙動の違い」を正しく理解する必要がある。

`dbOpenDynaset`(ダイナセット型)の本質

ダイナセットは、「生きたデータ(Editable)」を扱うための双方向カーソルだ。

  • 特徴: レコードセットを開いた後も、自分自身や他のユーザーによるデータの追加・更新・削除がリアルタイムに反映される。
  • コスト: これを実現するため、Jet/ACEデータベースエンジンは「ロッキング情報」や「変更検知のためのオーバーヘッド」を常に抱え込む。さらに、リモートのSQL Server(ODBC接続)などを相手にしている場合、ダイナセットの維持はネットワークを行き交うトラフィックを爆発的に増加させる。

`dbOpenSnapshot`(スナップショット型)の本質

スナップショットは、「過去の静止画(Read-Only)」である。

  • 特徴: オープンした瞬間のデータの「スナップショット(静止画)」をメモリ(または一時ファイル)上に切り取る。以降、他のユーザーがデータを変えようが知ったことではない。
  • コスト: 変更を監視する必要がないため、エンジン側の負荷が圧倒的に低い。更新・追加・削除を行わない「集計」「帳票出力」「データ移行・エクスポート」のシーンにおいて、これに勝る選択肢はない。

2. 【原則】バッチ処理・参照処理は「スナップショット」一択

業務効率化ツールでありがちなアンチパターンがこれだ。

> 「データを読み込んで、条件に合致するか判定してフラグを立てるだけ」の処理なのに、`dbOpenDynaset` でレコードセットを開き、`.Edit` と `.Update` を連発している。

データを書き換える必要がない、あるいは一括処理(UPDATEクエリなど)で済むはずの処理をVBAのループで回す場合、カーソルは必ず `dbOpenSnapshot` にすべきだ。

メモリ消費量とパフォーマンスの差は、データ量が数万件を超えたあたりから顕著になる。`dbOpenDynaset` がクライアントPCのメモリを圧迫し、スワップが発生してガタガタになる一方で、`dbOpenSnapshot` は最小限のメモリフットプリントで軽快に駆け抜ける。

3. 実践!堅牢で高速なプロダクションコード例

では、実際の業務アプリケーションでどのように書き分けるべきか。
ここでは、「大量データの読み込み・集計」「安全なトランザクション制御を伴う更新」の2つのパターンで、プロダクション品質のコードを提示する。

パターンA:参照・集計専用(圧倒的な速度と省メモリを誇るスナップショット)

Option Explicit

”’

”’ 大量データを高速に集計・参照するためのプロシージャ
”’ 読み取り専用のため dbOpenSnapshot を使用する
”’

Public Sub ProcessLargeVolumeData()
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim sql As String

‘ CurrentDbの乱用を防ぐため、変数に参照を保持する
Set db = CurrentDb

‘ 必要なカラムだけを絞り込んだSQL(SELECT は厳禁)
sql = “SELECT OrderID, CustomerID, OrderDate, Amount FROM T_Orders WHERE Processed = 0;”

‘ 【重要】更新しないことが確実な場合は dbOpenSnapshot を指定
Set rs = db.OpenRecordset(sql, dbOpenSnapshot, dbForwardOnly)

‘ dbForwardOnly を併用することで、前方移動のみの極限まで最適化されたカーソルになる

On Error GoTo ErrorHandler

If rs.EOF Then
MsgBox “処理対象のデータはありません。”, vbInformation
GoTo Cleanup
End If

‘ レコードが存在する場合の処理ループ
Do While Not rs.EOF
‘ データの読み取り(例)
‘ Debug.Print rs!OrderID

rs.MoveNext
Loop

MsgBox “データ処理が正常に完了しました。”, vbInformation

Cleanup:
‘ リソースの確実な解放
If Not rs Is Nothing Then rs.Close: Set rs = Nothing
If Not db Is Nothing Then Set db = Nothing
Exit Sub

ErrorHandler:
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical
Resume Cleanup
End Sub

パターンB:安全な更新処理(トランザクションとダイナセットの適切な運用)

どうしてもレコード単位で書き込みが必要な場合(複雑なビジネスロジックが絡む場合など)は、`dbOpenDynaset` を用いるが、トランザクションを必ず明示的に貼り、一連の処理の原子性(Atomicity)を担保する。

Option Explicit

”’

”’ 条件に応じてデータを更新する堅牢なトランザクション処理
”’

Public Sub UpdateDataWithTransaction()
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim sql As String
Dim isTransStarted As Boolean

Set db = CurrentDb
sql = “SELECT ID, Status, UpdatedDate FROM T_Target WHERE Status = ‘Pending’;”

‘ 更新を伴うため dbOpenDynaset を使用
Set rs = db.OpenRecordset(sql, dbOpenDynaset, dbDenyWrite)

On Error GoTo TransactionError

If rs.EOF Then Exit Sub

‘ トランザクションの開始
db.BeginTrans
isTransStarted = True

Do While Not rs.EOF
rs.Edit
rs!Status = “Completed”
rs!UpdatedDate = Now
rs.Update

rs.MoveNext
Loop

‘ トランザクションのコミット
db.CommitTrans
isTransStarted = False

MsgBox “更新処理が完了しました。”, vbInformation

Cleanup:
If Not rs Is Nothing Then rs.Close: Set rs = Nothing
If Not db Is Nothing Then Set db = Nothing
Exit Sub

TransactionError:
‘ エラー時は必ずロールバック
If isTransStarted Then
db.Rollback
End If
MsgBox “エラーが発生したためロールバックしました: ” & Err.Description, vbCritical
Resume Cleanup
End Sub

4. プロフェッショナルが守るべき設計の鉄則

最後に、Access VBAでパフォーマンスと堅牢性を極めるための指針をまとめる。

1. `CurrentDb` の複数回呼び出しを避ける
`CurrentDb` は呼び出すたびに新しいデータベースオブジェクトのインスタンスを生成する。ループ内で `CurrentDb.OpenRecordset` を書くなどはってのほかである。必ず変数に格納して使い回せ。
2. `SELECT ` を絶対に書かない
メモリ消費量を抑える最大の防御壁は「必要な列だけをSELECTすること」だ。ネットワーク経由(FE/BE分離構成)の場合、不要な列をフェッチするだけで数倍のパフォーマンス劣化を招く。
3. `dbForwardOnly` を使い倒せ
レコードセットを「上から順に一度だけ読み捨てる」ことが分かっているなら、`dbOpenSnapshot` と組み合わせて `dbForwardOnly` を指定せよ。Accessが内部で管理するレコードのインデックス構築すら省略され、最速のパフォーマンスを発揮する。

道具の仕様を正しく理解し、適材適所でカーソルを使い分けること。それこそが、現場のユーザーを「フリーズの恐怖」から解放し、真に信頼される業務システムを作り上げる唯一の道である。

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