【テクニカル・上級編】DAO.Recordsetの「Filter」プロパティを使い、クエリを作成せずにデータを絞り込む高速化手法 – Access VBA解析バイブル

スポンサーリンク

DAO.Recordset「Filter」プロパティの極限活用:一時クエリを根絶し、メモリ上で爆速データ抽出を実現する技術

レガシーシステムの全盛期から現代に至るまで、Microsoft Accessは中小規模から大企業の現場業務までを支える基幹の要として君臨し続けている。
しかし、そのパフォーマンスの限界に直面したとき、多くの開発者が犯す最大の過ちがこれだ。

「条件分岐のたびに、一時クエリ(QueryDef)を動的に生成・破棄する」

このアプローチは、Accessの内部エンジン(Jet / ACE)に無駄な構文解析とシステムカタログへの書き込みを強要し、ネットワークやストレージI/Oのボトルネックを引き起こす。結果として、アプリケーション全体のレスポンスは雪だるま式に悪化していく。

真に洗練されたチーフアーキテクトであれば、データベースの物理的な往復を最小限に抑え、メモリ上で完結するアーキテクチャを選ぶ。今回は、`DAO.Recordset` の `Filter` プロパティと `OpenRecordset`(Cloneメソッド)を組み合わせ、クエリの生成コストを完全にゼロにする極限の高速化手法を解き明かす。

1. なぜ「動的クエリ生成」は悪なのか?

フォームやレポートでユーザーが条件を指定するたびに、以下のようなコードを書く者が後を絶たない。

‘ 【アンチパターン】毎回QueryDefをいじる愚行
Dim qdf As DAO.QueryDef
Set qdf = CurrentDb.QueryDefs(“qryDynamicSearch”)
qdf.SQL = “SELECT FROM T_Orders WHERE Status = ‘” & Me.txtStatus & “‘”
Set Me.Recordset = qdf.OpenRecordset(dbOpenDynaset)

このコードの何が問題か。
1. システムカタログの肥大化: `QueryDef` のSQL書き換えは、内部的にAccessのシステムテーブル(MSysObjects等)への書き込みが発生する。これを高頻度で行うと、データベースファイルのフラグメンテーション(断片化)が急速に進む。
2. プランの再構築コスト: AccessデータベースエンジンはSQL文を受け取るたびに、クエリの実行プラン(Query Plan)を再計算する。このオーバーヘッドは、数千件程度のテーブル操作であっても確実に体感速度を落とす。

メモリ上で完結する「Filter」という名の解

テーブルまたはベースとなるクエリから一度だけレコードセットをメモリ上に読み込み、以降の絞り込みはすべて `Recordset.Filter` プロパティを使ってメモリ空間内で行う。これが、I/Oコストを限界まで削ぎ落す唯一にして最善の解である。

2. 【実装コード】極限のパフォーマンスを発揮するDAOフィルタリング

以下のコードは、フォーム上に配置された大規模なレコードセットに対し、クエリを一切生成せず、ミリ秒単位のレスポンスでデータを絞り込む実用的なプロシージャである。

Option Compare Database
Option Explicit

‘ クラスモジュールまたはフォームモジュールレベルでの保持
Private m_rstBase As DAO.Recordset
Private m_rstFiltered As DAO.Recordset

‘ =====================================================================
‘ @Title: 初期データロード(一度だけ物理I/Oを発生させる)
‘ =====================================================================
Public Sub InitializeOrderData()
On Error GoTo ErrorHandler

Dim db As DAO.Database
Set db = CurrentDb

‘ ベースとなるレコードセットをダイナセットでメモリ上に展開
‘ ※データ量が膨大な場合は必要なカラムとWHERE句に絞ること
Set m_rstBase = db.OpenRecordset( _
“SELECT OrderID, CustomerID, OrderDate, Status, TotalAmount ” & _
“FROM T_Orders WHERE OrderDate >= #2023-01-01#”, _
dbOpenDynaset, dbDenyWrite) ‘ 読み取り専用でロック競合を回避

Exit Sub

ErrorHandler:
MsgBox “初期化エラー: ” & Err.Description, vbCritical
End Sub

‘ =====================================================================
‘ @Title: フィルタ適用とフォームへのバインド(メモリ上での高速絞り込み)
‘ @Param: strStatus 抽出するステータス
‘ =====================================================================
Public Sub ApplyFilterToView(ByVal strStatus As String)
On Error GoTo ErrorHandler

‘ ベースレコードセットが未初期化の場合は弾く
If m_rstBase Is Nothing Then Exit Sub

‘ 既存のフィルタ済みレコードセットがあれば確実に解放(メモリリーク防止)
If Not m_rstFiltered Is Nothing Then
m_rstFiltered.Close
Set m_rstFiltered = Nothing
End If

‘ Cloneメソッドでレコードセットのポインタ(参照)を複製
Set m_rstFiltered = m_rstBase.Clone()

‘ Filterプロパティの設定(SQLのWHERE句と同じ構文)
If Len(Trim(strStatus)) > 0 Then
m_rstFiltered.Filter = “Status = ‘” & Replace(strStatus, “‘”, “””) & “‘”

‘ フィルタ条件を適用した実体を子レコードセットとして取得
Set m_rstFiltered = m_rstFiltered.OpenRecordset()
End If

‘ フォームのRecordsetに直接割り当て(描画の高速化)
Set Me.Recordset = m_rstFiltered

Exit Sub

ErrorHandler:
MsgBox “フィルタ適用エラー: ” & Err.Description, vbCritical
End Sub

‘ =====================================================================
‘ @Title: 終了時のクリーンアップ
‘ =====================================================================
Private Sub Form_Close()
‘ オブジェクトの明示的解放(VBAのガベージコレクションを信用するな)
If Not m_rstFiltered Is Nothing Then
m_rstFiltered.Close
Set m_rstFiltered = Nothing
End If

If Not m_rstBase Is Nothing Then
m_rstBase.Close
Set m_rstBase = Nothing
End If
End Sub

3. チーフアーキテクトが教える「死角なき」実装の極意

上記のコードを現場に投入するにあたり、シニアエンジニアとして知っておくべき「VBAの暗部」と最適化のノウハウを共有する。

① `Clone` メソッドの優位性

`m_rstBase.Clone()` は、テーブルからデータを再読み込みするのではなく、メモリ上に展開された既存のレコードセットの「ビュー(視点)」を複製する。これにより、メモリ消費量を最小限に抑えつつ、独立したカレントレコードの位置やフィルタを持つことができる。

② 明示的なメモリ解放(Object Lifecycle Management)の徹底

VBAのランタイムは、プロシージャ終了時に変数を自動解放すると言われているが、DAOのRecordsetオブジェクトに関してはその神話を信じてはならない。
Jet / ACEエンジンが保持するネイティブヒープ上のメモリは、プロシージャが終了しても即座に解放されず、Accessのプロセスが肥大化する原因(メモリリーク)になる。必ず `Close` メソッドを呼び出した上で `Set xxx = Nothing` で参照を切断すること。

③ SQLインジェクションおよび構文エラー対策

`Filter` プロパティに渡す文字列内でシングルクォート(`’`)が含まれている場合、構文エラーが発生する。上記のコード例のように `Replace(strStatus, “‘”, “””)` によるエスケープ処理を挟むか、パラメータクエリの思想を取り入れた堅牢な文字列構築を徹底すること。

4. 限界を超える:さらにパフォーマンスを追求する者へ

もし、扱うデータが数十万レコードを超え、メモリ上のフィルタリングであってもわずかなもたつきを感じる場合は、以下のアーキテクチャ上のハックを検討せよ。

  • Indexの最適化: ベースとなる `OpenRecordset` の元ネタとなるテーブル側で、検索対象となるフィールド(今回の例では `Status` や `OrderDate`)に必ずインデックスを貼っておくこと。DAOはメモリ上であっても、インデックス構造を利用して高速にヒット位置を特定する。
  • クライアントサイド・キャッシュ: 頻繁に参照されるマスターデータなどは、アプリケーション起動時に一度だけローカルのDAO.Recordset(またはメモリ上のDisconnected Recordset)に読み込み、ネットワークトラフィックやディスクI/Oを完全に断つ「オフラインファースト」の設計が、レガシーAccessシステムを現代のWebアプリ並みに疾走させる究極の鍵となる。

安易なクエリ生成に頼る時代は終わった。
オブジェクトのライフサイクルを支配し、メモリのフローを制御する者だけが、Access VBAの真のパフォーマンスを引き出すことができるのだ。

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