【テクニカル・上級編】DAO.QueryDefを利用した「非連結フォーム」へのデータバインド高速化テクニック – Access VBA解析バイブル

スポンサーリンク

Access VBAを掌握する極限の知見:DAO.QueryDefと非連結フォームによる限界突破のデータバインド高速化

Accessによる大規模業務システム開発において、避けて通れない壁が「パフォーマンスの劣化」である。
特に、数万件以上のレコードを持つテーブルや複雑なJOINを含むクエリを、そのままフォームの `RecordSource` にバインドした瞬間、Jet/ACEエンジンは悲鳴を上げ、ネットワークトラフィックとローカルメモリを無駄に消費する。フォームの初期表示に数秒の遅延が発生し、実用に耐えないレガシーシステムへと成り下がる。

このボトルネックを根本から粉砕し、UIの応答速度を限界まで引き上げるための最終解が 「DAO.QueryDefを活用したパラメータ化と、非連結フォームへの手動データローディング(配列展開)」 である。

本稿では、一般的な解説書が触れないオブジェクトのライフサイクル、ACEエンジンのキャッシュ機構、そしてメモリ管理の極限領域に踏み込み、真にプロフェッショナルな開発者のための実装パターンを提示する。

—

1. なぜ「連結フォーム」は大規模データで破綻するのか

Accessフォームの `RecordSource` に直接テーブル名やSELECT文を指定した場合、以下のオーバーヘッドが常時発生する。

  • 暗黙の全件フェッチ: フォームを開いた瞬間、Jet/ACEエンジンはビューポート(画面上の表示領域)に収まらないレコードも含めてクライアント側へデータを引き込もうとする。
  • オブザーバーパターンの負荷: 各コントロールがレコードセットの変更を監視し続けるため、ナビゲーションやフィルタリングの度に不要なCPUサイクルが消費される。
  • トランザクションとロックの呪縛: フォームが開きっぱなしの間、不要な共有ロックが維持され、他ユーザーの更新を阻害する。

これを解決するため、「非連結フォーム(Unbound Form)」を採用し、必要なデータだけを `DAO.QueryDef` 経由で取得してメモリ上に展開するアーキテクチャへとシフトする。

—

2. アーキテクチャの全体像:QueryDefのパラメータ最適化

動的SQLをその都度 `CurrentDb.OpenRecordset` で実行するアプローチは、Jet/ACEエンジンに毎回実行プラン(Execution Plan)の再コンパイルを強いるため、極めて非効率である。

あらかじめデータベースコンテナ内に永続的な `QueryDef` オブジェクトを作成し、パラメータ(Parameters)コレクションを明示的に定義しておく。これにより、エンジンは実行プランをキャッシュし、2回目以降の呼び出し速度が劇的に向上する。

事前準備:最適化されたパラメータ・クエリの作成

Accessのクエリデザイナ、またはイミディエイトウィンドウ等で以下のSQLを `qryGetTargetData` として保存しておく。

PARAMETERS prmCustomerID Long, prmBaseDate DateTime;
SELECT OrderID, OrderDate, ProductCode, Quantity, UnitPrice
FROM tblOrders
WHERE CustomerID = [prmCustomerID] AND OrderDate >= [prmBaseDate]
ORDER BY OrderDate DESC;

—

3. 実装コード:DAO.QueryDefと配列による非連結フォーム高速化

以下に、実務で即座に使える極限最適化されたVBAコードを示す。
Recordsetのレコードを `GetRows` メソッドを用いて一括して二次元配列(Variant)に流し込み、非連結のリストボックスやコントロール群へ一気に転送する。これにより、ループ処理によるCOMの往復(Marshal)コストを排除する。

Option Compare Database
Option Explicit

‘ ==============================================================================
‘ モジュール名: フォームデータローダー
‘ 概要: DAO.QueryDefとGetRowsを用いた超高速データバインド実装
‘ ==============================================================================

Public Sub LoadDataToUnboundForm(frm As Form, lngCustomerID As Long, dtmBaseDate As Date)
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim rst As DAO.Recordset
Dim vntData As Variant

‘ タイマー計測開始(パフォーマンス測定用)
Dim sngTime As Single
sngTime = Timer

On Error GoTo ErrorHandler

‘ 1. カレントデータベースの参照を取得(CurrentDbの乱用を防ぐため変数保持)
Set db = CurrentDb()

‘ 2. 永続QueryDefの取得
Set qdf = db.QueryDefs(“qryGetTargetData”)

‘ 3. パラメータの明示的なバインド(型安全性の確保と実行プランのキャッシュ利用)
qdf.Parameters(“prmCustomerID”).Value = lngCustomerID
qdf.Parameters(“prmBaseDate”).Value = dtmBaseDate

‘ 4. レコードセットのオープン(ForwardOnly, ReadOnlyで最速化)
‘ ※ dbOpenForwardOnly と dbReadOnly の組み合わせがパフォーマンスの極み
Set rst = qdf.OpenRecordset(dbOpenForwardOnly, dbReadOnly)

‘ 5. データが存在するかチェック
If rst.EOF Then
MsgBox “対象データが存在しません。”, vbInformation, “情報”
GoTo Cleanup
End If

‘ 6. GetRowsによるメモリ一括取得(COM境界を1度だけ跨ぐことで爆速化を実現)
‘ 引数に -1 を指定することで、全レコードを一括取得
vntData = rst.GetRows(-1)

‘ 7. 非連結コントロール(例: リストボックス)への一括流し込み
With frm.lstResults
.RowSourceType = “Value List”
.RowSource = “” ‘ 一旦クリア

‘ 列数の設定(クエリのフィールド数に合わせる)
.ColumnCount = UBound(vntData, 1) + 1

‘ 配列を直接バインド(ColumnDataに二次元配列を渡す内部最適化を利用)
‘ ※大量データの場合は、独自文字列構築やAPI連携へ拡張可能
.Column(0) = vntData ‘ 簡易的な割当(実際にはデータ数に応じた文字列化が必要な場合あり)
End With

‘ ※実務での大規模データ表示の際は、vntDataをループまたはAPI経由で
‘ ListViewや仮想リスト、あるいはテンポラリテーブルへ高速バルクインサートする手法も有効。

Debug.Print “データロード完了処理時間: ” & (Timer – sngTime) & ” 秒”

Cleanup:
‘ 8. オブジェクトの明示的かつ厳格な解放(メモリリークの完全防止)
If Not rst Is Nothing Then
rst.Close
Set rst = Nothing
End If
Set qdf = Nothing
Set db = Nothing
Exit Sub

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

—

4. チーフアーキテクトが教える「さらなる極限領域」の知見

ここまでの実装でも十分高速であるが、エンタープライズ環境や極限のレスポンスが求められる現場では、さらに以下のテクニックを組み合わせる。

① `CurrentDb` の乱用禁止とオブジェクト変数のスコープ管理

`CurrentDb` プロパティは、呼び出すたびに新しい `Database` オブジェクトのインスタンスをメモリ上に生成する(内部でCOMの `GetDatabase` を叩いている)。これをループ内や頻繁に実行されるプロシージャで呼ぶと、メモリリークやJetエンジンのリソース枯渇を招く。
必ず冒頭で `Set db = CurrentDb()` として変数に保持し、処理終了時に `Set db = Nothing` で解放すること。

② メモリ最適化とガベージコレクションのタイミング

VBAのランタイムは参照カウンタ方式をとっているため、`Set obj = Nothing` を怠ると、フォームを閉じてもメモリ上にオブジェクトが残存し、Accessのプロセスサイズが肥大化する(いわゆる「メモリ肥大化バグ」)。
特に `GetRows` で巨大なバリアント型配列(Variant Array)をメモリ上に展開した後は、不要になった時点で `Erase vntData` を実行し、即座にヒープ領域を解放する習慣をつけよ。

③ システム間連携(外部SQL Server等)との融合

もしバックエンドがSQL Server等のRDBMSである場合(ADPが廃止された現代においてはLinked Table、またはODBCDirect/ADODBによる接続)、Accessのローカルエンジンに処理をさせず、Pass-Throughクエリ または ADOCommand + Stored Procedure を `QueryDef` の代わりに、あるいは同様のアーキテクチャで非連結フォームに流し込むべきだ。
ネットワーク帯域の負荷を最小限にし、データベースサーバー側のインデックスを100%活用した超高速レスポンスを実現できる。

—

総括

Access VBAは「おもちゃの言語」などではない。その内部構造(DAOとJet/ACEエンジン)のライフサイクルを完全に理解し、リソースの生成・破棄をエンジニアが手動で完全にコントロール下置くことで、C#やVB.NET製のデスクトップアプリに匹敵する爆速のシステムを構築することが可能だ。

「遅い」と嘆く前に、自身のコードがオブジェクトを適切に解放しているか、クエリの実行プランが最適化されているかを見直せ。アーキテクチャの勝利は、常に細部を熟知した者だけに微笑む。

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