LINQ to Objects で グループ化
SQL を勉強しようと↓の本を読んでいます。SQL ってうまいの?ってレベルの私にもわかりやすく、薄いのに丁寧でよい本です。順に読めば SQL がしっかり理解できます。個々の DB について詳しい本ではなく、SQL 特にクエリーについての「言語の入門書」といった感じ。MySQL で解説していますが、一部 MySQL にないものでも記述があったり、Oracle ではこう、SQL Server ではこうと記述も(ちょっと)あります。各 DB の詳しい本とあわせて用意するとよいかと思います。唯一の欠点は表紙がきもいこと!常にひっくり返して置くこと!
- 作者: Alan Beaulieu,株式会社クイープ
- 出版社/メーカー: オライリージャパン
- 発売日: 2006/04/01
- メディア: 単行本
- 購入: 4人 クリック: 39回
- この商品を含むブログ (28件) を見る
今、グループ化のあたりを読んでいるので、LINQ to Objects ではどう書くのか試してみました。
コード
データはこないだ使った Person クラスのやつ。ちなみに、Person クラスの「public string Name { get; set; }」を「public string Name { get; private set; }」にしたら、エラーでした。「new Person { Name = "Wataru Abe", Gender = Gender.Male, IsActive = true },」形式の初期化を使ってるため、set_Name などを暗黙のうちに使ってしまってたのが原因です。そういう形式のコンストラクタを暗黙に作って呼べばいいのに!β2では匿名型が不変になるので、もしかしてそうなるんでしょうか。
まずはコードと実行結果から。Orcasβ1で試しました。
using System; using System.Collections.Generic; using System.Linq; class Program { static void Main() { var persons = new[] { new Person { Name = "Wataru Abe", Gender = Gender.Male, IsActive = true }, new Person { Name = "Masao Sueda", Gender = Gender.Male, IsActive = true }, new Person { Name = "Sae Nakarai", Gender = Gender.Female, IsActive = true }, new Person { Name = "Shiori Yamamoto", Gender = Gender.Female, IsActive = true }, new Person { Name = "Satoshi Hatakeyama", Gender = Gender.Male, IsActive = false }, }; // (1) var list = from p in persons group p.Name by p.Gender; foreach ( var x in list ) { Console.Write( x.Key + ": " ); foreach ( var y in x ) { Console.Write( y + ", " ); } Console.WriteLine(); } Console.WriteLine(); // (2) var keyNum = persons.GroupBy( p => p.Gender ).Count(); Console.WriteLine( keyNum ); Console.WriteLine(); // (3) // SELECT gender, COUNT(*) num // FROM persons // GROUP BY gender; var num = persons.GroupBy( p => p.Gender ) .Select( g => new { Gender = g.Key, Num = g.Count() } ); foreach ( var x in num ) Console.WriteLine( x ); Console.WriteLine(); // (4) // SELECT gender, COUNT(*) num // FROM persons // GROUP BY gender // HAVING COUNT(*) > 2; var num2 = persons.GroupBy( p => p.Gender ) .Select( g => new { Gender = g.Key, Num = g.Count() } ) .Where( t => 2 < t.Num ); foreach ( var x in num2 ) Console.WriteLine( x ); Console.WriteLine(); Console.ReadKey(); } } public enum Gender { Male, Female } sealed class Person { public string Name { get; set; } public bool IsActive { get; set; } public Gender Gender { get; set; } }
実行結果。
Male: Wataru Abe, Masao Sueda, Satoshi Hatakeyama, Female: Sae Nakarai, Shiori Yamamoto, 2 { Gender = Male, Num = 3 } { Gender = Female, Num = 2 } { Gender = Male, Num = 3 }
クエリが4つあります。順に見ていきます。
グループ化とは?
まずはグループ化とは何か。一つ目の例を見ればわかると思います。ここではSQL 風の「クエリ式」で書いてみました。
var list = from p in persons group p.Name by p.Gender; foreach ( var x in list ) { Console.Write( x.Key + ": " ); foreach ( var y in x ) { Console.Write( y + ", " ); } Console.WriteLine(); }
persons を Gender の値によって2つのグループに分けました。最終結果には名前だけを取り出しました。「group p.Name by p.Gender」は p.Gender の値ごとにグループ化し、p.Name を取り出しています。
Male: Wataru Abe, Masao Sueda, Satoshi Hatakeyama, Female: Sae Nakarai, Shiori Yamamoto,
var listと書きましたが、正確には IEnumerable< IGrouping< Gender, string > > です。IGrouping は Key プロパティと GetEnumerator() メソッドを持ち、グループ一つを表します。グループのシーケンスが結果となっているわけです。
この例では Key = Gender.Male のグループと、Key = Gender.Female のグループがあり、外側の foreach でこれを取り出します。内側の foreach では string の Name を取り出しています。
IEnumerable
var を使わずに書けばこうなります。見ただけでひるんでしまいます。
IEnumerable<IGrouping<Gender, string>> list2 = from p in persons group p.Name by p.Gender; foreach ( IGrouping<Gender, string> x in list2 ) { Console.Write( x.Key + ": " ); foreach ( string y in x ) { Console.Write( y + ", " ); } Console.WriteLine(); } Console.WriteLine();
グループ化と集約演算
グループ化した結果に対して、集約関数を使うのはよくある操作です。集約演算は Count, LongCount, Sum, Min, Max, Average そして何でも屋の Aggregate。
男性は何人いて、女性は何人いるか調べてみます。グループ化と Count を使えばよさそうです。今度はドット表記で書いてみます。
var keyNum = persons.GroupBy( p => p.Gender ).Count();
こうすると結果は 2 とだけ返ってきてうまくいきません。これは、データには Gender が2種類あったことを調べてしまっています。
正しいクエリをSQLで書いてみます。
SELECT gender, COUNT(*) num FROM persons GROUP BY gender;
結果には 2列必要で、Gender と その数 です。これを LINQ にしてみます。
var num = persons.GroupBy( p => p.Gender ) .Select( g => new { Gender = g.Key, Num = g.Count() } );
GroupBy の結果は、グループのシーケンスなので、グループの Key と グループ内のデータ数を数えたもの を最終結果にしました。今度は期待通りの結果が返ってきました。
{ Gender = Male, Num = 3 } { Gender = Female, Num = 2 }
HAVING は LINQ to Objects でどう書く?
次はグループにフィルタ条件をかけてみます。前の例で、数が 2 より大きいグループだけを表示したいとします。つまり、Male だけが表示されるようにしてみます。
SQL でグループにフィルタ条件を指定するには where ではなく having を使います。これは、SQL では where 節が group 節より先に評価されるため、where を評価する時点ではまだグループがないためです。薄っぺらな本なのにこんなところまで丁寧に説明してあります。
SELECT gender, COUNT(*) num FROM persons GROUP BY gender HAVING COUNT(*) > 2;
しかし、LINQ では評価順序をユーザが決めれるので、where で書けてしまったりします。
var num2 = persons.GroupBy( p => p.Gender ) .Select( g => new { Gender = g.Key, Num = g.Count() } ) .Where( t => 2 < t.Num );
{ Gender = Male, Num = 3 }
何それ!って感じですが、SQL と LINQ to Objects の違いとして受け入れるのがよいと思います。
select と where を逆に書くこともできますね。
var num3 = persons.GroupBy( p => p.Gender ) .Where( t => 2 < t.Count() ) .Select( g => new { Gender = g.Key, Num = g.Count() } );
練習: すべての例を、クエリ式とドット表記をここを参考にしながら、もう一方の表記に書き換えてみてください。ちょっと難しいパズルです。
GroupBy( F, G ) が group G by F になることに注意してください。また、group by の後には into を使ってクエリーを継続します。
参考: グループ化や集約関数の説明。