ねらい:辞書と配列を「なんとなく」ではなく「型」として使いこなす
辞書(Scripting.Dictionary)と配列は、VBAで高速化するときの主役です。
でも、なんとなく使うと「速いときもあるけど、たまに遅い・読みにくい・バグりやすい」状態になりがちです。
辞書×配列の最適化テンプレのゴールは、こうです。
「このパターンで書けば、だいたい速くて、読みやすくて、再利用しやすい」という“型”を持つこと。
特に、JOIN・集計・検索・マスタ参照のような処理を、安定して書けるようにします。
基本パターン:シート → 配列 → 辞書 → 配列 → シート
全体の流れを一枚の絵として持つ
辞書×配列の最適化は、だいたい次の流れに収まります。
シートから、必要な範囲だけを配列に一括読み込みする
配列をループしながら、辞書に「キー→値」を詰める(または辞書を参照する)
結果を別の配列に組み立てる
最後に、その配列をシートに一括で書き戻す
この「Excelと話すのは最初と最後だけ」「中身は全部メモリ内で完結」という構造が、
速さと安定性の源です。
例題1:辞書×配列で「マスタ参照JOIN」を最適化する
やりたいことのイメージ
明細シート Data にこういう列があるとします。
A列:顧客コード
B列:売上金額
マスタシート Customer にこういう列があるとします。
A列:顧客コード
B列:顧客名
やりたいことは、「Data に顧客名を付ける」=顧客コードでマスタ参照するJOINです。
これを辞書×配列で最適化します。
マスタを辞書に載せるテンプレ
まず、「顧客コード→顧客名」の辞書を作る関数を用意します。
Function BuildCustomerDict() As Object
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Customer")
Dim lastRow As Long
lastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row
If lastRow < 2 Then
Set BuildCustomerDict = Nothing
Exit Function
End If
Dim src As Variant
src = ws.Range("A2:B" & lastRow).Value ' 見出し除く
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
dict.CompareMode = vbTextCompare ' 大文字小文字を区別しない場合
Dim i As Long
For i = 1 To UBound(src, 1)
Dim key As String
key = CStr(src(i, 1))
If key <> "" Then
If Not dict.Exists(key) Then
dict.Add key, CStr(src(i, 2))
End If
End If
Next i
Set BuildCustomerDict = dict
End Function
VBここでの最適化ポイントを押さえます。
一度だけ配列に読み込んでから辞書を作る(Cellsで1行ずつ読まない)
CompareMode を設定しておくと、後の検索が安定する
重複キーは「最初の1件だけ採用」など、ルールを決めておく
辞書を作る処理は、JOIN以外でも何度も使うので、必ず関数化しておきます。
明細側に顧客名を付けるテンプレ
次に、Data シートに顧客名を付ける処理です。
Sub JoinCustomerName()
Dim dict As Object
Set dict = BuildCustomerDict()
If dict Is Nothing Then
MsgBox "Customerマスタが空です。", vbExclamation
Exit Sub
End If
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Data")
Dim lastRow As Long
lastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row
If lastRow < 2 Then Exit Sub
Dim src As Variant
src = ws.Range("A1:B" & lastRow).Value ' A:顧客コード, B:売上
Dim rCount As Long
rCount = UBound(src, 1)
Dim dst() As Variant
ReDim dst(1 To rCount, 1 To 1)
dst(1, 1) = "顧客名"
Dim i As Long
For i = 2 To rCount
Dim key As String
key = CStr(src(i, 1))
If dict.Exists(key) Then
dst(i, 1) = dict(key)
Else
dst(i, 1) = "" ' 見つからない場合の扱いはルール化しておく
End If
Next i
ws.Range("C1").Resize(rCount, 1).Value = dst
End Sub
VBここでの辞書×配列の最適化ポイントは、こうです。
マスタ側は一度だけ辞書化し、明細側は配列でループしながら辞書を参照する
結果は別配列にためてから一括書き戻しする
辞書の Exists→Item のセットを1回のループで完結させる
このパターンは、「マスタ参照JOIN」の基本テンプレとして、そのまま使い回せます。
例題2:辞書×配列で「高速集計」を最適化する
やりたいことのイメージ
Data シートに、次のような明細があるとします。
A列:商品コード
B列:売上金額
これを「商品コードごとの合計売上」に集計したい。
ピボットではなく、VBAで柔軟に扱いたいときのテンプレです。
辞書で集計し、配列で出力するテンプレ
Sub AggregateSalesByItem()
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Data")
Dim lastRow As Long
lastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row
If lastRow < 2 Then Exit Sub
Dim src As Variant
src = ws.Range("A2:B" & lastRow).Value ' 見出し除く
Dim rCount As Long
rCount = UBound(src, 1)
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
dict.CompareMode = vbTextCompare
Dim i As Long
For i = 1 To rCount
Dim key As String
key = CStr(src(i, 1))
Dim val As Double
If IsNumeric(src(i, 2)) Then
val = CDbl(src(i, 2))
Else
val = 0
End If
If dict.Exists(key) Then
dict(key) = dict(key) + val
Else
dict.Add key, val
End If
Next i
Dim n As Long
n = dict.Count
Dim dst() As Variant
ReDim dst(0 To n, 1 To 2)
dst(0, 1) = "商品コード"
dst(0, 2) = "合計売上"
Dim idx As Long
idx = 1
Dim k As Variant
For Each k In dict.Keys
dst(idx, 1) = k
dst(idx, 2) = dict(k)
idx = idx + 1
Next k
Dim wsOut As Worksheet
On Error Resume Next
Set wsOut = ThisWorkbook.Worksheets("AggItem")
On Error GoTo 0
If wsOut Is Nothing Then
Set wsOut = ThisWorkbook.Worksheets.Add
wsOut.Name = "AggItem"
End If
wsOut.Cells.Clear
wsOut.Range("A1").Resize(n + 1, 2).Value = dst
End Sub
VBここでの最適化ポイントは、次の通りです。
集計対象は配列に一括読み込み
集計は辞書の中だけで完結(シートに触らない)
結果は配列に詰めて一括書き戻し
「辞書=集計用の箱」「配列=入出力の器」として役割を分けると、コードの見通しが良くなります。
例題3:辞書×配列で「差分検出」を最適化する
やりたいことのイメージ
Old シートと New シートに、同じ構造のデータがあるとします。
両方とも A列:顧客コード、B列:顧客名。
やりたいことは、「NewにあってOldにない顧客(追加分)」を抽出することです。
Oldを辞書化して、New配列を走査するテンプレ
Function BuildKeyDict(ByVal ws As Worksheet) As Object
Dim lastRow As Long
lastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row
If lastRow < 2 Then
Set BuildKeyDict = Nothing
Exit Function
End If
Dim src As Variant
src = ws.Range("A2:A" & lastRow).Value
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
dict.CompareMode = vbTextCompare
Dim i As Long
For i = 1 To UBound(src, 1)
Dim key As String
key = CStr(src(i, 1))
If key <> "" Then
If Not dict.Exists(key) Then
dict.Add key, True
End If
End If
Next i
Set BuildKeyDict = dict
End Function
Sub ExtractNewCustomers()
Dim wsOld As Worksheet, wsNew As Worksheet
Set wsOld = ThisWorkbook.Worksheets("Old")
Set wsNew = ThisWorkbook.Worksheets("New")
Dim dictOld As Object
Set dictOld = BuildKeyDict(wsOld)
If dictOld Is Nothing Then
MsgBox "Oldシートにデータがありません。", vbExclamation
Exit Sub
End If
Dim lastRow As Long
lastRow = wsNew.Cells(wsNew.Rows.Count, 1).End(xlUp).Row
If lastRow < 2 Then Exit Sub
Dim src As Variant
src = wsNew.Range("A1:B" & lastRow).Value
Dim rCount As Long
rCount = UBound(src, 1)
Dim dst() As Variant
ReDim dst(1 To rCount, 1 To 2)
dst(1, 1) = src(1, 1)
dst(1, 2) = src(1, 2)
Dim outRow As Long
outRow = 2
Dim i As Long
For i = 2 To rCount
Dim key As String
key = CStr(src(i, 1))
If key <> "" Then
If Not dictOld.Exists(key) Then
dst(outRow, 1) = src(i, 1)
dst(outRow, 2) = src(i, 2)
outRow = outRow + 1
End If
End If
Next i
Dim wsOut As Worksheet
On Error Resume Next
Set wsOut = ThisWorkbook.Worksheets("DiffNew")
On Error GoTo 0
If wsOut Is Nothing Then
Set wsOut = ThisWorkbook.Worksheets.Add
wsOut.Name = "DiffNew"
End If
wsOut.Cells.Clear
wsOut.Range("A1").Resize(outRow - 1, 2).Value = dst
End Sub
VBここでは、「Oldのキー集合を辞書に載せておき、New配列を走査しながら辞書にないものだけ抜く」という型を使っています。
差分ツールの中身は、ほぼこのパターンの応用です。
辞書×配列最適化の“重要ポイント”をもう一段深掘りする
キーと値の設計を最初に決める
辞書を使うときに一番大事なのは、「キーに何を使うか」「値に何を持たせるか」を最初に決めることです。
キーは「一意に識別できるもの」
値は「後で欲しくなる情報をまとめたもの」
例えば、JOIN用なら「キー=顧客コード」「値=顧客名だけ」でもいいですが、
将来「顧客区分も欲しい」となりそうなら、「値=顧客名&顧客区分の小さな配列」でも構いません。
dict.Add key, Array(customerName, customerType)
VBこうしておくと、後で dict(key)(0) で名前、dict(key)(1) で区分、という形で取り出せます。
「値に何を持たせるか」を設計しておくと、辞書が“ただのマスタ”から“柔軟なレコード”になります。
CompareMode を意識しておく
辞書の CompareMode を設定しないと、「ABC」と「abc」が別キーとして扱われます。
顧客コードや商品コードが「大文字小文字を区別しない」前提なら、必ずこうしておきます。
dict.CompareMode = vbTextCompare
VB逆に、「完全一致で区別したい」場合は、デフォルトのままでも構いません。
ここを曖昧にすると、「見た目同じなのにJOINできない」という地味に嫌なバグになります。
配列のサイズは「最大サイズで確保→使った分だけ出す」
ReDim Preserve をループの中で何度も使うと、メモリコピーが発生して遅くなります。
なので、辞書×配列テンプレでは、次のような方針を取ります。
出力行数の上限が分かるなら、その行数で最初に配列を確保
ループの中では outRow を増やしていくだけ
最後に outRow までの範囲だけを書き戻す
これだけで、数十万行〜数百万行でも安定して動きます。
まとめ:辞書×配列の最適化テンプレは「3つの型」を押さえれば十分
今日の話をぎゅっとまとめると、辞書×配列の最適化テンプレは次の3パターンです。
マスタ参照JOIN:マスタを辞書化 → 明細配列をループしながら辞書参照 → 結果配列を一括出力
集計:明細配列を辞書で集計 → 辞書内容を結果配列に展開 → 一括出力
差分:片方を辞書化 → もう片方の配列を走査しながら辞書にないものだけ結果配列へ
どれも共通しているのは、「シートは最初と最後だけ」「中身は配列と辞書だけ」という構造です。
