みなさんこんにちは!ヒロポンです!
DataTable の行を「SQL の GROUP BY みたいに C# 側で集計したい」。業務系だとこの場面、しょっちゅう来ますよね。
DB から取ってきた DataTable を地域別に合計したい。金額順に並べ替えたい。マスタと突き合わせて名前を付けたい。
これ、全部 LINQ でいけます。しかも SQL 書けるなら、そのまま翻訳できる。知ってました??
今回は C# LINQ GroupBy を軸に、OrderBy / Join まで含めた「集計・並び替え・結合」の実務3パターンを、コピペで動く形でまとめました。
💡 DataTable の LINQ の基本操作(フィルタ・分割)は C# DataTable を LINQ で扱う3パターン にあります。この記事は集計・並び替え・結合の3点に絞った続きです。
環境は田村さんの現場に多い .NET Framework 4.7.2 / C# 7.3 で動く構文にしてます。
結論: この3パターンをコピペすればいい
先に全部入りを置いときます。DataTable dt(列 Region / Amount)を集計・並び替えする例です。
using System.Data;
using System.Linq;
// パターン1: GroupBy で地域別に件数と合計を集計
var summary = dt.AsEnumerable()
.GroupBy(r => r.Field<string>("Region"))
.Select(g => new {
Region = g.Key,
Count = g.Count(),
Total = g.Sum(r => r.Field<int>("Amount")),
})
.ToList();
// パターン2: 金額の降順 → 地域の昇順で並び替え
var sorted = dt.AsEnumerable()
.OrderByDescending(r => r.Field<int>("Amount"))
.ThenBy(r => r.Field<string>("Region"))
.ToList();
AsEnumerable() で DataTable を LINQ に流し込む。ここがスタート地点です。あとは SQL とほぼ同じ発想で書けます。こんな感じで、頭の中の SQL をそのまま写す感覚。
3つ目の Join は少し長いので、下で個別に見ていきます。
SQL の GROUP BY / ORDER BY / JOIN を LINQ に翻訳する
SQL を書けるなら、対応はほぼ1対1です。頭の中でこう変換するだけ。
GROUP BY 列→.GroupBy(r => r.Field<T>("列"))- 集計
SUM(x)→g.Sum(r => r.Field<int>("x"))、COUNT(*)→g.Count() ORDER BY a, b→.OrderBy(a).ThenBy(b)、降順はOrderByDescendingINNER JOIN→.Join(...)、LEFT JOIN→.GroupJoin(...).SelectMany(...DefaultIfEmpty())
違いは「LINQ は遅延評価」という一点だけ。ここが、あとで罠になります。
パターン1: GroupBy でキー別に集計する
一番よく使うのがこれ。GroupBy でキーごとにまとめて、Select で件数や合計を取り出します。
var summary = dt.AsEnumerable()
.GroupBy(r => r.Field<string>("Region")) // GROUP BY Region
.Select(g => new {
Region = g.Key, // グループのキー
Count = g.Count(), // COUNT(*)
Total = g.Sum(r => r.Field<int>("Amount")), // SUM(Amount)
Average = g.Average(r => r.Field<int>("Amount")),
})
.ToList();
実行結果(地域別に件数・合計が集計される・.NET 9 / Mono で確認):

g.Key がグループのキー(= Region の値)、g がそのグループの行の集まり。Sum / Count / Average / Max あたりは SQL の集計関数そのまま。読み替えの手間はほぼゼロです。
複数キーでグループ化したいなら、匿名型をキーにします。
.GroupBy(r => new { R = r.Field<string>("Region"), Y = r.Field<int>("Year") })
これで「地域 × 年度」の組み合わせごとに集計できます。SQL の GROUP BY Region, Year と同じ動き。いい感じにハマります。
パターン2: OrderBy / ThenBy で複数キー並び替え
並び替えは OrderBy。第2キー以降は ThenBy をつなげます。
var sorted = dt.AsEnumerable()
.OrderByDescending(r => r.Field<int>("Amount")) // ORDER BY Amount DESC
.ThenBy(r => r.Field<string>("Region")) // , Region ASC
.ToList();
ポイントは、2つ目以降は必ず ThenBy / ThenByDescending を使うこと。OrderBy を2回書くと、後ろの OrderBy が前の並び順を上書きします。第1キーが丸ごと効かなくなる。地味に間違えるやつです。
パターン3: Join と GroupJoin で2つの表を突き合わせる
2つの DataTable をキーで結合します。内部結合は Join。
// INNER JOIN: 両方に存在する行だけ
var joined = orders.AsEnumerable()
.Join(customers.AsEnumerable(),
o => o.Field<int>("CustomerId"), // 左のキー
c => c.Field<int>("Id"), // 右のキー
(o, c) => new {
OrderId = o.Field<int>("OrderId"),
Name = c.Field<string>("Name"),
})
.ToList();
左外部結合(LEFT JOIN 相当=マッチしない左の行も残す)は、GroupJoin と DefaultIfEmpty を組み合わせます。
// LEFT JOIN 相当: マッチしない注文も「(不明)」で残す
var leftJoined = orders.AsEnumerable()
.GroupJoin(customers.AsEnumerable(),
o => o.Field<int>("CustomerId"),
c => c.Field<int>("Id"),
(o, cs) => new { o, cs })
.SelectMany(x => x.cs.DefaultIfEmpty(),
(x, c) => new {
OrderId = x.o.Field<int>("OrderId"),
Name = c == null ? "(不明)" : c.Field<string>("Name"),
})
.ToList();
Join が INNER、GroupJoin + SelectMany + DefaultIfEmpty が LEFT。ここは理屈を追うより、パターンとして丸暗記でいいです!!
ハマりポイント: 知らないと集計が合わないやつ
LINQ の集計まわりで業務SEがよく踏むのは、この2つ。
① GroupBy は遅延評価 — ToList() しないと毎回再計算
正直に言うと、俺もこれで集計が合わずに30分溶かしました。
LINQ のクエリは書いた時点では実行されません(遅延評価)。foreach で回したり Count() を呼んだ、その瞬間に初めて計算が走る。
var groups = dt.AsEnumerable().GroupBy(r => r.Field<string>("Region"));
// ❌ groups を2回使うと、そのたびに全部計算し直す
var kinds = groups.Count(); // ここで1回計算
foreach (var g in groups) { ... } // ここでもう一度、頭から計算
これ、元になる dt の中身が途中で変わったり、乱数やカウンタが絡むと、2回目の結果が変わって「集計が合わない」になります。
// ✅ ToList() で確定させてから使い回す
var list = groups.ToList(); // ここで1回だけ計算
var kinds = list.Count;
foreach (var g in list) { ... }
教訓は、集計結果を2回以上使うなら、必ず ToList() で確定させる。これだけで再計算の事故は消えます。
② Join が0件になる — 型不一致はビルドエラー、値不一致は静かに0件
これもやられました。「JOIN してるのに結果が0件」。ん?なんで結合されへんの??と。原因は2つあって、性質が真逆なんですよね。
(1) キーの型が違う → その場でコンパイルエラー(CS0411)
Join は左右のキーが同じ型である必要があります。片方が int・片方が string だと、型推論が通らずそもそもビルドできません。
// ❌ 左は int、右は string → CS0411 でビルドが通らない
.Join(customers.AsEnumerable(),
o => o.Field<int>("CustomerId"), // int
c => c.Field<string>("Id"), // string
(o, c) => new { /* ... */ });
実行結果(型不一致の Join は CS0411 でビルドが通らない・実機確認):

これ、実は親切な方なんです。VS で赤波線が出て、写経したその瞬間に気づける。int.Parse などで型を揃えれば直ります。
c => int.Parse(c.Field<string>("Id")), // ✅ int に揃える
Nullable(int?)も int とは別の型なので、こちらの (1) と同じくビルドエラー側です。GetValueOrDefault() で int に揃えてください。
(2) 型は同じだが値が一致しない → エラーなしで静かに0件
本当に怖いのはこっち。型を両方 string に揃えたのに、片方に前後の空白や全角/半角が混じっていて一致しない。よくあるやつです。これはエラーにならず、静かに0件で返ります。
// 型は string で揃ってるが " 100"(前空白)と "100" は別物 → 0件
// → Trim() や大文字小文字の正規化でキーを揃える
o => o.Field<string>("Code").Trim(),
教訓は、Join が0件なら「まずビルドが通ってるか(型)→ 次にキーの値が本当に一致してるか(空白・桁・大小文字)」の順で疑う。この順番で追えば、結合の事故はほぼ潰せます。
現場メモ: どれを使うか迷ったら
使い分けを1枚にまとめておきます。こんな感じで SQL の対応も並べたので、「これ SQL だと何だっけ」で迷ったらここを見てください。

俺の現場ルールはシンプルです。集計や結合の結果は、その場で ToList() して確定させてから使う。遅延評価の罠を、入口で潰しておく。それだけ。
まとめ
要点はこれだけ。
- 集計は
GroupBy→SelectでCount/Sum。複数キーは匿名型 - 並び替えは
OrderBy→ThenBy(2つ目以降をOrderByにしない) - 内部結合は
Join、左外部結合はGroupJoin + SelectMany + DefaultIfEmpty - 結果を2回使うなら
ToList()で確定。Join が0件なら型(ビルドエラー)→ 値(空白・桁)の順で疑う
C# の LINQ は奥が深くて、言語仕様系の解説書を1冊持っておくと、この手の「なんで集計が合わない?」を調べる時間がまるっと浮きます。
このパターン集、DataTable の集計処理にそのまま貼って使ってください。
よくある質問
Q1. クエリ構文(from … group by)とメソッド構文、どちらを使うべきですか?
結果は同じなので好みです。GroupBy や Join を鎖でつなぐメソッド構文のほうが、他の LINQ と混ぜやすく、業務コードでは主流。SQL に近い見た目が好きならクエリ構文でも構いませんが、チーム内でどちらかに寄せると読みやすくなります。
Q2. GroupBy の結果を DataTable に戻せますか?
匿名型のままでは戻せません。集計結果を新しい DataTable に詰め直すか、そのまま画面表示・CSV 出力に使うのが実務的です。DataGridView にバインドするだけなら、匿名型のリストをそのまま DataSource に渡せます。
Q3. Join で複数キー(複合キー)で結合するには?
キーセレクタで匿名型を返します。o => new { o.A, o.B } と c => new { c.A, c.B } のように、左右で同じ順・同じ型のプロパティ名にすれば複合キーで結合できます。ここでも型が揃っていないとコンパイルエラー、型は同じでも値がズレていると0件になるので注意してください。
次に読むべき記事





以上!
同じ「集計が合わない」で溶かした人いたら、どんどんシェア待ってるぜ!!
執筆者
バイブス父さん — 業務 SE 7 年 (SIer 正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SIer の正社員からフリーランスに転じ、 複数のエージェント経由で案件を回してきた経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。
🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る


