索引の深淵を覗け:DAO.FindFirstにおける「Like演算子」の罠と、インデックスを強制駆動させる極限の最適化術
実務におけるAccessアプリケーションのパフォーマンス劣化、その大半は「ネットワーク越しに展開される巨大なレコードセット」と「インデックスを無効化する不適切な検索条件」の衝突によって引き起こされる。
特に、フロントエンドとバックエンドを分離したリンクテーブル環境において、`DAO.Recordset.FindFirst` メソッドに `Like` 演算子を組み合わせて使用する際、多くのエンジニアが「動けば良い」という安易な設計思想から、致命的なスローダウンを招いている。
本稿では、単なる書き方の解説ではない。B-Treeインデックスの構造的特性と、DAOエンジンがどのように検索を実行するかというアーキテクチャの観点から、検索を「高速なインデックス走査」へと昇華させるための技術的知見を提示する。
1. なぜ `Like “*キーワード”` は死を招くのか
結論から述べよう。`Like “*ABC”` のように、ワイルドカードを前方(接頭辞)に配置した瞬間、データベースエンジンが構築したB-Treeインデックスは、その価値を完全に喪失する。
インデックスの構造的限界
B-Treeインデックスは、値をソートされた状態で保持している。例えば「氏名」列にインデックスがある場合、エンジンは「あ」で始まるレコードを高速に特定できる。これは、ツリーのルートから特定の分岐を辿るだけで、探索範囲を劇的に絞り込めるからだ。
しかし、`Like “*ABC”` という条件は、「末尾がABCであるもの」を探せという命令だ。インデックスはソートされているが、末尾の値に基づいてソートされているわけではない。結果として、エンジンはインデックスの枝を辿ることを諦め、テーブルの全レコードを一行ずつ読み込む 「フルテーブルスキャン(Full Table Scan)」 を強制されることになる。
リンクテーブル環境では、この「全件読み込み」がネットワーク帯域を圧迫し、クライアント側のメモリを食いつぶし、システム全体のハングアップを引き起こす。
2. DAOにおける「検索の作法」:SARGableなクエリへの転換
検索条件がインデックスを活用できる状態を、専門用語で SARGable (Search ARGumentable) と呼ぶ。`FindFirst` を使う際も、この概念を徹底しなければならない。
インデックスを有効活用できるパターン
- `Like “ABC*”` (前方一致):インデックスの走査が可能。
- `Like “ABC?*”` (特定文字数指定):インデックスの走査が可能。
- `= “ABC”` (完全一致):最も高速。
インデックスを殺すパターン
- `Like “*ABC”` (後方一致):フルスキャンを誘発。
- `Like “*ABC*”` (部分一致):フルスキャンを誘発。
- `Field Like ‘ABC’` (型不一致):暗黙の型変換が発生し、インデックスが無視されることがある。
3. 実装における極限の最適化:ハイブリッド・アプローチ
シニアエンジニアが取るべき戦略は、`FindFirst` にすべてを委ねるのではない。「SQLによるインデックス走査」と「DAOによる特定レコードへの直接アクセス」を組み合わせるハイブリッド・アプローチ である。
以下のコード例では、大量のデータが存在するリンクテーブルに対し、どのようにしてインデックスを最大限に活用し、かつオブジェクトのライフサイクルを適切に管理するかを示す。
''' <summary>
''' 高速インデックス活用型検索プロシージャ
''' </summary>
''' <param name="searchKeyword">検索キーワード(前方一致を想定)</param>
Public Sub OptimizedSearch(ByVal searchKeyword As String)
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim rsTarget As DAO.Recordset
Dim strSql As String
Dim targetID As Long
On Error GoTo ErrorHandler
' 1. コンテキストの確立
Set db = CurrentDb
' 2. 戦略的アプローチ:
' FindFirstで巨大なレコードセットを走査するのではなく、
' まずSQLで「インデックスを利用してIDのみ」を高速に特定する。
' これにより、ネットワーク転送量を最小化(IDのみの転送)する。
' ※検索キーワードにワイルドカードが含まれる場合、エスケープ処理や
' 前方一致への強制的な書き換えが必要。
strSql = "SELECT [ID] FROM [T_MasterTable] " & _
"WHERE [CustomerName] Like '" & Replace(searchKeyword, "'", "''") & "*';"
' 3. 最小限のデータセットを開く
' ここで使用するカーソルタイプは、読み取り専用かつ前方スクロールで十分
Set rs = db.OpenRecordset(strSql, dbOpenSnapshot)
If Not rs.EOF Then
' 最初のヒットしたIDを取得
targetID = rs.Fields("ID").Fields(0).Value
' 4. 特定したIDを用いて、目的のレコードへ「直接アクセス」する
' これはインデックス(主キー)による一撃の検索である。
' FindFirstを巨大なRecordsetに対して実行するより遥かに高速。
Set rsTarget = db.OpenRecordset("SELECT * FROM [T_MasterTable] WHERE [ID] = " & targetID, dbOpenDynaset)
If Not rsTarget.EOF Then
Debug.Print "Target Found: " & rsTarget.Fields("CustomerName").Value
' ここで業務ロジックを実行
End If
' オブジェクトの明示的解放
rsTarget.Close
Set rsTarget = Nothing
Else
Debug.Print "No matching record found."
End If
CleanUp:
' 5. メモリリーク防止のための徹底した解放
' エラー発生時も確実にオブジェクトを破棄する。
On Error Resume Next
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 Sub
ErrorHandler:
' ログ出力等のエラーハンドリング(実務ではWindows API等を用いたログ出力が望ましい)
MsgBox "Critical Error: " & Err.Description, vbCritical
Resume CleanUp
End Sub
4. アーキテクチャ設計における「重み」の理解
この実装において、私が最も重視しているのは以下の3点だ。
I/Oコストの最小化
`db.OpenRecordset(“SELECT * FROM …”)` で全フィールドをメモリに展開するのは、プロフェッショナルの仕事ではない。まず `SELECT [ID]` でインデックスの恩恵を受けながら「針(ターゲット)」を見つけ、その後に「糸(全データ)」を引き寄せる。この二段構えが、ネットワーク負荷を最小化する鍵となる。
カーソルタイプの選択
`dbOpenSnapshot` は、データの変更を許さない読み取り専用の静的なスナップショットだ。更新の必要がない検索フェーズでは、`dbOpenDynaset` よりもオーバーヘッドが少なく、メモリ消費も抑えられる。
オブジェクトのライフサイクル管理
VBAのガベージコレクションは、C++やJavaほど厳密ではない。特にリンクテーブルを扱う場合、`Recordset` オブジェクトがメモリ上に残存していると、バックエンドのテーブルロックや、ファイル共有の競合を引き起こす原因となる。`Set rs = Nothing` は単なる作法ではなく、システムの安定稼働を担保するための「防衛策」である。
結論
`FindFirst` を使う際、あなたは「今、インデックスが効いているか?」を常に自問自答しなければならない。
ワイルドカードを前に置くという、一見無害なコードが、システムの心臓部を停止させる猛毒になり得ることを忘れてはならない。
インデックスを支配せよ。それが、レガシーな環境においても、堅牢で高速なシステムを構築できる唯一の道である。
