DAO.FindFirstにおける「Like演算子」の罠:インデックスを殺さない極限の検索戦略
業務システムの規模が拡大し、レコード数が数十万、数百万の領域に踏み込んだとき、昨日まで軽快に動いていたAccessアプリケーションが、突如として「重い」という名の致命的な欠陥を露呈することがある。
多くの開発者は、その原因を「PCのスペック不足」や「ネットワークの遅延」に求める。だが、真のアーキテクトであれば、その背後にあるB-treeインデックスの無効化と、クエリのSARGability(Search ARGumentable:検索引数可能性)の欠如を真っ先に疑うべきだ。
本稿では、DAO(Data Access Objects)における`FindFirst`メソッドを用いたワイルドカード検索において、なぜパフォーマンスが劇的に劣化するのか、そしてどのように実装すればインデックスを最大限に活用し、極限のレスポンス速度を引き出せるのかを解説する。
1. インデックスの真実:なぜ「*」一つで世界が変わるのか
データベースエンジン(ACE/Jet)がデータを検索する際、インデックスが構築されているカラムに対しては、B-tree構造を利用して高速に目的のデータへ到達する。これを「Index Seek」と呼ぶ。
しかし、`Like`演算子を用いた検索において、ワイルドカード(`*`)をどこに配置するかによって、この挙動は劇的に変化する。
インデックスが機能するケース(Prefix Search)
`Like ‘ABC*’` のように、検索文字列の前方にワイルドカードがない場合、エンジンはインデックスの特定の範囲をスキャンするだけで済む。これは「Index Seek」に近い挙動であり、計算量は $O(\log n)$ で済む。
インデックスが死滅するケース(Infix/Suffix Search)
`Like ‘*ABC’` や `Like ‘*ABC*’` のように、検索文字列の前方にワイルドカードを置いた瞬間、インデックスの階層構造は意味をなさなくなる。エンジンはインデックスの全ノードを走査するか、あるいはインデックスを放棄してテーブルの全レコードを読み込む「Full Table Scan」へと転落する。計算量は $O(n)$ となり、レコード数に比例して処理時間は指数関数的に増大する。
シニアエンジニアとして覚えておくべき鉄則は、「前方一致(Prefix)は高速だが、中間一致・後方一致(Infix/Suffix)はコストの塊である」ということだ。
2. 実装における極限の最適化:実践的コードパターン
以下に、パフォーマンスを最大限に引き出すための`FindFirst`の実装パターンを示す。単に検索するだけでなく、オブジェクトのライフサイクル管理とトランザクションの整合性にも配慮した、プロフェッショナルなコードである。
Option Compare Database
Option Explicit
'---------------------------------------------------------------------------------------
' 関数名: FindUserByPrefix
' 概要 : インデックスを最大限活用した前方一致検索
' 引数 : strSearchTerm - 検索したい文字列の接頭辞
' 戻り値: Boolean - 見つかった場合はTrue
'---------------------------------------------------------------------------------------
Public Function FindUserByPrefix(ByVal strSearchTerm As String) As Boolean
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim strCriteria As String
' 検索文字列のバリデーション(空文字による全件スキャンを防止)
If Len(strSearchTerm) = 0 Then
FindUserByPrefix = False
Exit Function
End If
On Error GoTo ErrorHandler
Set db = CurrentDb
' 【重要】インデックスを有効にするため、ワイルドカードは末尾のみに配置する
' 誤った例: strCriteria = "UserName Like '*" & strSearchTerm & "*'" (Full Scan)
' 正しい例: strCriteria = "UserName Like '" & strSearchTerm & "*'" (Index Seek)
strCriteria = "UserName Like '" & Replace(strSearchTerm, "'", "''") & "*'"
' OpenRecordsetのオプションとして、インデックスを利用しやすいよう設定
' dbOpenDynasetはインデックスを利用した検索に適している
Set rs = db.OpenRecordset("tblUsers", dbOpenDynaset, dbReadWrite)
' FindFirstの実行
rs.FindFirst strCriteria
If Not rs.NoMatch Then
' 見つかった場合の処理(例としてフィールド値を取得)
Debug.Print "Found: " & rs!UserName
FindUserByPrefix = True
Else
FindUserByPrefix = False
End If
CleanUp:
' 【極限の知見】オブジェクトの明示的な解放
' 参照カウントを確実にゼロにすることで、メモリリークとファイルロックを防ぐ
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
If Not db Is Nothing Then
Set db = Nothing
End If
Exit Function
ErrorHandler:
MsgBox "Critical Error in FindUserByPrefix: " & Err.Description, vbCritical
Resume CleanUp
End Function
3. アーキテクトの規律:メモリ管理とトランザクション
大規模なバッチ処理や、長時間稼働するシステム管理ツールを設計する場合、コードの論理的正しさだけでは不十分だ。OSのリソースをいかに制御するかが問われる。
オブジェクトのライフサイクル管理
VBAにおいて、`Set obj = Nothing` による明示的な解放は、単なる「作法」ではない。特にDAOの`Recordset`オブジェクトは、内部的にACEエンジン(Windowsのメモリ空間)のリソースを大量に消費する。
ループ内で`FindFirst`を繰り返す際、適切に`Close`および`Set Nothing`を行わないと、メモリの断片化(Fragmentation)を引き起こし、最終的に `Out of Memory` エラーを誘発する。これはWindows APIレベルでのメモリ管理の失敗に等しい。
トランザクションの境界線
`FindFirst`自体は読み取り操作だが、検索結果に基づきデータを更新する場合、必ずトランザクション(`BeginTrans` / `CommitTrans`)の境界を意識せよ。
' トランザクションを用いた安全な更新パターン
On Error GoTo TransError
DBEngine.BeginTrans
' 1. 高速なFindFirstで対象を特定
' 2. 対象レコードをEdit
' 3. 更新してUpdate
' 4. CommitTrans
DBEngine.CommitTrans
もし、中間一致検索(`*term*`)がどうしても業務要件として避けられない場合は、テーブル設計を見直すべきだ。例えば、検索用の「検索インデックス用カラム」を別途用意し、あらかじめ正規化や加工を行ったデータを格納しておく。これは、計算コストをストレージコスト(ディスク容量)に転嫁する、極めて合理的なアーキテクチャの判断である。
結論
`DAO.FindFirst`における`Like`演算子の扱いは、初級者には「動けば良い」コードに見え、熟練者には「爆弾」に見える。
1. 前方一致(`term*`)を徹底し、インデックスの恩恵を享受せよ。
2. 中間一致(`*term*`)が必要なら、設計レベルでの解決(検索用カラムの導入)を検討せよ。
3. オブジェクトの解放とトランザクション管理を疎かにせず、メモリとデータの整合性を死守せよ。
これらを守ることこそが、レガシーな環境においても、モダンな大規模システムにおいても、揺るぎない信頼性を担保する唯一の道である。
