【テクニカル・上級編】DAOのFindFirstメソッドとADOのFindメソッド:検索速度と柔軟性の比較 – Access VBA解析バイブル

スポンサーリンク

DAOの `FindFirst` と ADOの `Find`:大容量データ検索におけるアーキテクチャの真実と極限最適化

レガシーシステムの寿命は、我々が想像するよりもはるかに長い。社内の基幹を支えるMicrosoft Accessアプリケーションにおいて、数万、あるいは数百万件のレコードを抱えるテーブルからのデータ検索は、常にパフォーマンス上のボトルネックとして立ちはだかる。

「なぜ、この検索処理はこれほどまでに遅いのか?」
「DAOとADO、一体どちらを使うべきなのか?」

この永遠の問いに対し、単なる「古い」「新しい」という稚拙な二元論で語るフェーズはとうに過ぎた。今回は、DAO(Data Access Objects)の `FindFirst` メソッドと、ADO(ActiveX Data Objects)の `Find` メソッドの内部挙動、メモリモデル、そして実測値に基づくアーキテクチャの差異を、極限の知見とともに解き明かす。

—

1. 根底にあるアーキテクチャの相違:Jet/ACEエンジン vs OLE DB

まず、両者の本質的な違いを理解しなければならない。

  • DAO (`FindFirst`):

Accessのネイティブデータベースエンジンである Microsoft Jet(または ACE) と直接対話する。インデックス(Index)を完全に活用し、メモリ空間内でのダイレクトなポインタ走査を行うため、Access内部テーブル(あるいはリンクテーブル)に対する親和性は圧倒的だ。

  • ADO (`Find`):

Recordsetオブジェクトが保持するクライアントサイド(またはサーバーサイド)のキャッシュ内をシーケンシャルに検索する。SQL Server等の外部RDBMSからデータを取得し、それをローカルのメモリ上に展開した状態での検索においては強力だが、インデックスの効き方は接続プロバイダやカーソルタイプに強く依存する。

—

2. パフォーマンス検証:インデックスとデータ構造の罠

数百万件のオーダーを持つローカルテーブル(またはパスマンスルーではないリンクテーブル)に対し、特定の文字列や数値でレコードを1件ピンポイント、あるいは条件一致で抽出する際の挙動を比較する。

DAO `FindFirst` の優位性と限界

DAOの `FindFirst` は、検索条件に合致するフィールドにインデックスが存在する場合、Bツリーアルゴリズムの恩恵をダイレクトに受ける。

‘ 【DAO】インデックスを最大限に活かすFindFirstの実装パターン
Public Sub ExecuteDAOFinder(ByVal searchKey As String)
Dim db As DAO.Database
Dim rs As DAO.Recordset

‘ 最速のパフォーマンスを引き出すための定数指定
Set db = CurrentDb
Set rs = db.OpenRecordset(“T_LargeData”, dbOpenDynaset, dbDenyWrite)

‘ 検索対象フィールドにインデックスが貼られていることが前提
‘ 主キーまたはIndexed = Yes (Duplicates OK)
rs.FindFirst “TargetCode = ‘” & searchKey & “‘”

If Not rs.NoMatch Then
Debug.Print “DAO Hit: ” & rs.Fields(“ID”).Value
Else
Debug.Print “DAO: 該当レコードなし”
End If

‘ 【重要】オブジェクトの明示的解放(メモリリークの根絶)
rs.Close
Set rs = Nothing
Set db = Nothing
End Sub

チーフアーキテクトの知見:
DAOの `dbOpenDynaset` や `dbOpenSnapshot` において、`FindFirst` は検索条件がインデックス化されていれば爆発的な速度を叩き出す。しかし、インデックスのないフィールドに対して `FindFirst` を実行した場合、Jetエンジンはフルテーブルスキャン(全件走査)を余儀なくされ、パフォーマンスは劇的に劣化する。

ADO `Find` のメモリ空間とカーソル

一方、ADOの `Find` はRecordsetのオープンモード(CursorLocation)に支配される。

‘ 【ADO】クライアントサイドカーソルによるFindの実装パターン
Public Sub ExecuteADOFinder(ByVal searchKey As String)
Dim conn As Object
Dim rs As Object

Set conn = CreateObject(“ADODB.Connection”)
Set rs = CreateObject(“ADODB.Recordset”)

‘ Access CurrentProjectを用いた高速ローカル接続
conn.Open “Provider=Microsoft.ACE.OLEDB.12.0;Data Source=” & CurrentDb.Name

‘ クライアントサイドカーソルを指定しなければFindメソッドは使えない場合がある
rs.CursorLocation = 3 ‘ adUseClient
rs.Open “SELECT ID, TargetCode FROM T_LargeData”, conn, 1, 3 ‘ adOpenKeyset, adLockOptimistic

‘ ADOのFind構文(条件式はSQLのWHERE句の一部に準拠)
rs.Find “TargetCode = ‘” & searchKey & “‘”, 1, 1 ‘ 1 = adSearchForward

If Not rs.EOF Then
Debug.Print “ADO Hit: ” & rs.Fields(“ID”).Value
Else
Debug.Print “ADO: 該当レコードなし”
End If

‘ 明示的な切断と解放
rs.Close
conn.Close
Set rs = Nothing
Set conn = Nothing
End Sub

チーフアーキテクトの知見:
ADOの `Find` は、クライアントサイドカーソル(`adUseClient`)を使用するため、テーブルの全データをローカルメモリ上に一度引き込んでから検索を行う。これが何を意味するか。ネットワーク経由のバックエンドDB(SQL Server等)から数百万件を引っ張る場合、メモリ枯渇(Out of Memory)とネットワーク帯域の飽和を引き起こす。
しかし、すでにメモリ上に存在する静的な小規模〜中規模のキャッシュセット内を条件を変えて何度も高速に絞り込むようなユースケースでは、往復のオーバーヘッドがないADO `Find` が優位に立つこともある。

—

3. 実測値ベースの比較サマリー(100万件テーブルでの挙動)

| 評価項目 | DAO `FindFirst` (インデックス有) | DAO `FindFirst` (インデックス無) | ADO `Find` (adUseClient) |
| :— | :— | :— | :— |
| 初回の検索速度 | 極めて高速 (数ms) | 極めて低速 (数秒〜) | データのロード時間に依存 |
| メモリ消費量 | 最小限 (ポインタ参照のみ) | 最小限 | 最大 (全データをメモリ展開) |
| ネットワーク負荷 | ほぼゼロ (Jetが最適化) | 高い | 初回ロード時に全帯域を消費 |
| 柔軟性 | Jet SQLの構文に依存 | 同左 | 比較的柔軟な条件指定が可能 |

—

4. 極限の最適化:Windows APIとメモリ管理の深層

VBAプログラマブルな環境において、最大の悪夢は「メモリリークによるAccessの肥大化と突然のクラッシュ」である。DAOやADOのオブジェクトを適切に解放しないままループ処理を回すと、COMコンポーネントの参照カウンタが崩壊し、VBAのガベージコレクションが追いつかなくなる。

確実なオブジェクト解放の鉄則

‘ 例外発生時(エラーハンドリング時)をも想定した安全なパターン
Public Sub SafeDataSearch(ByVal searchKey As String)
Dim db As DAO.Database
Dim rs As DAO.Recordset

On Error GoTo ErrorHandler

Set db = CurrentDb
Set rs = db.OpenRecordset(“T_LargeData”, dbOpenSnapshot)

rs.FindFirst “TargetCode = ‘” & searchKey & “‘”
‘ 処理…

CleanExit:
‘ 【極重要】必ず逆順かつエラータダシ書きなしで確実に解放する
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:
MsgBox “エラー発生: ” & Err.Description, vbCritical
Resume CleanExit
End Sub

Windows APIを活用したパフォーマンス測定

ミリ秒単位の処理時間を正確に計測するためには、VBAの `Timer` 関数(精度が約10ms〜55msと粗い)ではなく、Windows APIの `QueryPerformanceCounter` を使用するべきである。プロファイリングの精度を極限まで高めることで、どちらのメソッドが真のボトルネックになっているかを暴くことができる。

Private Declare PtrSafe Function QueryPerformanceFrequency Lib “kernel32” (ByRef lpFrequency As Currency) As Long
Private Declare PtrSafe Function QueryPerformanceCounter Lib “kernel32” (ByRef lpPerformanceCount As Currency) As Long

Public Sub MeasurePerformance()
Dim freq As Currency, startTick As Currency, endTick As Currency
Dim elapsedTime As Double

QueryPerformanceFrequency freq
QueryPerformanceCounter startTick

‘ — 計測対象の処理(DAO or ADO) —
Call ExecuteDAOFinder(“TEST-KEY-001”)
‘ ———————————-

QueryPerformanceCounter endTick
elapsedTime = CDbl(endTick – startTick) / CDbl(freq) 1000 ‘ ミリ秒換算

Debug.Print “Execution Time: ” & Format(elapsedTime, “0.0000”) & ” ms”
End Sub

—

5. チーフアーキテクトからの最終提言:我々はどちらを選ぶべきか?

結論を下そう。

1. Accessローカル(またはJet/ACE形式のバックエンド)で完結するシステムにおいて、数万件以上の検索を行う場合:
迷うことなく DAOの `FindFirst` を採用せよ。ただし、大前提として検索対象フィールドに必ずインデックス(Index)を付与すること。インデックスなき `FindFirst` は百害あって一利なしである。
2. 外部RDBMS(SQL Server, PostgreSQL等)をODBC/OLE DB経由で接続し、既に取得済みのレコードセット内を高速にフィルター・検索したい場合:
ADOの `Find`(あるいはRecordsetの `Filter` プロパティ)の活用を検討せよ。ただし、サーバーサイドのクエリ(`SELECT … WHERE …`)で絞り込めるのであれば、そもそもクライアント側に全データをロードする設計自体を見直すべきである。検索はデータベースエンジンに喋らせるのが、アーキテクチャの基本原則なのだから。

レガシーとモダンが交錯するAccess開発の現場において、土台となるミドルウェアの挙動を熟知している者だけが、システムの寿命を延ばし、圧倒的なパフォーマンスを叩き出すことができる。感覚でコードを書く時代は終わった。理論と実測値に基づき、コードの隅々まで支配せよ。

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