インデックスを殺す「Like演算子」の罠:DAO.FindFirst を極限まで高速化する設計思想
「開発環境では一瞬で終わっていた。しかし、本番環境でデータが10万件を超えた途端、システムが沈黙した――。」
Access VBAを用いた業務アプリケーションの開発において、これほど絶望的な瞬間はない。もし君が、検索処理を `DAO.Recordset.FindFirst` と `Like “*キーワード*”` の組み合わせだけで実装しているなら、君の設計には「爆弾」が仕込まれていると言わざるを得ない。
なぜ、単純な文字列検索がこれほどまでにパフォーマンスを悪化させるのか。そして、プロフェッショナルはどう設計すべきなのか。今回は、DAO(Data Access Objects)の内部メカニズムに基づいた、検索ロジックの真髄を伝授する。
1. なぜ `Like “*キーワード*”` は遅いのか:B-Treeインデックスの死
まず、エンジニアとして「なぜ」を理解せよ。単に「遅いから避ける」のは素人の仕事だ。
Accessのテーブル(Jet/ACEエンジン)において、インデックスは「B-Tree(バランス木)」という構造で管理されている。これは、辞書のようにデータがソートされて並んでいる状態をイメージすればいい。
- 前方一致 (`Like “キーワード*”`) の場合:
辞書を引くとき、「サ」で始まる単語を探すのは容易だ。インデックス(B-Tree)を辿り、該当する範囲を特定できる。これをIndex Seekと呼ぶ。
- 中間一致・後方一致 (`Like “*キーワード*”`) の場合:
辞書の中で「〇〇」という文字がどこに含まれているかを探すには、最初から最後まで全てのページをめくるしかない。これがFull Table Scan(フルスキャン)だ。
`FindFirst` メソッドに中間一致の `Like` 演算子を渡した瞬間、エンジンはインデックスを完全に放棄し、ディスク上の全レコードを一行ずつ読み込んで比較し始める。レコード数が増えるほど、このコストは指数関数的に増大するのだ。
2. アーキテクトが取るべき3つの回避戦略
設計レベルでこの問題を解決するには、以下の3つのアプローチを使い分ける必要がある。
戦略I:検索範囲の事前絞り込み(Scope Reduction)
最も基本的かつ強力な手法だ。`Like` を使う前に、インデックスが確実に効く「非文字列フィールド」でレコードセットを絞り込む。
例えば、「2023年度の顧客」の中から名前を検索する場合、まず `年度` フィールドでフィルタリングを行い、検索対象の母集団(Search Space)を最小化してから `FindFirst` を実行する。
戦略II:前方一致への設計変更
業務要件を精査せよ。「部分一致」が本当に不可欠か? 「入力補完」のような挙動であれば、前方一致 (`Like “キーワード*”`) への変更だけで、パフォーマンスは劇的に改善する。
戦略III:SQLによる一括抽出へのシフト
`FindFirst` は「見つかるまで一行ずつ探す」命令だ。もし、検索結果が複数になる可能性がある、あるいは大量のデータを処理する必要があるなら、`FindFirst` をループで回すような設計は即刻破棄し、`OpenRecordset` に直接 SQL (WHERE句) を渡すべきだ。
3. プロダクション・コードの実装例
以下に、堅牢性とパフォーマンスを両立させた検索モジュールの実装例を示す。単なるコピペ用ではない。エラーハンドリング、オブジェクトのライフサイクル管理、そして「インデックスを意識した設計」を組み込んでいる。
Option Compare Database
Option Explicit
' ======================================================================================
' Module: modSearchEngine
' Description: 高速かつ堅牢な検索ロジックを提供する
' Author: Lead Architect
' ======================================================================================
Public Enum SearchMode
smPrefix ' 前方一致 (高速)
smContains ' 中間一致 (低速・要注意)
End Enum
''' <summary>
''' 指定された条件に基づき、レコードセットから高速にレコードを特定する。
''' </summary>
''' <param name="db">DAO.Databaseオブジェクト</param>
''' <param name="tableName">対象テーブル名</param>
''' <param name="searchField">検索対象フィールド名</param>
''' <param name="keyword">検索キーワード</param>
''' <param name="mode">検索モード (前方一致 or 中間一致)</param>
''' <returns>見つかった場合はそのレコード、見つからない場合はNothing</returns>
Public Function FindOptimizedRecord( _
ByRef db As DAO.Database, _
ByVal tableName As String, _
ByVal searchField As String, _
ByVal keyword As String, _
ByVal mode As SearchMode) As DAO.Recordset
Dim rs As DAO.Recordset
Dim strCriteria As String
Dim strFilter As String
' 1. エラーハンドリングの準備
On Error GoTo ErrorHandler
' 2. キーワードのサニタイズ(シングルクォート対策)
' ※実務では、入力値に ' が含まれる場合の対策は必須
Dim safeKeyword As String
safeKeyword = Replace(keyword, "'", "''")
' 3. 検索条件の構築
Select Case mode
Case smPrefix
' 前方一致: インデックスが効く (Index Seek)
strCriteria = searchField & " Like '" & safeKeyword & "*'"
Case smContains
' 中間一致: インデックスが効かない (Full Scan)
' 警告: 大規模データではパフォーマンス低下の要因となる
strCriteria = searchField & " Like '*" & safeKeyword & "*'"
End Select
' 4. 【重要】アーキテクチャの最適解:
' FindFirst を使う前に、SQLによるフィルタリング済みのRecordsetを開く。
' これにより、エンジンレベルで最適化された検索が行われる。
' FindFirst をループ内で回すよりも、SQLのWHERE句で絞り込む方が圧倒的に速い。
strFilter = "SELECT * FROM [" & tableName & "] WHERE " & strCriteria
Set rs = db.OpenRecordset(strFilter, dbOpenSnapshot)
' 5. 結果の返却
' Snapshotモードで開いているため、読み取り専用で高速。
If Not rs.EOF Then
Set FindOptimizedRecord = rs
Else
' 見つからない場合は、空のRecordsetを返すのではなくNothingを返す設計
rs.Close
Set rs = Nothing
Set FindOptimizedRecord = Nothing
End If
ExitFunction:
Exit Function
ErrorHandler:
' ログ出力等の処理をここに実装
Debug.Print "Error in FindOptimizedRecord: " & Err.Description
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
Set FindOptimizedRecord = Nothing
Resume ExitFunction
End Function
' --- テスト用プロシージャ ---
Public Sub TestSearch()
Dim db As DAO.Database
Dim rs As DAO.Recordset
Set db = CurrentDb
' 例:顧客名の中間一致検索(低速だが、SQLで絞り込み済み)
Set rs = FindOptimizedRecord(db, "T_Customers", "CustomerName", "田中", smContains)
If Not rs Is Nothing Then
Debug.Print "Found: " & rs!CustomerName
rs.Close
Set rs = Nothing
Else
Debug.Print "Not Found."
End If
Set db = Nothing
End Sub
4. まとめ:設計者が守るべき鉄則
君が今後、大規模なデータを扱うシステムを設計する際、以下の3箇条を胸に刻んでおいてほしい。
1. `FindFirst` を「検索エンジン」として過信しない:
`FindFirst` はあくまで「開いているレコードセット内でのポインタ移動」に近い。広大なデータセットに対しては、最初から `OpenRecordset(SQL)` を使い、SQLエンジンに検索の主導権を渡せ。
2. インデックスの効力を常に意識せよ:
`Like “*”` を書いた瞬間に、君が苦労して作成したインデックスはゴミと化す。ワイルドカードの位置が、システムの生死を分ける。
3. データ型に厳格であれ:
文字列検索の前に、数値や日付によるフィルタリング(Scope Reduction)を組み込む設計を標準化せよ。
優れたエンジニアは、コードを書く前に、データの動きを予測する。
「動くコード」ではなく、「スケールする設計」を。それが、プロフェッショナルへの第一歩だ。
