【実務・中級編】DAOのFindFirstでワイルドカード検索を高速化する:Like演算子の罠と回避策 – Access VBA解析バイブル

スポンサーリンク
スポンサーリンク

インデックスを殺す「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)を組み込む設計を標準化せよ。

優れたエンジニアは、コードを書く前に、データの動きを予測する。
「動くコード」ではなく、「スケールする設計」を。それが、プロフェッショナルへの第一歩だ。

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