【テクニカル・上級編】QueryDefの実行結果を「非連結フォーム」のレコードソースに動的バインドする際の最適化手法 – Access VBA解析バイブル

スポンサーリンク

序論:なぜあなたのAccessフォームは遅延するのか

Microsoft Accessが四半世紀以上にわたり基幹業務を支え続けている理由は、そのラピッドプロトタイピング性能にある。しかし、業務規模の拡大とデータ量の爆発に伴い、多くの社内システムは例外なく「画面遷移の引っ掛かり」「フォーム描画時のプチフリーズ」という壁に突き当たる。

その主因は、Access初学者が無批判に採用する「テーブルや重厚なビューを直接バインドした連結フォーム」と「文字列連結による`Form.RecordSource`への動的SQL代入」にある。

`Me.RecordSource = “SELECT FROM …”` という代入文を実行した瞬間、ACE(Access Connectivity Engine / 旧Jet)の内部では以下の重篤な処理が同期的に走る。

1. 文字列SQLの構文解析(パーシング)
2. 実行計画(クエリプラン)のゼロベース再生成
3. コントロール群と全フィールドの動的再バインディング
4. 不要な列情報を含む全メタデータの走査とクライアントメモリへの展開

このオーバーヘッドを排除し、マイクロ秒単位で応答するエンタープライズグレードのフォームを構築するには、「非連結フォーム」を基盤とし、インメモリの一時QueryDef(Query Definition)でコンパイル済みの実行プランを生成したうえで、`DAO.Recordset`を`Form.Recordset`へ直結バインドする設計が不可欠となる。

本稿では、COMオブジェクトの厳密なライフサイクル管理、高精度Windows APIによる性能プロファイリング、そしてACEの内部挙動を考慮した「極限の最適化手法」を提示する。

—

1. 文字列SQL直接代入 vs 一時QueryDef直接バインドのアーキテクチャ差

フォームのソース更新において、`RecordSource`へのSQL代入と`QueryDef`経由の`Recordset`バインドでは、下位レイヤーで生じている処理が根本から異なる。

| 評価軸 | `Me.RecordSource = strSQL` | 一時QueryDef + `Set Me.Recordset = rs` |
| :— | :— | :— |
| 実行プランの生成 | 実行ごとに毎回インタープリタ的に完全解析 | コンパイルされたネイティブプランを即時実行 |
| システムカタログへの影響 | `MSysObjects`/`MSysQueries`の内部スワップを誘発 | メモリ上(インメモリ)のみで完結しカタログ非破壊 |
| 型解決(Type Resolution) | 文字列からの暗黙的キャストに依存 | `DAO.DataTypeEnum`による明示的かつ即時の解決 |
| SQLインジェクション対策 | 自前エスケープ必須(脆弱性の温床) | パラメーターオブジェクトによる完全なデータ分離 |
| 描画開始タイミング | 全メタデータ同期完了までUIブロック | カソルオープン後、即座にウィンドウハンドル描画 |

最も重要な点は、名前を持たない「一時QueryDef(Name引数に空文字 `””` を指定)」を生成する手法だ。データベースファイル(.accdb/.mdb)のシステムテーブルを一切汚染することなく、純粋なインメモリQueryDefオブジェクトとしてACE内部の最適化エンジンを駆動できる。

—

2. 実装:QueryPerformanceCounterによるマイクロ秒プロファイリングと動的バインド

以下に、実戦投入可能な最適化モジュールを示す。計測にはミリ秒精度の`Timer`関数ではなく、ハードウェアクロックに直結するWindows API `QueryPerformanceCounter`を使用し、ナノ秒単位のジッターまで可視化する。

2.1. API宣言と最適化バインドコアロジック

標準モジュール(例:`mod_FormOptimizer`)またはフォームの宣言セクションに記述する。

Option Compare Database
Option Explicit

‘ ==============================================================================
‘ 高精度システムタイマー (Windows API)
‘ ==============================================================================
If VBA7 Then
Private Declare PtrSafe Function QueryPerformanceCounter Lib “kernel32” ( _
ByRef lpPerformanceCount As Currency) As Long
Private Declare PtrSafe Function QueryPerformanceFrequency Lib “kernel32” ( _
ByRef lpFrequency As Currency) As Long
Else
Private Declare Function QueryPerformanceCounter Lib “kernel32” ( _
ByRef lpPerformanceCount As Currency) As Long
Private Declare Function QueryPerformanceFrequency Lib “kernel32″ ( _
ByRef lpFrequency As Currency) As Long
End If

‘ ==============================================================================
‘ 構造体/定数定義
‘ ==============================================================================
Private Const DB_FAIL_ON_ERROR As Long = &H80

”’

”’ 非連結フォームに対し、最適化された一時QueryDef経由でDAO.Recordsetを高速バインドする
”’

”’ 対象フォームインスタンス(Meを渡す) ”’ 抽出パラメーター:部門ID ”’ 抽出パラメーター:基準日 Public Sub BindOptimizedRecordset( _
ByRef targetForm As Access.Form, _
ByVal deptId As Long, _
ByVal targetDate As Date)

Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim rs As DAO.Recordset
Dim strSQL As String

‘ 計測用変数
Dim curFreq As Currency
Dim curStart As Currency
Dim curEnd As Currency
Dim dblElapsedMs As Double

QueryPerformanceFrequency curFreq
QueryPerformanceCounter curStart

On Error GoTo Err_Handler

‘ 1. カレントデータベースのポインタ取得
‘ ※ DBEngine(0)(0) は CurrentDb よりも高速だが、
‘ スコープ管理を安全にするためここでは CurrentDb を明示取得
Set db = CurrentDb()

‘ 2. 射影(PROJECTION)と選択(SELECTION)の極限化
‘ SELECT は厳禁。フォームの表示・計算に必要な最小限の列のみを列挙する。
‘ PARAMETER句を厳格に定義し、ACEのクエリパーサーに型を直接教え込む。
strSQL = “PARAMETERS prmDeptId LONG, prmTargetDate DATETIME; ” & vbCrLf & _
“SELECT ” & _
” E.EmpID, ” & _
” E.EmpName, ” & _
” E.Salary, ” & _
” E.JoinedDate ” & _
“FROM tbl_Employees AS E ” & _
“WHERE E.DepartmentID = [prmDeptId] ” & _
” AND E.JoinedDate <= [prmTargetDate] " & _ "ORDER BY E.EmpID ASC;" ' 3. 一時QueryDefの生成(第1引数に空文字列を指定することでインメモリ生成) Set qdf = db.CreateQueryDef("", strSQL) ' 4. パラメーターのバインド(型キャストオーバーヘッドの完全排除) qdf.Parameters("prmDeptId").Value = deptId qdf.Parameters("prmTargetDate").Value = targetDate ' 5. 最適なカーソルタイプによるRecordsetオープン ' 更新可能なサブフォーム/単票の場合は dbOpenDynaset ' 参照専用であれば dbOpenSnapshot + dbForwardOnly が最高速だが、 ' フォームのRecordsetプロパティには双方向スクロール可能なカーソルが必須。 Set rs = qdf.OpenRecordset(dbOpenDynaset, dbSeeChanges) ' 6. フォームへのRecordset直接バインド ' RecordSource文字列ではなく、ACEメモリ空間上のRecordsetインターフェースを直接渡す Set targetForm.Recordset = rs QueryPerformanceCounter curEnd dblElapsedMs = ((curEnd - curStart) / curFreq) 1000# Debug.Print "[PERF] Recordset Binding Completed: " & _ Format$(dblElapsedMs, "0.000") & " ms" Exit_Handler: ' -------------------------------------------------------------------------- ' オブジェクトの完全なカスケード解放 ' -------------------------------------------------------------------------- ' targetForm.Recordset にアサインされたRecordsetはフォーム側で参照カウントが ' 保持されるため、ローカルポインタは安全にデリファレンス(Nothing)可能。 If Not rs Is Nothing Then rs.Close Set rs = Nothing End If If Not qdf Is Nothing Then qdf.Close Set qdf = Nothing End If If Not db Is Nothing Then Set db = Nothing End If Exit Sub Err_Handler: Debug.Print "[CRITICAL] Error " & Err.Number & ": " & Err.Description ' エラー発生時は即座にリソースを解放して上位にエラーを投げる Resume Exit_Handler End Sub ---

3. 非連結フォームのコントロール最適化と遅延バインド

フォーム側の初期設計も徹底する必要がある。フォームのデザインビューにおいて、以下のプロパティを事前に固定しておくこと。

フォーム側の設定

  • レコードソース(RecordSource): 空白(完全な非連結状態)
  • レコードロック(RecordLocks): 編集済みレコード(No Locks)
  • 読み込み時取得(FetchDefaults): いいえ(False)

コントロール側の設定(テキストボックス等)

  • 各コントロールの `コントロールソース(ControlSource)` には、SQLで射影した列名(例: `EmpID`, `EmpName`)をあらかじめ静的に設定しておく。

[UIレイヤー: コントロール]
txtEmpID.ControlSource –> “EmpID”
txtEmpName.ControlSource –> “EmpName”
▲
│ (Set Me.Recordset = rs が発行された瞬間に自動接続)
│
[ACEレイヤー: DAO.Recordset]
Fields(“EmpID”)
Fields(“EmpName”)

非連結フォームにコントロールソースだけを静的に埋め込んでおくと、デザイン時およびロード直後は一時的に `#Name?` や空白となるが、`Set Me.Recordset = rs` がコールされた瞬間に、コントロールは再描画イベントを最小限に抑えつつ、一瞬で値に吸着(バインド)する。

このアプローチにより、Accessがフォームオープン時に全テーブルを走査してメタデータをフェッチする「暗黙の初期通信」を100%遮断できる。

—

4. エンタープライズ運用の深淵:メモリリークの遮断とODBC環境の最適化

大規模運用および基盤保守において、シニアエンジニアが担保すべき2つの重要項目を解説する。

4.1. フォーム破棄時の循環参照(Circular Reference)問題

フォームの `Recordset` プロパティに外部生成した `DAO.Recordset` を代入した場合、フォームがアンロードされた後も内部COM参照がリークし、Accessプロセスのコミットチャージが単調増加し続けるケースが存在する。

これを防ぐため、フォームの `Form_Close` イベントで参照を明示的に切断するコードを必ず組み込む。

Private Sub Form_Close()
On Error Resume Next
‘ フォームのRecordset参照を切断し、内部COM参照カウンタを正常にデクリメントさせる
Set Me.Recordset = Nothing
On Error GoTo 0
End Sub

4.2. SQL Serverバックエンド(ODBCリンク)時のパススルーQueryDef

バックエンドがSQL ServerやAzure SQL Databaseの場合、ACEのクエリエンジンを介在させること自体がネットワークトラフィックの無駄となる。

ODBC接続環境下では、一時QueryDefをパススルーモード(`Connect`プロパティを設定)へ切り替える。

‘ パススルー用の一時QueryDef構築例
Set qdf = db.CreateQueryDef(“”)
qdf.Connect = “ODBC;DRIVER={ODBC Driver 18 for SQL Server};SERVER=srv-sql01;DATABASE=ProductionDB;Trusted_Connection=Yes;Encrypt=Yes;”
qdf.SQL = “EXEC dbo.usp_GetDepartmentEmployees @DeptID = ” & deptId & “;”
qdf.ReturnsRecords = True

‘ パススルー結果をスナップショットとして受け取りフォームへ流し込む
Set rs = qdf.OpenRecordset(dbOpenSnapshot, dbSQLPassThrough)
Set Me.Recordset = rs

これにより、T-SQLのコンパイルおよび実行はSQL Server側の強力なリソースで完結し、Accessクライアントは返却されたストリームをそのままコントロールに描画する「純粋なGUIクライアント」へと昇華する。

—

結論:レガシーの皮を被った超高速エンジンの運用

Accessが遅いのではない。Accessに過剰な仕事を同期的に強要するコードが遅いのだ。

1. 連結フォームの「暗黙のフルスキャン」を捨て、非連結フォームをベースとする。
2. 文字列SQLの再代入を廃止し、インメモリの一時QueryDef(`CreateQueryDef(“”, …)`) で型安全な実行計画を固定化する。
3. `Form.RecordSource` ではなく、`Form.Recordset` への直接参照注入を行う。
4. コントロールの `ControlSource` を静的マッピングし、描画パイプラインのフリッカー(ちらつき)と再評価を完全に抑え込む。

これらの原則を徹底することで、何十万行ものレコードを抱える基幹サブシステムであっても、Webネイティブの最新SPAフレームワークに劣らないミリ秒単位のレスポンスを実現できる。アーキテクチャの根底を理解し、ランタイムの挙動を支配することこそが、レガシーを真の資産へと変える技術者の責務である。

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