逆引きJOIN
やりたいJOINから選べる“逆引きテンプレ”。「明細にマスタを左結合したい」「一致する行だけ内結合」「両方のキーを全部出したい完全結合」「片側にしかない=アンチ結合」まで、初心者でも壊れないコードを用途別にまとめました。
使い分け早見
- 左結合(LEFT JOIN): 明細を基準に、マスタ項目を横付け。未一致は埋める。
- 内結合(INNER JOIN): 一致するキーの行だけ出力。
- 完全結合(FULL OUTER JOIN): 両側のキーをすべて出し、欠損は補完。
- 右結合(RIGHT JOIN): マスタ基準で、明細項目を横付け。
- アンチ結合(ANTI JOIN): 片側にしかない行だけ抽出(例:明細にあるがマスタにない)。
いずれも共通の“壊れないコツ”は「キーの正規化」「見出し名で列特定」「配列→辞書→一括貼付」。
下準備ユーティリティ(見出し特定・新シート)
Option Explicit
Private Function EnsureSheet(ByVal sheetName As String, Optional ByVal clear As Boolean = True) As Worksheet
Dim ws As Worksheet
On Error Resume Next
Set ws = Worksheets(sheetName)
On Error GoTo 0
If ws Is Nothing Then
Set ws = Worksheets.Add(After:=Worksheets(Worksheets.Count))
ws.Name = sheetName
End If
If clear Then ws.Cells.Clear
Set EnsureSheet = ws
End Function
Private Function FindHeader(ByVal headerRow As Range, ByVal name As String) As Long
Dim hit As Range
Set hit = headerRow.Find(What:=name, LookAt:=xlWhole, LookIn:=xlValues, MatchCase:=False)
FindHeader = IIf(hit Is Nothing, 0, hit.Column)
End Function
VB- ポイント
- 見出し名参照で列順変動に強く、何度実行しても安全に新シートを用意。
左結合(LEFT JOIN):明細基準でマスタを横付け
「明細A1起点、マスタA1起点。キー=コード、付与=名称・単価」の例。
Sub Join_Left()
'明細: Sheet("明細") … コード/数量
'マスタ: Sheet("マスタ") … コード/名称/単価
Dim rgD As Range: Set rgD = Worksheets("明細").Range("A1").CurrentRegion
Dim rgM As Range: Set rgM = Worksheets("マスタ").Range("A1").CurrentRegion
Dim vD As Variant: vD = rgD.Value
Dim vM As Variant: vM = rgM.Value
'見出し列
Dim cKeyD As Long: cKeyD = FindHeader(rgD.Rows(1), "コード")
Dim cQtyD As Long: cQtyD = FindHeader(rgD.Rows(1), "数量")
Dim cKeyM As Long: cKeyM = FindHeader(rgM.Rows(1), "コード")
Dim cNameM As Long: cNameM = FindHeader(rgM.Rows(1), "名称")
Dim cPriceM As Long: cPriceM = FindHeader(rgM.Rows(1), "単価")
If cKeyD * cQtyD * cKeyM * cNameM * cPriceM = 0 Then MsgBox "見出し不足": Exit Sub
'マスタ辞書(正規化キー→(名称,単価))
Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
Dim i As Long, k As String
For i = 2 To UBound(vM, 1)
k = UCase$(Trim$(CStr(vM(i, cKeyM))))
If Len(k) > 0 Then dict(k) = Array(CStr(vM(i, cNameM)), CDbl(Val(vM(i, cPriceM))))
Next
'出力(明細基準)
Dim out() As Variant: ReDim out(1 To UBound(vD, 1), 1 To 5)
out(1, 1) = "コード": out(1, 2) = "名称": out(1, 3) = "単価": out(1, 4) = "数量": out(1, 5) = "金額"
Dim r As Long
For r = 2 To UBound(vD, 1)
Dim kd As String: kd = UCase$(Trim$(CStr(vD(r, cKeyD))))
Dim qty As Double: qty = CDbl(Val(vD(r, cQtyD)))
out(r, 1) = vD(r, cKeyD)
out(r, 4) = qty
If dict.Exists(kd) Then
out(r, 2) = dict(kd)(0)
out(r, 3) = dict(kd)(1)
out(r, 5) = dict(kd)(1) * qty
Else
out(r, 2) = "#N/A": out(r, 3) = 0: out(r, 5) = 0
End If
Next
With EnsureSheet("LEFT_JOIN", True)
.Range("A1").Resize(UBound(out, 1), UBound(out, 2)).Value = out
.Rows(1).Font.Bold = True
.Columns.AutoFit
End With
End Sub
VB- ポイント
- 未一致は「#N/A/0」で見える化。
- 金額は単価×数量をその場で作る。
内結合(INNER JOIN):一致キーのみ出力
一致する行だけに絞りたいとき。
Sub Join_Inner()
Dim rgD As Range: Set rgD = Worksheets("明細").Range("A1").CurrentRegion
Dim rgM As Range: Set rgM = Worksheets("マスタ").Range("A1").CurrentRegion
Dim vD As Variant: vD = rgD.Value
Dim vM As Variant: vM = rgM.Value
Dim cKeyD As Long: cKeyD = FindHeader(rgD.Rows(1), "コード")
Dim cQtyD As Long: cQtyD = FindHeader(rgD.Rows(1), "数量")
Dim cKeyM As Long: cKeyM = FindHeader(rgM.Rows(1), "コード")
Dim cNameM As Long: cNameM = FindHeader(rgM.Rows(1), "名称")
Dim cPriceM As Long: cPriceM = FindHeader(rgM.Rows(1), "単価")
If cKeyD * cQtyD * cKeyM * cNameM * cPriceM = 0 Then MsgBox "見出し不足": Exit Sub
Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
Dim i As Long, k As String
For i = 2 To UBound(vM, 1)
k = UCase$(Trim$(CStr(vM(i, cKeyM))))
dict(k) = Array(CStr(vM(i, cNameM)), CDbl(Val(vM(i, cPriceM))))
Next
'件数見積もり(内結合のみ)
Dim countIn As Long: countIn = 1
Dim r As Long
For r = 2 To UBound(vD, 1)
k = UCase$(Trim$(CStr(vD(r, cKeyD))))
If dict.Exists(k) Then countIn = countIn + 1
Next
Dim out() As Variant: ReDim out(1 To countIn, 1 To 5)
out(1, 1) = "コード": out(1, 2) = "名称": out(1, 3) = "単価": out(1, 4) = "数量": out(1, 5) = "金額"
Dim w As Long: w = 2
For r = 2 To UBound(vD, 1)
k = UCase$(Trim$(CStr(vD(r, cKeyD))))
If dict.Exists(k) Then
Dim qty As Double: qty = CDbl(Val(vD(r, cQtyD)))
out(w, 1) = vD(r, cKeyD)
out(w, 2) = dict(k)(0)
out(w, 3) = dict(k)(1)
out(w, 4) = qty
out(w, 5) = dict(k)(1) * qty
w = w + 1
End If
Next
With EnsureSheet("INNER_JOIN", True)
.Range("A1").Resize(countIn, 5).Value = out
.Rows(1).Font.Bold = True
.Columns.AutoFit
End With
End Sub
VB- ポイント
- 未一致はそもそも行を出さない。
- 見積もり→配列確保で無駄なく高速。
完全結合(FULL OUTER JOIN):両側のキーを全部出す
両側の状態を棚卸したい・監査したい場面向け。
Sub Join_FullOuter()
Dim vA As Variant: vA = Worksheets("明細").Range("A1").CurrentRegion.Value
Dim vB As Variant: vB = Worksheets("マスタ").Range("A1").CurrentRegion.Value
Dim cKeyA As Long: cKeyA = FindHeader(Worksheets("明細").Range("A1").CurrentRegion.Rows(1), "コード")
Dim cQtyA As Long: cQtyA = FindHeader(Worksheets("明細").Range("A1").CurrentRegion.Rows(1), "数量")
Dim cKeyB As Long: cKeyB = FindHeader(Worksheets("マスタ").Range("A1").CurrentRegion.Rows(1), "コード")
Dim cNameB As Long: cNameB = FindHeader(Worksheets("マスタ").Range("A1").CurrentRegion.Rows(1), "名称")
Dim cPriceB As Long: cPriceB = FindHeader(Worksheets("マスタ").Range("A1").CurrentRegion.Rows(1), "単価")
If cKeyA * cQtyA * cKeyB * cNameB * cPriceB = 0 Then MsgBox "見出し不足": Exit Sub
'辞書
Dim dA As Object: Set dA = CreateObject("Scripting.Dictionary")
Dim dB As Object: Set dB = CreateObject("Scripting.Dictionary")
Dim i As Long, k As String
For i = 2 To UBound(vA, 1)
k = UCase$(Trim$(CStr(vA(i, cKeyA))))
If Len(k) > 0 Then dA(k) = Array(vA(i, cKeyA), CDbl(Val(vA(i, cQtyA))))
Next
For i = 2 To UBound(vB, 1)
k = UCase$(Trim$(CStr(vB(i, cKeyB))))
If Len(k) > 0 Then dB(k) = Array(CStr(vB(i, cNameB)), CDbl(Val(vB(i, cPriceB))))
Next
'和集合キー
Dim all As Object: Set all = CreateObject("Scripting.Dictionary")
For Each k In dA.Keys: all(k) = True: Next
For Each k In dB.Keys: all(k) = True: Next
'出力
Dim n As Long: n = all.Count
Dim out() As Variant: ReDim out(1 To n + 1, 1 To 6)
out(1, 1) = "コード": out(1, 2) = "名称": out(1, 3) = "単価": out(1, 4) = "数量": out(1, 5) = "金額": out(1, 6) = "分類"
Dim r As Long: r = 2
Dim key As Variant
For Each key In all.Keys
Dim hasA As Boolean: hasA = dA.Exists(key)
Dim hasB As Boolean: hasB = dB.Exists(key)
out(r, 1) = IIf(hasA, dA(key)(0), key)
out(r, 2) = IIf(hasB, dB(key)(0), "#N/A")
out(r, 3) = IIf(hasB, dB(key)(1), 0)
out(r, 4) = IIf(hasA, dA(key)(1), 0)
out(r, 5) = IIf(hasA And hasB, dA(key)(1) * dB(key)(1), 0)
out(r, 6) = IIf(hasA And hasB, "両側", IIf(hasA, "明細のみ", "マスタのみ"))
r = r + 1
Next
With EnsureSheet("FULL_JOIN", True)
.Range("A1").Resize(n + 1, 6).Value = out
.Rows(1).Font.Bold = True
.Columns.AutoFit
End With
End Sub
VB- ポイント
- 欠損は明示的に補完し、分類列で可視化。
- 監査・棚卸に最適。
右結合(RIGHT JOIN):マスタ基準で明細を横付け
左結合と逆。マスタを“基準側”にしたいとき。
Sub Join_Right()
'列名はLEFTの逆方向で読み替え
Dim rgD As Range: Set rgD = Worksheets("明細").Range("A1").CurrentRegion
Dim rgM As Range: Set rgM = Worksheets("マスタ").Range("A1").CurrentRegion
Dim vD As Variant: vD = rgD.Value
Dim vM As Variant: vM = rgM.Value
Dim cKeyD As Long: cKeyD = FindHeader(rgD.Rows(1), "コード")
Dim cQtyD As Long: cQtyD = FindHeader(rgD.Rows(1), "数量")
Dim cKeyM As Long: cKeyM = FindHeader(rgM.Rows(1), "コード")
Dim cNameM As Long: cNameM = FindHeader(rgM.Rows(1), "名称")
Dim cPriceM As Long: cPriceM = FindHeader(rgM.Rows(1), "単価")
If cKeyD * cQtyD * cKeyM * cNameM * cPriceM = 0 Then MsgBox "見出し不足": Exit Sub
Dim dA As Object: Set dA = CreateObject("Scripting.Dictionary")
Dim i As Long, k As String
For i = 2 To UBound(vD, 1)
k = UCase$(Trim$(CStr(vD(i, cKeyD))))
dA(k) = CDbl(Val(vD(i, cQtyD)))
Next
Dim out() As Variant: ReDim out(1 To UBound(vM, 1), 1 To 5)
out(1, 1) = "コード": out(1, 2) = "名称": out(1, 3) = "単価": out(1, 4) = "数量": out(1, 5) = "金額"
Dim r As Long
For r = 2 To UBound(vM, 1)
k = UCase$(Trim$(CStr(vM(r, cKeyM))))
out(r, 1) = vM(r, cKeyM)
out(r, 2) = vM(r, cNameM)
out(r, 3) = CDbl(Val(vM(r, cPriceM)))
Dim qty As Double: qty = IIf(dA.Exists(k), dA(k), 0)
out(r, 4) = qty
out(r, 5) = qty * out(r, 3)
Next
With EnsureSheet("RIGHT_JOIN", True)
.Range("A1").Resize(UBound(vM, 1), 5).Value = out
.Rows(1).Font.Bold = True
.Columns.AutoFit
End With
End Sub
VB- ポイント
- マスタが主、明細が従の出力にしたいときに使う。
アンチ結合(ANTI JOIN):片側だけの行を抽出
JOIN前後の“漏れ”を洗い出すとき。
Sub Join_Anti()
'明細にあるがマスタに無い(Aのみ)を抽出
Dim vA As Variant: vA = Worksheets("明細").Range("A1").CurrentRegion.Value
Dim vB As Variant: vB = Worksheets("マスタ").Range("A1").CurrentRegion.Value
Dim cKeyA As Long: cKeyA = FindHeader(Worksheets("明細").Range("A1").CurrentRegion.Rows(1), "コード")
Dim cKeyB As Long: cKeyB = FindHeader(Worksheets("マスタ").Range("A1").CurrentRegion.Rows(1), "コード")
Dim cNameA As Long: cNameA = FindHeader(Worksheets("明細").Range("A1").CurrentRegion.Rows(1), "名称")
If cKeyA * cKeyB * cNameA = 0 Then MsgBox "見出し不足": Exit Sub
Dim setB As Object: Set setB = CreateObject("Scripting.Dictionary")
Dim i As Long, k As String
For i = 2 To UBound(vB, 1)
k = UCase$(Trim$(CStr(vB(i, cKeyB))))
setB(k) = True
Next
Dim out() As Variant: ReDim out(1 To UBound(vA, 1), 1 To 2)
out(1, 1) = "コード": out(1, 2) = "名称"
Dim w As Long: w = 2
For i = 2 To UBound(vA, 1)
k = UCase$(Trim$(CStr(vA(i, cKeyA))))
If Len(k) > 0 And Not setB.Exists(k) Then
out(w, 1) = vA(i, cKeyA)
out(w, 2) = vA(i, cNameA)
w = w + 1
End If
Next
With EnsureSheet("ANTI_JOIN", True)
If w > 2 Then .Range("A1").Resize(w - 1, 2).Value = out Else .Range("A2").Value = "(未該当)"
.Rows(1).Font.Bold = True
.Columns.AutoFit
End With
End Sub
VB- ポイント
- 逆方向(マスタのみ)も同じ要領で作れる。
例題で練習
'例1:明細基準の左結合
Sub Example_Left()
Join_Left
End Sub
'例2:一致のみの内結合
Sub Example_Inner()
Join_Inner
End Sub
'例3:両側を全部出す完全結合
Sub Example_Full()
Join_FullOuter
End Sub
'例4:マスタ基準の右結合
Sub Example_Right()
Join_Right
End Sub
'例5:片側のみ(アンチ結合)の抽出
Sub Example_Anti()
Join_Anti
End Sub
VBよくある落とし穴と対策
- キー表記揺れでJOIN漏れ
- 対策:
Trim/UCaseを徹底。必要なら半角化・不要記号除去も追加。
- 対策:
- 列順変更で壊れる
- 対策: 見出し名で列特定(FindHeader)。ハードコード禁止。
- 数値が文字列になって並びや計算が狂う
- 対策: 取り込み時に
Val→CDblで数値化。書式は後で整える。
- 対策: 取り込み時に
- 大規模で遅い
- 対策: 範囲→配列→辞書→一括貼付。前後で
ScreenUpdating=Falseや計算停止を入れると安定。
- 対策: 範囲→配列→辞書→一括貼付。前後で
