Access VBAを掌握する極限の知見:動的RecordSource制御による高速検索画面のアーキテクチャ
レガシーシステムの寿命は、しばしば「検索画面のパフォーマンス」と「保守性」によって決まる。
Accessをフロントエンドに据えたクライアント・サーバー(またはローカルファイル)構成において、数万〜数百万件のレコードをさばく検索画面を作る時、初学者が陥る罠が「全件読み込みローカルフィルタリング」や「非効率なクエリの直書き」だ。
本稿では、フォームの `RecordSource` をVBAで動的に書き換える手法を軸に、メモリのライフサイクル、JET/ACEデータベースエンジンの挙動、そして実務で耐えうる堅牢な検索画面の構築手法を、チーフアーキテクトの視点から徹底的に解説する。
—
1. なぜ「動的RecordSource」なのか?
検索条件に応じてクエリの定義を毎回デザインビューで変更するのは、プログラミングのアンチパターンにおける最上位に位置する。オブジェクトの破損(腐敗)を招くし、マルチユーザー環境では致命的な排他制御の競合を引き起こす。
真にスケーラブルなアプローチは、「ベースとなるSQL文字列をVBAで組み立て、フォームの `RecordSource` プロパティへ直接流し込む」ことだ。
しかし、これを雑に実装すると、SQLインジェクションのリスク、型の不一致によるJet/ACEのクエリ最適化失敗、そして何よりメモリリークと肥大化(Bloat)を引き起こす。このトレードオフをどう制圧するか。それが本稿の核心である。
—
2. 実装アーキテクチャ:動的SQL生成と安全な型ハンドリング
以下のコードは、単なる文字列連結によるSQL生成ではない。SQLインジェクションを完全に防ぎ、JET/ACEエンジンが最適化しやすいパラメータ構造を意識した、プロダクション品質の動的検索ロジックだ。
Option Compare Database
Option Explicit
‘ ==============================================================================
‘ フォーム名: frmSearch
‘ 概要: 検索条件をもとにRecordSourceを動的に構築し、高速な絞り込みを行う
‘ ==============================================================================
Private Sub cmdSearch_Click()
On Error GoTo ErrorHandler
Dim sqlBase As String
Dim sqlWhere As String
Dim sqlFinal As String
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
‘ 1. 画面の再描画を停止し、ちらつきと不要なUIイベントを抑制
Application.Echo False
Me.Painting = False
Set db = CurrentDb()
‘ 2. ベースクエリの定義(ビューやストアドの代わりとして最適化されたSELECT句)
sqlBase = “SELECT CustomerID, CompanyName, ContactName, Phone, CreatedDate ” & _
“FROM m_Customers ”
sqlWhere = BuildWhereClause()
‘ 3. WHERE句が存在する場合は結合
If sqlWhere <> “” Then
sqlFinal = sqlBase & “WHERE ” & sqlWhere & ” ORDER BY CustomerID;”
Else
sqlFinal = sqlBase & “ORDER BY CustomerID;”
End If
‘ 【極限の知見】
‘ RecordSourceに直接長大なSQLを代入するのではなく、一時的なQueryDef(またはパラメータクエリ)
‘ を経由するか、直接代入するかはデータ量に依存する。
‘ 数千件程度であれば直接代入で十分だが、複雑なサブクエリを含む場合は
‘ あらかじめ用意した保存済みクエリのSQLプロパティを書き換える方が、
‘ アクセスの実行計画キャッシュにおいて有利な場合がある。
Me.RecordSource = sqlFinal
Me.Requery
‘ 4. 検索結果のレコード数に応じたハンドリング
If Me.Recordset.RecordCount = 0 Then
MsgBox “該当するデータが存在しません。”, vbInformation, “検索結果”
End If
CleanUp:
‘ 5. オブジェクトの明示的解放(VBAのガベージコレクタを信用するな)
If Not qdf Is Nothing Then
qdf.Close
Set qdf = Nothing
End If
Set db = Nothing
Me.Painting = True
Application.Echo True
Exit Sub
ErrorHandler:
Application.Echo True
Me.Painting = True
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“Error No: ” & Err.Number & vbCrLf & _
“Description: ” & Err.Description, vbCritical, “システムエラー”
Resume CleanUp
End Sub
‘ ==============================================================================
‘ 補助関数: UIの入力値から安全なWHERE句を動的に構築する
‘ ==============================================================================
Private Function BuildWhereClause() As String
Dim conditions As String
conditions = “”
‘ テキストボックス(部分一致・SQLインジェクション対策としてのエスケープ処理)
If Not IsNull(Me.txtCompanyName) And Trim(Me.txtCompanyName) <> “” Then
‘ シングルクォーテーションのサニタイジング
Dim safeValue As String
safeValue = Replace(Me.txtCompanyName, “‘”, “””)
conditions = AddCondition(conditions, “CompanyName LIKE ‘” & safeValue & “‘”)
End If
‘ 日付範囲(From)
If Not IsNull(Me.txtDateFrom) Then
‘ Jet/ACEは日付を # で囲む必要がある。フォーマットの厳密な統制が必須。
conditions = AddCondition(conditions, “CreatedDate >= #” & Format(Me.txtDateFrom, “yyyy/mm/dd”) & “#”)
End If
‘ 日付範囲(To)
If Not IsNull(Me.txtDateTo) Then
‘ 終了日の場合は時刻を23:59:59にするか、< #翌日# とするのが定石
Dim nextDay As Date
nextDay = DateAdd("d", 1, Me.txtDateTo)
conditions = AddCondition(conditions, "CreatedDate < #" & Format(nextDay, "yyyy/mm/dd") & "#")
End If
' コンボボックス(完全一致)
If Not IsNull(Me.cmbStatus) And Me.cmbStatus <> 0 Then
conditions = AddCondition(conditions, “StatusID = ” & Me.cmbStatus)
End If
BuildWhereClause = conditions
End Function
‘ ==============================================================================
‘ 補助関数: 条件を AND で安全に結合する
‘ ==============================================================================
Private Function AddCondition(ByVal currentCond As String, ByVal newCond As String) As String
If currentCond = “” Then
AddCondition = newCond
Else
AddCondition = currentCond & ” AND ” & newCond
End If
End Function
—
3. チーフアーキテクトが警鐘を鳴らす「罠」と最適化戦略
上記のコードは美しく堅牢に見えるが、Access特有のダークマター(暗黒物質)を理解していないと、現場で必ず破綻する。以下の3点を心に刻んでほしい。
① フォームの肥大化(Bloat)とメモリリーク
VBAで `Me.RecordSource = …` を頻繁に実行すると、Accessの内部エンジン(JET/ACE)は、裏で一時的なクエリ定義の作成と破棄を繰り返す。これが原因で `.accdb` ファイルが急速に肥大化し、ページの断片化を引き起こす。
対策: 検索フォームには必ず「クエリの最適化(Compact & Repairの自動化検討)」を意識させるとともに、複雑なJOINを含む場合は、ベースとなるパラメータ付きQueryDefをあらかじめ `.accdb` 内に作成しておき、VBAからは `Parameters` コレクション経由で値を渡す設計に昇華させるべきだ。
② `Application.Echo` と `Painting` の呪縛
大規模データを扱う検索画面で最もユーザーイライラを誘発するのが「画面のちらつき」と「描画遅延」である。
コード内にある `Application.Echo False` と `Me.Painting = False` はセットで使わなければ意味がない。
- `Application.Echo False`: Accessウィンドウ全体の再描画を停止。
- `Me.Painting = False`: 対象フォーム個別の描画を停止。
これを怠ると、RecordSourceが書き換わる瞬間にコントロールが1つずつ再描画され、CPU使用率が跳ね上がり、描画のゴースト現象が発生する。
③ インデックスの効かないワイルドカード
`LIKE ‘キーワード’` という前方・後方一致検索は、データベースのB-Treeインデックスを完全に無効化する(フルテーブルスキャンが発生する)。
数万件程度ならAccessは瞬殺してくれるが、数十万件を超えた途端にフリーズする。
もし数百万件規模をAccessのフロントエンドで検索させるという狂気的な要件があるならば、VBA側でSQLを組み立てるのではなく、SQL Server等のRDBへODBC接続し、サーバーサイドでストアドプロシージャを実行する形にアーキテクチャを移行するべきだ。その際も、今回解説した `RecordSource` の動的書き換え(厳密にはパススルー・クエリの動的変更)の概念がそのまま武器になる。
—
4. 結言:レガシーをレガシーで終わらせないために
「Accessだから適当に書いても動く」という時代は終わった。
オブジェクトのライフサイクルを管理し、メモリを解放し、SQLインジェクションや型エラーを完璧に潰しこんだコードこそが、現場の業務を支える真のエンジニアリングである。
動的 `RecordSource` の制御は、Access VBAにおける数少ない「モダンな動的クエリ構築のロジック」を適用できる聖域だ。本稿のコードと知見をあなたのシステムに導入し、圧倒的なパフォーマンスと安定性を手に入れてほしい。
