C# 데이터테이블에서 중간 소계를 구하는 방법, 괜찮은 방법이 없을까요? 생각해보다가 만들어보았는데요, 다음 예시보다 좋은 방법이 있다면 공유 좀 해주세요~ 😘
데이터 분석이나 보고서를 작성할 때 카테고리별 또는 서브카테고리별 소계를 자동으로 계산해주는 기능이 있다면 작업의 효율을 크게 높일 수 있을 것 같아요.
특히 C#에서는 DataTable을 사용하여 데이터를 관리하는 경우가 많은데요, 소계 행을 동적으로 생성하여 DataTable에 추가하는 방법을 구현할 수 있습니다. 다음은 DataTable을 기반으로, 특정 조건에 따라 중간 소계를 구하고, 이를 테이블에 추가하는 방법을 다룬 예시 코드입니다.
예시 코드
다음 코드는 DataTable에 데이터 행을 추가하고, Category 및 Subcategory별로 소계를 계산해 추가하는 함수입니다.
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();
}코드 분석
- 데이터 생성 및 컬럼 정의 
 첫 번째로,- DataTable객체를 만들고- Category,- Subcategory,- Amount,- Tax,- Description컬럼을 추가합니다. 각각의 컬럼에 데이터 유형을 설정하고, 예제 데이터를 추가할게요.
- 합계 행 추가 
 합계를 구하여 행을 추가합니다.- Compute메서드를 사용할 텐데요, 아래에서 소계를 구할 때에도 다시 활용할 예정입니다~
- 그룹별 소계 계산 및 추가 - GroupBy메서드를 사용해- Category와- Subcategory별로 데이터를 그룹화하고,- Compute메서드를 통해- Amount와- Tax컬럼의 소계를 계산합니다. 각각의 소계 행은- Category와- Subcategory이름에 '소계'라는 텍스트가 추가된 상태로- DataTable에 추가해 볼게요.
- 결과 정렬 
 마지막으로,- OrderBy와- ThenBy를 이용해- Category와- Subcategory값이 '소계'와 '합계'인 행이 지정된 순서대로 위치하도록 정렬합니다.
요약 및 결론
데이터 분석이나 보고서에서 각 그룹별 데이터의 합계를 한눈에 파악할 수 있도록 돕고자 해보았습니다~ 특히, 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;
}