Access VBAを掌握する極限の知見:DAO.Recordset.Filterによるクエリレス高速化の極意
こんにちは。開発プロジェクトの現場で、日々数百万レコードの処理速度やリソースの限界と格闘しているチーフアーキテクトの私だ。
Access VBAによる開発において、パフォーマンス低下の最大のボトルネックは何だと思う?
「ループ処理の多用」? それもある。だが、プロフェッショナルが最も警戒すべきは、「不要なクエリの動的生成と破棄の繰り返し」だ。
画面上の条件が変わるたびに `QueryDef` を書き換えたり、一時的なSELECTクエリを裏でバシバシ生成したりしていないか?
そのアプローチは、Access(Jet/ACEエンジン)のシステムテーブルに負荷をかけ、ディスクI/Oを発生させ、確実にアプリケーションを脂肪肝へと導く。
今回は、クエリを一切作成せず、メモリ上に展開された `DAO.Recordset` の `Filter` プロパティを極限まで使い倒すことで、爆速かつ堅牢なデータ絞り込みを実現する実践的テクニックを伝授しよう。
—
1. なぜ「一時クエリの動的生成」は悪なのか?
実務でよく見かけるアンチパターンとして、条件が変わるたびに以下のようなコードを書く開発者がいる。
‘ 【非効率なアンチパターン】毎回クエリを作り直す
Dim qdf As DAO.QueryDef
Set qdf = CurrentDb.QueryDefs(“qryTemp”)
qdf.SQL = “SELECT FROM T_Order WHERE Status = ‘” & Me.txtStatus.Value & “‘”
Set rs = qdf.OpenRecordset()
このコードの何が問題か?
1. システムカタログの肥大化とフラグメンテーション: クエリの定義を変更・保存するたびに、Accessの内部データベース(System Catalogs)への書き込みが発生し、ファイルが肥大化する。
2. コンパイルオーバーヘッド: SQL文字列が渡されるたびに、クエリプロセッサが構文解析と実行計画の最適化を毎回行う。
3. 排他制御の競合: 複数ユーザーで共有する環境において、システムオブジェクトへの頻繁なアクセスはロック競合や破損(Corruption)のリスクを跳ね上げる。
解決策:メモリ上で完結させる「Filter + OpenRecordset」
テーブルやベースとなるクエリから一度だけすべてのレコードセットをメモリ(ワークスペース)に読み込み、以後の絞り込みはすべて `Recordset.Filter` プロパティと `OpenRecordset`(子レコードセットの生成)で行う。これが、Access VBAにおける真の高速化アプローチだ。
—
2. 実装のキモ:Filterプロパティの作法
DAOの `Filter` プロパティは、SQLの `WHERE` 句とほぼ同じ構文を使えるが、いくつか厳格なルールがある。ここを誤ると、バグるか期待通りのパフォーマンスが出ない。
- 文字列リテラルはシングルクォート (`’`) で囲む。
- 日付型はシャープ (`#`) で囲む(例: `OrderDate >= #2023/04/01#`)。
- フィルタを設定しただけではレコードセットの中身は変わらない。`Set rsFiltered = rsOriginal.OpenRecordset()` を実行して初めて絞り込まれた実体が生まれる。
—
3. 【プロダクションコード】コピペで使える堅牢な実装例
実際の業務システムを想定した、エラーハンドリング完備のプロシージャを提示する。
数万件の受注データから、指定した顧客IDと日付以降のレコードをノーウェイトで抽出する例だ。
Option Compare Database
Option Explicit
Public Sub ExecuteHighSpeedFiltering(lngCustomerID As Long, dtmTargetDate As Date)
‘ ————————————————————————-
‘ 目的: クエリを一切生成せず、DAOのFilter機能を用いてメモリ上で高速にデータを絞り込む
‘ ————————————————————————-
Dim db As DAO.Database
Dim rsBase As DAO.Recordset
Dim rsFiltered As DAO.Recordset
Dim strSQL As String
Dim strFilter As String
On Error GoTo ErrorHandler
Set db = CurrentDb()
‘ 1. ベースとなる全データを一度だけメモリ(キャッシュ)に読み込む
‘ ※必要最小限の列に絞ることで、メモリ消費量を抑える
strSQL = “SELECT OrderID, CustomerID, OrderDate, Amount, Status FROM T_Order”
Set rsBase = db.OpenRecordset(strSQL, dbOpenSnapshot)
‘ レコードが存在しない場合は即座に抜け
If rsBase.EOF Then
MsgBox “対象データが存在しません。”, vbInformation, “通知”
GoTo CleanUp
End If
‘ 2. フィルタ条件の組み立て(SQLのWHERE句と同じ要領)
‘ ※数値はそのまま、日付は # で囲む
strFilter = “CustomerID = ” & lngCustomerID & _
” AND OrderDate >= #” & Format(dtmTargetDate, “yyyy/mm/dd”) & “#” & _
” AND Status = ‘Active'”
‘ 3. ベースレコードセットにフィルタを適用
rsBase.Filter = strFilter
‘ 4. フィルタ済みの新しいレコードセットを生成
Set rsFiltered = rsBase.OpenRecordset()
‘ 5. データの処理(ここでは件数確認とイミディエイト出力)
If rsFiltered.EOF Then
Debug.Print “条件に一致するレコードはありませんでした。”
Else
rsFiltered.MoveLast
Debug.Print “ヒット件数: ” & rsFiltered.RecordCount & ” 件”
rsFiltered.MoveFirst
Do While Not rsFiltered.EOF
‘ 実際の業務処理をここに記述
Debug.Print “OrderID: ” & rsFiltered!OrderID & “, 金額: ” & rsFiltered!Amount
rsFiltered.MoveNext
Loop
End If
CleanUp:
‘ 6. オブジェクトの解放(逆順が鉄則)
If Not rsFiltered Is Nothing Then: rsFiltered.Close: Set rsFiltered = Nothing
If Not rsBase Is Nothing Then: rsBase.Close: Set rsBase = Nothing
Set db = Nothing
Exit Sub
ErrorHandler:
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“Error: ” & Err.Number & ” – ” & Err.Description, vbCritical, “システムエラー”
Resume CleanUp
End Sub
—
4. チーフアーキテクトが教える「現場の罠」と回避策
この手法は非常に強力だが、プロダクション環境で運用する際には以下の点に留意してほしい。
① `dbOpenSnapshot` を使い倒せ
ベースのレコードセットを開く際、必ず `dbOpenSnapshot`(スナップショットタイプ)を指定すること。ダイナセット型 (`dbOpenDynaset`) にすると、マルチユーザー環境での更新ロックや余計なオーバヘッドが発生し、メモリ上であっても処理が重くなる。絞り込み・参照用途であればスナップショット一択だ。
② メモリ消費量(フットプリント)の意識
数百万レコードを抱える巨大なテーブルに対してこの手法を使うと、クライアントPCのメモリを圧迫する。
「ベーステーブルのレコード数が数万件程度まで」もしくは「WHERE句で初期の母数をある程度絞り込める場合」にこのテクニックを適用するのが、アーキテクチャ設計上の正しい判断となる。データ量が億単位に迫る場合は、素直にサーバーサイド(SQL Server等)のビューやストアドプロシージャに処理を委譲すべきだ。
—
総括
Access VBAは、書き手のエゴや設計の甘さをダイレクトにパフォーマンスの低下として映し出す、ある意味でシビアな環境だ。
今回紹介した `DAO.Recordset.Filter` による高速化は、Accessの特性(Jet/ACEエンジンの挙動)を深く理解している者だけが使いこなせる「現場の武器」となる。無駄なクエリ生成を排除し、メモリ上でスマートにデータを裁く――この洗練された設計思想を、ぜひあなたの開発プロジェクトにも取り入れてほしい。
