[C#] linq datatable group by sum. 중간 소계 구하는 방법, 예시 코드

C# 데이터테이블에서 중간 소계를 구하는 방법, 괜찮은 방법이 없을까요? 생각해보다가 만들어보았는데요, 다음 예시보다 좋은 방법이 있다면 공유 좀 해주세요~ 😘

데이터 분석이나 보고서를 작성할 때 카테고리별 또는 서브카테고리별 소계를 자동으로 계산해주는 기능이 있다면 작업의 효율을 크게 높일 수 있을 것 같아요.

특히 C#에서는 DataTable을 사용하여 데이터를 관리하는 경우가 많은데요, 소계 행을 동적으로 생성하여 DataTable에 추가하는 방법을 구현할 수 있습니다. 다음은 DataTable을 기반으로, 특정 조건에 따라 중간 소계를 구하고, 이를 테이블에 추가하는 방법을 다룬 예시 코드입니다.

예시 코드

다음 코드는 DataTable에 데이터 행을 추가하고, CategorySubcategory별로 소계를 계산해 추가하는 함수입니다.

void BeomSang()
{
    DataTable dataTable = new DataTable();
    dataTable.Columns.Add("Category", typeof(string));// 키1 컬럼
    dataTable.Columns.Add("Subcategory", typeof(string));// 키2 컬럼
    dataTable.Columns.Add("Amount", typeof(decimal));// 소계를 계산할 컬럼
    dataTable.Columns.Add("Tax", typeof(decimal));// 소계를 계산할 컬럼
    dataTable.Columns.Add("Description", typeof(string));// 소계에 포함되지 않을 텍스트 컬럼

    // 예제 데이터 추가
    dataTable.Rows.Add("A", "a", 100, 10, "BEOMSANG");
    dataTable.Rows.Add("A", "a", 200, 20, "BEOM");
    dataTable.Rows.Add("A", "b", 500, 50, "SANG");
    dataTable.Rows.Add("B", "b", 150, 15, "BS");
    dataTable.Rows.Add("B", "b", 50, 5, "SB");

    // 합계 행 추가
    DataRow totalRow = dataTable.NewRow();
    totalRow["Category"] = "합계";
    totalRow["Subcategory"] = "합계";
    foreach (DataColumn column in dataTable.Columns)
    {
        switch (column.ColumnName)
        {
            case "Category":
            case "Subcategory":
                break;
            default:
                if (column.DataType == typeof(decimal))
                {
                    totalRow[column.ColumnName] = dataTable.Compute($"SUM({column.ColumnName})", string.Empty);
                }
                break;
        }
    }
    dataTable.Rows.Add(totalRow);

    // 그룹별 소계 계산 변수
    var groups = dataTable.AsEnumerable()
        .Where(x => x["Category"].ToString() != "합계" && x["Subcategory"].ToString() != "합계")
        .GroupBy(x => new { key1 = x["Category"].ToString(), key2 = x["Subcategory"].ToString() })
        .Select(x => new { KeyCol = x.Key });

    // 소계 행 추가
    foreach (var group in groups)
    {
        DataRow subTotalRow = dataTable.NewRow();
        subTotalRow["Category"] = $"{group.KeyCol.key1} 소계";
        subTotalRow["Subcategory"] = $"{group.KeyCol.key2 } 소계";
        for (int c = 0; c < dataTable.Columns.Count; c++)
        {
            switch (dataTable.Columns[c].ColumnName)
            {
                case "Category":
                case "Subcategory":
                    break;
                default:
                    if (dataTable.Columns[c].DataType == typeof(decimal))
                    {
                        subTotalRow[dataTable.Columns[c]] = dataTable.Compute($"SUM({dataTable.Columns[c].ColumnName})", $"Category = '{group.KeyCol.key1}' AND Subcategory = '{group.KeyCol.key2}'");
                    }
                    break;
            }
        }
        dataTable.Rows.Add(subTotalRow);
    }

    // 결과 데이터 정렬            
    dataTable = dataTable.AsEnumerable().OrderBy(x => x["Category"].ToString() == "합계" ? 1 : 0)
        .ThenBy(x => x["Category"].ToString().Replace(" 소계", ""))
        .ThenBy(x => x["Subcategory"].ToString().Replace(" 소계", ""))
        .ThenBy(x => x["Category"].ToString().EndsWith("소계") ? 1 : 0)
        .ThenBy(x => x["Subcategory"].ToString().EndsWith("소계") ? 1 : 0)
        .CopyToDataTable();
}
linq datatable group by sum

코드 분석

  1. 데이터 생성 및 컬럼 정의
    첫 번째로, DataTable 객체를 만들고 Category, Subcategory, Amount, Tax, Description 컬럼을 추가합니다. 각각의 컬럼에 데이터 유형을 설정하고, 예제 데이터를 추가할게요.

  2. 합계 행 추가
    합계를 구하여 행을 추가합니다. Compute 메서드를 사용할 텐데요, 아래에서 소계를 구할 때에도 다시 활용할 예정입니다~

  3. 그룹별 소계 계산 및 추가
    GroupBy 메서드를 사용해 CategorySubcategory 별로 데이터를 그룹화하고, Compute 메서드를 통해 AmountTax 컬럼의 소계를 계산합니다. 각각의 소계 행은 CategorySubcategory 이름에 '소계'라는 텍스트가 추가된 상태로 DataTable에 추가해 볼게요.

  4. 결과 정렬
    마지막으로, OrderByThenBy를 이용해 CategorySubcategory 값이 '소계'와 '합계'인 행이 지정된 순서대로 위치하도록 정렬합니다.

요약 및 결론

데이터 분석이나 보고서에서 각 그룹별 데이터의 합계를 한눈에 파악할 수 있도록 돕고자 해보았습니다~ 특히, DataTable.Compute 메서드를 활용해 간단하게 합계를 구할 수 있으므로, C#을 활용해 데이터 테이블을 구성하고 계산하는 과정에서 유용하게 활용할 수 있을 것 같아요. 참고하여 사용하면서 더 좋은 코드가 있다면 공유 좀 해주세요~

메서드 예제

중간 소계 구하는 방법

메서드를 간단하게 만들어보았습니다. 데이터에 "|" 문자가 없는 것을 가정하여 조인으로 처리하였는데요, 필요 시 그룹화 부분을 커스텀 하세요.

확장메서드로 수정할 수도 있을 것이며, 데이터테이블에 기본키(public System.Data.DataColumn[] PrimaryKey { get; set; })를 추가하여, 별도의 매개변수 없이 사용해도 되겠지요?

아, 그리고 정렬은 제외했는데 이 부분도 필요하면 추가하세요.

void BeomSang()
{
    DataTable dataTable = new DataTable();
    dataTable.Columns.Add("Category", typeof(string));// 키1 컬럼
    dataTable.Columns.Add("Subcategory", typeof(string));// 키2 컬럼
    dataTable.Columns.Add("Amount", typeof(decimal));// 소계를 계산할 컬럼
    dataTable.Columns.Add("Tax", typeof(decimal));// 소계를 계산할 컬럼
    dataTable.Columns.Add("Description", typeof(string));// 소계에 포함되지 않을 텍스트 컬럼

    // 예제 데이터 추가
    dataTable.Rows.Add("A", "a", 100, 10, "BEOMSANG");
    dataTable.Rows.Add("A", "a", 200, 20, "BEOM");
    dataTable.Rows.Add("A", "b", 500, 50, "SANG");
    dataTable.Rows.Add("B", "b", 150, 15, "BS");
    dataTable.Rows.Add("B", "b", 50, 5, "SB");    

    DataTable result = AddTotal(dataTable, new string[] { "Category", "Subcategory" });            
}

public static DataTable AddTotal(DataTable _dataTable, string[] _keyColumns)
{
    // 전체 합계 행 추가
    DataRow totalRow = _dataTable.NewRow();
    foreach (DataColumn column in _dataTable.Columns)
    {
        if (_keyColumns.Contains(column.ColumnName))
        {
            totalRow[column.ColumnName] = "합계";
        }
        else
        {
            if (column.DataType == typeof(decimal))
            {
                totalRow[column.ColumnName] = _dataTable.Compute($"SUM({column.ColumnName})", string.Empty);
            }
        }
    }
    _dataTable.Rows.Add(totalRow);

    // 그룹별 소계 계산 변수
    var groups = _dataTable.AsEnumerable()
        .Where(row => _keyColumns.Any(col => !row[col].ToString().EndsWith("합계"))) // 모든 키 컬럼에 대해 "합계"가 포함되지 않은 행만 필터링
        .GroupBy(row => string.Join("|", _keyColumns.Select(col => row[col].ToString()))); // 키로 그룹화            

    // 그룹별 소계 행 추가
    foreach (var group in groups)
    {
        var keys = group.Key.Split('|');
        DataRow subTotalRow = _dataTable.NewRow();
        for (int i = 0; i < _keyColumns.Length; i++)
        {
            subTotalRow[_keyColumns[i]] = $"{keys[i]} 소계";
        }
        foreach (DataColumn column in _dataTable.Columns)
        {
            if (!_keyColumns.Contains(column.ColumnName) && column.DataType == typeof(decimal))
            {
                subTotalRow[column.ColumnName] = group.Sum(row => row.Field<decimal>(column.ColumnName));
            }
        }
        _dataTable.Rows.Add(subTotalRow);
    }
    return _dataTable;
}
댓글 쓰기
가져가실 때, 출처 표시 부탁드려요! 감사합니다. 💗