「動く」コードと「速い」コードは違う。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*”` は、動くコードとしては正解かもしれない。しかし、「プロフェッショナルのコード」としては、不合格だ。
インデックスの仕組みを理解し、検索範囲を物理的に制御する。この一歩が、君の作るシステムの信頼性を決定づけるのである。
