【C#】LINQとDataTableでスマートにフィルタ・ソート・集計する方法
LINQを使えばDataTableをスマートに絞り込み、ソート、集計できます。 AsEnumerableで行列を列挙型へ変換し、whereやOrderByで条件指定や並び替え、GroupByと集計関数でグループ処理もスムーズに実装可能です。 SQLライクな記述により読み書きの手間を削減し、Selec
LINQを使えばDataTableをスマートに絞り込み、ソート、集計できます。 AsEnumerable で行列を列挙型へ変換し、 where や OrderBy で条件指定や並び替え、 GroupBy と集計関数でグループ処理もスムーズに実装可能です。 SQLライクな記述により読み書きの手間を削減し、 Select メソッドより可読性と保守性が向上しやすいのが利点です。 必要に応じて CopyToDataTable で結果をDataTableへ戻せるため、後続処理への受け渡しも簡単です。
DataTableとLINQの基本
DataTableとはDataTable は、.NET Frameworkの System.Data 名前空間に含まれるクラスで、表形式のデータをメモリ上で管理するためのデータ構造です。
データベースのテーブルのように、行 DataRow と列 DataColumn で構成されており、複数のデータ型を持つ列を自由に定義できます。
DataAdapter を使ってデータベースからデータを取得し、 DataTable に格納したり、逆に DataTable の変更をデータベースに反映したりできます。
たとえば、以下のように DataTable を作成し、列を定義してデータを追加できます。
using System; using System.Data; class Program < static void Main() < // DataTableの作成 DataTable dt = new DataTable("SampleTable"); // 列の定義 dt.Columns.Add("ID", typeof(int)); dt.Columns.Add("Name", typeof(string)); dt.Columns.Add("Age", typeof(int)); // データの追加 dt.Rows.Add(1, "田中", 28); dt.Rows.Add(2, "佐藤", 35); dt.Rows.Add(3, "鈴木", 22); // データの表示 foreach (DataRow row in dt.Rows) < Console.WriteLine($"ID: , Name: , Age: "); > > > ID: 1, Name: 田中, Age: 28 ID: 2, Name: 佐藤, Age: 35 ID: 3, Name: 鈴木, Age: 22このコードでは、 ID 、 Name 、 Age の3列を持つ DataTable を作成し、3件のデータを追加しています。
LINQとはLINQ(Language Integrated Query)は、C#やVB.NETに組み込まれたクエリ言語で、コレクションやデータベース、XMLなどのデータソースに対して統一的な方法で問い合わせや操作を行うことができます。
配列やリスト、 DataTable 、データベース、XMLなど、さまざまなデータソースに対して同じ構文でクエリを記述できます。
Where 、 Select 、 OrderBy 、 GroupBy など、多彩な拡張メソッドを使ってデータをフィルタリング、変換、並べ替え、集計できます。
SQLに似た構文で、 from 、 where 、 select などのキーワードを使います。
using System; using System.Linq; class Program < static void Main() < int[] numbers = < 1, 2, 3, 4, 5, 6 >; // クエリ式 var evenNumbersQuery = from n in numbers where n % 2 == 0 select n; // メソッドチェーン var evenNumbersMethod = numbers.Where(n => n % 2 == 0); Console.WriteLine("偶数(クエリ式):"); foreach (var num in evenNumbersQuery) < Console.WriteLine(num); >Console.WriteLine("偶数(メソッドチェーン):"); foreach (var num in evenNumbersMethod) < Console.WriteLine(num); >> > 偶数(クエリ式): 2 4 6 偶数(メソッドチェーン): 2 4 6このコードでは、 numbers 配列から偶数だけを抽出し、2通りの書き方で表示しています。
DataTableをLINQで扱うメリットDataTable は非常に便利なデータ構造ですが、従来の操作方法では Rows コレクションをループして条件分岐を行うなど、コードが冗長になりがちです。
元の DataTable を壊さずに、条件に合った行だけを抽出したり、新しい順序で並べ替えたりできます。
たとえば、 DataTable のデータから年齢が30歳以上の人だけを抽出し、名前の昇順で並べ替える例を示します。
using System; using System.Data; using System.Linq; class Program < static void Main() < DataTable dt = new DataTable(); dt.Columns.Add("ID", typeof(int)); dt.Columns.Add("Name", typeof(string)); dt.Columns.Add("Age", typeof(int)); dt.Rows.Add(1, "田中", 28); dt.Rows.Add(2, "佐藤", 35); dt.Rows.Add(3, "鈴木", 40); dt.Rows.Add(4, "高橋", 25); var filteredSortedRows = dt.AsEnumerable() .Where(row =>row.Field("Age") >= 30) .OrderBy(row => row.Field("Name")); foreach (var row in filteredSortedRows) < Console.WriteLine($"Name: , Age: "); > > > Name: 佐藤, Age: 35 Name: 鈴木, Age: 40このように、 AsEnumerable で DataTable をLINQで扱える形に変換し、 Field で型安全に列の値を取得しながら、 Where で条件を指定し、 OrderBy で並べ替えています。
以上の理由から、 DataTable とLINQを組み合わせることは、C#でのデータ操作をスマートに行うための有効な手段となっています。
準備作業
AsEnumerableで列挙型へ変換LINQで扱うためには、 DataTable の行を列挙可能な形式に変換する必要があります。
これを実現するのが AsEnumerable メソッドです。
AsEnumerable は DataTable の拡張メソッドで、 DataRow のシーケンス IEnumerable を返します。
これにより、LINQの Where や Select 、 OrderBy などのメソッドを使って柔軟にデータを操作できます。
using System; using System.Data; using System.Linq; class Program < static void Main() < DataTable dt = new DataTable(); dt.Columns.Add("ID", typeof(int)); dt.Columns.Add("Name", typeof(string)); dt.Rows.Add(1, "田中"); dt.Rows.Add(2, "佐藤"); dt.Rows.Add(3, "鈴木"); // AsEnumerableでDataTableを列挙可能に変換 var enumerableRows = dt.AsEnumerable(); foreach (var row in enumerableRows) < Console.WriteLine($"ID: ("ID")>, Name: ("Name")>"); > > > ID: 1, Name: 田中 ID: 2, Name: 佐藤 ID: 3, Name: 鈴木このように AsEnumerable を使うことで、 DataTable の行をLINQで扱える形に変換し、 Field メソッドで列の値を取得しています。
System.Data.DataSetExtensionsの参照追加AsEnumerable メソッドは System.Data.DataSetExtensions アセンブリに定義されています。
プロジェクトでLINQを使って DataTable を操作する場合は、このアセンブリへの参照が必要です。
- ソリューションエクスプローラーでプロジェクトを右クリックし、「参照の追加」を選択。
- 「アセンブリ」→「フレームワーク」タブを開きます。
- System.Data.DataSetExtensions にチェックを入れて「OK」をクリック。
通常は using System.Linq; だけで AsEnumerable が使えますが、参照がないとコンパイルエラーになるため注意してください。
.NET Coreや.NET 5以降の環境では、 System.Data.DataSetExtensions はNuGetパッケージとして提供されている場合があります。
その場合は、NuGetパッケージマネージャーから System.Data.DataSetExtensions をインストールしてください。
Fieldメソッドで型安全にアクセスDataRow の列データにアクセスする際、従来は row["列名"] のようにインデクサーを使って値を取得していました。
しかし、この方法は戻り値が object 型であるため、明示的なキャストが必要で、型の不一致による例外が発生しやすくなります。
LINQと組み合わせて使う場合は、 Field 拡張メソッドを使うことで、指定した型で安全に値を取得できます。
Field は DBNull を自動的に扱い、 null 許容型にも対応しているため、より堅牢なコードが書けます。
using System; using System.Data; using System.Linq; class Program < static void Main() < DataTable dt = new DataTable(); dt.Columns.Add("ID", typeof(int)); dt.Columns.Add("Name", typeof(string)); dt.Columns.Add("Age", typeof(int)); dt.Rows.Add(1, "田中", 28); dt.Rows.Add(2, "佐藤", DBNull.Value); // 年齢不明 foreach (DataRow row in dt.Rows) < int string name = row.Field("Name"); int? age = row.Field("Age"); // null許容型で取得 Console.WriteLine($"ID: , Name: , Age: "); > > > ID: 1, Name: 田中, Age: 28 ID: 2, Name: 佐藤, Age: 不明この例では、 Age 列に DBNull.Value が含まれていても、 Field で安全に null として扱えます。
Field を使うことで、キャストミスや DBNull の扱いに起因する例外を防げるため、LINQクエリ内でも安心して列の値を取得できます。
匿名型と強く型付けしたクラスの選択LINQのクエリ結果を格納する際、匿名型と強く型付けしたクラス(POCO: Plain Old CLR Object)のどちらを使うかは、用途や規模によって使い分けます。
匿名型の特徴- クエリ内で簡単に作成できる
- プロパティは読み取り専用で、型はコンパイラが推論
- メソッドの戻り値やローカル変数で使うのに適している
- クラス名がないため、メソッドの外に持ち出せない
たとえば、 DataTable の特定の列だけを抽出して表示する場合に使います。
var query = dt.AsEnumerable() .Select(row => new < Name = row.Field("Name") >); foreach (var item in query) < Console.WriteLine($"ID: , Name: "); > 強く型付けしたクラスの特徴- クラス名があり、メソッドの引数や戻り値、フィールドとして使える
- プロパティの読み書きが可能
- 複雑なロジックや再利用性の高いコードに向いている
フィルタリングの実践
where句で条件抽出LINQの where 句は、 DataTable の行を条件に基づいて抽出する際に使います。
AsEnumerable で DataTable を列挙可能に変換した後、 where 句で条件を指定して必要な行だけを取得できます。
単一条件たとえば、 Age 列が30以上の行だけを抽出する場合は以下のように記述します。
using System; using System.Data; using System.Linq; class Program < static void Main() < DataTable dt = new DataTable(); dt.Columns.Add("Name", typeof(string)); dt.Columns.Add("Age", typeof(int)); dt.Rows.Add("田中", 28); dt.Rows.Add("佐藤", 35); dt.Rows.Add("鈴木", 40); var filteredRows = dt.AsEnumerable() .Where(row =>row.Field("Age") >= 30); foreach (var row in filteredRows) < Console.WriteLine($"Name: ("Name")>, Age: "); > > > Name: 佐藤, Age: 35 Name: 鈴木, Age: 40このコードでは、 Age が30以上の行だけを抽出し、名前と年齢を表示しています。
複数条件複数の条件を組み合わせる場合は、論理演算子 && (AND)や || (OR)を使います。
たとえば、 Age が30以上かつ Name が「佐藤」である行を抽出する例です。
var filteredRows = dt.AsEnumerable() .Where(row => row.Field("Age") >= 30 && row.Field("Name") == "佐藤"); using System; using System.Data; using System.Linq; class Program < static void Main() < DataTable dt = new DataTable(); dt.Columns.Add("Name", typeof(string)); dt.Columns.Add("Age", typeof(int)); dt.Rows.Add("田中", 28); dt.Rows.Add("佐藤", 35); dt.Rows.Add("鈴木", 40); var filteredRows = dt.AsEnumerable() .Where(row =>row.Field("Age") >= 30 && row.Field("Name") == "佐藤"); foreach (var row in filteredRows) < Console.WriteLine($"Name: , Age: "); > > > Name: 佐藤, Age: 35 AnyとAllで存在チェックLINQの Any と All は、条件に合致する行が存在するかどうかを判定するのに便利です。
- Any は、条件を満たす要素が1つでもあれば true を返します
- All は、すべての要素が条件を満たす場合に true を返します
たとえば、 Age が40以上の人がいるかどうかを調べる例です。
bool hasAge40OrMore = dt.AsEnumerable() .Any(row => row.Field("Age") >= 40); Console.WriteLine($"Ageが40以上の人がいるか: "); Ageが40以上の人がいるか: True逆に、全員が30歳以上かどうかを調べる場合は All を使います。
bool allAge30OrMore = dt.AsEnumerable() .All(row => row.Field("Age") >= 30); Console.WriteLine($"全員が30歳以上か: "); 全員が30歳以上か: Falseこのように、 Any と All を使うことで、条件に合致する行の存在チェックを簡単に行えます。
Like検索をContainsやStartsWithで代替DataTable.Select メソッドのようにSQLの LIKE 検索はLINQにはありませんが、文字列の部分一致や前方一致は Contains や StartsWith メソッドで代替できます。
たとえば、 Name 列に「田」が含まれる行を抽出する場合は Contains を使います。
var filteredRows = dt.AsEnumerable() .Where(row => row.Field("Name").Contains("田")); using System; using System.Data; using System.Linq; class Program < static void Main() < DataTable dt = new DataTable(); dt.Columns.Add("Name", typeof(string)); dt.Columns.Add("Age", typeof(int)); dt.Rows.Add("田中", 28); dt.Rows.Add("佐藤", 35); dt.Rows.Add("鈴木", 40); dt.Rows.Add("高田", 22); var filteredRows = dt.AsEnumerable() .Where(row =>row.Field("Name").Contains("田")); foreach (var row in filteredRows) < Console.WriteLine($"Name: , Age: ("Age")>"); > > > Name: 田中, Age: 28 Name: 高田, Age: 22また、 Name が「佐」で始まる行を抽出する場合は StartsWith を使います。
var filteredRows = dt.AsEnumerable() .Where(row => row.Field("Name").StartsWith("佐"));このように、 Contains や StartsWith を使うことで、SQLの LIKE に近い柔軟な文字列検索が可能です。
大文字・小文字の区別を無視したい場合は、 ToLower() や ToUpper() を組み合わせて比較する方法もあります。
ソートの実践
OrderByとOrderByDescendingLINQの OrderBy と OrderByDescending は、 DataTable の行を指定した列の値で昇順または降順に並べ替えるために使います。
AsEnumerable で DataTable を列挙可能に変換した後、これらのメソッドを適用してソートを行います。
数値列の昇降順数値列を昇順に並べ替えるには OrderBy を使い、降順に並べ替えるには OrderByDescending を使います。
たとえば、 Age 列を基準に昇順・降順で並べ替える例を示します。
using System; using System.Data; using System.Linq; class Program < static void Main() < DataTable dt = new DataTable(); dt.Columns.Add("Name", typeof(string)); dt.Columns.Add("Age", typeof(int)); dt.Rows.Add("田中", 28); dt.Rows.Add("佐藤", 35); dt.Rows.Add("鈴木", 22); dt.Rows.Add("高橋", 40); // Age列で昇順に並べ替え var ascending = dt.AsEnumerable() .OrderBy(row =>row.Field("Age")); Console.WriteLine("Age昇順:"); foreach (var row in ascending) < Console.WriteLine($"Name: ("Name")>, Age: "); > // Age列で降順に並べ替え var descending = dt.AsEnumerable() .OrderByDescending(row => row.Field("Age")); Console.WriteLine("\nAge降順:"); foreach (var row in descending) < Console.WriteLine($"Name: ("Name")>, Age: "); > > > Age昇順: Name: 鈴木, Age: 22 Name: 田中, Age: 28 Name: 佐藤, Age: 35 Name: 高橋, Age: 40 Age降順: Name: 高橋, Age: 40 Name: 佐藤, Age: 35 Name: 田中, Age: 28 Name: 鈴木, Age: 22このように、 OrderBy は指定した列の値を小さい順に並べ替え、 OrderByDescending は大きい順に並べ替えます。
文字列列の昇降順文字列列も同様に OrderBy と OrderByDescending で並べ替えられます。
たとえば、 Name 列を昇順・降順で並べ替える例です。
var ascendingByName = dt.AsEnumerable() .OrderBy(row => row.Field("Name")); Console.WriteLine("Name昇順:"); foreach (var row in ascendingByName) < Console.WriteLine($"Name: , Age: ("Age")>"); > var descendingByName = dt.AsEnumerable() .OrderByDescending(row => row.Field("Name")); Console.WriteLine("\nName降順:"); foreach (var row in descendingByName) < Console.WriteLine($"Name: , Age: ("Age")>"); > Name昇順: Name: 佐藤, Age: 35 Name: 鈴木, Age: 22 Name: 高橋, Age: 40 Name: 田中, Age: 28 Name降順: Name: 田中, Age: 28 Name: 高橋, Age: 40 Name: 鈴木, Age: 22 Name: 佐藤, Age: 35大文字・小文字の区別を無視したい場合は、 StringComparer を使ったカスタム比較も可能ですが、基本的な使い方では OrderBy と OrderByDescending で十分です。
ThenByで二次ソートOrderBy や OrderByDescending で一次ソートを行った後、さらに別の列で二次ソートを行うには ThenBy や ThenByDescending を使います。
たとえば、 Age で昇順に並べ替えた後、同じ年齢の人は Name で昇順に並べ替える例です。
var sortedRows = dt.AsEnumerable() .OrderBy(row => row.Field("Age")) .ThenBy(row => row.Field("Name")); Console.WriteLine("Age昇順、同じAgeはName昇順:"); foreach (var row in sortedRows) < Console.WriteLine($"Name: , Age: "); > Age昇順、同じAgeはName昇順: Name: 鈴木, Age: 22 Name: 田中, Age: 28 Name: 佐藤, Age: 35 Name: 高橋, Age: 40もし Age で降順に並べ替え、同じ年齢の人は Name で降順に並べ替えたい場合は、以下のように書きます。
var sortedRowsDesc = dt.AsEnumerable() .OrderByDescending(row => row.Field("Age")) .ThenByDescending(row => row.Field("Name")); Console.WriteLine("Age降順、同じAgeはName降順:"); foreach (var row in sortedRowsDesc) < Console.WriteLine($"Name: , Age: "); > Age降順、同じAgeはName降順: Name: 高橋, Age: 40 Name: 佐藤, Age: 35 Name: 田中, Age: 28 Name: 鈴木, Age: 22ThenBy や ThenByDescending は、一次ソートの結果を保持しつつ、同じキーの要素をさらに細かく並べ替えるために使います。
集計の実践
CountとLongCountLINQの Count メソッドは、条件に合致する要素の数を取得するのに使います。
LongCount は Count と同様ですが、戻り値が long 型で、大量のデータを扱う場合に適しています。
たとえば、 DataTable の中で Age が30以上の行数を数える例です。
using System; using System.Data; using System.Linq; class Program < static void Main() < DataTable dt = new DataTable(); dt.Columns.Add("Name", typeof(string)); dt.Columns.Add("Age", typeof(int)); dt.Rows.Add("田中", 28); dt.Rows.Add("佐藤", 35); dt.Rows.Add("鈴木", 40); dt.Rows.Add("高橋", 22); // Ageが30以上の行数をカウント int count = dt.AsEnumerable() .Count(row =>row.Field("Age") >= 30); long longCount = dt.AsEnumerable() .LongCount(row => row.Field("Age") >= 30); Console.WriteLine($"Count: "); Console.WriteLine($"LongCount: "); > > Count: 2 LongCount: 2Count は int 型の戻り値なので、数百万件以上のデータを扱う場合は LongCount を使うと安全です。
SumとAverageSum は指定した列の値の合計を計算し、 Average は平均値を計算します。
以下は、 Age 列の合計と平均を計算する例です。
var totalAge = dt.AsEnumerable() .Sum(row => row.Field("Age")); var averageAge = dt.AsEnumerable() .Average(row => row.Field("Age")); Console.WriteLine($"合計年齢: "); Console.WriteLine($"平均年齢: "); 合計年齢: 125 平均年齢: 31.25Average の戻り値は double 型なので、小数点以下の桁数を指定して表示することも可能です。
MinとMaxMin と Max は、指定した列の最小値と最大値を取得します。
たとえば、 Age 列の最小値と最大値を求める例です。
var minAge = dt.AsEnumerable() .Min(row => row.Field("Age")); var maxAge = dt.AsEnumerable() .Max(row => row.Field("Age")); Console.WriteLine($"最小年齢: "); Console.WriteLine($"最大年齢: "); 最小年齢: 22 最大年齢: 40 集計結果を匿名型へ格納たとえば、 Age 列の合計、平均、最小、最大を一度に取得して匿名型にまとめる例です。
var summary = dt.AsEnumerable() .Aggregate(new < Count = 0, Sum = 0, Min = int.MaxValue, Max = int.MinValue >, (acc, row) => new < Count = acc.Count + 1, Sum = acc.Sum + row.Field("Age"), Min = Math.Min(acc.Min, row.Field("Age")), Max = Math.Max(acc.Max, row.Field("Age")) >); double average = (double)summary.Sum / summary.Count; Console.WriteLine($"件数: "); Console.WriteLine($"合計: "); Console.WriteLine($"平均: "); Console.WriteLine($"最小: "); Console.WriteLine($"最大: "); 件数: 4 合計: 125 平均: 31.25 最小: 22 最大: 40または、LINQの Count 、 Sum 、 Min 、 Max を個別に呼び出して匿名型にまとめる方法もあります。
var summary2 = new < Count = dt.AsEnumerable().Count(), Sum = dt.AsEnumerable().Sum(row =>row.Field("Age")), Min = dt.AsEnumerable().Min(row => row.Field("Age")), Max = dt.AsEnumerable().Max(row => row.Field("Age")), Average = dt.AsEnumerable().Average(row => row.Field("Age")) >; Console.WriteLine($"件数: "); Console.WriteLine($"合計: "); Console.WriteLine($"平均: "); Console.WriteLine($"最小: "); Console.WriteLine($"最大: "); 件数: 4 合計: 125 平均: 31.25 最小: 22 最大: 40グループ化の実践
GroupByの基本構文LINQの GroupBy メソッドは、指定したキーに基づいてデータをグループ化します。
DataTable の行をグループ化する際は、 AsEnumerable で列挙可能に変換した後、 GroupBy を使って特定の列の値をキーにグループを作成します。
var grouped = dt.AsEnumerable() .GroupBy(row => row.Field("列名"));たとえば、 Department 列でグループ化し、各グループの人数をカウントする例です。
using System; using System.Data; using System.Linq; class Program < static void Main() < DataTable dt = new DataTable(); dt.Columns.Add("Name", typeof(string)); dt.Columns.Add("Department", typeof(string)); dt.Rows.Add("田中", "営業"); dt.Rows.Add("佐藤", "開発"); dt.Rows.Add("鈴木", "営業"); dt.Rows.Add("高橋", "開発"); dt.Rows.Add("伊藤", "総務"); var grouped = dt.AsEnumerable() .GroupBy(row =>row.Field("Department")); foreach (var group in grouped) < Console.WriteLine($"部署: , 人数: "); > > > 部署: 営業, 人数: 2 部署: 開発, 人数: 2 部署: 総務, 人数: 1このように、 GroupBy は指定した列の値ごとに行をまとめ、グループごとに集計や処理が可能です。
複数列をキーにする方法たとえば、 Department と AgeGroup (年齢層)でグループ化する例です。
var grouped = dt.AsEnumerable() .GroupBy(row => new < Department = row.Field("Department"), AgeGroup = row.Field("Age") >= 30 ? "30歳以上" : "30歳未満" >); foreach (var group in grouped) < Console.WriteLine($"部署: , 年齢層: , 人数: "); > using System; using System.Data; using System.Linq; class Program < static void Main() < DataTable dt = new DataTable(); dt.Columns.Add("Name", typeof(string)); dt.Columns.Add("Department", typeof(string)); dt.Columns.Add("Age", typeof(int)); dt.Rows.Add("田中", "営業", 28); dt.Rows.Add("佐藤", "開発", 35); dt.Rows.Add("鈴木", "営業", 40); dt.Rows.Add("高橋", "開発", 25); dt.Rows.Add("伊藤", "総務", 30); var grouped = dt.AsEnumerable() .GroupBy(row =>new < Department = row.Field("Department"), AgeGroup = row.Field("Age") >= 30 ? "30歳以上" : "30歳未満" >); foreach (var group in grouped) < Console.WriteLine($"部署: , 年齢層: , 人数: "); > > > 部署: 営業, 年齢層: 30歳未満, 人数: 1 部署: 開発, 年齢層: 30歳以上, 人数: 1 部署: 営業, 年齢層: 30歳以上, 人数: 1 部署: 開発, 年齢層: 30歳未満, 人数: 1 部署: 総務, 年齢層: 30歳以上, 人数: 1 グループ内集計GroupBy の結果は IGrouping のコレクションで、各グループは列挙可能な要素の集合です。
Count や Sum 、 Average などの集計メソッドを使えます。
var grouped = dt.AsEnumerable() .GroupBy(row => row.Field("Department")) .Select(g => new < Department = g.Key, Count = g.Count(), AverageAge = g.Average(row =>row.Field("Age")) >); foreach (var group in grouped) < Console.WriteLine($"部署: , 人数: , 平均年齢: "); > 部署: 営業, 人数: 2, 平均年齢: 34.0 部署: 開発, 人数: 2, 平均年齢: 30.0 部署: 総務, 人数: 1, 平均年齢: 30.0このように、 Select で匿名型に集計結果をまとめて扱うことができます。
SelectManyでグループ展開GroupBy でグループ化した結果はグループごとにまとめられていますが、元の行単位で再び処理したい場合は SelectMany を使ってグループを展開できます。
var grouped = dt.AsEnumerable() .GroupBy(row => row.Field("Department")); var flattened = grouped.SelectMany(g => g); foreach (var row in flattened) < Console.WriteLine($"Name: , Department: "); > Name: 田中, Department: 営業 Name: 鈴木, Department: 営業 Name: 佐藤, Department: 開発 Name: 高橋, Department: 開発 Name: 伊藤, Department: 総務結合の実践
Joinで内部結合LINQの Join メソッドは、2つのデータソースを指定したキーで結合し、両方に存在する要素だけを結合結果として取得します。
単一キー結合単一の列をキーにして2つの DataTable を結合する例を示します。
たとえば、社員情報テーブルと部署情報テーブルを DepartmentID で結合し、社員名と部署名を取得します。
using System; using System.Data; using System.Linq; class Program < static void Main() < // 社員テーブル DataTable employees = new DataTable(); employees.Columns.Add("EmployeeID", typeof(int)); employees.Columns.Add("Name", typeof(string)); employees.Columns.Add("DepartmentID", typeof(int)); employees.Rows.Add(1, "田中", 10); employees.Rows.Add(2, "佐藤", 20); employees.Rows.Add(3, "鈴木", 10); employees.Rows.Add(4, "高橋", 30); // 部署テーブル DataTable departments = new DataTable(); departments.Columns.Add("DepartmentID", typeof(int)); departments.Columns.Add("DepartmentName", typeof(string)); departments.Rows.Add(10, "営業"); departments.Rows.Add(20, "開発"); departments.Rows.Add(30, "総務"); // Joinで内部結合 var query = employees.AsEnumerable() .Join(departments.AsEnumerable(), emp =>emp.Field("DepartmentID"), dept => dept.Field("DepartmentID"), (emp, dept) => new < EmployeeName = emp.Field("Name"), DepartmentName = dept.Field("DepartmentName") >); foreach (var item in query) < Console.WriteLine($"社員名: , 部署名: "); > > > 社員名: 田中, 部署名: 営業 社員名: 佐藤, 部署名: 開発 社員名: 鈴木, 部署名: 営業 社員名: 高橋, 部署名: 総務このコードでは、 employees と departments の DepartmentID をキーにして結合し、社員名と部署名を取得しています。
複数キー結合たとえば、社員テーブルとプロジェクト割当テーブルを DepartmentID と ProjectID の組み合わせで結合する例です。
using System; using System.Data; using System.Linq; class Program < static void Main() < // 社員テーブル DataTable employees = new DataTable(); employees.Columns.Add("EmployeeID", typeof(int)); employees.Columns.Add("Name", typeof(string)); employees.Columns.Add("DepartmentID", typeof(int)); employees.Columns.Add("ProjectID", typeof(int)); employees.Rows.Add(1, "田中", 10, 100); employees.Rows.Add(2, "佐藤", 20, 200); employees.Rows.Add(3, "鈴木", 10, 100); employees.Rows.Add(4, "高橋", 30, 300); // プロジェクト割当テーブル DataTable assignments = new DataTable(); assignments.Columns.Add("DepartmentID", typeof(int)); assignments.Columns.Add("ProjectID", typeof(int)); assignments.Columns.Add("ProjectName", typeof(string)); assignments.Rows.Add(10, 100, "プロジェクトA"); assignments.Rows.Add(20, 200, "プロジェクトB"); assignments.Rows.Add(30, 300, "プロジェクトC"); // 複数キーでJoin var query = employees.AsEnumerable() .Join(assignments.AsEnumerable(), emp =>new < Dept = emp.Field("DepartmentID"), Proj = emp.Field("ProjectID") >, assign => new < Dept = assign.Field("DepartmentID"), Proj = assign.Field("ProjectID") >, (emp, assign) => new < EmployeeName = emp.Field("Name"), ProjectName = assign.Field("ProjectName") >); foreach (var item in query) < Console.WriteLine($"社員名: , プロジェクト名: "); > > > 社員名: 田中, プロジェクト名: プロジェクトA 社員名: 佐藤, プロジェクト名: プロジェクトB 社員名: 鈴木, プロジェクト名: プロジェクトA 社員名: 高橋, プロジェクト名: プロジェクトC GroupJoinとDefaultIfEmptyで外部結合LINQの GroupJoin は、左側のデータソースの各要素に対して右側の関連する要素のグループを結合します。
using System; using System.Data; using System.Linq; class Program < static void Main() < // 社員テーブル DataTable employees = new DataTable(); employees.Columns.Add("EmployeeID", typeof(int)); employees.Columns.Add("Name", typeof(string)); employees.Columns.Add("DepartmentID", typeof(int)); employees.Rows.Add(1, "田中", 10); employees.Rows.Add(2, "佐藤", 20); employees.Rows.Add(3, "鈴木", DBNull.Value); // 部署なし employees.Rows.Add(4, "高橋", 30); // 部署テーブル DataTable departments = new DataTable(); departments.Columns.Add("DepartmentID", typeof(int)); departments.Columns.Add("DepartmentName", typeof(string)); departments.Rows.Add(10, "営業"); departments.Rows.Add(20, "開発"); departments.Rows.Add(30, "総務"); // GroupJoinで左外部結合 var query = employees.AsEnumerable() .GroupJoin(departments.AsEnumerable(), emp =>emp.Field("DepartmentID"), dept => dept.Field("DepartmentID"), (emp, depts) => new < EmployeeName = emp.Field("Name"), Department = depts.DefaultIfEmpty() .Select(d => d == null ? "未所属" : d.Field("DepartmentName")) .First() >); foreach (var item in query) < Console.WriteLine($"社員名: , 部署名: "); > > > 社員名: 田中, 部署名: 営業 社員名: 佐藤, 部署名: 開発 社員名: 鈴木, 部署名: 未所属 社員名: 高橋, 部署名: 総務このコードでは、 GroupJoin で社員ごとに対応する部署のグループを取得し、 DefaultIfEmpty で部署がない場合に null を返すようにしています。
Select で null チェックを行い、部署がない場合は「未所属」と表示しています。
結果をDataTableへ戻す
CopyToDataTableで結果を再構築LINQで DataTable の行をフィルタリングやソート、結合などの操作を行った後、結果を再び DataTable として扱いたい場合があります。
CopyToDataTable メソッドを使うと、LINQのクエリ結果 IEnumerable を簡単に新しい DataTable に変換できます。
CopyToDataTable は、元の DataTable のスキーマを引き継いだ新しい DataTable を作成し、クエリ結果の行をコピーします。
たとえば、 Age が30以上の行だけを抽出し、新しい DataTable に格納する例です。
using System; using System.Data; using System.Linq; class Program < static void Main() < DataTable dt = new DataTable(); dt.Columns.Add("Name", typeof(string)); dt.Columns.Add("Age", typeof(int)); dt.Rows.Add("田中", 28); dt.Rows.Add("佐藤", 35); dt.Rows.Add("鈴木", 40); dt.Rows.Add("高橋", 25); // LINQでフィルタリング var filteredRows = dt.AsEnumerable() .Where(row =>row.Field("Age") >= 30); // CopyToDataTableで新しいDataTableに変換 DataTable filteredTable = filteredRows.CopyToDataTable(); // 結果の表示 foreach (DataRow row in filteredTable.Rows) < Console.WriteLine($"Name: ("Name")>, Age: "); > > > Name: 佐藤, Age: 35 Name: 鈴木, Age: 40注意点として、 CopyToDataTable は空のシーケンスに対して呼び出すと例外が発生します。
空の場合に備えて、 Any() で要素があるか確認するか、例外処理を行うことが推奨されます。
DataTable filteredTable = filteredRows.Any() ? filteredRows.CopyToDataTable() : dt.Clone();このように、 CopyToDataTable はLINQの結果を DataTable に戻す際に非常に便利です。
新規スキーマを作成する手順LINQのクエリ結果が匿名型や異なるスキーマのデータの場合、 CopyToDataTable は使えません。
その場合は、新しい DataTable を手動で作成し、スキーマ(列定義)を設定してからデータを追加する必要があります。
以下は、匿名型のクエリ結果を新しい DataTable に変換する例です。
using System; using System.Data; using System.Linq; class Program < static void Main() < DataTable dt = new DataTable(); dt.Columns.Add("Name", typeof(string)); dt.Columns.Add("Age", typeof(int)); dt.Columns.Add("Department", typeof(string)); dt.Rows.Add("田中", 28, "営業"); dt.Rows.Add("佐藤", 35, "開発"); dt.Rows.Add("鈴木", 40, "営業"); dt.Rows.Add("高橋", 25, "開発"); // 匿名型でクエリ結果を作成(NameとDepartmentのみ抽出) var query = dt.AsEnumerable() .Select(row =>new < Name = row.Field("Name"), Department = row.Field("Department") >); // 新しいDataTableを作成しスキーマを定義 DataTable newTable = new DataTable(); newTable.Columns.Add("Name", typeof(string)); newTable.Columns.Add("Department", typeof(string)); // クエリ結果をDataTableに追加 foreach (var item in query) < var newRow = newTable.NewRow(); newRow["Name"] = item.Name; newRow["Department"] = item.Department; newTable.Rows.Add(newRow); >// 結果の表示 foreach (DataRow row in newTable.Rows) < Console.WriteLine($"Name: , Department: "); > > > Name: 田中, Department: 営業 Name: 佐藤, Department: 開発 Name: 鈴木, Department: 営業 Name: 高橋, Department: 開発- 新しい DataTable を作成。
- 必要な列を Columns.Add で定義。
- LINQのクエリ結果をループで回し、 NewRow で新しい行を作成。
- 各列に値をセットして Rows.Add で追加。
この手順により、元の DataTable とは異なるスキーマのテーブルを自由に作成できます。
匿名型や複数のテーブルを結合した結果など、 CopyToDataTable が使えないケースで有効です。
パフォーマンス最適化
LINQとDataTable.Selectの速度比較DataTable のデータをフィルタリングする方法として、 DataTable.Select メソッドとLINQの AsEnumerable + Where を使う方法があります。
DataTable.Select は内部的に文字列で条件を解析し、 DataRow[] を返します。
一方、LINQは型安全で柔軟なクエリが書けますが、 AsEnumerable での変換やラムダ式の評価に若干のオーバーヘッドがあります。
using System; using System.Data; using System.Diagnostics; using System.Linq; class Program < static void Main() < DataTable dt = new DataTable(); dt.Columns.Add("ID", typeof(int)); dt.Columns.Add("Value", typeof(int)); // 大量データの追加 for (int i = 0; i < 100000; i++) < dt.Rows.Add(i, i % 100); >Stopwatch sw = new Stopwatch(); // DataTable.Selectの速度計測 sw.Start(); DataRow[] selectRows = dt.Select("Value >= 50"); sw.Stop(); Console.WriteLine($"Select() 処理時間: ms"); // LINQの速度計測 sw.Restart(); var linqRows = dt.AsEnumerable().Where(row => row.Field("Value") >= 50).ToList(); sw.Stop(); Console.WriteLine($"LINQ 処理時間: ms"); > > Select() 処理時間: 35 ms LINQ 処理時間: 6 ms メソッド処理速度の特徴 DataTable.Select 条件が単純であれば高速だが、文字列解析のオーバーヘッドありLINQ柔軟で型安全、複雑な条件に強いが若干遅い場合がある大量データで単純な条件の場合は Select が速いこともありますが、複雑な条件や型安全性を重視するならLINQが適しています。
遅延実行と即時実行の使い分けつまり、クエリは定義しただけでは実行されず、結果を列挙したり、 ToList() や ToArray() などの即時実行メソッドを呼ぶまで処理が遅延されます。
遅延実行のメリット- 不要な処理を避けられる
- 複数のクエリを組み合わせて最適化できる
- クエリ結果を固定化できる
- 複数回の列挙によるパフォーマンス低下を防げる
大量データを何度も処理する場合は、 ToList() などで即時実行して結果をキャッシュするのがパフォーマンス向上につながります。
メモリ消費を抑えるコツ可能な限り IEnumerable のまま処理し、メモリの節約を図ります。
これらを踏まえ、LINQと DataTable を組み合わせる際は、処理内容やデータ量に応じて適切な方法を選択し、パフォーマンスとメモリ効率のバランスを取ることが重要です。
nullとDBNullの扱い
DBNull.Valueの検出と置換DataTable や DataRow で扱うデータベース由来の値には、 null ではなく DBNull.Value が使われます。
null とは異なるため、 null チェックだけでは空の値を検出できません。
DBNull.Value を検出するには、 DataRow の IsNull メソッドや値の比較を使います。
using System; using System.Data; class Program < static void Main() < DataTable dt = new DataTable(); dt.Columns.Add("Name", typeof(string)); dt.Columns.Add("Age", typeof(int)); dt.Rows.Add("田中", 28); dt.Rows.Add("佐藤", DBNull.Value); // 年齢不明 foreach (DataRow row in dt.Rows) < if (row.IsNull("Age")) < Console.WriteLine($"さんの年齢は不明です。"); > else < Console.WriteLine($"さんの年齢は歳です。"); > > > > 田中さんの年齢は28歳です。 佐藤さんの年齢は不明です。IsNull メソッドは指定した列が DBNull.Value かどうかを判定します。
これにより、 DBNull.Value を安全に検出できます。
また、 DBNull.Value を別の値に置換したい場合は、 IsNull で判定してから代替値を設定します。
foreach (DataRow row in dt.Rows) < int age = row.IsNull("Age") ? -1 : (int)row["Age"]; Console.WriteLine($"さんの年齢はです。"); >このように、 DBNull.Value を検出して適切に置換することで、後続の処理で例外を防ぎ、扱いやすいデータに変換できます。
Field拡張メソッドとNull許容型LINQと組み合わせて DataRow の列値を取得する際は、 Field 拡張メソッドを使うのが一般的です。
Field は DBNull.Value を自動的に null に変換し、 null 許容型(Nullable)にも対応しています。
たとえば、 Age 列が DBNull.Value の場合に null として扱うには、 int? Nullable を指定します。
using System; using System.Data; using System.Linq; class Program < static void Main() < DataTable dt = new DataTable(); dt.Columns.Add("Name", typeof(string)); dt.Columns.Add("Age", typeof(int)); dt.Rows.Add("田中", 28); dt.Rows.Add("佐藤", DBNull.Value); var query = dt.AsEnumerable() .Select(row =>new < Name = row.Field("Name"), Age = row.Field("Age") // Null許容型で取得 >); foreach (var item in query) < string ageText = item.Age.HasValue ? item.Age.Value.ToString() : "不明"; Console.WriteLine($"さんの年齢はです。"); > > > 田中さんの年齢は28です。 佐藤さんの年齢は不明です。Field は内部で DBNull.Value を null に変換するため、 int? のようなNull許容型を使うと、 DBNull.Value を安全に扱えます。
これにより、 DBNull.Value の判定やキャストの手間が省け、コードがシンプルになります。
また、文字列型の場合も Field は DBNull.Value を null に変換するため、 null チェックで空データを判定できます。
このように、 Field 拡張メソッドとNull許容型を組み合わせることで、 DataTable の DBNull.Value を自然に扱い、例外の発生を防ぎつつ安全にデータ操作が可能です。
例外対策
型変換エラーの回避DataTable の列データをLINQで操作する際、型変換エラーが発生しやすいポイントは、 DataRow から値を取得するときのキャストや Field の型指定です。
特に、列のデータ型と異なる型でアクセスしたり、 DBNull.Value を適切に処理しなかった場合に例外が起こります。
例えば、 int 型の列に対しては Field 、文字列列には Field を使います。
型が合わないと InvalidCastException が発生します。
DBNull.Value が含まれる可能性がある列は、 Field や Field などNull許容型で取得すると安全です。
以下は、 Field で型変換エラーを防ぐ例です。
using System; using System.Data; using System.Linq; class Program < static void Main() < DataTable dt = new DataTable(); dt.Columns.Add("ID", typeof(int)); dt.Columns.Add("Name", typeof(string)); dt.Columns.Add("Age", typeof(int)); dt.Rows.Add(1, "田中", 28); dt.Rows.Add(2, "佐藤", DBNull.Value); // 年齢不明 foreach (DataRow row in dt.Rows) < try < int? age = row.Field("Age"); // Null許容型で安全に取得 Console.WriteLine($"Name: ("Name")>, Age: "); > catch (InvalidCastException ex) < Console.WriteLine($"型変換エラー: "); > > > > Name: 田中, Age: 28 Name: 佐藤, Age: 不明このように、 Field とNull許容型を組み合わせることで、 DBNull.Value や型不一致による例外を防げます。
クエリ評価時の例外処理LINQクエリは遅延実行のため、クエリの定義時には例外が発生せず、実際に列挙( foreach や ToList() など)したときに例外が発生することがあります。
例外が発生する可能性のある foreach や ToList() の呼び出し部分で捕捉します。
クエリ全体ではなく、各要素の処理で例外が起きる場合は、ループ内で try-catch を使います。
using System; using System.Data; using System.Linq; class Program < static void Main() < DataTable dt = new DataTable(); dt.Columns.Add("ID", typeof(int)); dt.Columns.Add("Name", typeof(string)); dt.Columns.Add("Age", typeof(object)); // 故意にobject型で不正データを混入 dt.Rows.Add(1, "田中", 28); dt.Rows.Add(2, "佐藤", "不正なデータ"); // 型不一致 var query = dt.AsEnumerable() .Where(row =>< try < int age = row.Field("Age"); return age >= 20; > catch < // 型変換エラーがあれば除外 return false; >>); foreach (var row in query) < try < Console.WriteLine($"Name: ("Name")>, Age: "); > catch (Exception ex) < Console.WriteLine($"データ処理エラー: "); > > > > Name: 田中, Age: 28この例では、 Age 列に不正な文字列が混入していますが、 Where 句内で try-catch を使い、型変換エラーのある行を除外しています。
さらに、 foreach 内でも例外処理を行い、安全に処理を継続しています。
まとめ
この記事では、C#の DataTable とLINQを組み合わせてスマートにデータをフィルタリング、ソート、集計、グループ化、結合する方法を解説しました。
AsEnumerable や Field を活用し、型安全かつ効率的に操作するテクニックを紹介しています。
また、結果を DataTable に戻す方法やパフォーマンス最適化、 DBNull の扱い、例外対策も詳述しました。