【実務・中級編】QueryDefの実行時間を計測し、遅いSQLを自動特定する「パフォーマンスプロファイラ」の自作 – Access VBA解析バイブル

スポンサーリンク

はじめに:なぜあなたのAccessアプリは遅いのか?

こんにちは。開発プロジェクトの現場で、数々のレガシーなAccessシステムをモダンで堅牢なアーキテクチャへと蘇らせてきたチーフアーキテクトだ。

現場でよく耳にする悲鳴がある。
「ボタンを押してから画面が固まるまで数秒かかる」「データが増えてから極端に遅くなった」

この原因の9割は、場当たり的に書かれたクエリと、その実行コストを誰も計測していない「ブラックボックス状態」にある。SQLのパフォーマンスチューニングにおいて、推測は最大の悪だ。感覚で「ここが遅いはずだ」と修正しても、無駄骨に終わることが多い。

必要なのは、「どのQueryDefが、何秒かかっているのか」を客観的な数値としてログに刻み、ボトルネックを物理的に特定するメカニズムである。

今回は、Access VBAの真骨頂である`QueryDef`オブジェクトと`Timer`関数を極限まで活用し、遅いSQLを自動特定する「パフォーマンスプロファイラ」の全コードを授けよう。

—

1. アーキテクチャ設計:なぜ「DBEngine.Idle」と「QueryDef」なのか?

実務で使えるプロファイラを作るには、Access(Jet/ACEエンジン)特有のクセを熟知していなければならない。

トラップ1:非同期実行とキャッシュの罠

VBAでクエリを実行した際、Accessはバックグラウンドで遅延書き込みやクエリプランのキャッシュを行う。そのため、連続計測すると2回目以降の実行速度が異常に速くなり、実測値を見誤る。正確な計測には `DBEngine.Idle` を挟み、キャッシュやペンディング中の書き込みを強制フラッシュする必要がある。

トラップ2:DoCmd.OpenQueryの限界

UI用の `DoCmd.OpenQuery` は、デバッグやログ採取の文脈では使い物にならない。エラーハンドリングが難しく、実行時間の精密な計測もできない。
我々は、DAOの `QueryDef` オブジェクトを直接操作し、パラメータのバインドから実行、エラーキャッチまでをコード内で完全にコントロールする。

—

2. プロダクションコード:セルフホスト型パフォーマンスプロファイラ

以下のコードは、指定したQueryDefを安全に実行し、実行時間をミリ秒単位で計測。さらに、閾値を超えた「重いクエリ」を自動的にローカルのログテーブル(またはテキスト)へ記録するプロファイラクラスの完成形だ。

プロジェクトに標準モジュールを1つ追加し、以下のコードをそのまま貼り付けてほしい。

実装コード(Standard Module: `modQueryProfiler`)

Option Compare Database
Option Explicit

‘ =========================================================================
‘ 模块名: modQueryProfiler
‘ 概要: QueryDefの実行時間を計測し、遅延クエリを検知・記録するプロファイラ
‘ アーキテクト特記事項:
‘ – 実行前後にDBEngine.Idleを挟み、純粋なクエリ処理時間を計測する
‘ – エラー発生時も確実にログを残し、トランザクションの整合性を保つ
‘ =========================================================================

‘ 警告とする実行時間の閾値(秒)
Private Const WARNING_THRESHOLD_SEC As Double = 1.0

Public Sub ExecuteAndProfileQuery(ByVal queryName As String, Optional ByVal logTableName As String = “T_QueryLog”)
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim startTime As Double
Dim elapsedTime As Double
Dim isError As Boolean
Dim errDesc As String

On Error GoTo ErrorHandler

Set db = CurrentDb

‘ 存在チェック
On Error Resume Next
Set qdf = db.QueryDefs(queryName)
On Error GoTo ErrorHandler

If qdf Is Nothing Then
MsgBox “指定されたクエリが見つかりません: ” & queryName, vbCritical, “プロファイラエラー”
Exit Sub
End If

‘ 計測前のエンジンフラッシュ(正確な計測のため)
DBEngine.Idle dbRefreshCache

‘ — 計測開始 —
startTime = Timer

‘ クエリの種類に応じた実行
‘ ※アクションクエリ(更新・追加・削除)と選択クエリで分岐
Select Case qdf.Type
Case dbQAction, dbQAppend, dbQDelete, dbQUpdate, dbQMakeTable
qdf.Execute dbFailOnError
Case Else
‘ レコードセットとして開いて即閉じることで、最適化エンジンを完全に走らせる
Dim rs As DAO.Recordset
Set rs = qdf.OpenRecordset(dbOpenSnapshot, dbOpenForwardOnly)
If Not rs.EOF Then rs.MoveLast ‘ 全レコードをフェッチして真の負荷を測る
rs.Close
Set rs = Nothing
End Select

‘ — 計測終了 —
elapsedTime = Timer – startTime

‘ ログテーブルへの書き込み
Call WriteLog(db, logTableName, queryName, elapsedTime, False, “”)

‘ 閾値超過アラート(開発者へのフィードバック)
If elapsedTime >= WARNING_THRESHOLD_SEC Then
Debug.Print “[性能警告] 処理遅延検知: ” & queryName & ” (” & Format(elapsedTime, “0.000”) & “秒)”
‘ 必要に応じてここでポップアップやSlack通知APIを叩く設計も可能
End If

CleanUp:
On Error Resume Next
Set qdf = Nothing
Set db = Nothing
Exit Sub

ErrorHandler:
isError = True
errDesc = Err.Description
elapsedTime = Timer – startTime

‘ エラー時もログに記録
Call WriteLog(db, logTableName, queryName, elapsedTime, True, errDesc)

MsgBox “クエリ実行中にエラーが発生しました。” & vbCrLf & _
“クエリ名: ” & queryName & vbCrLf & _
“エラー内容: ” & errDesc, vbCritical, “プロファイラ異常終了”

Resume CleanUp
End Sub

Private Sub WriteLog(ByVal db As DAO.Database, ByVal logTable As String, ByVal qName As String, ByVal execTime As Double, ByVal hasError As Boolean, ByVal errMsg As String)
On Error GoTo LogError

‘ ログテーブルが存在しない場合は自動生成する(自己修復型設計)
If Not TableExists(db, logTable) Then
CreateLogTable db, logTable
End If

Dim sql As String
sql = “INSERT INTO [” & logTable & “] (LogTime, QueryName, ExecutionTimeSec, IsError, ErrorMessage, UserName) ” & _
“VALUES (” & _
# & Format(Now, “yyyy/mm/dd hh:nn:ss”) & # & “, ” & _
“‘” & Replace(qName, “‘”, “””) & “‘, ” & _
Replace(CStr(execTime), “,”, “.”) & “, ” & _
IIf(hasError, True, False) & “, ” & _
“‘” & Replace(errMsg, “‘”, “””) & “‘, ” & _
“‘” & Environ(“USERNAME”) & “‘)”

db.Execute sql, dbFailOnError
Exit Sub

LogError:
‘ ログ書き込み自体のエラーで本体を止めないよう、Debug.Printにフォールバック
Debug.Print “[致命的] ログ書き込み失敗: ” & Err.Description
End Sub

Private Function TableExists(ByVal db As DAO.Database, ByVal tableName As String) As Boolean
Dim tdf As DAO.TableDef
TableExists = False
For Each tdf In db.TableDefs
If tdf.Name = tableName Then
TableExists = True
Exit For
End If
Next
End Function

Private Sub CreateLogTable(ByVal db As DAO.Database, ByVal tableName As String)
Dim sql As String
sql = “CREATE TABLE [” & tableName & “] (” & _
“LogID AUTOINCREMENT PRIMARY KEY, ” & _
“LogTime DATETIME, ” & _
“QueryName TEXT(255), ” & _
“ExecutionTimeSec DOUBLE, ” & _
“IsError YESNO, ” & _
“ErrorMessage MEMO, ” & _
“UserName TEXT(100))”
db.Execute sql, dbFailOnError
End Sub

—

3. このプロファイラが現場にもたらす3つのメリット

1. 「なんとなく遅い」からの脱却
`T_QueryLog` テーブルにデータが蓄積されるため、「どのクエリが、いつ、何秒かかったのか」のトレンド分析が可能になる。データ件数が膨れ上がった時にどのSQLが破綻するのかが数字で一目瞭然となる。
2. 自動テーブル生成(ゼロコンフィグ)による導入の容易さ
初めてこのコードを実行した際、ログテーブル `T_QueryLog` が存在しなくても、コードが自律的にスキーマを構築する。デプロイ時の手数が減り、保守性が飛躍的に向上する。
3. トランザクションとエラーハンドリングの堅牢性
万が一クエリがエラーを起こした場合でも、その実行時間とエラーメッセージを正確にキャプチャしてログに残す。開発者がデバッグしやすい環境を強制的に作り出す。

—

4. 現場で今すぐ実践するための使い方

例えば、フォームのボタンクリックイベントなどで重いクエリを呼び出すときは、以下のように書き換えるだけだ。

Private Sub cmdRunHeavyProcess_Click()
‘ 従来:DoCmd.OpenQuery “qryMonthlyAggregation” など(ブラックボックス)

‘ 改善後:プロファイラ経由で実行し、時間を計測・記録する
Call ExecuteAndProfileQuery(“qryMonthlyAggregation”)

MsgBox “処理が完了しました。”, vbInformation, “完了”
End Sub

これだけで、裏側で実行時間が計測され、1秒を超えたものは自動的に警告され、ログテーブルに蓄積されていく。

—

おわりに:プロフェッショナルとしてのコードを書け

Accessは「誰でも簡単に作れる」がゆえに、パフォーマンスへの配慮が欠落したスパゲッティコードが蔓延しやすい。しかし、アーキテクチャさえ正しく設計すれば、企業の中核を担う高速な基幹システムへと生まれ変わらせることが可能だ。

今回紹介したプロファイラをあなたの開発環境に組み込み、まずは「遅さの正体」を数値化することから始めてほしい。コードの質が変われば、システムの寿命も確実に延びる。

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