【テクニカル・上級編】コレクション(Collection)と辞書(Dictionary)の使い分け:高速なデータ検索を実現する – Excel VBA解析バイブル

スポンサーリンク

コレクション(Collection)と辞書(Dictionary)の使い分け:高速なデータ検索を実現する

VBA(Visual Basic for Applications)のパフォーマンスチューニングにおいて、最大のボトルネックとなるのは「データの検索と突合」である。数万件のレコードを持つワークシートに対し、ループを回して `Cells(i, 1).Value` を総当たりで比較するようなコードを書いているうちは、プロフェッショナルとは言えない。

大量のリストから特定のキーで瞬時にデータを引き当てるために不可欠なのが、`Collection` オブジェクトと `Scripting.Dictionary` オブジェクトである。本稿では、この2つのデータ構造の内部挙動、メモリ最適化、そしてレガシー環境における罠まで、シニアエンジニアが知るべき極限の知見を解説する。

1. 内部アーキテクチャの比較:なぜDictionaryは高速なのか

まずは、VBA標準の `Collection` と、外部コンポーネントである `Scripting.Dictionary` の本質的な違いを理解する必要がある。

VBA.Collection

  • 内部構造: 片方向リスト(Singly Linked List)に近い構造+インデックス管理。
  • 検索計算量: $O(N)$ (最悪の場合、全件走査)。
  • 特徴: データの追加順序を保持するが、キーによるランダムアクセスはハッシュテーブルではなく線形探索に近い挙動をするため、数万件規模のデータになると急激にパフォーマンスが劣化する。また、既存キーの「上書き(存在確認)」が標準ではできず、エラーハンドリング(On Error Resume Next)を強要される設計上の欠陥がある。

Scripting.Dictionary (Microsoft Scripting Runtime)

  • 内部構造: ハッシュテーブル(Hash Table)。
  • 検索計算量: 平均 $O(1)$ (要素数に依存しない定数時間の検索)。
  • 特徴: キーと値をペアで保持する。ハッシュ関数によりメモリ上の格納位置を直接算出するため、10万件のデータからでも一瞬で値を引き当てることができる。さらに `.Exists(key)` メソッドを持ち、キーの重複チェックがスマートに行える。

2. 参照設定の罠と「レイトバインディング」の極意

`Scripting.Dictionary` を利用する際、多くの開発者が犯す致命的なミスが、VBAの「参照設定(COMコンポーネント)」への依存である。

‘ 【悪手】早期バインディング(Early Binding)
‘ ツール > 参照設定 から “Microsoft Scripting Runtime” にチェックが必要
Dim dict As Scripting.Dictionary
Set dict = New Scripting.Dictionary

このアプローチは開発時にはインテリセンスが効いて便利だが、配布先のエクセル環境(Officeのバージョン、32bit/64bitの差異、レジストリの破損など)によって「COM Error (Class does not support automation)」を引き起こす。社内システムや配布型ツールにおいて、ユーザーのPC環境に依存した参照設定を強いることはシステム障害の温床となる。

堅牢性を極めたレイトバインディング(Late Binding)

実戦投入するシステムでは、参照設定を一切行わず、実行時バインディングでインスタンスを生成するのがプロの鉄則である。

‘ 【推奨】レイトバインディング(Late Binding)
Dim dict As Object
Set dict = CreateObject(“Scripting.Dictionary”)

これにより、GUID解決のオーバーヘッドがわずかに発生するものの、環境差異によるコンパイルエラーを完全に回避できる。パフォーマンスへの影響は、数万回のループを行わない限り無視できるレベルである。

3. 実装コード:10万件のデータ突合を極限まで高速化する

以下に、ワークシート上の大量データを `Scripting.Dictionary` に読み込み、別データと高速に突合・集計する実用的なコードを示す。メモリの明示的解放(ガベージコレクションの自作)まで組み込んだ、プロダクション品質のコードだ。

Option Explicit

Public Sub HighSpeedDataMatching()
Dim wsMaster As Worksheet
Dim wsTarget As Worksheet
Dim rngMaster As Range
Dim rngTarget As Range
Dim masterData As Variant
Dim targetData As Variant
Dim dict As Object
Dim i As Long
Dim key As String
Dim startTime As Double

startTime = Timer

‘ 画面描画と自動計算を停止し、描画エンジンを沈黙させる
With Application
.ScreenUpdating = False
.Calculation = xlCalculationManual
.EnableEvents = False
End With

On Error GoTo ErrorHandler

‘ ワークシートの定義
Set wsMaster = ThisWorkbook.Sheets(“Master”)
Set wsTarget = ThisWorkbook.Sheets(“Target”)

‘ 1. マスタデータを配列として一括取得(Range.Valueは一瞬でメモリ上の配列に変換される)
‘ A列: キー, B列: 名称
With wsMaster
Set rngMaster = .Range(“A2”, .Cells(.Rows.Count, “A”).End(xlUp)).Resize(, 2)
End With
masterData = rngMaster.Value

‘ 2. Dictionaryの生成とデータ格納(O(1)の検索インデックス構築)
Set dict = CreateObject(“Scripting.Dictionary”)
dict.CompareMode = TextCompare ‘ キーの大文字小文字を区別しない設定(必要に応じて)

For i = 1 To UBound(masterData, 1)
key = CStr(masterData(i, 1))
If Len(key) > 0 Then
If Not dict.Exists(key) Then
‘ 値としてB列の名称を格納
dict.Add key, masterData(i, 2)
End If
End If
Next i

‘ 3. ターゲットデータを一括取得
With wsTarget
Set rngTarget = .Range(“A2”, .Cells(.Rows.Count, “A”).End(xlUp))
End With
targetData = rngTarget.Value

‘ 結果格納用の配列を準備(メモリ上で処理を完結させる)
Dim resultData() As Variant
ReDim resultData(1 To UBound(targetData, 1), 1 TO 1)

‘ 4. 高速突合処理(ワークシートへのアクセスを完全に排除)
For i = 1 To UBound(targetData, 1)
key = CStr(targetData(i, 1))
If dict.Exists(key) Then
resultData(i, 1) = dict(key) ‘ マッチした場合はマスタの値を設定
Else
resultData(i, 1) = “未登録”
End If
Next i

‘ 5. 結果をワークシートへ一括書き戻し
wsTarget.Range(“B2”).Resize(UBound(resultData, 1), 1).Value = resultData

MsgBox “処理完了: ” & Format(Timer – startTime, “0.00秒”), vbInformation

CleanUp:
‘ 【極限の知見】オブジェクトの明示的解放
‘ VBAのCOMオブジェクトはスコープ抜けだけではメモリリークの原因になることがある
If Not dict Is Nothing Then
dict.RemoveAll
Set dict = Nothing
End If

‘ アプリケーション設定の復元
With Application
.ScreenUpdating = True
.Calculation = xlCalculationAutomatic
.EnableEvents = True
End With
Exit Sub

ErrorHandler:
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical
Resume CleanUp
End Sub

4. シニアエンジニアが知るべきメモリ管理と「落とし穴」

オブジェクトの明示的解放 (`Set dict = Nothing`)

VBAのランタイムはガーベージコレクション(GC)のタイミングが曖昧である。特に `Scripting.Dictionary` はCOMオブジェクト(外部プロセス管理に近いメモリ領域)を消費するため、数万件の要素を抱えたままモジュールが終了すると、Excelのプロセス内にメモリ断片化やリークを残す原因となる。
処理の最後には必ず `dict.RemoveAll` で内部要素をクリアし、`Set dict = Nothing` で参照を切断すること。

Keyの型と大文字小文字の挙動

  • `Dictionary` のキーには、オブジェクトや配列を除くほとんどのプリミティブ型(String, Long, Doubleなど)が使える。
  • デフォルトでは `CompareMode = BinaryCompare` (大文字小文字を厳密に区別する:`”ABC”` と `”abc”` は別物)である。業務システムでは、ユーザーの入力揺れに対応するため、`dict.CompareMode = TextCompare` (大文字小文字を区別しない)を明示的に指定することを強く推奨する。

Collectionを使うべき唯一の場面

ここまで `Dictionary` の優位性を説いてきたが、`Collection` にも存在価値がある。それは 「データの追加順序を維持したまま、簡易的なユニークリスト(重複排除リスト)を作りたい時」 かつ 「データ量が数千件以下で速度が問題にならない時」 である。
しかし、キーの存在確認や値の更新が煩雑であるため、実務上のデータ処理においては、最初から `Scripting.Dictionary` を標準採用する方がバグの少ない堅牢なコードに仕上がる。

総括

VBAにおけるパフォーマンスチューニングの基本方針は一つしかない。「ワークシートへのアクセス回数を極限まで減らし、メモリ上の配列とハッシュテーブル(Dictionary)で殴る」 ことだ。

`Cells` や `Range` の読み書きは、ハードディスクの読み書きに匹敵するほどの圧倒的なオーバーヘッドを持つ。今回紹介した「配列一括取得 + Dictionary高速検索 + 配列一括書き戻し」の黄金パターンをマスターすれば、数万行のレガシーExcelレポートであっても、コンマ数秒の世界で動作する超高速システムへと生まれ変わらせることが可能となる。

アーキテクトとしての誇りを持ち、泥臭いループ処理とは決別せよ。

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