【実務・中級編】動的SQLにおける「IN句」の制限を突破する:配列データをパラメータクエリへ効率的に渡す実装例 – Access VBA解析バイブル

スポンサーリンク

Access VBAの限界を突き抜ける:IN句の制約を「一時テーブルJOIN」で完全攻略する

Access開発の現場で避けて通れないのが、「大量のIDリストを条件にデータを抽出したい」という要件だ。多くの初学者はここで、文字列結合によるSQL生成という「禁じ手」に手を染める。

`”SELECT FROM T_Sales WHERE ID IN (” & strIDs & “)”`

これを見た瞬間、私は即座にコードレビューを差し戻す。なぜか? SQLインジェクションのリスク、文字数制限(255文字)、そして何よりプランキャッシュを汚染しパフォーマンスを破壊するからだ。

今日は、私が大規模業務システムで標準採用している、最も堅牢かつ高速な「一時テーブルJOIN」パターンを伝授する。

—

なぜ文字列結合のIN句が「悪」なのか

1. 実行計画の使い回しが効かない: SQL文が文字列として変化するたび、データベースエンジンはクエリプランを再生成する。これが重い。
2. 限界突破の恐怖: IN句のリストが長くなればなるほどSQLの実行制限に触れ、システムは唐突にクラッシュする。
3. セキュリティ: ユーザー入力を直接SQLに埋め込むのは、穴だらけのセキュリティを作るのと同じだ。

—

解決策:一時テーブルへの「バルクロード」と「JOIN」

アプローチはシンプルだ。
1. `Temp_Filter` のような一時テーブルを用意する。
2. 抽出したいIDを、DAOの `Recordset` を用いて高速に書き込む。
3. 本命のクエリで、その一時テーブルと `INNER JOIN` を行う。

これだけで、パラメータクエリの恩恵を最大限に受けつつ、SQLの柔軟性を完全に確保できる。

—

【実装例】堅牢な一時テーブルフィルタリング・エンジン

以下のコードは、単なるサンプルではなく、プロダクション環境でそのまま使える実用的な実装だ。

‘ ———————————————————————-
‘ @brief ID配列を一時テーブルに展開し、効率的に抽出を行う
‘ @param arrIDs 抽出対象のID配列
‘ ———————————————————————-
Public Sub RunDynamicFilter(ByRef arrIDs() As Long)
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim i As Long

Set db = CurrentDb

‘ 1. 一時テーブルをクリア(事前にテーブルは作成しておくこと)
db.Execute “DELETE FROM T_Temp_Filter”, dbFailOnError

‘ 2. トランザクションで高速書き込み
Set rs = db.OpenRecordset(“T_Temp_Filter”, dbOpenDynaset)
db.BeginTrans
On Error GoTo Rollback_Err

For i = LBound(arrIDs) To UBound(arrIDs)
rs.AddNew
rs!TargetID = arrIDs(i)
rs.Update
Next i

db.CommitTrans
rs.Close

‘ 3. JOINしたクエリを実行(UIへの反映等)
‘ クエリ定義「Q_Main_Selection」内で T_Sales INNER JOIN T_Temp_Filter を記述
DoCmd.OpenQuery “Q_Main_Selection”

Exit Sub

Rollback_Err:
db.Rollback
MsgBox “データの書き込みに失敗しました。”, vbCritical
End Sub

この設計が優れている理由

  • トランザクションの活用: `db.BeginTrans` を挟むことで、ディスクI/Oを最小限に抑え、書き込み速度を劇的に向上させている。
  • クエリの定型化: `Q_Main_Selection` は常に同じSQL構造を保つため、Access内部のクエリプランナが最適化を維持できる。
  • 保守性: SQL文がVBAコードの中に散逸しない。修正が必要な場合、クエリ定義をいじるだけで済む。

—

運用上の注意:マルチユーザー環境への配慮

この設計の唯一の弱点は「一時テーブルが共有されること」だ。複数ユーザーが同時にこの機能を使うと、データが混ざってしまう。

これを防ぐための「伝説的」な解法を教えよう。

1. セッションIDの導入: `T_Temp_Filter` に `SessionID (String)` カラムを追加する。
2. 実行時に識別: 起動時に `Environ(“UserName”)` やGUIDを生成し、そのセッションIDで書き込み・フィルタリングを行う。
3. クエリ側でフィルタ: `WHERE T_Temp_Filter.SessionID = [Forms]![Main]![txtSessionID]` とする。

これにより、何人が同時にアクセスしても、お互いのIDリストが干渉することはなくなる。

—

最後に:エンジニアとしての矜持

「動けばいい」というコードは、数ヶ月後の自分を苦しめる負債に過ぎない。今回紹介した「一時テーブルJOIN」パターンは、一見すると工数が増えるように感じるかもしれない。しかし、複雑なバグやパフォーマンス劣化の調査に費やす時間を考えれば、これがいかに「低コスト」であるかは自明だ。

Accessは古いツールと言われることもあるが、設計思想次第で現代のWeb APIに匹敵するスピードと堅牢性を発揮できる。技術は使いこなす側にこそ、その真価が宿る。

次の開発では、ぜひこのアーキテクチャを適用してみてほしい。あなたの書くコードが、プロフェッショナルの基準に達することを期待している。

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