【テクニカル・上級編】DAO.QueryDefの「再利用」がもたらす罠:キャッシュされた実行計画を強制的にリセットするコツ – Access VBA解析バイブル

スポンサーリンク

Access VBAの深淵:DAO.QueryDefの「呪縛」を解き、実行計画を強制リセットせよ

Accessのパフォーマンスチューニングにおいて、多くのエンジニアが陥る「見えない壁」がある。
「なぜか初回は速いのに、繰り返し実行すると劇的に遅くなる」「SQLを動的に書き換えているはずなのに、結果が古いまま(あるいは最適化されない)」。

もしあなたがDAO.QueryDefを単なる「SQLの入れ物」として扱っているなら、今すぐその認識を改めるべきだ。Access(Jet/ACEエンジン)は、あなたが想像する以上に執拗に「実行計画」をキャッシュする。このキャッシュが、巨大なデータセットや複雑な結合を伴う環境において、システムを窒息させる原因となる。

今日は、DAO.QueryDefのライフサイクルを掌握し、実行計画を強制的にリセットする「アーキテクトの作法」を伝授する。

—

1. なぜ「再利用」が罠となるのか

DAO.QueryDefオブジェクトは、名前を付けて `QueryDefs` コレクションに保存した時点で、ACEエンジンによってコンパイル・最適化される。

問題は、SQLを書き換えて `qdf.SQL = “…”` と更新した際だ。ACEエンジンはクエリの再コンパイルを試みるが、メモリ上の実行計画キャッシュが旧来の構造に固執し、最適化パスをスキップすることがある。 特に、パラメータの型やインデックスの分布が動的に変化するクエリでは、この「古い計画」が致命的なパフォーマンス劣化を招く。

2. 実行計画を「強制リセット」する技術

最も確実な手法は、「QueryDefを使い捨てのオブジェクトとして扱い、実行後に必ず物理的な削除と再作成を行う」ことだ。
コレクションに居座らせるのではなく、メモリ上で完結させ、スコープを抜ける際に完全に開放する。これが、大規模システムにおける鉄則である。

実践:動的SQLの最適化実装

以下は、実行計画の汚染を回避するためのテンプレートコードだ。

Public Sub ExecuteOptimizedQuery(ByVal strSQL As String)
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Const QUERY_NAME As String = “tmp_DynamicQuery”

Set db = CurrentDb

‘ 1. 同名クエリが存在する場合、キャッシュごと削除する
‘ これにより、前回の実行計画の残骸を完全に排除する
On Error Resume Next
db.QueryDefs.Delete QUERY_NAME
On Error GoTo 0

‘ 2. 一時的にクエリを作成
Set qdf = db.CreateQueryDef(QUERY_NAME, strSQL)

‘ 3. パラメータの明示的指定(型不一致による再コンパイル回避)
‘ ここで型を確定させることで、ACEエンジンの迷いを防ぐ
Dim prm As DAO.Parameter
For Each prm In qdf.Parameters
prm.Value = Eval(prm.Name)
Next

‘ 4. 実行
qdf.Execute dbFailOnError

‘ 5. 後処理:オブジェクトの解放とコレクションからの除外
qdf.Close
Set qdf = Nothing
db.QueryDefs.Delete QUERY_NAME
Set db = Nothing
End Sub

3. レガシー環境を救うメモリ管理の極意

この手法の肝は `db.QueryDefs.Delete` にある。
通常の開発者は `qdf.SQL` を書き換えるだけで満足するが、真のアーキテクトは「名前付きQueryDef」が持つメタデータキャッシュを信用しない。

さらに踏み込んだ最適化:Windows APIの活用

もし、非常に大規模なバッチ処理を行う場合、Accessのヒープメモリが断片化し、パフォーマンスが徐々に低下することがある。その際は、Windows APIを叩いてシステムメモリを明示的に解放するアプローチが有効だ。

‘ メモリの強制解放用API(標準モジュールに配置)
Private Declare PtrSafe Function SetProcessWorkingSetSize Lib “kernel32” _
(ByVal hProcess As LongPtr, ByVal dwMinimumWorkingSetSize As LongPtr, _
ByVal dwMaximumWorkingSetSize As LongPtr) As Long

Public Sub FlushAccessMemory()
‘ Access自身のプロセスハンドルのメモリ使用量を最小化し、OSにキャッシュを返還させる
SetProcessWorkingSetSize -1, -1, -1
End Sub

※大規模バッチのループ終了後にこの `FlushAccessMemory` を呼ぶことで、OSレベルでのメモリ再割り当てを強制できる。

4. アーキテクトからの提言

「コードが動く」ことと「システムが健全に回り続ける」ことは別次元の話だ。
DAO.QueryDefを使い回すことは、一見するとリソース節約に見えるかもしれない。しかし、複雑なSQLが絡み合うシステムにおいて、それは「時限爆弾」を抱えるようなものだ。

  • 動的SQLは、常に「生成→実行→抹消」のライフサイクルで管理せよ。
  • パラメータは推論に頼らず、型を明示的に注入せよ。
  • キャッシュが信頼できないときは、物理的な削除を恐れるな。

これが、レガシーを制御し、現代の過酷なデータ処理要求に応えるための唯一の道である。コードは短ければ良いわけではない。その背後にある「エンジンがどう動くか」を透かして見ることが、エンジニアとしての真の生存戦略となる。

君たちのシステムが、今日からより速く、より堅牢になることを期待している。

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