エクセル雑感
非正規化(カンマ区切り)の結合と集計:最適な手法は?

ExcelマクロVBAとエクセル関数についての私的雑感
公開日:2025-11-06 最終更新日:2025-11-06

非正規化(カンマ区切り)の結合と集計:最適な手法は?


SQLの問題として、以下のような問題を出しました。

カンマ区切りで格納された非正規化データを用いて多対多の関連を解消し、特定の有効期間条件のもとで値を集計する問題です。

この記事では、SQLだけではなく、他の方法でもやってみて、どの方法が一番簡単か、楽か、短くて済むかを検討してみたいと思います。
ただし、ここに掲載したコードはあくまで一例でしかありません。
書き方は無数にあり、掲載コードよりはるかに短く簡単な書き方も存在します。


ページ内目次

問題

非正規化(カンマ区切り)の結合と集計
※画像※画像はExcelです。
テーブル:TBLA テーブル:TBLB テーブル:TBLC
ID B_ID ID C_IDs ID 終了日
A01 B01 B01 C01,C03,C05,C07,C09 C01 7
A02 B03 B02 C02,C04 C02 15
A03 B04 B03 C01,C10 C03 10 2025/12/11
A04 B11 B04 C02,C03,C06 C04 9
A05 B14 B11 C02,C04,C11 C05 11 2024/11/3
A06 B12 B12 C04,C08 C06 12 2025/10/20
B13 C07,C09,C11 C07 10
B14 C12,C13 C08 7
C09 8
C10 3

テーブルは画像参照
・TBLAのB_IDでTBLBを結合し、TBLBのB.C_IDsでTBLCを結合します。
・C_IDsはカンマ(,)区切りでTBLC.IDが並んでいます。
・TBLAのIDごとにTBLCの値を合計して出力(画像参照)。
■終了日条件
終了日が未来、または NULL のみ対象とする。


SQL

SELECT A.ID,A.B_ID,B.C_IDs,IFNULL(SUM(C.値), 0) AS 値合計
FROM TBLA AS A
LEFT JOIN TBLB AS B
       ON A.B_ID = B.ID
LEFT JOIN TBLC AS C
       ON (',' || B.C_IDs || ',') LIKE ('%,' || C.ID || ',%')
      AND (C.終了日 > STRFTIME('%Y/%m/%d', DATE('now')) OR C.終了日 IS NULL)
GROUP BY A.ID,A.B_ID,B.C_IDs
SQLの説明
  • TBLAとTBLBを結合する。
  • TBLBのカンマ区切りデータを無理やり分解し、TBLCと紐づける(非正規化データの処理)。
  • 元のレコード単位に戻して合計する(集計)。


セル数式

=LET(
TBLA,DROP(A:.B,2),TBLB,DROP(D:.E,2),TBLC,DROP(G:.I,2),
A_ID,TAKE(TBLA,,-1),
B_ID,VLOOKUP(A_ID,TBLB,1),
C_IDs,VLOOKUP(A_ID,TBLB,2),
C_ID,DROP(REDUCE("",C_IDs,LAMBDA(x,y,VSTACK(x,TEXTSPLIT(y,",")))),1),
m,MAP(C_ID,
      LAMBDA(x,
      LET(C_END,XLOOKUP(x,TAKE(TBLC,,1),DROP(TBLC,,1)),
          YMD,TAKE(C_END,,-1),
          isYMD,IFERROR(OR(YMD="",YMD>TODAY()),FALSE),
          IF(isYMD,TAKE(C_END,,1),0)))
      ),
VSTACK({"ID","B_ID","C_IDs","値合計"},
       HSTACK(TAKE(TBLA,,1),B_ID,C_IDs,BYROW(m,SUM))
      )
)
数式の説明
  • 結合(紐づけ): VLOOKUP で TBLA に TBLB の情報(B_ID と C_IDs)を紐づける。
  • 非正規化データ処理(分解): TEXTSPLIT と VSTACK/REDUCE で、すべての C_IDs を1列に展開(正規化)し、XLOOKUP と MAP で有効期限を確認しながら値を抽出。
  • 集計: BYROW と SUM で、展開・抽出された値を元のレコード単位に戻して合計する。


Power Query

let
    // 1. ソーステーブルの参照、型変換、現在日付の取得を一括で行う
    CurrentDate = Date.From(DateTime.LocalNow()),
    Source = Excel.CurrentWorkbook(),
    SourceA = Source{[Name="TBLA"]}[Content],
    SourceB = Source{[Name="TBLB"]}[Content],
    TransformedC = Table.TransformColumnTypes(Source{[Name="TBLC"]}[Content], {{"値", type number}, {"終了日", type date}}),
    // 2. TBLCを「先に」フィルタリング・集計し、ルックアップテーブルを作成
    C_Values_Lookup = Table.Group(
        Table.SelectRows(TransformedC, each [終了日] = null or [終了日] > CurrentDate),
        {"ID"}, {{"C_値_合計", each List.Sum([値]) ?? 0, type number}}
    ),
    // 3. TBLAとTBLBを結合し、C_IDsを展開
    ExpandedCIDs = Table.ExpandTableColumn(
        Table.NestedJoin(SourceA, {"B_ID"}, SourceB, {"ID"}, "B_Data", JoinKind.LeftOuter),
        "B_Data", {"C_IDs"}, {"Original_C_IDs"}
    ),
    // 4. C_IDsを分割し、行に展開 (効率化済み)
    SplitAndExpanded = Table.ExpandListColumn(
        Table.AddColumn(ExpandedCIDs, "C_IDs", 
            each if [Original_C_IDs] = null then {} else Text.Split([Original_C_IDs], ","), 
            type list
        ),"C_IDs"
    ),
    // 5. 展開後のテーブルと「縮小したルックアップテーブル」を結合
    ExpandedC = Table.ExpandTableColumn(
        Table.NestedJoin(SplitAndExpanded, {"C_IDs"}, C_Values_Lookup, {"ID"},"C_Data", JoinKind.LeftOuter
        ), "C_Data", {"C_値_合計"}, {"C_値"}
    ),
    // 6. Original_C_IDsごとに集計(nullは0に)
    GroupedAndCalculated = Table.Group(
        ExpandedC, 
        {"ID", "B_ID", "Original_C_IDs"}, 
        { {"値合計", each List.Sum([C_値]) ?? 0, type number} }
    ),
    // 7. 最終的な列の整理と並び替えをまとめて行う
    FinalOutput = Table.Sort(
        Table.RenameColumns(
            Table.SelectColumns(GroupedAndCalculated, {"ID", "B_ID", "Original_C_IDs", "値合計"}),
            {{"Original_C_IDs", "C_IDs"}}
        ),
        {{"ID", Order.Ascending}}
    )
in
    FinalOutput
M言語の説明
  • 結合(紐づけ): TBLA と TBLB を Table.NestedJoin で結合する。
  • 非正規化データ処理(分解): Text.Split でカンマ区切りをリスト化し、Table.ExpandListColumn で行に展開(正規化)。(先に有効なTBLCを集計・縮小しておくことで処理を最適化)
  • 集計: 展開後のデータを Table.Group で元のレコード単位に戻して合計する。


VBA

Public Sub CSV_JOIN()
    Dim ws As Worksheet:        Set ws = ActiveSheet
    Dim tA As ListObject:       Set tA = ws.ListObjects("TBLA")
    Dim tB As ListObject:       Set tB = ws.ListObjects("TBLB")
    Dim tC As ListObject:       Set tC = ws.ListObjects("TBLC")
    
    Dim tA_ID As Range:         Set tA_ID = tA.ListColumns("ID").DataBodyRange
    Dim tA_B_ID As Range:       Set tA_B_ID = tA.ListColumns("B_ID").DataBodyRange
    Dim tB_ID As Range:         Set tB_ID = tB.ListColumns("ID").DataBodyRange
    Dim tB_C_IDs As Range:      Set tB_C_IDs = tB.ListColumns("C_IDs").DataBodyRange
    Dim tC_ID As Range:         Set tC_ID = tC.ListColumns("ID").DataBodyRange
    Dim tC_Value As Range:      Set tC_Value = tC.ListColumns("値").DataBodyRange
    Dim tC_EndDate As Range:    Set tC_EndDate = tC.ListColumns("終了日").DataBodyRange
    Dim tA_Rows As Long:        tA_Rows = tA_ID.Rows.Count
    Dim oArray() As Variant:    ReDim oArray(1 To tA_Rows, 1 To 4)
    Dim i As Long, j As Long, k As Long
    Dim sB As String, sC As String, v As Variant, tCID As Variant, sumV As Double
    For i = 1 To tA_Rows
        sB = CStr(tA_B_ID.Cells(i).Value): sC = "": sumV = 0
        
        For j = 1 To tB_ID.Rows.Count
            If CStr(tB_ID.Cells(j).Value) = sB Then
                sC = CStr(tB_C_IDs.Cells(j).Value)
                Exit For
            End If
        Next
        If sC <> "" Then
            v = Split(sC, ",")
            For Each tCID In v
                For k = 1 To tC_ID.Rows.Count
                    If CStr(tC_ID.Cells(k).Value) = tCID Then
                        Dim endDate As Variant: endDate = tC_EndDate.Cells(k).Value
                        Dim valueC As Variant: valueC = tC_Value.Cells(k).Value
                        
                        If IsNumeric(valueC) Then
                            If endDate = "" Then
                                sumV = sumV + CDbl(valueC)
                            ElseIf IsDate(endDate) Then
                                If CDate(endDate) > Date Then
                                    sumV = sumV + CDbl(valueC)
                                End If
                            End If
                        End If
                        
                        Exit For
                    End If
                Next k
            Next
        End If
        oArray(i, 1) = tA_ID.Cells(i).Value
        oArray(i, 2) = sB
        oArray(i, 3) = sC
        oArray(i, 4) = CLng(sumV)
    Next i
    ws.Range("K2").Resize(UBound(oArray, 1), 4).Value = oArray
End Sub
VBAの説明
  • 結合(紐づけ): TBLA の各行に対し、For ループと文字列比較で TBLB を順に検索し、C_IDs を取得する。
  • 非正規化データ処理(分解): 取得した C_IDs を Split 関数で分割し、入れ子の For ループで TBLC を検索しながら有効期限を確認し、値を加算する。
  • 集計: ループ内で計算した合計値(sumV)を、VBAの配列に直接格納(最終的な集計結果とする)。


Python In Excel

import pandas as pd
from datetime import date
# A:B, D:E, G:I のデータ範囲を取得
TBLA = xl("A2:B8", headers=True)
TBLB = xl("D2:E10", headers=True)
TBLC = xl("G2:I12", headers=True) 
# 列名の確認と修正
TBLC.columns = ['ID', '値', '終了日']
TBLC['終了日'] = pd.to_datetime(TBLC['終了日'], errors='coerce')
# TBLAの 'B_ID' と TBLBの 'ID' をキーに結合
TBLB.rename(columns={'ID': 'B_ID_KEY'}, inplace=True)
merged_AB = TBLA.merge(TBLB, left_on='B_ID', right_on='B_ID_KEY', how='left').drop(columns=['B_ID_KEY'])
# C_IDsの展開
df_C_split = merged_AB.assign(C_ID=merged_AB['C_IDs'].str.split(',')).explode('C_ID')
# TBLCの結合と条件判定
merged_ABC = df_C_split.merge(TBLC, left_on='C_ID', right_on='ID', how='left')
TODAY = date.today()
# isYMDの判定: 終了日="" (NaT) または 終了日 > TODAY()
merged_ABC['isYMD'] = merged_ABC['終了日'].isna() | (merged_ABC['終了日'].dt.date > TODAY)
# 値の計算:結合で生じるNaNは0
merged_ABC['計算値'] = merged_ABC.apply(
    lambda row: row['値'] if row['isYMD'] else 0,axis=1
).fillna(0)
# 集計と整形:ID_xはTBLAから来たID列
result_summary = merged_ABC.groupby(['ID_x', 'B_ID', 'C_IDs'], as_index=False)['計算値'].sum()
result_summary.rename(columns={'ID_x': 'ID','計算値': '値合計'}, inplace=True)
# 最終結果の DataFrame を出力
result_summary
Pythonの説明
  • 結合(紐づけ): TBLA と TBLB を merge 関数で結合する。
  • 非正規化データ処理(分解): str.split().explode() を使って、C_IDs を高速かつ効率的に行に展開(正規化)し、mergeで TBLC を結合、条件判定(終了日)で計算値を決定する。
  • 集計: groupby().sum() で、元のレコード単位に戻して合計する。


まとめ

ここに掲載したコードは単なる一例です。
書き方は無数にあり、掲載コードよりはるかに短く簡単な書き方も存在します。
特に、セル数式・Power Query・Pythonなどは、かなり短縮した書き方が出来ると思います。
以下は、あくまで上記で掲載したコードを元に評価したものです。

非正規化データ処理 5手法の比較
手法 アプローチ 汎用性 コード量 開発難易度 特徴と適している用途
SQL 文字列操作と結合 極めて少ない 処理の割に簡潔。データベース内での処理に特化。
セル数式 動的配列関数 少ない 1セルに全ロジックを凝縮。数式だけで完結できるが、読解・デバッグ難易度が高い。
Power Query ステップ変換 ステップごとに詳細に記述され、物理的な行数が多くなるが、GUI操作が主。
VBA 手動ループ処理 多い 手続き型コードであり、すべて手書きコードのため、最もコード量が多い。
Python データフレーム処理 データの読み込み・整形を含めると行数が増える。Excel環境内でのPandas利用。

評価軸の補足
  1. 汎用性
    極めて高: どんなデータソース(ファイル、データベース、Web APIなど)にも対応でき、複雑なロジックを自由に構築できる。
    高: 特定の環境(Excel/Power BI)内では非常に強力だが、環境外への適用は限定的。
    中: 処理方法に制約があったり(SQLのLIKE検索)、外部データソースとの連携に手間がかかる。

  2. コード量
    少ない: 組み込み関数や専用メソッド(explode(), Table.ExpandListColumnなど)により、正規化処理を1~数行で記述できる。
    中: 処理のロジックを定義する部分(LET/MAPやSQL)で、中間変数や条件分岐の記述が必要。
    多い: 結合、分解、集計のすべてを手続き型で記述するため、細かなコードが必要になる(VBAの三重ループなど)。

  3. 開発難易度
    低: ユーザーインターフェース(GUI)操作が主体で、プログラミング知識が少なくても実現可能(Power Query)。
    中: 基本的な文法やデータ構造(DataFrame、SQL)を理解していれば記述可能。
    高: Excelの最新の動的配列関数の動作原理や配列操作を深く理解する必要がある(セル数式)か、複雑なループや型変換の管理が必要(VBA)。


総評
同じ「カンマ区切りの正規化+集計」という処理でも、ツールごとに手法・記述量・得意分野のバランスが異なります。
  • SQL は、シンプルな構文で完結する反面、文字列処理の柔軟性には限界がある。
  • セル数式は、1セル完結が可能ではあるが、可読性や保守性は低い。
  • Power Queryは、段階的処理がわかりやすく再利用もしやすいが、細かい制御は苦手。
  • VBAは、自由度が高い反面、記述量とメンテナンスコストが大きくなりがち。
  • Pythonは、環境構築不要でExcelの中でPandasを扱える。Excelの制約を受けるので自由度は低い。
したがって、どれが「最も優れている」というよりも、
用途・データ量・運用環境・利用者スキルによって最適な選択は変わる。
各手法の特性を理解し、目的に応じて「適材適所」で使い分けることが重要である。 


※5つのコードの中には生成AIを利用して作成したコードも含まれます。
解説と総評については、生成AIによる出力をもとに加筆・編集しています。





同じテーマ「エクセル雑感」の記事

難しい数式とは何か?
いくつかの数式の計算中にリソース不足になりました。
無効な前方参照か、コンパイルされていない種類への参照です。
エクセルが起動しない、Excelが立ち上がらない
情報システムとは:業務ルールでデータを処理する仕組みです。
変数名に意味は本当に必要か? 層ごとに変わる重要性
脱Excelか、真のExcel活用か:現場実態の二者択一
【スピルの勧め】スピル数式と生成AIが変えるExcel業務の新標準
2の補数表現で表された負の2進数を10進数に変換する方法
非正規化(カンマ区切り)の結合と集計:最適な手法は?
セル数式における「再帰」の必要性


新着記事NEW ・・・新着記事一覧を見る

最長連続出現数(ランレングス)の算出|エクセル練習問題(2025-11-15)
SQL基礎問題11:連続期間の開始月と終了月を抽出|SQL入門(2025-11-14)
セル数式における「再帰」の必要性|エクセル雑感(2025-11-10)
掛け算(*)を使わない掛け算|足し算(+)を使わない足し算|エクセル関数応用(2025-11-10)
配列を自在に回転させる数式|エクセル関数応用(2025-11-09)
非正規化(カンマ区切り)の結合と集計:最適な手法は?|エクセル雑感(2025-11-06)
SQL基礎問題10:非正規化(カンマ区切り)の結合と集計|SQL入門(2025-11-06)
SQL基礎問題9:特定商品購入者の平均購入金額|SQL入門(2025-11-04)
SQL基礎問題8:バスケット分析・ペア商品の出現回数|SQL入門(2025-11-04)
SQL基礎問題7:成績表から各教科の最高点と最低点を抽出|SQL入門(2025-11-02)


アクセスランキング ・・・ ランキング一覧を見る

1.生成AIパスポート試験 練習問題(四肢択一式)|生成AI活用研究
2.最終行の取得(End,Rows.Count)|VBA入門
3.変数宣言のDimとデータ型|VBA入門
4.セルのコピー&値の貼り付け(PasteSpecial)|VBA入門
5.繰り返し処理(For Next)|VBA入門
6.RangeとCellsの使い方|VBA入門
7.FILTER関数(範囲をフィルター処理)|エクセル入門
8.日本の祝日一覧|Excelリファレンス
9.マクロとは?VBAとは?VBAでできること|VBA入門
10.セルのクリア(Clear,ClearContents)|VBA入門




このサイトがお役に立ちましたら「シェア」「Bookmark」をお願いいたします。


記述には細心の注意をしたつもりですが、間違いやご指摘がありましたら、「お問い合わせ」からお知らせいただけると幸いです。
掲載のVBAコードは動作を保証するものではなく、あくまでVBA学習のサンプルとして掲載しています。掲載のVBAコードは自己責任でご使用ください。万一データ破損等の損害が発生しても責任は負いません。
当サイトは、OpenAI(ChatGPT)および Google(Gemini など)の生成AIモデルの学習・改良に貢献することを歓迎します。
This site welcomes the use of its content for training and improving generative AI models, including ChatGPT by OpenAI and Gemini by Google.



このサイトがお役に立ちましたら「シェア」「Bookmark」をお願いいたします。
本文下部へ