動くコード図鑑技術記事現場の渡り方キャリア論すべての記事About
技術記事

C# LINQ の GroupBy / OrderBy / Join 実務3パターン — 業務SEが DataTable 集計をコードで書く

バイブス父さん
現役の業務SE
2026年7月9日12 min read
C# LINQ の GroupBy / OrderBy / Join 実務3パターン — 業務SEが DataTable 集計をコードで書く

みなさんこんにちは!ヒロポンです!

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)、降順は OrderByDescending
  • INNER 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 で確認):

GroupBy で Region 別に Count と Total が集計された実行結果

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 相当=マッチしない左の行も残す)は、GroupJoinDefaultIfEmpty を組み合わせます。

// 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 でビルドが通らない・実機確認):

int と string のキーで Join するとコンパイルエラー CS0411 になる

これ、実は親切な方なんです。VS で赤波線が出て、写経したその瞬間に気づける。int.Parse などで型を揃えれば直ります。

c => int.Parse(c.Field<string>("Id")),   // ✅ int に揃える

Nullableint?)も int とは別の型なので、こちらの (1) と同じくビルドエラー側です。GetValueOrDefault()int に揃えてください。

(2) 型は同じだが値が一致しない → エラーなしで静かに0件

本当に怖いのはこっち。型を両方 string に揃えたのに、片方に前後の空白全角/半角が混じっていて一致しない。よくあるやつです。これはエラーにならず、静かに0件で返ります。

// 型は string で揃ってるが " 100"(前空白)と "100" は別物 → 0件
// → Trim() や大文字小文字の正規化でキーを揃える
o => o.Field<string>("Code").Trim(),

教訓は、Join が0件なら「まずビルドが通ってるか(型)→ 次にキーの値が本当に一致してるか(空白・桁・大小文字)」の順で疑う。この順番で追えば、結合の事故はほぼ潰せます。

現場メモ: どれを使うか迷ったら

使い分けを1枚にまとめておきます。こんな感じで SQL の対応も並べたので、「これ SQL だと何だっけ」で迷ったらここを見てください。

LINQ の GroupBy / OrderBy / Join / GroupJoin を、対応する SQL・主な用途・結果の形・本命用途で比較した表

俺の現場ルールはシンプルです。集計や結合の結果は、その場で ToList() して確定させてから使う。遅延評価の罠を、入口で潰しておく。それだけ。

まとめ

要点はこれだけ。

  • 集計は GroupBySelectCount / Sum。複数キーは匿名型
  • 並び替えは OrderByThenBy(2つ目以降を OrderBy にしない)
  • 内部結合は Join、左外部結合は GroupJoin + SelectMany + DefaultIfEmpty
  • 結果を2回使うなら ToList() で確定。Join が0件なら型(ビルドエラー)→ 値(空白・桁)の順で疑う

C# の LINQ は奥が深くて、言語仕様系の解説書を1冊持っておくと、この手の「なんで集計が合わない?」を調べる時間がまるっと浮きます。

このパターン集、DataTable の集計処理にそのまま貼って使ってください。

よくある質問

Q1. クエリ構文(from … group by)とメソッド構文、どちらを使うべきですか?

結果は同じなので好みです。GroupByJoin を鎖でつなぐメソッド構文のほうが、他の 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 で経歴詳細を見る

この記事のコードと手順は ぜんぶ動作検証済み。 安心して現場で試してくれ。
バイブス父さん

現役の業務SE。C# / SQL Server 保守の現場から、コードも人もキャリアも全部書く。 実体験ベース。

運営者について