【中級】DAO.Recordsetの「dbOpenSnapshot」と「dbOpenDynaset」:メモリ消費と速度のトレードオフ
開発現場でよく見かける光景がある。「とりあえず `dbOpenDynaset` を使っておけば間違いない」という思い込みだ。
データの集計、レポートの出力、他システムへの連携データのエクスポート――。画面上でユーザーがレコードを編集・追加するわけでもないのに、すべての `OpenRecordset` に Dynaset を指定している。もし君のプロジェクトでこれを行っているなら、Accessデータベースのエンジン(ACE/Jet)に不必要な負荷をかけ、パフォーマンスをドブに捨てていると言わざるを得ない。
今回は、DAO(Data Access Objects)における `dbOpenSnapshot` と `dbOpenDynaset` の本質的な違い、メモリと速度のトレードオフ、そして実務で迷わないための選択基準を、チーフアーキテクトの視点からロジカルに解説する。
—
1. 内部挙動の理解:なぜ Snapshot は速いのか?
まずは、それぞれのオブジェクトがメモリ上で何をしているのかを知る必要がある。ここを理解していれば、迷うことはなくなる。
`dbOpenDynaset`(ダイナセット)
- 挙動: ライブな(生きた)レコードセット。テーブルやクエリのデータ変更がリアルタイムに反映される。
- コスト: マルチユーザー環境での同時実行制御(ロッキング)や、データの変更を追跡するためのオーバーヘッドが常時発生する。特に複雑なJOINや集計を含むクエリに対してDynasetを開くと、Jet/ACEエンジンは膨大な一時ファイルを生成し、メモリとCPUを激しく消費する。
`dbOpenSnapshot`(スナップショット)
- 挙動: 開いた瞬間のデータを静的な静止画(スナップショット)としてメモリ上に焼き付ける。
- コスト: データを読み込む一瞬のコストはあるが、一度メモリに展開してしまえば、他者の変更に影響されない。ロックの管理も不要なため、エンジンへの負荷が極めて低い。結果として、読み取り専用の処理においては圧倒的なパフォーマンスを発揮する。
—
2. 実務における選択基準:鉄のルール
開発現場の設計指針として、以下のルールをチームに徹底してほしい。
1. 「書き込み(編集・追加・削除)」が発生しない処理は、100% `dbOpenSnapshot` を採用する。
- マスターデータの参照、ログの集計、帳票印刷のためのデータ抽出、Excel/CSVへのエクスポートなど。
2. 「ユーザーが画面やコード経由でデータを更新する」場合のみ、`dbOpenDynaset` を採用する。
- ただし、レコード数が数万件を超えるような巨大なテーブルに対して、不必要に Dynaset を開くのは設計ミスである。更新対象のレコードを絞り込む(WHERE句を厳格にする)ことが大前提となる。
3. 単一レコードの参照や高速な存在チェックには `dbOpenTable` を検討する。
- インデックスが効いているテーブルであれば、テーブル直接オープンが最速だが、適用範囲が狭いため本記事では割愛する。
—
3. 【プロダクションコード】堅牢かつ高速なデータ処理実装例
実務でそのまま使える、エラーハンドリングとリソース解放を完璧に押さえたコード例を提示する。
このコードは、「読み取り専用の重い集計・エクスポート処理」を想定し、`dbOpenSnapshot` を用いてメモリ効率を極限まで高めたものである。
Option Compare Database
Option Explicit
‘ =========================================================================
‘ 処理名 : ExportActiveOrdersToLog
‘ 概要 : 未完了の受注データをスナップショットで高速に読み込み、処理を行う
‘ 引数 : なし
‘ 戻り値 : 成功時はTrue、失敗時はFalse
‘ =========================================================================
Public Function ExportActiveOrdersToLog() As Boolean
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim strSQL As String
Dim recordCount As Long
On Error GoTo ErrorHandler
‘ 現在のデータベース参照を取得
Set db = CurrentDb()
‘ 抽出用SQLの構築(インデックスが効く設計にしておくこと)
strSQL = “SELECT OrderID, CustomerID, OrderDate, TotalAmount ” & _
“FROM T_Orders ” & _
“WHERE Status = ‘Processing’ ” & _
“ORDER BY OrderDate DESC;”
‘ 【重要】編集を行わないため、dbOpenSnapshotを指定する。
‘ これにより、不要なロックオーバーヘッドとメモリ消費を回避する。
Set rs = db.OpenRecordset(strSQL, dbOpenSnapshot, dbReadOnly)
‘ レコードが存在しない場合のガード
If rs.EOF Then
MsgBox “対象となるデータはありません。”, vbInformation, “情報”
GoTo CleanUp
End If
‘ レコードカウントの取得(Snapshotなら正確なRecordCountが即座に取れるケースが多い)
rs.MoveLast
recordCount = rs.RecordCount
rs.MoveFirst
Debug.Print “処理開始: 対象レコード数 = ” & recordCount & ” 件”
‘ データ処理ループ
Do While Not rs.EOF
‘ —————————————————————–
‘ ここにビジネスロジック(ログ出力や別テーブルへの書き込み等)を記述
‘ 例: Debug.Print rs!OrderID
‘ —————————————————————–
rs.MoveNext
Loop
Debug.Print “処理完了: 正常終了”
ExportActiveOrdersToLog = True
CleanUp:
‘ ———————————————————————
‘ リソースの確実な解放(メモリリークの防止)
‘ ———————————————————————
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
Set db = Nothing
Exit Function
ErrorHandler:
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“エラー内容: ” & Err.Description, vbCritical, “システムエラー”
ExportActiveOrdersToLog = False
Resume CleanUp
End Function
コードのアーキテクチャ的解説
- `dbReadOnly` オプションの併用:
`dbOpenSnapshot` と同時に `dbReadOnly` を明示的に指定することで、コードの意図(「絶対にこのデータは書き換えない」)を明確にし、エンジンへの無駄な書き込み権限要求をカットする。
- 厳格なリソース解放 (`CleanUp` パターン):
VBAのガベージコレクションに頼らず、異常系・正常系問わず `CleanUp` ラベルへジャンプして `Recordset` と `Database` を確実に解放している。Access VBAにおいて、これを怠るとファイルサイズが肥大化(Bloat現象)する主原因となる。
—
4. まとめ:プロフェッショナルとしての誇り
「動けばいい」というコードは、データ量が数千件のうちは許されるかもしれない。しかし、運用フェーズに入り、データが数十万件に膨れ上がり、複数ユーザーが同時にアクセスし始めた瞬間、そのコードはシステムのボトルネックとして牙をむく。
`dbOpenSnapshot` を適切に使い分けることは、単なるチューニングのテクニックではない。データベースという共有リソースに対する敬意であり、プロフェッショナルなエンジニアリングの証明である。
今日の設計から、すべての「読み取り処理」におけるカーソルタイプを見直してほしい。アプリケーションの挙動が見違えるほど軽快になるはずだ。
