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

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

「動く」コードと「速い」コードは違う。DAO.FindFirstにおける「Like演算子の罠」を回避せよ

業務自動化の現場において、多くの開発者が陥る「死の落とし穴」がある。それは、「小規模なテストデータでは完璧に動作し、本番環境の膨大なデータに直面した瞬間に、システムが沈黙する」という現象だ。

特にAccess VBAを用いたローカルデータベース開発において、`Recordset.FindFirst` メソッドに `Like` 演算子を組み合わせてワイルドカード検索を行う手法は、もっともこの罠に嵌まりやすい。

君が書いたその一行が、インデックスというデータベースの心臓部を破壊し、フルスキャン(全件走査)という名の地獄を招いていないか? 今日は、そのメカニズムと、プロフェッショナルが取るべき設計思想を伝授する。

1. なぜ、その検索は遅いのか?:インデックスの崩壊

まず、データベースの高速化の要である「インデックス(B-Tree構造)」を理解せよ。インデックスは、辞書のようにデータが整列して格納されている仕組みだ。

インデックスが効くケース(Index Seek)

例えば、`Like “田中*”` という検索を行う場合、データベースエンジンはインデックスの「た」の項目へ直接ジャンプし、そこから「田中」で始まるレコードを効率よく拾い上げることができる。これを Index Seek と呼ぶ。

インデックスが死ぬケース(Full Table Scan)

問題は、`Like “*田中*”` や `Like “*田中”` のように、前方(左側)にワイルドカードを置いた瞬間だ。
インデックスは「先頭文字」でソートされている。先頭が何かわからない検索に対して、エンジンはインデックスを役立たずと判断し、テーブルの先頭から最後の一件まで、すべてのレコードを物理的に読み込むことを決断する。これが Full Table Scan だ。

データが100件なら一瞬だ。しかし、10万件、100万件となったとき、君のアプリケーションは「応答なし」のフリーズ状態に陥る。これが、初心者が仕掛ける「Like演算子の罠」の正体である。

2. アーキテクチャ設計の鉄則:SARGableなクエリを書け

プロフェッショナルは、クエリが SARGable(Search Argumentable:検索引数として利用可能) かどうかを常に意識する。つまり、「インデックスを利用できる書き方をしているか?」という問いだ。

実務における戦略は以下の3段階に集約される。

1. 前方一致への設計変更(最優先): ビジネスルールを見直し、`*前方一致` ではなく `前方一致*` で検索できないか検討する。
2. 検索用補助カラムの作成(次善策): どうしても部分一致が必要な場合、あらかじめ検索用のインデックス付きカラムを用意する。
3. 検索範囲の絞り込み(防御策): `Like` を使う前に、日付やIDなどのインデックスが効く条件で、対象レコードを極限まで絞り込んでから `FindFirst` を実行する。

3. プロダクション・クオリティのコード実装

以下に、単なる「動くコード」ではなく、エラーハンドリング、オブジェクトのライフサイクル管理、そしてパフォーマンスへの配慮を組み込んだ、実戦的な実装例を示す。

ここでは、「まずインデックスが効く条件で絞り込み、その後に部分一致を行う」という、実務で最も堅牢なパターンを実装する。

Option Compare Database
Option Explicit

' =================================================================
' Module: modDatabaseEngine
' Description: 高速かつ堅牢なデータ検索エンジン
' Author: Chief Architect
' =================================================================

Public Type SearchResult
    IsFound As Boolean
    RecordID As Long
    ' 必要に応じて他のフィールドを拡張
End Type

''' <summary>
''' 特定の条件に基づいてレコードを高速に検索する。
''' 前方一致を基本とし、どうしても必要な場合のみ絞り込み後のFindを使用する設計。
''' </summary>
''' <param name="targetID">絞り込みに使用するインデックス項目(例:部署IDなど)</param>
''' <param name="searchKeyword">検索したい文字列</param>
''' <returns>検索結果を格納したSearchResult構造体</returns>
Public Function FindRecordOptimized(ByVal targetID As Long, ByVal searchKeyword As String) As SearchResult
    Dim db As DAO.Database
    Dim rs As DAO.Recordset
    Dim strSQL As String
    Dim result As SearchResult
    
    ' 初期化
    result.IsFound = False
    result.RecordID = 0
    
    On Error GoTo ErrorHandler

    Set db = CurrentDb
    
    ' 【設計の肝】
    ' 1. いきなり Like "*keyword*" で検索してはいけない。
    ' 2. まず、インデックスが確実に効く「targetID」でRecordsetを絞り込む。
    ' 3. 絞り込まれた小さな集合に対してのみ、Like演算子を適用する。
    strSQL = "SELECT ID, TargetName FROM T_MasterTable WHERE DepartmentID = " & targetID
    
    ' OpenRecordsetでは、可能な限り読み取り専用(dbOpenSnapshot)を指定し、メモリ負荷を抑える
    Set rs = db.OpenRecordset(strSQL, dbOpenSnapshot)

    If Not rs.EOF Then
        ' 絞り込まれた集合に対して、部分一致検索を実行
        ' ここでの Like は、全件走査ではなく「絞り込まれた数件」に対してのみ行われるため高速
        rs.FindFirst "TargetName Like '*" & Replace(searchKeyword, "'", "''") & "*'"
        
        If Not rs.NoMatch Then
            result.IsFound = True
            result.RecordID = rs!ID
        End If
    End If

CleanUp:
    ' オブジェクトのライフサイクル管理を徹底すること。
    ' メモリリークとファイルロックを防ぐための必須工程。
    On Error Resume Next
    If Not rs Is Nothing Then
        rs.Close
        Set rs = Nothing
    End If
    Set db = Nothing
    Exit Function

ErrorHandler:
    ' 実務ではログ出力処理をここに挟む
    MsgBox "Critical Error in FindRecordOptimized: " & Err.Description, vbCritical, "System Error"
    Resume CleanUp
End Function

''' <summary>
''' トランザクション処理を伴うデータ更新の例
''' </summary>
Public Sub ExecuteSecureTransaction(ByVal targetID As Long, ByVal newValue As String)
    Dim db As DAO.Database
    Set db = CurrentDb
    
    On Error GoTo TransactionError

    ' トランザクション開始
    DBEngine.BeginTrans
    
    ' 更新処理
    db.Execute "UPDATE T_MasterTable SET TargetName = '" & Replace(newValue, "'", "''") & "' " & _
               "WHERE ID = " & targetID, dbFailOnError
               
    ' 全ての処理が成功したら確定
    DBEngine.CommitTrans
    Debug.Print "Transaction Committed Successfully."

CleanUp:
    Set db = Nothing
    Exit Sub

TransactionError:
    ' エラーが発生した場合は、中途半端な状態を防ぐためにロールバック
    DBEngine.Rollback
    MsgBox "Transaction Rolled Back due to error: " & Err.Description, vbExclamation
    Resume CleanUp
End Sub

4. 結論:アーキテクトとしての教訓

君が業務ツールを設計する際、常に自分に問いかけてほしい。

  • 「この検索は、データが増えた後も同じ速度で動くか?」
  • 「インデックスという資産を、無駄なワイルドカードで浪費していないか?」
  • 「エラーが発生したとき、データベースの整合性を守る仕組み(トランザクション)はあるか?」

`Like “*keyword*”` は、動くコードとしては正解かもしれない。しかし、「プロフェッショナルのコード」としては、不合格だ。

インデックスの仕組みを理解し、検索範囲を物理的に制御する。この一歩が、君の作るシステムの信頼性を決定づけるのである。

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