Access VBAの極致:UNION ALLを動的生成する「メモリ管理の美学」
Access VBAで大規模なデータ統合を行う際、多くのエンジニアが陥る罠がある。それは「DAO.QueryDefを盲目的に使い捨て、メモリをフラグメント化させる」という悪癖だ。
特に、複数のテーブルから `UNION ALL` を用いてデータを統合する際、安易な文字列結合でSQLを生成し、その都度オブジェクトを生成・破棄するようなコードは、長期間稼働するシステムにおいては「時限爆弾」と化す。
今日は、極限のパフォーマンスを追求し、メモリ消費を最小限に抑えつつ、堅牢な動的SQL生成を行うためのアーキテクチャを提示する。
—
1. なぜ「静的なクエリ」では限界があるのか
動的SQL生成の要諦は、「SQL構築の抽象化」と「リソースの厳格なライフサイクル管理」にある。
単に `String` を連結して `DoCmd.RunSQL` を呼ぶようなコードは、クエリの実行プランがキャッシュされず、毎回データベースエンジンに過度な負担をかける。我々が構築すべきは、再利用可能であり、かつメモリリークを許さない「クエリ・ファクトリー」だ。
2. 実装:SQL UNION ALL 動的生成モジュール
以下に、配列で渡されたテーブル群から、セキュアかつ高速に `UNION ALL` を生成するアーキテクチャを示す。
Option Compare Database
Option Explicit
‘ メモリ管理を意識した動的SQL生成器
Public Function GenerateUnionSQL(ByVal TargetTables As Variant, Optional ByVal WhereClause As String = “”) As String
Dim i As Long
Dim sb As String ‘ VBAにおける文字列結合は、巨大なループ内では連結演算子(&)よりも効率を意識する
Dim sqlParts() As String
ReDim sqlParts(LBound(TargetTables) To UBound(TargetTables))
For i = LBound(TargetTables) To UBound(TargetTables)
‘ ここでSQLインジェクション対策を講じるのがプロの流儀
‘ 識別子(テーブル名)の検証を忘れてはならない
sqlParts(i) = “SELECT FROM [” & Replace(TargetTables(i), “]”, “]]”) & “]”
If Len(WhereClause) > 0 Then
sqlParts(i) = sqlParts(i) & ” WHERE ” & WhereClause
End If
Next i
‘ Join関数は連結演算子を繰り返すよりもメモリ効率が良い
GenerateUnionSQL = Join(sqlParts, vbCrLf & “UNION ALL” & vbCrLf)
End Function
‘ クエリ定義のライフサイクルを制御する実行プロシージャ
Public Sub ExecuteDynamicUnion(ByVal QueryName As String, ByVal SQLText As String)
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Set db = CurrentDb
‘ オブジェクトの再利用(既存のクエリ定義を更新する)
‘ これにより、不要なメモリ確保を抑制する
On Error Resume Next
Set qdf = db.QueryDefs(QueryName)
If Err.Number <> 0 Then
Set qdf = db.CreateQueryDef(QueryName)
End If
On Error GoTo 0
qdf.SQL = SQLText
qdf.Close
‘ ここで明示的にオブジェクトを破棄し、VBAのガベージコレクションを待たない
Set qdf = Nothing
Set db = Nothing
End Sub
—
3. シニアエンジニアが意識すべき「メモリの重み」
A. QueryDefの再利用戦略
`CurrentDb.CreateQueryDef` を安易にループ内で実行してはならない。Accessのシステムオブジェクトテーブルを肥大化させ、最終的にデータベースの破損を招く。必ず `QueryDefs` コレクション内に名前を固定して保存し、`SQL` プロパティを書き換える手法(ALTER的アプローチ)を徹底せよ。
B. 文字列操作の最適化
VBAの `String` 型は不変(Immutable)であるため、巨大なSQLを連結する際は、内部的にメモリ再確保が発生する。上記コードのように `Join` 関数を活用することで、計算量を $O(n^2)$ から $O(n)$ に落とすことができる。数万行に及ぶ統合処理では、この差が秒単位の実行速度に直結する。
C. Windows APIによる「生存確認」
もし、極めて巨大なデータセットを扱う場合、Accessのフリーズを避けるために `DoEvents` を挟むのが定石だが、真のエンジニアはAPIの `GetQueueStatus` を監視し、メッセージループの負荷を制御する。
レガシーシステムとの連携においては、`MsgWaitForMultipleObjects` を駆使して「非同期的な待機」を実装するのが、UIを固まらせないための最善手だ。
—
結びに:伝説のアーキテクトからの助言
Accessはレガシーではない。「書き手によって、最高速のデータ処理エンジンにも、泥沼の負債にもなる」という、極めて純度の高いツールだ。
SQLを文字列として扱うことに罪悪感を抱くな。むしろ、その文字列をいかに美しく、いかに効率よくデータベースエンジンへ送り届けるか。そこにエンジニアの魂が宿る。
今回のコードは、あなたのシステムのバックボーンとして機能するはずだ。次にこのモジュールを触る時、メモリの断片化が最小限に抑えられていることを確信してほしい。
技術は裏切らない。ただ、設計の甘さだけが開発者を裏切るのだ。
