DAO.RecordsetとFindFirstの極限最適化:メモリ上での高速検索エンジン構築術
Access VBAによる業務システム開発において、パフォーマンスのボトルネックの大部分は「どこにあるか」が決まっている。その大半は、UIとデータストア(Jet/ACEエンジン)の無駄なラウンドトリップ、そして場当たり的なSQL発行にある。
数万〜数十万件規模のレコードを扱う画面で、ユーザーが検索ボタンを押すたびに `SELECT` クエリを発行し、フォームの `RecordSource` を再バインドするような設計は、シニアエンジニアの基準から言えば悪手だ。ネットワーク(あるいはローカルのディスクI/O)を跨ぎ、クエリプランを毎回生成するオーバーヘッドは、VBAのアプリケーション層を確実に蝕む。
我々が目指すべきは、一度メモリ上に展開したDAO(Data Access Objects)のレコードセットをインメモリ・データベースとして酷使し、`FindFirst` と `NoMatch` 判定によって極限までレイテンシーを削ぎ落とした検索エンジンの構築である。
今回は、Accessオブジェクトモデルの深層を知り尽くしたアーキテクトの視点から、DAO.Recordsetを極限までチューニングし、実務で即座に使える超高速検索エンジンを実装する手法を解説する。
—
1. なぜ `CurrentDb.OpenRecordset` なのか:ADOとの決定的な違い
まず大前提として、Access VBAにおいてローカルのJet/ACEデータベースを操作する場合、ADO(ActiveX Data Objects)ではなく DAO を選択すべきである。
ADOの `Recordset` は汎用性が高い一方で、AccessのネイティブエンジンであるJet/ACEに対して無駄なラッパー層を挟むため、純粋なパフォーマンスにおいてDAOの後塵を拝する。特に、インメモリでのカーソル操作や、インデックスをフル活用した `FindFirst` の速度において、DAOの右に出るものはない。
しかし、DAOを使う上でシニアが絶対に犯してはならないタブーがある。それが 「暗黙の `CurrentDb` 生成によるメモリリークとパフォーマンス低下」 だ。
‘ 【悪手】これをループ内で書いてはならない
Dim rs As DAO.Recordset
Set rs = CurrentDb.OpenRecordset(“T_Data”, dbOpenDynaset)
`CurrentDb` プロパティを呼び出すたびに、Accessは内部で新しいデータベースオブジェクトを生成し、参照カウントのオーバーヘッドを生む。さらに、このオブジェクト変数を取り逃がすと、Accessの内部メモリが徐々に圧迫され、長期間稼働する業務システムでは確実によからぬ挙動を引き起こす。
高速な検索エンジンを作るための第一歩は、データベースインスタンスを適切に保持し、スコープを完全に制御することにある。
—
2. 実装:インメモリ・高速検索エンジンクラスの構築
ここでは、フォームの背後で常駐し、メモリ上にロックされたDAOレコードセットを保持、`FindFirst` によって一瞬でレコードを捕捉するクラスモジュール(例: `clsSearchEngine`)の実装を示す。
クラスモジュール: `clsSearchEngine`
Option Explicit
‘ メンバ変数(カプセル化の徹底)
Private m_db As DAO.Database
Private m_rs As DAO.Recordset
Private m_isInitialized As Boolean
‘ 初期化:フォームオープン時に一度だけコールし、メモリ上にインデックス付きで展開
Public Function Initialize(ByVal tableName As String, ByVal primaryKeyField As String) As Boolean
On Error GoTo ErrorHandler
‘ CurrentDbは一度だけ変数に格納し、オブジェクトの生成コストを排除する
Set m_db = CurrentDb()
‘ パフォーマンスを最大化するため、dbOpenSnapshot(またはdbOpenDynaset)を使用
‘ ※検索のみであればSnapshotが最も軽量かつ高速
Set m_rs = m_db.OpenRecordset(“SELECT FROM ” & tableName, dbOpenSnapshot)
m_isInitialized = True
Initialize = True
Exit Function
ErrorHandler:
m_isInitialized = False
Initialize = False
‘ ログ出力基盤等へのフックをここに記述
End Function
‘ 高速検索実行メソッド
Public Function SearchAndNavigate(ByVal criteria As String, ByRef targetForm As Access.Form) As Boolean
If Not m_isInitialized Then
SearchAndNavigate = False
Exit Function
End If
‘ トランザクションや余計なイベントを抑制し、検索処理に集中させる
With m_rs
.FindFirst criteria
If .NoMatch Then
‘ 該当なし
SearchAndNavigate = False
Else
‘ フォームのBookmarkをRecordsetに同期させ、一瞬で表示を切り替える
‘ ※フォームのRecordSourceが同一のレコードセット、あるいはクエリに基づいていることが前提
targetForm.Bookmark = .Bookmark
SearchAndNavigate = True
End If
End With
End Function
‘ 終了時の確実な解放(デストラクタ的役割)
Private Sub Class_Terminate()
On Error Resume Next
If Not m_rs Is Nothing Then
m_rs.Close
Set m_rs = Nothing
End If
Set m_db = Nothing
End Sub
—
3. `FindFirst` と `NoMatch` の極意:高速化のメカニズム
上記のコードで使用している `.FindFirst` と `.NoMatch` だが、これを真に高速に動作させるためには、Accessの裏側で動いているJet/ACEエンジンの仕様を理解しておく必要がある。
① 検索対象フィールドのインデックス設計
`FindFirst` は、検索条件に指定されたフィールドにインデックス(Index)が貼られているかどうかで、実行速度が文字通り「桁違い」になる。
- インデックスなし: テーブル全体を上から順に舐めるため(フルスキャン)、レコード数が数万件を超えると一瞬でフリーズのような状態に陥る。
- インデックスあり: B-Tree構造を辿るため、O(log n) のオーダーで瞬時にヒットする。
検索エンジンとして運用するテーブルの検索キーには、必ずあらかじめインデックス(主キーまたは重複インデックス)を付与しておくこと。
② `Bookmark` 同期によるUI描画の最適化
検索にヒットした際、レコードセットのポインタを動かすだけでは画面(フォーム)は追従しない。
通常の開発者は、ここで `Requery` を発行しがちだが、これこそがパフォーマンスを殺す元凶である。
‘ 【悪手】再クエリはディスクI/Oを伴うため極めて遅い
Me.RecordSource = “SELECT FROM T_Data WHERE ID = ” & foundID
Me.Requery
代わりに、クラス内で示したように `targetForm.Bookmark = .Bookmark` を実行する。これにより、メモリ上のレコードセット位置と画面の描画位置が直接リンクし、データベースへの再問い合わせを一切発生させずに、フォームの表示レコードをダイレクトに切り替えることが可能となる。
—
4. 現場の罠:マルチユーザー環境とキャッシュの同期
シニアエンジニアが直面する現実の課題は、単機での速度ではない。「他のユーザーがデータを更新したとき、このインメモリ・レコードセットはどうなるのか」という排他制御とキャッシュの整合性である。
`dbOpenSnapshot` を使った場合、メモリ上のデータは「オープンした時点のスナップショット」になるため、他者が追加・変更した最新データは反映されない。
もしリアルタイム性が求められる環境であれば、`dbOpenDynaset` を選択し、必要に応じて `.Requery` メソッドをレコードセット自体に対して発行する必要がある。
‘ キャッシュを強制的に最新化する場合の処理
Public Sub RefreshCache()
If m_isInitialized Then
m_rs.Requery
End If
End Sub
ただし、頻繁な `.Requery` はインメモリのメリットを相殺するため、検索ボタンの押下時ではなく、タイマーイベントや明示的な「最新情報取得」ボタンなど、ユースケースに応じた戦略的な配置が求められる。
—
5. チーフアーキテクトからの総括
Access VBAは「レガシーな言語」とやゆされることがある。しかし、それは言語の問題ではなく、使う側のアーキテクチャの理解不足に起因することがほとんどだ。
今回紹介した、DAO.Recordsetをメモリ上に常駐させ、`FindFirst` と `Bookmark` 同期によって検索・描画を完結させる手法は、適切に設計されたC/SシステムやモダンなインメモリDBの思想と何ら変わらない。
Accessの限界を嘆く前に、オブジェクトのライフサイクルを制御し、不必要なI/Oを徹底的に排除せよ。あなたの構築するVBAアプリケーションは、見違えるほど軽快な「本物のシステム」へと生まれ変わるはずだ。
