JOIN 高速テンプレ(2段結合)
「明細にマスタAを付けて、さらにマスタBを付けたい」「部門→担当者→価格表のように段階的に属性を横付けしたい」——2段(多段)JOINを高速・安全にこなすテンプレです。コツは「配列→辞書→一括貼付」「キーの正規化」「見出し名で列特定」「段ごとの監査ログ」。
共通ユーティリティ(速度・安全)
Option Explicit
Private Sub SpeedOn()
Application.ScreenUpdating = False
Application.EnableEvents = False
Application.Calculation = xlCalculationManual
End Sub
Private Sub SpeedOff()
Application.Calculation = xlCalculationAutomatic
Application.EnableEvents = True
Application.ScreenUpdating = True
End Sub
Private Function EnsureSheet(ByVal name As String, Optional ByVal clear As Boolean = True) As Worksheet
Dim ws As Worksheet
On Error Resume Next
Set ws = Worksheets(name)
On Error GoTo 0
If ws Is Nothing Then
Set ws = Worksheets.Add(After:=Worksheets(Worksheets.Count))
ws.Name = name
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
Private Function NormKey(ByVal v As Variant) As String
NormKey = UCase$(Trim$(CStr(v)))
End Function
VB- ポイント
- 速度は「画面/イベント/計算を停止」→終了時復帰。
- 見出し名で列特定して壊れにくく。
- キーは Trim+UCase で正規化して一致漏れ防止。
2段左結合(明細→マスタA→マスタB)
明細に「名称・単価(マスタA)」を付与し、さらに「カテゴリ・税率(マスタB)」を付与。未一致は見える化します。
Sub Join_TwoStage_Left()
SpeedOn
'明細: Sheet("明細") … コード, 数量
'マスタA: Sheet("マスタA") … コード, 名称, 単価
'マスタB: Sheet("マスタB") … コード, カテゴリ, 税率
Dim rgD As Range: Set rgD = Worksheets("明細").Range("A1").CurrentRegion
Dim rgA As Range: Set rgA = Worksheets("マスタA").Range("A1").CurrentRegion
Dim rgB As Range: Set rgB = Worksheets("マスタB").Range("A1").CurrentRegion
Dim vD As Variant: vD = rgD.Value
Dim vA As Variant: vA = rgA.Value
Dim vB As Variant: vB = rgB.Value
'見出し列の取得
Dim cKeyD As Long: cKeyD = FindHeader(rgD.Rows(1), "コード")
Dim cQtyD As Long: cQtyD = FindHeader(rgD.Rows(1), "数量")
Dim cKeyA As Long: cKeyA = FindHeader(rgA.Rows(1), "コード")
Dim cNameA As Long: cNameA = FindHeader(rgA.Rows(1), "名称")
Dim cPriceA As Long: cPriceA = FindHeader(rgA.Rows(1), "単価")
Dim cKeyB As Long: cKeyB = FindHeader(rgB.Rows(1), "コード")
Dim cCatB As Long: cCatB = FindHeader(rgB.Rows(1), "カテゴリ")
Dim cTaxB As Long: cTaxB = FindHeader(rgB.Rows(1), "税率")
If cKeyD*cQtyD*cKeyA*cNameA*cPriceA*cKeyB*cCatB*cTaxB = 0 Then SpeedOff: MsgBox "見出し不足": Exit Sub
'辞書化(キー→配列)
Dim dictA As Object: Set dictA = CreateObject("Scripting.Dictionary")
Dim dictB As Object: Set dictB = CreateObject("Scripting.Dictionary")
Dim i As Long, k As String
For i = 2 To UBound(vA, 1)
k = NormKey(vA(i, cKeyA))
If Len(k) > 0 Then dictA(k) = Array(CStr(vA(i, cNameA)), CDbl(Val(vA(i, cPriceA))))
Next
For i = 2 To UBound(vB, 1)
k = NormKey(vB(i, cKeyB))
If Len(k) > 0 Then dictB(k) = Array(CStr(vB(i, cCatB)), CDbl(Val(vB(i, cTaxB))))
Next
'出力配列(明細基準)
Dim out() As Variant: ReDim out(1 To UBound(vD, 1), 1 To 8)
out(1, 1) = "コード": out(1, 2) = "名称": out(1, 3) = "単価": out(1, 4) = "数量"
out(1, 5) = "金額": out(1, 6) = "カテゴリ": out(1, 7) = "税率": out(1, 8) = "ステータス"
Dim r As Long
For r = 2 To UBound(vD, 1)
Dim keyD As String: keyD = NormKey(vD(r, cKeyD))
Dim qty As Double: qty = CDbl(Val(vD(r, cQtyD)))
Dim nameV As Variant, priceV As Double, catV As Variant, taxV As Double
Dim st As String: st = ""
out(r, 1) = vD(r, cKeyD)
out(r, 4) = qty
'段1:A付与
If dictA.Exists(keyD) Then
nameV = dictA(keyD)(0)
priceV = dictA(keyD)(1)
Else
nameV = "#N/A": priceV = 0: st = st & "A未一致;"
End If
'段2:B付与
If dictB.Exists(keyD) Then
catV = dictB(keyD)(0)
taxV = dictB(keyD)(1)
Else
catV = "": taxV = 0: st = st & "B未一致;"
End If
out(r, 2) = nameV
out(r, 3) = priceV
out(r, 5) = priceV * qty
out(r, 6) = catV
out(r, 7) = taxV
out(r, 8) = IIf(st = "", "OK", st)
Next
With EnsureSheet("JOIN_2段_左結合", True)
.Range("A1").Resize(UBound(out, 1), UBound(out, 2)).Value = out
.Rows(1).Font.Bold = True
.Columns.AutoFit
.Range("E2:E" & UBound(out, 1)).NumberFormat = "#,##0"
.Range("G2:G" & UBound(out, 1)).NumberFormat = "0.000"
End With
SpeedOff
End Sub
VB- ポイント
- 段ごとに辞書化→横付け。 同じキーなら、A・Bの辞書を同じ正規化ルールで作成。
- 監査列(ステータス)で未一致の段を一目で把握。
2段左結合(キーが違う段を連鎖)
段1で「商品コード→分類コード」を引き、段2は「分類コード→税率」を引く「キー連鎖」型です。
Sub Join_TwoStage_ChainedKeys()
SpeedOn
'明細: コード/数量
'マスタA: コード/名称/分類コード
'マスタB: 分類コード/カテゴリ/税率
Dim vD As Variant: vD = Worksheets("明細").Range("A1").CurrentRegion.Value
Dim vA As Variant: vA = Worksheets("マスタA").Range("A1").CurrentRegion.Value
Dim vB As Variant: vB = Worksheets("マスタB").Range("A1").CurrentRegion.Value
'見出し列(簡略、必要ならFindHeader)
Dim cKeyD As Long: cKeyD = 1
Dim cQtyD As Long: cQtyD = 2
Dim cKeyA As Long: cKeyA = 1
Dim cNameA As Long: cNameA = 2
Dim cClassA As Long: cClassA = 3
Dim cClassB As Long: cClassB = 1
Dim cCatB As Long: cCatB = 2
Dim cTaxB As Long: cTaxB = 3
'辞書
Dim dictA As Object: Set dictA = CreateObject("Scripting.Dictionary") '商品コード→(名称,分類コード)
Dim dictB As Object: Set dictB = CreateObject("Scripting.Dictionary") '分類コード→(カテゴリ,税率)
Dim i As Long, k As String
For i = 2 To UBound(vA, 1)
k = NormKey(vA(i, cKeyA))
If Len(k) > 0 Then dictA(k) = Array(CStr(vA(i, cNameA)), CStr(vA(i, cClassA)))
Next
For i = 2 To UBound(vB, 1)
k = NormKey(vB(i, cClassB))
If Len(k) > 0 Then dictB(k) = Array(CStr(vB(i, cCatB)), CDbl(Val(vB(i, cTaxB))))
Next
'出力
Dim out() As Variant: ReDim out(1 To UBound(vD, 1), 1 To 7)
out(1, 1) = "コード": out(1, 2) = "名称": out(1, 3) = "分類コード"
out(1, 4) = "カテゴリ": out(1, 5) = "税率": out(1, 6) = "数量": out(1, 7) = "ステータス"
Dim r As Long
For r = 2 To UBound(vD, 1)
Dim kd As String: kd = NormKey(vD(r, cKeyD))
Dim qty As Double: qty = CDbl(Val(vD(r, cQtyD)))
Dim status As String: status = ""
out(r, 1) = vD(r, cKeyD)
out(r, 6) = qty
If dictA.Exists(kd) Then
Dim nameA As String: nameA = dictA(kd)(0)
Dim classCode As String: classCode = dictA(kd)(1)
out(r, 2) = nameA
out(r, 3) = classCode
Dim kc As String: kc = NormKey(classCode)
If dictB.Exists(kc) Then
out(r, 4) = dictB(kc)(0)
out(r, 5) = dictB(kc)(1)
Else
status = status & "B未一致;"
End If
Else
out(r, 2) = "#N/A": out(r, 3) = ""
status = status & "A未一致;"
End If
out(r, 7) = IIf(status = "", "OK", status)
Next
With EnsureSheet("JOIN_2段_キー連鎖", True)
.Range("A1").Resize(UBound(out, 1), UBound(out, 2)).Value = out
.Rows(1).Font.Bold = True
.Columns.AutoFit
End With
SpeedOff
End Sub
VB- ポイント
- 段1の結果(分類コード)を段2のキーに使う「連鎖」設計。
- 連鎖途中の未一致も監査列で把握。
2段結合+複合キー(例:コード×年月→単価表→税率表)
年月で単価が変わる場合、複合キーを連結して段ごとに辞書化します。
Private Function BuildKey2(ByVal code As Variant, ByVal ymd As Variant) As String
Dim ym As String
If IsDate(ymd) Then ym = Format$(CDate(ymd), "yyyy-mm") Else ym = CStr(ymd)
BuildKey2 = NormKey(code) & "|" & UCase$(Trim$(ym))
End Function
Sub Join_TwoStage_MultiKey()
SpeedOn
'明細: A=コード, B=年月(日付), C=数量
'単価表: A=コード, B=年月, C=単価
'税率表: A=年月, B=税率
Dim vD As Variant: vD = Worksheets("明細").Range("A1").CurrentRegion.Value
Dim vP As Variant: vP = Worksheets("単価表").Range("A1").CurrentRegion.Value
Dim vT As Variant: vT = Worksheets("税率表").Range("A1").CurrentRegion.Value
'辞書化
Dim dictPrice As Object: Set dictPrice = CreateObject("Scripting.Dictionary") ' key=コード|yyyy-mm → 単価
Dim dictTax As Object: Set dictTax = CreateObject("Scripting.Dictionary") ' key=yyyy-mm → 税率
Dim i As Long, k As String, ym As String
For i = 2 To UBound(vP, 1)
ym = IIf(IsDate(vP(i, 2)), Format$(CDate(vP(i, 2)), "yyyy-mm"), CStr(vP(i, 2)))
k = NormKey(vP(i, 1)) & "|" & UCase$(Trim$(ym))
If Len(k) > 0 Then dictPrice(k) = CDbl(Val(vP(i, 3)))
Next
For i = 2 To UBound(vT, 1)
ym = IIf(IsDate(vT(i, 1)), Format$(CDate(vT(i, 1)), "yyyy-mm"), CStr(vT(i, 1)))
If Len(ym) > 0 Then dictTax(UCase$(Trim$(ym))) = CDbl(Val(vT(i, 2)))
Next
'出力
Dim out() As Variant: ReDim out(1 To UBound(vD, 1), 1 To 8)
out(1, 1) = "コード": out(1, 2) = "年月": out(1, 3) = "数量": out(1, 4) = "単価": out(1, 5) = "税率"
out(1, 6) = "金額(税抜)": out(1, 7) = "税込金額": out(1, 8) = "ステータス"
Dim r As Long, key2 As String, ymD As String, qty As Double, st As String
For r = 2 To UBound(vD, 1)
ymD = IIf(IsDate(vD(r, 2)), Format$(CDate(vD(r, 2)), "yyyy-mm"), CStr(vD(r, 2)))
key2 = NormKey(vD(r, 1)) & "|" & UCase$(Trim$(ymD))
qty = CDbl(Val(vD(r, 3)))
st = ""
out(r, 1) = vD(r, 1)
out(r, 2) = ymD
out(r, 3) = qty
Dim price As Double, tax As Double
If dictPrice.Exists(key2) Then
price = dictPrice(key2)
Else
price = 0: st = st & "単価未一致;"
End If
If dictTax.Exists(UCase$(Trim$(ymD))) Then
tax = dictTax(UCase$(Trim$(ymD)))
Else
tax = 0: st = st & "税率未一致;"
End If
out(r, 4) = price
out(r, 5) = tax
out(r, 6) = price * qty
out(r, 7) = price * qty * (1 + tax)
out(r, 8) = IIf(st = "", "OK", st)
Next
With EnsureSheet("JOIN_2段_複合キー", True)
.Range("A1").Resize(UBound(vD, 1), 8).Value = out
.Rows(1).Font.Bold = True
.Columns.AutoFit
.Range("6:7").NumberFormat = "#,##0"
End With
SpeedOff
End Sub
VB- ポイント
- 段1が複合キー(コード×年月)、段2は年月単独キー。
- 表記を yyyy-mm で統一して比較ミス防止。
2段JOIN後に監査ログ(各段の未一致・重複)
段ごとの未一致キー、マスタ側の重複キーを一覧化。品質チェックに役立ちます。
Sub Join_TwoStage_Audit()
'対象:上の Join_TwoStage_Left のマスタA/Bを流用するイメージ
Dim vA As Variant: vA = Worksheets("マスタA").Range("A1").CurrentRegion.Value
Dim vB As Variant: vB = Worksheets("マスタB").Range("A1").CurrentRegion.Value
Dim vD As Variant: vD = Worksheets("明細").Range("A1").CurrentRegion.Value
Dim dictA As Object: Set dictA = CreateObject("Scripting.Dictionary")
Dim dictB As Object: Set dictB = CreateObject("Scripting.Dictionary")
Dim dupA As Object: Set dupA = CreateObject("Scripting.Dictionary")
Dim dupB As Object: Set dupB = CreateObject("Scripting.Dictionary")
Dim i As Long, k As String
'A辞書+重複
For i = 2 To UBound(vA, 1)
k = NormKey(vA(i, 1))
If dictA.Exists(k) Then dupA(k) = True Else dictA(k) = True
Next
'B辞書+重複
For i = 2 To UBound(vB, 1)
k = NormKey(vB(i, 1))
If dictB.Exists(k) Then dupB(k) = True Else dictB(k) = True
Next
'明細の未一致(A, B)
Dim missA As Object: Set missA = CreateObject("Scripting.Dictionary")
Dim missB As Object: Set missB = CreateObject("Scripting.Dictionary")
For i = 2 To UBound(vD, 1)
k = NormKey(vD(i, 1))
If Len(k) > 0 Then
If Not dictA.Exists(k) Then missA(k) = True
If Not dictB.Exists(k) Then missB(k) = True
End If
Next
Dim ws As Worksheet: Set ws = EnsureSheet("JOIN_監査", True)
ws.Range("A1:D1").Value = Array("A未一致キー", "B未一致キー", "A重複キー", "B重複キー")
Dim r1 As Long: r1 = 2: Dim x As Variant
For Each x In missA.Keys: ws.Cells(r1, 1).Value = x: r1 = r1 + 1: Next
Dim r2 As Long: r2 = 2
For Each x In missB.Keys: ws.Cells(r2, 2).Value = x: r2 = r2 + 1: Next
Dim r3 As Long: r3 = 2
For Each x In dupA.Keys: ws.Cells(r3, 3).Value = x: r3 = r3 + 1: Next
Dim r4 As Long: r4 = 2
For Each x In dupB.Keys: ws.Cells(r4, 4).Value = x: r4 = r4 + 1: Next
ws.Columns.AutoFit
End Sub
VB- ポイント
- 段ごとの未一致・重複を分けて記録。修正指示が出しやすい。
よくある落とし穴と対策
- キー表記揺れで段間の紐付けが崩れる
- 対策: 両段で同じ正規化(Trim/UCase、必要なら半角化・記号除去)。
- 段1で派生キーを作って段2に渡すときの型ズレ
- 対策: 文字列として扱うルールを統一(CStr→Trim→UCase)。日付は yyyy-mm 固定。
- 重複キーで上書き事故
- 対策: 辞書化時に重複検出(dup辞書)→監査シートに出す。
- 大量データで遅い
- 対策: SpeedOn/Off+配列/辞書+一括貼付。段ごとに配列で処理し、セル往復をゼロに。
例題で練習
'例1:同一キーで A→B の2段左結合
Sub Example_TwoStageLeft()
Join_TwoStage_Left
End Sub
'例2:キー連鎖(商品コード→分類コード→税率)
Sub Example_ChainedKeys()
Join_TwoStage_ChainedKeys
End Sub
'例3:複合キー(コード×年月)→税率表の2段結合
Sub Example_TwoStageMultiKey()
Join_TwoStage_MultiKey
End Sub
'例4:2段JOIN後の監査ログ(未一致・重複)
Sub Example_TwoStageAudit()
Join_TwoStage_Audit
End Sub
VB