「FindFirst」の罠:Like演算子でインデックスを殺さないための極限設計術
実務におけるAccess開発において、多くの開発者が「最初は動いていたのに、データが増えた途端にツールが重くなる」という泥沼に足を取られる。その原因の多くは、コードの書き方ではなく、「データベースエンジンがインデックスをどう扱うか」というアーキテクチャへの理解不足にある。
特に、DAOの`FindFirst`メソッドを用いたワイルドカード検索は、一歩間違えれば、数万件のレコードを力任せに走査する「フルテーブルスキャン」を誘発し、ネットワーク越しにリンクされたバックエンド(BE)を窒息させる。
本稿では、中級者から上級者へとステップアップするために避けては通れない、「インデックスを有効活用する検索設計」の極意を伝授する。
1. なぜあなたの `FindFirst` は遅いのか?
まず、エンジニアとして「なぜ遅いのか」を論理的に特定する癖をつけなければならない。
多くの開発者は、以下のような実装を行い、検索速度の低下に頭を抱える。
' --- アンチパターン:インデックスを殺す検索 ---
Dim rs As DAO.Recordset
Set rs = db.OpenRecordset("T_Customer", dbOpenSnapshot) ' 全件取得
' ワイルドカードを先頭に付ける「中間一致」検索
rs.FindFirst "CustomerName Like '*田中*'"
If rs.NoMatch Then
' ...
End If
致命的な設計ミス:中間一致の代償
B-Tree構造を持つインデックスは、辞書のようなものだ。「たなか」という名前を探すとき、辞書が「た」のページから順に引けるのは、前方一致だからである。
しかし、`*田中*`(中間一致)を指定した瞬間、エンジンはインデックスの構造を利用できなくなる。インデックスのどこに「田中」が含まれているか予測がつかないため、エンジンはすべてのレコードを一つずつ開き、文字列パターンマッチングを行う「フルテーブルスキャン」を強制される。
さらに、これが「リンクテーブル(分散データベース)」環境である場合、悲劇は倍増する。
ローカルのメモリ上で検索しているのではなく、ネットワーク越しに「このレコードはパターンに合致するか?」という問い合わせを、全レコード分、バックエンドに対して発行している可能性があるのだ。
2. SARGability(検索引数可能性)を確保せよ
プロフェッショナルな設計において、我々が追求すべきはSARGability (Search ARGumentable) である。これは、クエリの条件式がインデックスを効率的に利用できる状態にあることを指す。
解決策 A:前方一致への設計変更
もし業務要件が許すのであれば、検索ロジックを「中間一致」から「前方一致」へ変更する。これだけで、検索コストは劇的に下がる。
' --- プロフェッショナル:インデックスを活用する検索 ---
' 「田中」から始まる名前を検索する場合
rs.FindFirst "CustomerName Like '田中*'"
解決策 B:Recordsetのオープン時にWHERE句を叩き込む
ここが最も重要なアーキテクチャの差だ。`OpenRecordset` でテーブル全体を開いてから `FindFirst` で探すのは、「図書館の本をすべて机に広げてから、目的の一冊を探す」ような愚行である。
正しい設計は、「最初から目的の本だけをカウンターに持ってこさせる」ことだ。つまり、SQLの`WHERE`句を使用して、エンジンレベルでフィルタリングを完了させる。
3. プロダクション品質の検索実装例
以下に、実務でそのまま利用できる、堅牢で高速な検索モジュールの実装例を示す。単なる検索ではなく、エラーハンドリングとリソース管理(ライフサイクル)を徹底した設計としている。
''' <summary>
''' 指定した条件でレコードを高速に検索する
''' </summary>
''' <param name="strSearchTerm">検索キーワード</param>
''' <returns>一致したレコードのID(見つからない場合はEmpty)</returns>
Public Function GetCustomerIdByPrefix(ByVal strSearchTerm As String) As Variant
On Error GoTo ErrorHandler
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim strSql As String
Dim resultId As Variant
' 検索語が空の場合は即時終了(ガード句)
If Len(Trim(strSearchTerm)) = 0 Then
GetCustomerIdByPrefix = Empty
Exit Function
End If
Set db = CurrentDb
resultId = Empty
' 【設計の肝】
' FindFirstではなく、SQLのWHERE句で絞り込む。
' これにより、リンクテーブル環境では「必要なデータのみ」がネットワークを流れる。
' また、前方一致('term*')にすることでインデックスをフル活用する。
strSql = "SELECT CustomerID FROM T_Customer " & _
"WHERE CustomerName Like '" & Replace(strSearchTerm, "'", "''") & "*' " & _
"ORDER BY CustomerName;" ' 決定論的な結果を得るためにソートを推奨
' dbOpenForwardOnly + dbOpenReadOnly は、検索専用の最も軽量なモード
Set rs = db.OpenRecordset(strSql, dbOpenForwardOnly, dbOpenReadOnly)
If Not rs.EOF Then
' 最初の1件を取得
resultId = rs!CustomerID
End If
CleanUp:
' オブジェクトのライフサイクルを適切に管理
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
Set db = Nothing
GetCustomerIdByPrefix = resultId
Exit Function
ErrorHandler:
' 実務ではログ出力などの処理をここに挟む
MsgBox "検索中に予期せぬエラーが発生しました: " & Err.Description, vbCritical, "Error"
resultId = Empty
Resume CleanUp
End Function
このコードが「堅牢」である理由
1. SQLインジェクション対策: `Replace(strSearchTerm, “‘”, “””)` により、シングルクォートを含む入力によるエラーや攻撃を防止している。
2. 最小限のオーバーヘッド: `dbOpenForwardOnly` と `dbOpenReadOnly` を指定することで、メモリ消費とロックの発生を最小限に抑えている。
3. ネットワーク負荷の最適化: `OpenRecordset` の引数に `WHERE` 句を含めることで、バックエンド側でインデックス検索を完結させ、フロントエンドには結果のみを転送させている。
4. 決定論的動作: `ORDER BY` を指定することで、複数ヒットした場合でも常に同じレコードが返ることを保証している。
4. まとめ:アーキテクトとしての視点
「動くコード」を書くのは初級者であり、「速く、壊れないコード」を書くのがプロフェッショナルである。
- `FindFirst` は最終手段: Recordsetを開いた後に使うものではなく、SQLで絞り込めない場合の「フォールバック」として考えるべきだ。
- ワイルドカードの配置を意識せよ: `*text` はインデックスを破壊し、`text*` はインデックスを加速させる。
- データ移動を最小化せよ: リンクテーブル環境では、ロジックを「データの近く(SQL/Server側)」に寄せる。
この設計思想を脳に刻め。次回のコードレビューで、あなたの書いたクエリがチームのパフォーマンスを劇的に変えることを期待している。
