DAO.QueryDefにおける「IN句」の呪縛を解く:関係代数に基づく動的配列処理の極致
Access VBAの設計において、避けては通れない難問がある。`SELECT … WHERE ID IN (1, 2, 3, …)` という動的なリストに基づく抽出だ。
多くの凡庸なエンジニアは、この問題を「文字列結合」で解決しようとする。`Join(array, “,”)` でSQLを構築し、`CurrentDb.OpenRecordset` に放り込む。だが、それは設計者としての敗北を意味する。SQLインジェクションの脆弱性を招き、ACE/JETエンジンのクエリプラン・キャッシュを汚染し、さらにはSQL構文の長さ制限(約64KB)という物理限界に突き当たるからだ。
真のシニアエンジニアが採るべき道は、SQLを動的に書き換えることではない。「データ」と「ロジック」を分離し、一時的なリレーションを構築することにある。
今回は、DAO.QueryDefの限界を超え、可変長配列を「一時テーブルとJOINさせる」ことで、堅牢かつ高速に処理する「トランジェント・リレーション手法」を解説する。
—
1. なぜ「文字列結合」は悪なのか
AccessのACEエンジンは、一度実行されたクエリの実行計画をキャッシュしようとする。しかし、`IN (1, 2)` と `IN (1, 2, 3)` は別物のクエリとして認識される。ユーザーが検索条件を変えるたびにエンジンは解析(パース)を強いられ、システムのメモリ空間とパフォーマンスをジワジワと侵食していく。
また、DAO.QueryDefのパラメータ(`Parameters`コレクション)は、スカラー値しか受け付けない。`IN (?)` に配列を渡すことは、VBAの言語仕様上、不可能だ。
2. アーキテクチャ:一時テーブルによる仮想パラメータ化
解決策はシンプルだが奥が深い。
1. 作業用テーブル(Local Temp Table)を定義する。
2. 配列の内容を、トランザクションを用いて高速にそのテーブルへ流し込む。
3. 固定されたQueryDefで、目的のテーブルと作業用テーブルをINNER JOINさせる。
この手法の美点は、QueryDefのSQLが「完全固定」されることにある。
3. 極限の知見を詰め込んだ実装コード
以下に、実戦配備可能なクラスモジュール級のコードを示す。ここでは、`DBEngine.BeginTrans` による高速化と、オブジェクトの確実な解放、そしてACEエンジンの特性を考慮した実装を行っている。
‘—————————————————————————————
‘ Procedure : ExecuteInClauseWithTempTable
‘ Author : Legendary Chief Architect
‘ Purpose : 配列をIN句として扱うための、一時テーブル結合型クエリ実行
‘—————————————————————————————
Public Sub ExecuteInClauseWithTempTable(ByRef targetIds() As Long)
On Error GoTo Error_Handler
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim rs As DAO.Recordset
Dim ws As DAO.Workspace
Dim i As Long
Set db = CurrentDb
Set ws = DBEngine.Workspaces(0)
‘ 1. 作業用テーブルのクリーンアップ
‘ ※ 予め “tmp_IDList” (ID Long) というテーブルを作成しておくのが定石
db.Execute “DELETE FROM tmp_IDList;”, dbFailOnError
‘ 2. 高速データ投入:トランザクションの開始
‘ 大量の配列要素を1つずつInsertするとディスクI/Oがボトルネックになる。
‘ BeginTransでラップすることで、メモリ上での一括処理を強制する。
ws.BeginTrans
Set rs = db.OpenRecordset(“tmp_IDList”, dbOpenTable, dbAppendOnly)
For i = LBound(targetIds) To UBound(targetIds)
rs.AddNew
rs.Fields(“ID”).Value = targetIds(i)
rs.Update
Next i
rs.Close
ws.CommitTrans ‘ ディスクへの書き込みを一括確定
‘ 3. 固定QueryDefの実行
‘ SQL例:
‘ SELECT T. FROM T_Main AS T
‘ INNER JOIN tmp_IDList AS L ON T.ID = L.ID;
‘ このSQLは一切動的に変更する必要がない。
Set qdf = db.QueryDefs(“qry_FilteredMainData”)
Set rs = qdf.OpenRecordset(dbOpenSnapshot)
‘ — データ処理ロジック —
Do Until rs.EOF
‘ Debug.Print rs!TargetFieldName
rs.MoveNext
Loop
Clean_Up:
‘ オブジェクトのライフサイクル管理を徹底する
‘ 解放の順序を誤ることは、メモリリークへの片道切符である。
If Not rs Is Nothing Then rs.Close: Set rs = Nothing
If Not qdf Is Nothing Then Set qdf = Nothing
Set db = Nothing
Exit Sub
Error_Handler:
If Not ws Is Nothing Then
‘ トランザクション実行中のエラーであればロールバック
‘ これによりデータベースの不整合を防ぐ
ws.Rollback
End If
MsgBox “Critical Error: ” & Err.Description, vbCritical, “System Failure”
Resume Clean_Up
End Sub
4. パフォーマンスを掌握するための補足
Windows APIによる高精度計測
もし、この処理が数万件の配列を扱うのであれば、`GetTickCount` や `QueryPerformanceCounter` を用いて、ボトルネックが「Insert」にあるのか「JOIN」にあるのかを特定すべきだ。ACEエンジンにおいて、`dbOpenTable` と `dbAppendOnly` の組み合わせは、最も高速なレコード追加手法である。
メモリ最適化と「膨張」対策
Access(.accdb)には「2GBの壁」がある。一時テーブルへの挿入と削除を繰り返すと、データベースファイルが肥大化する。これを防ぐには、「一時テーブル専用の外部データベース(Work.accdb)」を `DBEngine.CreateDatabase` で動的に作成し、処理が終わればファイルごと削除する、あるいは `RefreshLink` でリンクを張り替える手法が有効だ。これは大規模システムにおける定石である。
QueryDefの再利用性
`QueryDefs` コレクションに保存されたクエリは、Accessによってコンパイル済みとなる。文字列結合による `DoCmd.RunSQL` や `db.Execute(strSQL)` と比較して、実行プランの評価時間がゼロになるため、ループ内で繰り返し呼ばれる処理において圧倒的な差が出る。
5. 結論:シニアエンジニアとしての矜持
「動くコード」を書くことは難しくない。しかし、「10年後の保守担当者が、その意図の美しさに感嘆するコード」を書くことは極めて困難だ。
配列をIN句に渡すという些細な問題に対し、安易な文字列操作に逃げず、関係代数(JOIN)の原則に立ち返ること。そして、ハードウェアの特性(ディスクI/O、トランザクション)を理解し、ACEエンジンの潜在能力を引き出すこと。
その積み重ねこそが、レガシーと呼ばれるVBA環境において、現代のWebアーキテクチャにも劣らない堅牢なシステムを構築する唯一の道である。貴殿のコードが、単なる命令の羅列ではなく、洗練されたアーキテクチャの一部であることを願ってやまない。
