【テクニカル・上級編】DAO.Recordsetの「FindFirst」メソッドと「NoMatch」判定による効率的なデータ抽出 – Access VBA解析バイブル

スポンサーリンク

DAO.Recordsetの「FindFirst」極限チューニング:静的・動的カーソルを支配し、レガシーAccessの限界を突破する

Access VBAによる業務システム開発において、数万件から数十万件規模のレコード群から特定の条件に合致するデータを抽出するシーンは日常茶飯事である。
特に、ローカルのJet/ACEエンジンを叩くのか、あるいはODBC経由でSQL ServerなどのリモートRDBをバックエンドに据えているのかによって、データの取得戦略は根本から異ならなければならない。

今回は、DAO(Data Access Objects)における `FindFirst` メソッドと、その成否を判定する `NoMatch` プロパティの本質に焦点を当てる。
ただ動くだけのコードは素人に任せておけばいい。プロのアーキテクトが知るべきは、「メモリ空間の占有状態」「カーソルタイプの選定」「JETエンジンの最適化機構(Rushmoreテクノロジー)」を完全に制御下に置くことだ。

1. なぜ `FindFirst` なのか? `Seek` や `FindNext` との決定的な違い

大量データからの検索手法として、開発者はしばしば以下の選択肢に直面する。

1. `Seek` メソッド: インデックスが設定されたテーブル(Table-type Recordset限定)に対して最速でヒットする。しかし、クエリやODBC接続(Dynaset/Snapshot)では使用不可という致命的な縛りがある。
2. `Find` ファミリー(`FindFirst`, `FindLast`, `FindNext`, `FindPrevious`): Dynaset(ダイナセット)およびSnapshot(スナップショット)型レコードセットで使用可能。クエリベースでも動作するため、実務での汎用性は圧倒的に高い。

この中で `FindFirst` は、検索条件(Criteria)に一致する最初のレコードへとカレントレコードポインタを瞬間移動させる。
ここで重要なのは、`FindFirst` はインデックスの恩恵を受けるが、条件式(SQLのWHERE句に類似した構文)の書き方一つで、フルスキャン(全件走査)に堕ちるという事実だ。

2. パフォーマンスの急所:Rushmoreテクノロジーを殺すな

AccessのJet/ACEデータベースエンジンには、Rushmore(ラシュモア)クエリ最適化テクノロジーが組み込まれている。これは、条件式に含まれるフィールドにインデックスが存在する場合、ビットマップ演算を用いて該当レコードのID群を高速に特定する機構である。

しかし、`FindFirst` の条件式に以下のような記述を行うと、Rushmoreは無効化され、メモリ上で1件ずつ評価を行う最悪のパフォーマンス(O(N)の線形探索)を引き起こす。

  • NGパターン: 関数や演算子をフィールドに適用する(例:`Format([Date], “yyyy/mm”) = ‘2023/10’` や `Left([Code], 2) = ‘AB’`)
  • OKパターン: フィールドを生のまま比較演算子で結ぶ(例:`[Date] >= #2023/10/01# AND [Date] <= #2023/10/31#`)

極限のパフォーマンスを求めるならば、`FindFirst` に渡す文字列は、常にインデックスが効くプレーンな列挙でなければならない。

3. 【実装コード】安全かつ高速な `FindFirst` と `NoMatch` 制御

実務でそのまま利用できる、メモリリーク防止(オブジェクトの明示的解放)と堅牢なエラーハンドリングを網羅した実装例を提示する。

Option Compare Database
Option Explicit

‘ ==============================================================================
‘ 処理名: ProcessTargetRecordWithFindFirst
‘ 概要: 大量レコードセットから条件に合致するデータをFindFirstで高速抽出し、
‘ 適切なNoMatch判定とメモリ最適化を行うプロフェッショナル実装。
‘ ==============================================================================
Public Sub ProcessTargetRecordWithFindFirst(ByVal targetID As Long)
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim criteria As String

‘ タイマー計測(パフォーマンス検証用)
Dim startTime As Double
startTime = Timer

On Error GoTo ErrorHandler

‘ 1. 現在のデータベース参照を取得(CurrentDbの乱用を防ぎ、変数に保持する)
Set db = CurrentDb()

‘ 2. パフォーマンスを最大化するため、必要な列のみを抽出するダイナセットを開く
‘ ※テーブル全体を直接開くのではなく、必要最小限のフィールドとレコードに絞るのが鉄則
Dim sql As String
sql = “SELECT ID, CustomerCode, Status, UpdatedDate FROM T_LargeData WHERE Status = 1”

‘ dbOpenDynaset を明示し、変更可能なカーソルを生成
Set rs = db.OpenRecordset(sql, dbOpenDynaset)

‘ 3. レコードが存在するか事前チェック(空のレコードセットに対するFindFirstは無意味)
If rs.EOF And rs.BOF Then
MsgBox “対象となるレコードが存在しません。”, vbInformation, “情報”
GoTo Cleanup
End If

‘ 4. 検索条件の構築(インデックスが効くフィールド名を直に指定)
criteria = “ID = ” & targetID

‘ 5. FindFirstの実行
rs.FindFirst criteria

‘ 6. NoMatchプロパティによる判定
If rs.NoMatch Then
‘ 該当データなしの場合のハンドリング
MsgBox “指定されたID: ” & targetID & ” のアクティブなレコードは見つかりませんでした。”, vbExclamation, “検索結果”
Else
‘ 該当データありの場合の処理
‘ トランザクション制御や値の更新をここに記述
Debug.Print “— 検索ヒット —”
Debug.Print “CustomerCode: ” & rs!CustomerCode
Debug.Print “UpdatedDate: ” & rs!UpdatedDate

‘ 例:データの編集モード移行
‘ rs.Edit
‘ rs!Status = 2
‘ rs.Update
End If

Debug.Print “処理完了時間: ” & Format(Timer – startTime, “0.000秒”)

Cleanup:
‘ ————————————————————————–
‘ オブジェクトの明示的解放(メモリリークの根絶)
‘ ————————————————————————–
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
Set db = Nothing
Exit Sub

ErrorHandler:
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“エラー内容: ” & Err.Description, vbCritical, “致命的エラー”
Resume Cleanup
End Sub

4. チーフアーキテクトが指摘する「見落とされがちな罠」

① `CurrentDb` の評価とインスタンスの寿命

多くの初学者や中級者は、コードのあちこちで `CurrentDb.OpenRecordset` を直叩きする。これは極めて危険である。
`CurrentDb` は呼び出すたびに新しいデータベースオブジェクトのインスタンスをメモリ上に生成する。これをループ内や頻繁に実行されるメソッド内で乱用すると、内部の変数テーブル(Table of Open Objects)が肥大化し、Access特有の「リソース不足(Out of Memory)」エラーを引き起こす。
上記コードのように、一度 `db` 変数に格納し、処理が終わったら確実に解放する設計がマストである。

② Dynasetにおけるブックマークの挙動とパフォーマンス

`FindFirst` がヒットした位置を保持しておきたい場合、`rs.Bookmark` プロパティを利用する。
ダイナセット型のレコードセットでは、ブックマークを使用することで瞬時にポインタを再配置できるが、マルチユーザー環境において他のユーザーが該当レコードを削除・変更した際の挙動(ODBCDirectやトランザクション分離レベル)には細心の注意が必要だ。

③ 複数条件の `FindFirst`

`FindFirst` の引数には `AND` や `OR` を用いた複合条件を指定できる。

rs.FindFirst “Status = 1 AND CategoryCode = ‘A'”

この場合も、結合されるすべてのフィールドに個別のインデックス、あるいは複合インデックスが適切に貼られていなければ、Jetエンジンは泣きながら全件走査を行うことになる。数万件程度なら数ミリ秒で終わる処理も、百萬件を超えた途端にフリーズしたような挙動を示す原因はここにある。

総括

`FindFirst` と `NoMatch` は、Access VBAにおけるデータ操作の基本パーツにすぎない。しかし、その背後にあるデータベースエンジンの挙動、メモリ管理のライフサイクル、そしてインデックス設計の理論を理解しているか否かで、組んだシステムの寿命は文字通り天と地ほどの差が出る。

「動けばいい」という妥協を捨て去り、一瞬の遅延をも許さない極限のコードベースを構築すること。それこそが、レガシーシステムを延命させ、真のプロフェッショナルたる所以である。

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