2015年7月6日月曜日

C#でLEFT OUTER JOIN (左外部結合)

LINQを使って、「LEFT OUTER JOIN」をしたかった。普通のJoinだとINNER JOINなので、少し工夫する必要がありそう。ググってみると、以下のようなコードが一般的なようだ。

var query = from person in people
  join pet in pets on person equals pet.Owner into gj
  from subpet in gj.DefaultIfEmpty()
  select new
  {
    person.FirstName, 
    PetName = (subpet == null ? String.Empty : subpet.Name)
  };

ちなみにこれは、MSDNの「方法 : 左外部結合を実行する (C# プログラミング ガイド)」から。

大体どこを見てもこんな感じで、ほとんど左外部結合をしたいときの慣用句(イディオム)みたいなもののよう。とはいえ、このコードを見て「あ、左外部結合させたいんだな」と思える人がどれくらいいるか。要は、コードから意図が解りづらいので、あまり好みじゃないなぁ。と。

もう少し、コードを見て左外部結合であることが解るように、拡張メソッドを作ってみた。こんなの。

public static class EnumerableEx
{
  public static IEnumerable<TResult> OuterJoin<TOuter, TInner, TKey, TResult>
    (this IEnumerable<TOuter> outer,
    IEnumerable<TInner> inner,
    Func<TOuter, TKey> outerKeySelector,
    Func<TInner, TKey> innerKeySelector,
    Func<TOuter, TInner, TResult> resultSelector,
    TInner innerDefaultValue)
  {
    return outer
      .GroupJoin(
        inner,
        outerKeySelector,
        innerKeySelector,
        (o, i) => new { Out = o, Ins = i.DefaultIfEmpty(innerDefaultValue) })
      .SelectMany(g => g.Ins, (g, i) => resultSelector(g.Out, i));
  }
}

Enumerable.Joinメソッドと比べると、最後のパラメータが余計についてます。これはOuterの要素に対して合致するInnerの要素がなかった時の代替要素を指定するもの。Null Objectだと思っておけばよいと思います。

あと、名前は長いのを嫌い、「OuterJoin」としています。タイプパラメータからも、左が外なのは明らかなので、特に問題はないと思っています。

ちなみに、これをライブラリ的に用意するなら、GroupJoinするときにEqautityComparerを指定するパターン、それと、DefaultIfEmptyでTInnerのデフォルト値を自動的に使うパターン、その組み合わせで4つのオーバーロードを、用意しておくのがよいと思います。

さて、テストします。

static void Main(string[] args)
{
  var products = new[] 
  {
    new { ProductId = 1, Name = "えんぴつ" },
    new { ProductId = 2, Name = "けしごむ" },
    new { ProductId = 3, Name = "コンパス" },
    new { ProductId = 4, Name = "クレヨン" },
  };

  var sales = new[]
  {
    new { SaleId = 1, ProductId = 1, Buyer = "○○商会", Quantity = 2 },
    new { SaleId = 2, ProductId = 1, Buyer = "××文具店", Quantity = 4 },
    new { SaleId = 3, ProductId = 2, Buyer = "△屋", Quantity = 3 },
    new { SaleId = 4, ProductId = 4, Buyer = "○○商会", Quantity = 1 },
    new { SaleId = 5, ProductId = 5, Buyer = "○○商会", Quantity = 1 },
  };

  var nosaled
    = new { SaleId = 0, ProductId = 0, Buyer = "(no sales)", Quantity = 0 };

  var salesInfo = products.OuterJoin(
    sales, 
    p => p.ProductId, 
    s => s.ProductId, 
    (p, s) => new { p.Name, s.Buyer, s.Quantity }, 
    nosaled);

  foreach (var i in salesInfo)
  {
    Console.WriteLine("{0}, {1}, {2}", i.Name, i.Buyer, i.Quantity);
  }
}

結果はこう。
えんぴつ, ○○商会, 2
えんぴつ, ××文具店, 4
けしごむ, △屋, 3
コンパス, (no sales), 0
クレヨン, ○○商会, 1

…右外部結合?RIGHT OUTER JOINか。僕自身使ったことないですが、必要なら右左を入れ替えてあげればいいはず。上のコードの20行目から28行目を、こんな感じに置き換えてみる。
var noproduct = new { ProductId = 0, Name = "(no item)" };

var salesInfo = sales.OuterJoin(
  products,
  s => s.ProductId,
  p => p.ProductId,
  (s, p) => new { p.Name, s.Buyer, s.Quantity },
  noproduct);

結果はこう。
えんぴつ, ○○商会, 2
えんぴつ, ××文具店, 4
けしごむ, △屋, 3
クレヨン, ○○商会, 1
(no item), ○○商会, 1

うん。いんじゃないかな?
…完全外部結合?…FULL OUTER JOIN…。必要?それ。

使い道が解らなくてモチベーションゼロだけど、要するに左外部結合+右のみに存在するレコードを、編集してUnionすればいいんじゃないかな?多分。

2015年7月3日金曜日

GroupByとToLookup

ここしばらく、C#関連記事ではGroupByを使ったものが続いています。

ところで、.net Frameworkには、GroupByと非常によく似た「ToLookup」というEnumerable拡張メソッドがあります。大体名前のイメージで、GroupByは遅延実行で、ToLookupは即時実行だと思い、基本的に使い分けていました。

もう少し調べてみると、ToLookupはその名の通りルックアップテーブルを作るメソッドで、戻されるLookupクラスは、キーからのルックアップを機能として有している。と。

ところがふと、過去にGroupByを使っていて、ToLookupを使ったほうがよいケースがあったりするんじゃないかと不安になってきまして。たとえば何も考えずにGroupByを使っていた箇所で、ToLookupを使ったほうが実はよかった。とかいうケースがあったらいやだなぁ。と。

なので、とりあえず処理時間を計ってみました。使ったのはこんなコード。

static void Main(string[] args)
{
  var rnd = new Random(DateTime.Now.Millisecond);
  var items = Enumerable.Range(0, 10000000).Select(_ => new
  {
    Key1 = rnd.Next(10),
    Key2 = rnd.Next(100),
    Value = rnd.Next(10000)
  });

  var sw = new Stopwatch();
  sw.Start();

  var dic = items.GroupBy(i => new { i.Key1, i.Key2 })
    .ToDictionary(g => g.Key, g => g.First().Value);

  sw.Stop();

  Console.WriteLine("Elapsed:{0}msec", sw.ElapsedMilliseconds);
}

先日の『ToDictionaryで重複のない辞書を作る』と同様に、コレクションからキー重複を排除した辞書を作る処理にGroupByを使ってみて、単純にGroupBy→ToLookupに置き換えたときにどれくらい処理速度の差があるか?ほんの少しGroupByが早いんじゃないかな?と予想。

結果は、
GroupBy … 6155msec。
ToLookup … 6081msec。
となり、予想に反してほんの誤差レベルとはいえGroupByのほうが遅い結果に。なんでだろう?と思ってソースコードを調べてみた。

GroupByは大体こんな感じ。

public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(
  this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
  return new GroupedEnumerable<TSource, TKey, TSource>(
    source, keySelector, IdentityFunction<TSource>.Instance, null);
}

internal class GroupedEnumerable<TSource, TKey, TElement>
  : IEnumerable<IGrouping<TKey, TElement>>
{
  public IEnumerator<IGrouping<TKey, TElement>> GetEnumerator()
  {
    return Lookup<TKey, TElement>.Create<TSource>(
      source, keySelector, elementSelector, comparer).GetEnumerator();
  }
}

それに対して、ToLookupはこんな感じ。

public static ILookup<TKey, TSource> ToLookup<TSource, TKey>(
  this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
  return Lookup<TKey, TSource>.Create(
    source, keySelector, IdentityFunction<TSource>.Instance, null);
}

あー。これだとほぼ即時実行か遅延実行かの違いしかないですねぇ。これならToLookupが若干速いのも納得できる。

というか、そうするとGroupByの存在意義が解らなくなったので、ちょっと調べてみたら、Stackoverflowに「Why are ToLookup and GroupBy different?」こんな質問があって、その回答が以下。
What happens when you call ToLookup on an object representing a remote database table with a billion rows in it?
なるほど。例えばデータソースがObjectではなくDatabaseだったりしたときに、即時実行しないで済むならそうしたい。LINQのメソッドはデータソースを問わないわけだから、基本的にはGroupByを使い、明示的に即時実行にしたい場合や、実行後のルックアップが必要なら、ToLookupを使うべし。と、解釈して納得できた。

気にしなければならない速度差では全くないですしね。