大規模プロジェクトにおけるリソース情報の読み込みを高速化する「Dictionary」活用術
Project VBAの領域において、数千行を超えるリソース、タスク、そしてそれらに紐づくコストや稼働率(Resource Assignment)のデータを扱うとき、多くの開発者が「なぜか処理が途方もなく遅い」という壁にぶ当たる。
特に、リソースIDやユニークIDをキーにして、割り当てられたコストや稼働状況を突き合わせる処理を素朴な `For` ループのネストで行えば、$O(n^2)$ の計算量の泥沼にハマり、数万件規模のデータの前では進捗バーがフリーズしたかのように微動だにしなくなる。
Excel VBAやProject VBAの標準機能である `Find` メソッドやシート・テーブルの走査は、内部でCOMの境界を跨ぐオーバーヘッドを伴うため、実用的な速度を一切担保しない。
この残酷な現実を打破し、メモリ上でミリ秒単位の検索・更新を実現する唯一の解が、`Scripting.Dictionary` を用いたインメモリ・インデックス化 である。
今回は、長年、巨大なエンタープライズ・プロジェクト管理基盤の裏側を支えてきたチーフアーキテクトの視点から、VBAにおけるDictionaryの極限活用術と、オブジェクトのライフサイクル管理、そしてパフォーマンスの限界を突破する実装パターンを授ける。
—
1. なぜ標準の検索手法では破綻するのか
Project VBAにおいて、リソース情報(`Resource` オブジェクト)や割り当て情報(`Assignment` オブジェクト)にアクセスする際、以下のようなコードを書いたことはないか?
‘ 【アンチパターン】毎回コレクションから線形探索を行う愚行
Dim t As Task, a As Assignment, r As Resource
For Each t In ActiveProject.Tasks
If Not t Is Nothing Then
For Each a In t.Assignments
‘ ここで毎回リソース名やIDでリソースを引く(O(N)の走査が発生)
Set r = ActiveProject.Resources.Item(a.ResourceID)
‘ 処理…
Next a
If Next t
このコードの罪深さは、COMオブジェクトのラッパーをループのたびに生成・破棄している点と、コレクションの先頭から順にハッシュなしで探索(Linear Search)している点にある。データ量が数千件を超えた瞬間、CPU使用率は張り付き、ファンの回転音が響き始める。
我々が目指すべきは、「データを一度だけメモリ(ハッシュテーブル)に読み込み、以後の検索コストを $O(1)$ に収斂させる」 アーキテクチャの構築である。
—
2. 実装:高速化エンジン「ResourceDictionaryManager」の全貌
以下のコードは、Project内の全リソースおよびコスト・稼働率データを一括してメモリ上の `Scripting.Dictionary` に取り込み、高速にクエリ・更新を行うための実用的なクラスモジュール(または標準モジュール)のテンプレートだ。
‘ Option Explicitの強制はプロフェッショナルの大前提
Option Explicit
‘ リソース情報を保持するカスタム構造体
Public Type ResourceCacheData
ResourceID As Long
ResourceName As String
StandardRate As Double
OvertimeRate As Double
MaxUnits As Double
Cost As Double
End Type
‘ 高速データアクセスのためのメインエンジン
Private m_ResourceDict As Object
”’
”’
Public Sub InitializeResourceCache()
‘ 既に存在する場合は解放
If Not m_ResourceDict Is Nothing Then
m_ResourceDict.RemoveAll
Set m_ResourceDict = Nothing
End If
‘ Dictionaryのインスタンス生成(大文字小文字を区別しない設定)
Set m_ResourceDict = CreateObject(“Scripting.Dictionary”)
m_ResourceDict.CompareMode = vbTextCompare
Dim r As Resource
Dim cacheData As ResourceCacheData
‘ 画面描画とイベントを停止し、COMの通信オーバーヘッドを極限まで削減
Application.ScreenUpdating = False
On Error GoTo ErrorHandler
‘ Project内の全リソースを一度だけ走査してメモリへキャッシュ
For Each r In ActiveProject.Resources
If Not r Is Nothing Then
cacheData.ResourceID = r.ID
cacheData.ResourceName = r.Name
cacheData.StandardRate = r.StandardRate
cacheData.OvertimeRate = r.OvertimeRate
cacheData.MaxUnits = r.MaxUnits
cacheData.Cost = r.Cost
‘ ユニークIDまたはリソース名をキーとしてDictionaryに格納
‘ キーに文字列(Cstr)を使用することでハッシュ衝突と検索ミスを防ぐ
Dim key As String
key = CStr(r.ID)
If Not m_ResourceDict.Exists(key) Then
m_ResourceDict.Add key, cacheData
End If
End If
Next r
CleanUp:
Application.ScreenUpdating = True
Exit Sub
ErrorHandler:
MsgBox “リソースキャッシュの構築中に致命的なエラーが発生しました: ” & Err.Description, vbCritical
Resume CleanUp
End Sub
”’
”’
Public Function GetResourceData(ByVal resourceID As Long, ByRef outData As ResourceCacheData) As Boolean
Dim key As String
key = CStr(resourceID)
If m_ResourceDict.Exists(key) Then
outData = m_ResourceDict.Item(key)
GetResourceData = True
Else
GetResourceData = False
End If
End Function
”’
”’
Public Sub Dispose()
If Not m_ResourceDict Is Nothing Then
m_ResourceDict.RemoveAll
Set m_ResourceDict = Nothing
End If
End Sub
—
3. チーフアーキテクトが教える「メモリ最適化」と「ライフサイクル管理」の極意
VBAのガベージコレクション(特にマクロ終了時の自動解放)を信用してはならない。特にMicrosoft Projectのような巨大なCOMオブジェクトモデルを内包するホストアプリにおいて、メモリリークはExcelやProject自体の強制終了(ハングアップ)に直結する。
1. `Set Nothing` による参照カウンタの即時デクリメント
Dictionaryや各種COMオブジェクト(`Resource`, `Task`, `Assignment`)の変数は、使用が終わった瞬間に `Set xxx = Nothing` を明示的に実行し、COMコンポーネントの参照カウンタを即座にゼロに落とせ。
特にループ内で一時的に取得するオブジェクト変数こそ、スコープの管理と解放の徹底が必要だ。
2. キーの型規格の統一(Type Coercionの回避)
`Scripting.Dictionary` のキーに `Long` 型や `Integer` 型をそのまま渡すと、内部でのハッシュ計算時に暗黙の型変換(Variantへのラップ)が発生し、パフォーマンスが劣化する。
「キーは必ず `CStr()` で明示的に文字列化して渡す」 これが、数万件の検索をノーウェイトで行うための鉄則である。文字列キーであれば、VBAの内部ハッシュアルゴリズムが極めて効率的に動作する。
3. バッチ処理中の画面描画・イベント抑制
`Application.ScreenUpdating = False` は基本中の基本だが、Project VBAにおいてはこれに加え、バックグラウンドでの再計算やスケジュールエンジンの一時停止(可能であれば手動計算モードへの切り替え)を検討せよ。データ読み込みの瞬間にProjectが再計算走査を始めると、メモリ上でDictionaryを作っている最中にCOMエラーを引き起こす原因となる。
—
4. システム間連携を見据えた拡張性
このDictionaryによるインメモリキャッシュ手法の真価は、Excelエクスポート、外部データベース(SQL Server等)とのSync、あるいはJSON形式でのAPI連携の前処理として機能させるときに発揮される。
外部システムへ渡すコストや稼働率の集計データを構築する際、データベースへ都度クエリを飛ばしたり、Projectのオブジェクトモデルを何度も往復してプロパティを叩く必要はない。すべてを一度 `Scripting.Dictionary`(または入れ子構造にした複数のDictionary)に爆速で展開し、メモリ上で加工を完結させたのち、一気に吐き出す。
このアーキテクチャを採用したシステムでは、処理時間が数分から「1秒未満」へと劇的に短縮された実績が無数にある。
—
結びにかえて
VBAは「おもちゃの言語」ではない。その裏側にあるCOMの構造とメモリのライフサイクルを完全に理解し、適切なアルゴリズム(この場合はハッシュテーブルによるインデックス化)を適用すれば、現代のエンタープライズ要件にも余裕で追従する怪物的なパフォーマンスを発揮する。
「動けばいい」という妥協を捨て、メモリ上のデータを制圧せよ。それこそが、真のVBAエンジニアリングである。
