【テクニカル・上級編】【中級】DAO.Recordsetの「FindFirst」と「NoMatch」:クエリを使わずに高速検索を実現する – Access VBA解析バイブル

スポンサーリンク

Access VBAを掌握する極限の知見:DAO.Recordsetの「FindFirst」と「NoMatch」による非クエリ高速検索の極意

レガシーシステムの深部、数百万件のレコードを抱えるAccessデータベースのパフォーマンスチューニングにおいて、フォームの `Filter` プロパティや、その都度生成される重いSQLクエリに頼る設計は、シニアエンジニアにとって悪手でしかない。

UIの描画サイクルを巻き込み、不毛なJet/ACEエンジンのクエリ解析オーバーヘッドを発生させるアプローチは、デスクトップデータベースの限界を自ら引き起こす行為に他ならない。

今回は、DAO(Data Access Objects)の真髄である `Recordset` オブジェクトの `FindFirst` メソッドと `NoMatch` プロパティに焦点を当てる。メモリ上で直接インデックスを走査し、クエリを介さずに目的のレコードへ瞬時にジャンプする、極限まで無駄を削ぎ落とした高速検索の実装アーキテクチャを解説する。

1. なぜ「クエリ」や「フォームフィルタ」ではスケールしないのか

実務において、特定の1件を探すために以下のようなコードを書く者が後を絶たない。

‘ 【アンチパターン】毎回SQLを発行してレコードセットを作り直す
Dim rs As DAO.Recordset
Set rs = CurrentDb.OpenRecordset(“SELECT FROM T_Customer WHERE CustomerID = ‘C12345′”, dbOpenDynaset)

このアプローチは、小規模なデータであれば問題なく動作する。しかし、ネットワーク経由のバックエンド(SQL Server等)や、数万件以上のローカルACEテーブルにおいて、この「都度クエリ生成」は致命的なボトルネックを生む。SQLのパース、実行計画の最適化、そしてレコードセットの再構築という重い処理が毎回走るからだ。

また、フォームの `Me.Filter` を用いた検索も同様である。UIの再描画とレコードソース全体の再バインドが発生するため、体感速度は著しく低下する。

解決策:常駐レコードセットに対するメモリ内インデックス走査

あらかじめフォームの基盤となる、あるいはメモリ上に保持した `Recordset` に対し、`FindFirst` を実行する。これにより、Jet/ACEエンジンのキャッシュとインデックスが最大限に活用され、クエリの再コンパイルコストを完全に排除できる。

2. `FindFirst` と `NoMatch` のメカニズム

`FindFirst` は、ダイナセット型またはスナップショット型の `Recordset` オブジェクトに対し、指定した条件式に一致する最初のレコードを検索するメソッドである。

ここで重要なのは、検索対象のフィールドにインデックスが貼られているかどうかがパフォーマンスを決定づけるという点だ。インデックスが存在する場合、O(1)またはO(log n)に近い極めて高速なシークが行われる。

そして、検索の成否を判定するのが `NoMatch` プロパティである。

  • `True`: 条件に一致するレコードが見つからなかった。
  • `False`: 条件に一致するレコードが見つかり、カレントレコードがその位置に移動した。

このシンプルな真偽値判定を組み合わせることで、エラーハンドリングに依存しない堅牢なジャンプ処理が構築できる。

3. 【実務コード】極限まで最適化された検索・ジャンププロシージャ

以下のコードは、非連結あるいは連結フォーム上で、バックグラウンドの `Recordset` を直接操作して指定レコードへ高速にフォーカスを移動させる実務レベルのモジュールである。

メモリリークを防ぐためのオブジェクト解放、およびエラーハンドリングの作法を徹底している点に注目してほしい。

Option Compare Database
Option Explicit

‘ =========================================================================
‘ 処理名 : JumpToRecordByFindFirst
‘ 概要 : DAO.RecordsetのFindFirstを使用し、クエリを介さずに高速ジャンプする
‘ 引数 : frm – 対象のフォーム
‘ fieldName – 検索対象のフィールド名
‘ searchValue – 検索値
‘ 戻り値 : Boolean – 成功時はTrue、見つからない場合はFalse
‘ =========================================================================
Public Function JumpToRecordByFindFirst(ByRef frm As Access.Form, ByVal fieldName As String, ByVal searchValue As Variant) As Boolean
Dim rs As DAO.Recordset

On Error GoTo ErrorHandler

‘ フォームのクローンレコードセットを取得(UIのカレントレコードと同期させないためCloneを使用)
Set rs = frm.RecordsetClone

‘ レコードが存在しない場合は即座に抜け出す
If rs.RecordCount = 0 Then
JumpToRecordByFindFirst = False
GoTo CleanUp
End If

‘ 検索条件の構築(データ型に応じたエスケープ処理を考慮)
Dim criteria As String
criteria = BuildCriteria(fieldName, rs.Fields(fieldName).Type, searchValue)

‘ FindFirstの実行(インデックスが有効な場合、瞬時にヒットする)
rs.FindFirst criteria

‘ NoMatchプロパティによる成否判定
If rs.NoMatch Then
MsgBox “指定された条件に一致するレコードは見つかりませんでした。”, vbInformation, “検索結果”
JumpToRecordByFindFirst = False
Else
‘ フォームのBookmarkをレコードセットのBookmarkに同期させ、UIをジャンプさせる
frm.Bookmark = rs.Bookmark
JumpToRecordByFindFirst = True
End If

CleanUp:
‘ 【重要】RecordsetCloneの参照を明示的に解放し、メモリリークを防ぐ
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
Exit Function

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

コードのアーキテクチャ的解説

1. `frm.RecordsetClone` の活用
フォーム自体のレコードセットを直接操作するのではなく、そのクローンを取得している。これにより、フォームの表示位置(カレントレコード)を乱すことなく、バックグラウンドで安全に検索カーソルを動かすことができる。
2. `frm.Bookmark = rs.Bookmark` によるUI同期
`FindFirst` でヒットした `Recordset` の位置(Bookmark)を、フォーム側のBookmarkに代スワップする。これが、Access VBAにおける最もエレガントかつ高速なレコードジャンプの手法である。
3. `BuildCriteria` 関数の併用
VBA標準の `BuildCriteria` 関数を使用することで、文字列型、数値型、日付型それぞれのSQL構文上のリテラル表現(シングルクォートやシャープの囲みなど)を自動で安全にエスケープ・構築できる。インジェクション対策や型ミスの防止においても必須のテクニックである。

4. シニアエンジニアが知るべきパフォーマンスの罠と限界

`FindFirst` は強力だが、万能ではない。以下のアーキテクチャ上の制約を理解しておかなければ、大規模システムで痛い目を見る。

  • ダイナセット型とスナップショット型の選択

`RecordsetClone` は基本的にフォームの設定に依存する。データ量が膨大な場合、`dbOpenSnapshot` をベースにした独自の一時レコードセットをコード内で生成し、そこで `FindFirst` を行った上で、主キー(ID)だけをフォーム側に渡して `Seek` または `FindFirst` を再実行する手法の方がメモリ効率が良い場合がある。

  • ODBCデータソース(SQL Server等)との連携時の注意

バックエンドがRDBの場合、`FindFirst` はクライアント側(Accessのメモリ内)に全レコードをキャッシュしようとする挙動を引き起こす可能性がある(パススルーやODBCのフェッチサイズに依存)。数百万件のテーブルに対してテーブルリンク+`FindFirst` を行うと、ネットワーク帯域を圧迫する。真に巨大なデータに対しては、最初からサーバサイドのストアドプロシージャや、パラメータ付きのDAO `Seek`(インデックスがローカルISAM形式の場合)、あるいは適切なWHERE句を持つSQLによる再バインドを検討すべきである。

総括

Access VBAは「おもちゃの言語」ではない。その内部構造、特にJet/ACEデータベースエンジンの挙動とDAOのライフサイクルを完全に掌握すれば、企業の基幹を支える高速なデスクトップソリューションを構築し得ることが証明できる。

今回解説した `FindFirst` と `NoMatch` のコンビネーションは、無駄なクエリ生成を排除し、メモリの局所性を最大限に活かすためのシニアの必須武器である。レガシーの制約を嘆く前に、コードの血管を流れるオブジェクトの挙動を極限まで研ぎ澄ませ。

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