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*’` は、便利だが劇薬である。
それを制御するには、単なる構文の知識ではなく、インデックスの物理構造と検索空間の制御という、データベースエンジニアリングの本質的な理解が不可欠である。
優れたエンジニアはコードを書くが、伝説的なアーキテクトは、データが流れる「経路」を設計するのだ。
