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

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

DAOの「Like演算子」がシステムを殺す──ワイルドカード検索の計算量とアーキテクチャ的回避策

業務アプリケーションの規模が拡大し、レコード数が数十万件の領域に突入したとき、多くの開発者は「なぜか動作が重くなった」という現象に直面する。その原因の多くは、一見無害に見える `DAO.Recordset.FindFirst` と `Like` 演算子の組み合わせにある。

本稿では、単なる「書き方のコツ」ではない。Accessのデータエンジン(ACE/Jet)がどのようにインデックスを走査し、なぜワイルドカードがパフォーマンスを破壊するのか、その内部メカニズムに基づいた極限の回避策を提示する。

1. 敗北のメカニズム:なぜ `*` はインデックスを無効化するのか

エンジニアがまず理解すべきは、B-Tree構造を持つインデックスの物理的な動作だ。

通常、`FindFirst “Field = ‘Value'”` のような完全一致検索、あるいは `FindFirst “Field Like ‘Value*'”` のような前方一致検索が行われる場合、データベースエンジンはインデックス(B-Tree)を辿り、目的のデータへ高速にアクセス(Index Seek)できる。これは計算量的に $O(\log n)$ の効率を持つ。

しかし、問題は `Like ‘*Value*’` という中間一致・後方一致検索だ。

インデックス・スキャンへの転落

検索文字列の先頭にワイルドカード(`*`)を配置した瞬間、エンジンはインデックスの階層構造を利用できなくなる。インデックスは「値の順序」で並んでいるが、先頭が不明な文字列は、どの枝(Branch)を探すべきか判断できないからだ。

結果として、エンジンは全レコードを一つずつ読み込み、文字列照合を行う Full Table Scan(全件走査) を強制される。計算量は $O(n)$ へと悪化し、レコード数が増えるほど、CPU使用率とディスクI/Oは指数関数的に増大する。これが「システムが重くなる」正体である。

2. 戦略的回避策:アーキテクチャによる解決

この物理的な制約を突破するには、ロジックの書き換えではなく、「検索空間の限定」 という設計思想が必要になる。

戦略A:前方一致への強制的な設計変更

もし可能であれば、要件そのものを「前方一致」に変更させる。これは最も単純かつ強力な解決策だ。`’ABC*’` であればインデックスは機能する。

戦略B:インデックスを利用した「絞り込み」の二段構え

前方一致が不可能な場合、`Like` 演算子単体に頼るのではなく、「別のインデックス付きフィールド」で検索範囲を物理的に削り込んでから、`Like` を適用する。

例えば、`CustomerName`(中間一致検索が必要)に対して、`CustomerGroup`(インデックス済み)という属性がある場合、まず `CustomerGroup` でレコードセットを絞り込み、その限定されたサブセットに対してのみ `Like` を実行する。これにより、スキャン対象の $n$ を劇的に小さくできる。

3. 極限のコード実装:メモリ管理と検索最適化

以下に、シニアエンジニアが実装すべき、堅牢かつ最適化された検索モジュールのテンプレートを示す。ここでは、オブジェクトのライフサイクル管理(明示的な解放)と、エラーハンドリング、そして検索空間の最適化を組み込んでいる。

Option Explicit

' ==============================================================================
' Module: modDataEngineOptimization
' Description: DAOを用いた高速検索およびメモリ最適化の実装
' Author: Legendary Chief Architect
' ==============================================================================

Public Enum SearchStrategy
    ssExactMatch    ' 完全一致
    ssPrefixMatch   ' 前方一致 (Index Seek可能)
    ssWildcardMatch ' 中間一致 (Full Scan回避策を適用)
End Enum

''' <summary>
''' 高速化された検索ロジックを実行する。
''' </summary>
''' <param name="db">開いているDAO.Databaseオブジェクト</param>
''' <param name="targetTable">検索対象のテーブル名</param>
''' <param name="searchField">検索対象のフィールド名</param>
''' <param name="searchValue">検索値</param>
''' <param name="filterField">【重要】検索範囲を絞り込むためのインデックス済みフィールド名</param>
''' <param name="filterValue">絞り込みに使用する値</param>
Public Sub OptimizedSearch( _
    ByRef db As DAO.Database, _
    ByVal targetTable As String, _
    ByVal searchField As String, _
    ByVal searchValue As String, _
    ByVal filterField As String, _
    ByVal filterValue As Variant)

    Dim rs As DAO.Recordset
    Dim strCriteria As String
    Dim strSql As String
    
    On Error GoTo ErrorHandler

    ' 1. SQLによる検索空間の事前限定 (Pre-filtering)
    ' Like演算子を直接SQLに書かず、まずインデックス付きフィールドでレコードセットを絞り込む。
    ' これにより、メモリ上に展開されるレコード数を最小化する。
    strSql = "SELECT * FROM [" & targetTable & "] WHERE [" & filterField & "] = " & _
             FormatFilterValue(filterValue)

    ' 2. Recordsetのオープン
    ' dbOpenDynasetを使用し、インデックスへのアクセスを最適化する
    Set rs = db.OpenRecordset(strSql, dbOpenDynaset, dbFetchAhead)

    ' 3. FindFirstによる詳細検索
    ' 絞り込まれた結果に対してのみ、Like演算子を適用する
    ' ここでのLikeは、既に件数が絞られているため、O(n)のコストが無視できるレベルになる
    strCriteria = "[" & searchField & "] Like " & FormatFilterValue("*" & searchValue & "*")
    
    If rs.FindFirst(strCriteria) Then
        Debug.Print "Record Found: " & rs.Fields(0).Value
        ' 取得したレコードに対する処理をここに記述
    Else
        Debug.Print "No matching record found."
    End If

CleanUp:
    ' 4. オブジェクトの明示的解放 (Critical for Memory Management)
    ' Access VBAにおいて、Recordsetの解放漏れはメモリリークとファイルロックの原因となる。
    ' 特にループ内での検索では、この「儀式」を怠ってはならない。
    If Not rs Is Nothing Then
        rs.Close
        Set rs = Nothing
    End If
    Exit Sub

ErrorHandler:
    ' Windows APIレベルの深刻なエラーを想定したログ出力が必要
    MsgBox "Critical Error in OptimizedSearch: " & Err.Description, vbCritical
    Resume CleanUp
End Sub

''' <summary>
''' SQLインジェクションおよび型エラーを防ぐための値フォーマット関数
''' </summary>
Private Function FormatFilterValue(ByVal val As Variant) As String
    If IsNull(val) Then
        FormatFilterValue = "NULL"
    ElseIf IsNumeric(val) Then
        FormatFilterValue = val
    Else
        ' 文字列はシングルクォートでエスケープ
        FormatFilterValue = "'" & Replace(val, "'", "''") & "'"
    End If
End Function

4. アーキテクトの視点:レガシー環境の保守とシステム間連携

この知見を実戦に投入する際、以下の3点に留意せよ。

1. オブジェクトのライフサイクル(Lifecycle):
VBAのガベージコレクションは、C#やJavaのように完璧ではない。`Set rs = Nothing` を忘れたまま、ループ内で大量の `Recordset` を生成すれば、`Out of Memory` または `Too many active users` エラーが確実に発生する。これはコードの不備ではなく、リソース管理の設計ミスだ。

2. ADOへの移行検討(Scale-out):
もし「絞り込みフィールド」すら存在せず、かつ数百万件のデータに対して中間一致検索が必須であるならば、それはもはやAccess(ACEエンジン)の限界を超えている。その時点で、SQL Server等のRDBMSへ移行し、Full-Text Search (全文検索) インデックス を活用するアーキテクチャへの転換を提言すべきだ。

3. Windows APIとの境界:
極限のパフォーマンスを求める場合、大量のレコードをメモリにロードするのではなく、`MoveFirst` や `MoveNext` を繰り返すのではなく、SQLレベルで可能な限りデータを削ぎ落とすこと。VBA層でのループ処理は、常に「最後の手段」であるべきだ。

結論

`Like ‘*text*’` は、便利だが劇薬である。
それを制御するには、単なる構文の知識ではなく、インデックスの物理構造と検索空間の制御という、データベースエンジニアリングの本質的な理解が不可欠である。

優れたエンジニアはコードを書くが、伝説的なアーキテクトは、データが流れる「経路」を設計するのだ。

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