LINQ to Objects で グループ化

SQL を勉強しようと↓の本を読んでいます。SQL ってうまいの?ってレベルの私にもわかりやすく、薄いのに丁寧でよい本です。順に読めば SQL がしっかり理解できます。個々の DB について詳しい本ではなく、SQL 特にクエリーについての「言語の入門書」といった感じ。MySQL で解説していますが、一部 MySQL にないものでも記述があったり、Oracle ではこう、SQL Server ではこうと記述も(ちょっと)あります。各 DB の詳しい本とあわせて用意するとよいかと思います。唯一の欠点は表紙がきもいこと!常にひっくり返して置くこと!

初めてのSQL

初めてのSQL

今、グループ化のあたりを読んでいるので、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 を取り出しています。

IEnumerableLINQ では主に「シーケンス」と呼ばれますが、これを「データ構造」と考えるとすんなり理解できます。「遅延リスト」と考えてください。そう思えないうちは、var を使って LINQ でいろいろ遊んでいればそのうちなじむと思います。

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 }

何それ!って感じですが、SQLLINQ 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 を使ってクエリーを継続します。

参考: グループ化や集約関数の説明