【テクニカル・上級編】大量データ処理を高速化!QueryDefのキャッシュと再利用戦略 – Access VBA解析バイブル

スポンサーリンク

大量データ処理を高速化!QueryDefのキャッシュと再利用戦略

Access VBAにおけるパフォーマンスチューニングの金科玉条、それは「Jet/ACEデータベースエンジンがいかにSQLを嫌うか」を理解することから始まる。

素朴な開発者がやりがちな`.Run`や`CurrentDb.Execute`による都度SQL文字列の実行。これは、ループの回数だけSQLパーサーを起動し、構文解析(Parsing)と実行計画の最適化(Optimization)を繰り返すという、エンジンに対する暴力に他ならない。数万件のレコードを相手にするバッチ処理において、この「文字列の都度評価」は致命的なボトルネックとなる。

今回は、DAO(Data Access Objects)の`QueryDef`オブジェクトが持つプリコンパイル特性を極限まで引き出し、物理的な限界すれすれのパフォーマンスを発揮させるためのキャッシュと再利用戦略を解説する。

—

1. なぜ「都度SQL」は遅いのか? —— QueryDefのライフサイクル

Accessの裏側でうごめくJet(あるいはACE)データベースエンジンは、渡されたSQL文字列をそのまま実行するわけではない。

1. 構文解析(Lexical/Syntax Analysis): 文字列のタイポチェックやトークン化。
2. 最適化(Query Optimization): 統計情報に基づき、どのインデックスを使用するか、どの結合順序が最適かを計算(コストベースオプティマイザ)。
3. 実行計画の生成(Execution Plan Generation): バイトコードへのコンパイル。

この一連の重厚なプロセスを、ループ内の1回ごとに発生させるのが `CurrentDb.OpenRecordset(“SELECT … WHERE ID = ” & val)` や `CurrentDb.Execute` の悪癖である。

対して、一時QueryDef(Temporary QueryDef)や永続QueryDefをあらかじめメモリ上に生成し、パラメータ(Parameter)だけを差し替えて実行する手法をとった場合、ステップ1〜3は初回の一度のみに限定される。ループ内で行われるのは、コンパイル済みの実行計画への値のバインドと、ストリームの流し込みのみとなる。これが「QueryDefのキャッシュと再利用」の真髄である。

—

2. 実装パターン:動的パラメータクエリのインメモリ・キャッシュ

以下に、大量のトランザクションデータを高速に流し込むための、実戦投入仕様のコードを示す。
ここでは、一時QueryDefオブジェクトをループの外でインスタンス化し、メモリ上に保持し続けたままパラメータのみを書き換えて実行する。

Option Explicit

‘ =========================================================================
‘ 処理名: BulkInsertWithQueryDefCache
‘ 概要: 永続的あるいは一時的なQueryDefをキャッシュし、大量レコードの
‘ パラメータ化クエリ実行を極限まで高速化するサンプル
‘ =========================================================================
Public Sub BulkInsertWithQueryDefCache()
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim i As Long
Dim startTime As Double

‘ タイ測定開始
startTime = Timer

‘ CurrentDbプロパティの乱用を避け、オブジェクト変数をローカルにキャッシュ
Set db = CurrentDb

On Error GoTo ErrorHandler

‘ トランザクション開始によるI/Oオーバヘッドの削減
db.BeginTrans

‘ 【重要】QueryDefをループ外で一度だけ生成(プリコンパイルの強制)
‘ ※一時クエディションの場合、名前に “~” で始まる名前を付けるか、
‘ db.CreateQueryDef(“”, “…”) のように名前を省略する(DAO 3.6以降)
Set qdf = db.CreateQueryDef(“”, _
“INSERT INTO T_StockTransaction (ItemCode, Quantity, ProcessDate) ” & _
“VALUES ([pItemCode], [pQuantity], [pProcessDate]);”)

‘ ループ内でのパフォーマンスを最大化するため、パラメータコレクションへの参照を事前解決
‘ (コレクションのインデックス検索コストを排除)
Dim pItemCode As DAO.Parameter
Dim pQuantity As DAO.Parameter
Dim pProcessDate As DAO.Parameter

Set pItemCode = qdf.Parameters(“pItemCode”)
Set pQuantity = qdf.Parameters(“pQuantity”)
Set pProcessDate = qdf.Parameters(“pProcessDate”)

‘ 模擬的に10,000件の大量データを処理
For i = 1 to 10000
‘ パラメータに値をバインド(SQL文字列の結合は一切行わない)
pItemCode.Value = “ITEM-” & Format(i Mod 500, “0000”)
pQuantity.Value = (i Mod 100) + 1
pProcessDate.Value = Date

‘ 実行計画の再利用による超高速実行
qdf.Execute dbFailOnError
Next i

‘ コミット
db.CommitTrans

Debug.Print “処理完了 実行時間: ” & Format(Timer – startTime, “0.00秒”)

CleanUp:
‘ 【メモリ管理の鉄則】オブジェクトの明示的解放
‘ レガシー環境におけるメモリリークを防ぐため、確実に参照を切断する
If Not qdf Is Nothing Then
qdf.Close
Set qdf = Nothing
End If
Set pItemCode = Nothing
Set pQuantity = Nothing
Set pProcessDate = Nothing
Set db = Nothing
Exit Sub

ErrorHandler:
‘ 異常発生時はロールバック
db.Rollback
MsgBox “エラーが発生しました: ” & Err.Description, vbCritical
Resume CleanUp
End Sub

—

3. チーフアーキテクトが教える「極限の最適化」テクニック

上記のコードは基本形に過ぎない。実務の現場で遭遇する巨大なデータベースや、システム間連携の荒波を乗り切るための応用知見を授けよう。

① `CurrentDb` の乱用禁止(COMレイヤーのオーバーヘッド排除)

コード内で `CurrentDb.Execute` を何度も記述する開発者がいるが、これは最悪のアンチパターンである。`CurrentDb` を呼び出すたびに、Accessは新しいDAO.Databaseオブジェクトを裏側のCOMコンポーネントから取得し、内部キャッシュをクリアする。
必ず冒頭で `Set db = CurrentDb` とローカル変数に受け、スコープ内ではその参照を使い回せ。これだけで数パーセントから場合によっては数十パーセントの無駄なCPUサイクルを削減できる。

② パラメータコレクションの事前解決(Name Resolutionの排除)

`qdf.Parameters(“pItemCode”).Value = …` という記述は、一見問題なさそうに見えるが、ループ内でこれを実行すると、毎回DAOの内部コレクションから文字列キーによるハッシュ検索(Name Resolution)が発生する。
上記のコード例のように、ループに入る前にパラメータオブジェクトへの参照を変数にバインド(`Set pItemCode = qdf.Parameters(…)`)し、ループ内ではそのポインタを直接叩くことで、コレクション検索のオーバーヘッドを完全にゼロにできる。

③ トランザクション境界の制御 (`BeginTrans` / `CommitTrans`)

大量のインサート処理を行う際、デフォルトのままだと1レコード毎に暗黙のコミットとディスクへの書き込み(Flush)が発生し、物理I/Oがボトルネックになる。
必ず `db.BeginTrans` で囲み、メモリ上でトランザクションを完結させ、最後に一括して書き込ませること。ただし、Jet/ACEのトランザクションロック上限(通常約20,000〜30,000ロック)を超える場合は、適度な件数(例: 5,000件ごと)でコミットと `BeginTrans` の再発行を挟むパッチングロジックを組むべきである。

—

4. レガシー環境・システム間連携における実務的注意点

Accessを前端(フロントエンド)とし、バックエンドにSQL ServerやOracleを据えたADP/ODPS(またはリンクテーブル)環境において、このQueryDef戦略はさらに重要度を増す。

  • ODBC経由のパラメータ化クエリ: リンクテーブル経由で `QueryDef` を実行する場合、Jetエンジンはローカルでコンパイルした後、ODBCドライバーを介してリモートデータベース側へパラメタライズド・コマンド(SQL Serverであれば `sp_executesql` 等)として送信する。
  • これにより、SQLインジェクションのリスクを完全に排除できるだけでなく、リモートサーバー側でも実行計画がキャッシュされるため、ネットワーク帯域とサーバーCPUの双方を劇的に節約できる。

—

総括

VBAは「おもちゃの言語」などではない。その内部構造(DAOとJet/ACEエンジンのライフサイクル)を正確に理解し、メモリ管理とプリコンパイルの仕組みを意図通りにコントロールすれば、C#やJavaで書かれたバッチ処理に匹敵するスループットを叩き出すことが可能だ。

「なぜ遅いのか」を推測するな。計測し、アーキテクチャの原理原則に従ってコードを組み上げろ。それこそが、真のプロフェッショナル・エンジニアリングである。

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