ねらい:VLOOKUPではできない「1対多JOIN」をVBAで“型”にする
ふつうのJOIN(VLOOKUP的なもの)は「1対1」です。
顧客コード → 顧客名、商品コード → 商品名、のように「1つのキーに1行だけ」対応している世界。
でも実務ではこういうのが出てきます。
顧客1人に対して、複数の注文行がある(顧客 → 注文明細)。
案件1件に対して、複数の担当者が紐づく(案件 → 担当者一覧)。
商品1つに対して、複数の価格履歴がある(商品 → 価格履歴)。
これは「1対多」の関係です。
VLOOKUPでは「最初の1件」しか取れないので、“全部の子レコードをJOINする”には向きません。
ここで作る「拡張JOIN(1対多)」テンプレは、
親テーブル(顧客など)
子テーブル(注文明細など)
を用意して、
親1行に対して、子を「縦に増やす」か「横にまとめる」か、
どちらのパターンもこなせる“汎用の型”を作ることが目的です。
全体設計:親テーブルと子テーブルをどう扱うか
想定するシート構成(例:顧客 → 注文明細)
例として、次のような構成を想定します。
Customers シート(親)
A列:CustomerID
B列:CustomerName
Orders シート(子)
A列:OrderID
B列:CustomerID
C列:OrderDate
D列:Amount
やりたいことは、次の2パターンです。
親1行を「縦に増やす」パターン
→ 顧客1人に対して、注文明細を全部くっつけて、行を増やす(親×子の組み合わせを全部出す)。
親1行に「横にまとめる」パターン
→ 顧客1人に対して、「注文日1・金額1」「注文日2・金額2」…のように横に展開する。
まずは、実務でよく使う「縦に増やす」パターンから作っていきます。
コア部品:子テーブルを「CustomerID → 子行の配列」にまとめる
子テーブルをDictionaryに載せる(1キーに複数行)
1対多JOINのキモは、「1つのキーに複数行をぶら下げる」ことです。
Dictionaryの値に「子行の配列(またはCollection)」を持たせます。
ここでは、分かりやすく Collection を使います。
' ModJoin_OneToMany.bas
Option Explicit
Private Type OrderRow
OrderID As String
OrderDate As Variant
Amount As Double
End Type
Private Function LoadOrdersByCustomer() As Object
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Orders")
Dim lastRow As Long
lastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
dict.CompareMode = 1
Dim r As Long
Dim key As String
Dim o As OrderRow
Dim col As Collection
For r = 2 To lastRow
key = CStr(ws.Cells(r, 2).Value) ' CustomerID(B列)
If key <> "" Then
o.OrderID = CStr(ws.Cells(r, 1).Value)
o.OrderDate = ws.Cells(r, 3).Value
o.Amount = CDbl(IIf(ws.Cells(r, 4).Value = "", 0, ws.Cells(r, 4).Value))
If dict.Exists(key) Then
Set col = dict(key)
Else
Set col = New Collection
dict.Add key, col
End If
col.Add o
End If
Next
Set LoadOrdersByCustomer = dict
End Function
VBここでの重要ポイントを深掘りします。
OrderRow というユーザー定義型で「1行分の子データ」をひとまとめにしている。
Dictionary の値は Collection にして、「その顧客の注文行を全部ぶら下げる」。
同じ CustomerID が出てきたら、既存の Collection に追加していく。
これで、dict("C001") を見ると、「C001 の全注文行」が Collection として取れる状態になります。
親×子を「縦に増やす」1対多JOINテンプレ
出力先シートの設計
結果は、Result シートに出すことにします。
Result
A列:CustomerID
B列:CustomerName
C列:OrderID
D列:OrderDate
E列:Amount
親1行に対して、子の件数分だけ行を増やして出力します。
親をなめながら、子を展開して書き出す
Public Sub Join_OneToMany_Vertical()
Dim wsC As Worksheet, wsR As Worksheet
Set wsC = ThisWorkbook.Worksheets("Customers")
On Error Resume Next
Set wsR = ThisWorkbook.Worksheets("Result")
On Error GoTo 0
If wsR Is Nothing Then
Set wsR = ThisWorkbook.Worksheets.Add
wsR.Name = "Result"
Else
wsR.Cells.Clear
End If
wsR.Range("A1:E1").Value = Array("CustomerID", "CustomerName", "OrderID", "OrderDate", "Amount")
Dim dictOrders As Object
Set dictOrders = LoadOrdersByCustomer()
Dim lastRowC As Long
lastRowC = wsC.Cells(wsC.Rows.Count, 1).End(xlUp).Row
Dim rC As Long
Dim outRow As Long
outRow = 2
Dim custID As String
Dim custName As String
Dim col As Collection
Dim i As Long
Dim o As OrderRow
For rC = 2 To lastRowC
custID = CStr(wsC.Cells(rC, 1).Value)
custName = CStr(wsC.Cells(rC, 2).Value)
If custID <> "" Then
If dictOrders.Exists(custID) Then
Set col = dictOrders(custID)
For i = 1 To col.Count
o = col(i)
wsR.Cells(outRow, 1).Value = custID
wsR.Cells(outRow, 2).Value = custName
wsR.Cells(outRow, 3).Value = o.OrderID
wsR.Cells(outRow, 4).Value = o.OrderDate
wsR.Cells(outRow, 5).Value = o.Amount
outRow = outRow + 1
Next
Else
wsR.Cells(outRow, 1).Value = custID
wsR.Cells(outRow, 2).Value = custName
wsR.Cells(outRow, 3).Value = ""
wsR.Cells(outRow, 4).Value = ""
wsR.Cells(outRow, 5).Value = 0
outRow = outRow + 1
End If
End If
Next
wsR.Columns.AutoFit
MsgBox "1対多JOIN(縦展開)が完了しました。", vbInformation
End Sub
VBここでの重要ポイントを整理します。
Customers を1行ずつ読む。
その CustomerID に対して、子Dictionaryに注文があれば、件数分だけ行を増やして書き出す。
子が1件もない顧客も、「注文なし」として1行だけ出す(ここは運用に応じて変えられる)。
この形は、SQLでいうところの「LEFT JOIN+子側の複数行展開」に相当します。
ピボットや集計の前段として、「親×子の組み合わせを全部出したい」ときに非常に使えます。
例題イメージ:顧客別の全注文明細を1シートにまとめる
Customers
C001 佐藤商事
C002 鈴木工業
Orders
O001 C001 2024/01/01 10000
O002 C001 2024/01/05 15000
O003 C002 2024/01/03 8000
Result はこうなります。
C001 佐藤商事 O001 2024/01/01 10000
C001 佐藤商事 O002 2024/01/05 15000
C002 鈴木工業 O003 2024/01/03 8000
顧客を軸にした売上分析や、顧客別の明細レポートを作るときの“土台”として、そのまま使える形です。
応用:親1行に「横にまとめる」1対多JOIN
横展開の考え方
「顧客ごとに、直近3件の注文だけ横に並べたい」
「案件ごとに、担当者1〜3を横に並べたい」
こういうときは、親1行に対して「子の一部を横に展開」します。
Result(例)
CustomerID
CustomerName
Order1_Date
Order1_Amount
Order2_Date
Order2_Amount
Order3_Date
Order3_Amount
コード例:最大3件まで横に展開する
Public Sub Join_OneToMany_Horizontal_Max3()
Dim wsC As Worksheet, wsR As Worksheet
Set wsC = ThisWorkbook.Worksheets("Customers")
On Error Resume Next
Set wsR = ThisWorkbook.Worksheets("Result_H")
On Error GoTo 0
If wsR Is Nothing Then
Set wsR = ThisWorkbook.Worksheets.Add
wsR.Name = "Result_H"
Else
wsR.Cells.Clear
End If
wsR.Range("A1:H1").Value = Array( _
"CustomerID", "CustomerName", _
"Order1_Date", "Order1_Amount", _
"Order2_Date", "Order2_Amount", _
"Order3_Date", "Order3_Amount")
Dim dictOrders As Object
Set dictOrders = LoadOrdersByCustomer()
Dim lastRowC As Long
lastRowC = wsC.Cells(wsC.Rows.Count, 1).End(xlUp).Row
Dim rC As Long, outRow As Long
outRow = 2
Dim custID As String, custName As String
Dim col As Collection
Dim i As Long
Dim o As OrderRow
For rC = 2 To lastRowC
custID = CStr(wsC.Cells(rC, 1).Value)
custName = CStr(wsC.Cells(rC, 2).Value)
wsR.Cells(outRow, 1).Value = custID
wsR.Cells(outRow, 2).Value = custName
If custID <> "" And dictOrders.Exists(custID) Then
Set col = dictOrders(custID)
For i = 1 To Application.WorksheetFunction.Min(3, col.Count)
o = col(i)
wsR.Cells(outRow, 2 + (i - 1) * 2 + 1).Value = o.OrderDate
wsR.Cells(outRow, 2 + (i - 1) * 2 + 2).Value = o.Amount
Next
End If
outRow = outRow + 1
Next
wsR.Columns.AutoFit
MsgBox "1対多JOIN(横展開・最大3件)が完了しました。", vbInformation
End Sub
VBここでの重要ポイントは、
子が何件あっても、「最大3件まで」と決めて横に並べる。
列位置を「2 + (i – 1) * 2 + 1」のように計算して、Order1/2/3の列を決めている。
このパターンは、「担当者1〜3」「電話番号1〜3」など、
“最大件数が決まっている1対多”でよく使います。
重要ポイントの深掘り:拡張JOIN(1対多)を“実務で使える部品”にするコツ
子の順序をどうするか(ソートの問題)
1対多JOINでは、「どの順番で子を並べるか」が重要です。
注文なら、日付の昇順・降順。
担当者なら、役割順(主担当→副担当)。
今回のテンプレでは、Orders シートの行順のまま Collection に入れています。
必要なら、事前に Orders をソートしておくか、
Collection ではなく配列+ソートで順序を制御する、という発展形もあります。
子が多すぎる場合の扱い
ある顧客に対して子が100件ある、というケースも普通にあります。
縦展開なら問題ありませんが、横展開では「列が足りない」問題が出ます。
その場合は、
横展開は「直近3件だけ」にする。
それ以上は「件数だけ」を別列に持つ(例:OrderCount)。
詳細は別シート(縦展開結果)で見る。
というように、「どこまでを横に持つか」を業務として決めておく必要があります。
親に子が1件もない場合をどう扱うか
親に子がない場合、
縦展開では「親だけ1行出す」のか「出さない」のか。
横展開では「空欄のまま出す」のか。
ここも運用ルール次第です。
今回の縦展開テンプレでは、「親だけ1行出す」ようにしています。
「子がある親だけ欲しい」なら、その部分を削ればOKです。
まとめ:拡張JOIN(1対多)は「Dictionaryの値にCollectionをぶら下げる」だけで一気に世界が広がる
1対多JOINの本質は、とてもシンプルです。
子テーブルを「キー → 子行のCollection」としてDictionaryに載せる。
親を1行ずつ見ながら、そのキーにぶら下がっている子を
縦に展開するか
横に一部だけ並べるか
を決めて出力する。
この“型”さえ作っておけば、
顧客 → 注文
案件 → 担当者
商品 → 価格履歴
社員 → 資格一覧
など、あらゆる1対多の関係を、Excelの中で自在に扱えるようになります。
