Accessデータベースが「突然壊れる・重くなる」本当の原因:QueryDefの寿命とメモリ管理の極意
こんにちは。開発プロジェクトを率いる中で、数々のレガシーなAccessシステムを立て直してきたチーフアーキテクトの私だ。
日々の業務自動化、本当にご苦労様。
「動くものは正義」として、場当たり的にVBAを組み上げていないだろうか?
特に、Accessのパフォーマンスチューニングやファイルサイズ肥大化の文脈において、多くの開発者が「クエリの生成と消滅のライフサイクル」を完全に誤解している。
今回は、Access開発者が避けて通れない「保存済みクエリ」と「一時クエリ(Dynamic QueryDef)」の決定的な使い分けと、ファイル肥大化を防ぎ、かつ堅牢なシステムを構築するためのメモリ管理戦略を伝授しよう。
—
1. なぜあなたのAccessは勝手に太るのか?(非効率な設計の正体)
動的な条件(WHERE句やIN句のリストなど)を伴うSQLを実行するため、以下のようなコードを書いていないだろうか?
‘ 【アンチパターン】毎回QueryDefを保存してはいけない
Dim qdf As DAO.QueryDef
Set qdf = CurrentDb.CreateQueryDef(“TempQuery”, “SELECT FROM T_Sales WHERE Pref = ‘” & txtPref & “‘”)
‘ 処理…
DoCmd.OpenQuery “TempQuery”
‘ 削除を忘れたり、残し続けたりする
これをやり続けると、何が起きるか?
Access(JET/ACEエンジン)の仕様上、`CreateQueryDef` で名前を付けて作成したクエリは、明示的に削除するか、データベースをコンパクト化するまで、システム内部の予約領域(MSysObjects等)に物理的なゴミとして蓄積され続ける。
結果として:
1. ファイルの異常肥大化: 数十KBのコードのつもりが、数日運用するだけで数百MBに膨れ上がる。
2. システミックな破損(Corruption): 内部テーブルの断片化が極限に達し、ある日突然「データベースを修復できません」という致命的なエラーに見舞われる。
プロのエンジニアであれば、永続的な保存が必要なクエリと、その場限りの「使い捨て(一時クエリ)」は厳格に分離しなければならない。
—
2. アーキテクチャの原則:「保存」と「一時」の明確な境界線
設計の指針は極めてシンプルだ。
- 保存済みクエリ(Persistent QueryDef)
- 用途: フォームのレコードソース、レポートの基盤、あるいは複雑な結合(JOIN)や集計(GROUP BY)の事前コンパイル済みロジック。
- 特徴: 設計段階で固定されており、実行計画(Execution Plan)のキャッシュ恩恵を受けるもの。
- 一時クエリ(Transient/Dynamic QueryDef)
- 用途: 実行時のパラメータやユーザーの入力によってSQLの構造そのものが変動するもの。
- 特徴: 名前を付けずに生成し、メモリ上で完結させ、使い終わったら瞬時に破棄する。
ここで重要なのは、「名前を付けなければ、Accessのシステムカタログを汚さない」というDAOの仕様を逆手にとることだ。
—
3. 【プロダクションコード】メモリリークゼロの一時クエリ実行基盤
それでは、実務でそのまま使える、堅牢で美しいコードを提示しよう。
このコードは、動的SQLを安全にメモリ上で構築・実行し、確実に破棄(ガベージコレクション的なメモリ解放)を行うテンプレートだ。
Option Compare Database
Option Explicit
”’
”’ 永続的なクエリ定義を作らないため、ファイル肥大化を完全に防止する。
”’
”’ 実行するSQL文字列
”’
Public Function GetTransientRecordset(ByVal sqlText As String) As DAO.Recordset
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
On Error GoTo ErrorHandler
‘ 現在のデータベース参照を取得
Set db = CurrentDb
‘ 【核心】CreateQueryDefの第一引数に長さゼロの文字列(“”)を指定する。
‘ これにより、システムカタログに保存されない「一時QueryDef(匿名クエリ)」がメモリ上にのみ生成される。
Set qdf = db.CreateQueryDef(“”, sqlText)
‘ 必要であればパラメータのバインド(SQLインジェクション対策への布石)
‘ ※動的SQLを組み立てる際は、値のサニタイジングやパラメータオブジェクトの利用を推奨
‘ レコードセットのオープン(Snapshotでメモリフットプリントを最小化)
Set GetTransientRecordset = qdf.OpenRecordset(dbOpenSnapshot)
‘ 注意: qdfオブジェクトはこの時点で参照を切っても、
GOTO CleanUp
ErrorHandler:
MsgBox “クエリの実行中にエラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“詳細: ” & Err.Description, vbCritical, “データベースエンジンエラー”
Set GetTransientRecordset = Nothing
CleanUp:
‘ 【重要】QueryDefオブジェクトの参照を確実に解放
‘ 名前がないため、ここで参照を失うとメモリ上から即座に消滅する
If Not qdf Is Nothing Then
qdf.Close
Set qdf = Nothing
End If
Set db = Nothing
End Function
このコードの優れた設計ポイント
1. 名前なし `CreateQueryDef(“”, sqlText)`: システムカタログを一切汚さない。ファイルサイズが勝手に増える現象を根絶。
2. `dbOpenSnapshot` の採用: 読み取り専用の軽量なスナップショットとして開くことで、ロック競合を防ぎ、パフォーマンスを極限まで高めている。
3. 確実なクリーンアップ: エラーハンドリングとクローズ処理を分離し、メモリリークの温床となるオブジェクトの取り残しを完全に阻止。
—
4. 現場で使える実務応用:安全なデータ更新・一括処理への応用
SELECTだけでなく、INSERTやUPDATEといったアクションクエリ(DML)を実行する場合も、同様の思想が適用できる。
以下のプロシージャは、トランザクション制御と一時クエリを組み合わせた、業務システムに必須の堅牢な一括処理のサンプルだ。
”’
”’
Public Function ExecuteTransientAction(ByVal sqlText As String) As Boolean
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
ExecuteTransientAction = False
On Error GoTo TransactionError
Set db = CurrentDb
‘ トランザクション開始
db.BeginTrans
‘ 一時QueryDefの生成
Set qdf = db.CreateQueryDef(“”, sqlText)
‘ アクションクエリの実行
qdf.Execute dbFailOnError
‘ コミット
db.CommitTrans
ExecuteTransientAction = True
GoTo CleanUp
TransactionError:
‘ 異常時はロールバック
On Error Resume Next
db.Rollback
On Error GoTo 0
MsgBox “一括処理に失敗しました。変更はロールバックされました。” & vbCrLf & _
“エラー: ” & Err.Description, vbCritical, “トランザクション中断”
CleanUp:
If Not qdf Is Nothing Then
qdf.Close
Set qdf = Nothing
End If
Set db = Nothing
End Function
—
5. チーフアーキテクトからの提言
Accessは、正しく扱えば極めて強力で迅速なプロトタイピング&業務システム基盤となる。しかし、それは「適切なメモリ管理とライフサイクル設計」が行われている場合に限る。
- 「とりあえずクエリを作って保存する」という安易なコードは、技術的負債の塊だ。
- 動的SQLを扱うときは、名前を持たない一時クエリ(Anonymous QueryDef)を駆使し、使い捨ての精神でメモリをクリーンに保て。
この原則を守るだけで、あなたの作るAccessアプリケーションで見慣れないエラーや謎のファイル肥大化はピタリと止まるはずだ。
現場のコードを今すぐ見直し、プロフェッショナルなアーキテクチャへとアップデートしてほしい。健闘を祈る。
