非リレーショナルデータベースが結合演算を扱えない、、、という誤解を解く。

明日の勉強会の準備をしていたら、今回の本の範囲外ですが、ちゃんとまとめておかないと、というところを思い出したので書いておきます。だいぶ前に書いた気がしないでもないけど。

CouchDBではMapReduceを工夫することで関係モデルの結合演算を扱えます。なぜかKVSと一緒くたにされて、JOINができないから、だのなんだのいわれますが、SQLの関係演算の本質(? ... いや知らないけど)を見極めていれば、CouchDBMapReduceであらかたの演算ができます。

という話。だったら非リレーショナルデータベースっていうなよ、という話は抜きです。だってリレーショナルデータベースっていうとめんどくさいことになるんだもの。

まずは普通に外部結合

ブログエントリとブログのコメントのリレーション、だとエンタープライズ脳の人が納得しないようなので、取引伝票と取引明細でいきます。

まずは、取引伝票

{
   "_id": "order1"
   "title": 取引1",
   "description" : "..."
   "type": "Order"
}

で、明細。

{
   "_id" : "....",
   "name" : "りんご",
   "unit_price": 180,
   "num" : 5,
   "type" : "OrderDetail",
   "order_id" : "order1"
}
{
   "_id" : "....",
   "name" : "みかん",
   "unit_price": 140,
   "num" : 7,
   "type" : "OrderDetail",
   "order_id" : "order1"
}
{
   "_id" : "....",
   "name" : "ぶどう",
   "unit_price": 880,
   "num" : 2,
   "type" : "OrderDetail",
   "order_id" : "order1"
}

こんな感じ。いかにもリレーショナル。で、本物の伝票を作るときには、次のようにMapReduce(のMap)します。

function(doc){
  if( doc.type == "Order")           { emit([doc._id, 0], doc); }
  if( doc.type == "OrderDetail") { emit([doc.order_id, 1, doc.name], doc); }
}

こうすると次のような表ができあがるのです。

order_id 種類 商品名 データ
"order1" 0 undefined Orderドキュメント
"order1" 1 ぶどう OrderDetailドキュメント
"order1" 1 みかん OrderDetailドキュメント
"order1" 1 りんご OrderDetailドキュメント
"order2" 0 undefined Orderドキュメント
"order2" 1 XXXX OrderDetailドキュメント
"order2" 1 XXXX OrderDetailドキュメント
"order2" 1 XXXX OrderDetailドキュメント
"order3" 0 undefined Orderドキュメント
... ... ... ...

order_id <-> 商品名 の間はKeyです。

したがって、Order._id = OrderDetail.order_id で LEFT OUTER JOINを実行するには、startkey=["order1",0]&endkey=["order1", 1, "\u9999"] と指定します。
つまり、配列をkeyに試用して、先頭要素からN個目までをJOINキーにすることができる(この場合N=1)んです。CouchDBではemit(k,v)で登録したkの値はB+-Treeに格納されます。ほら、主キーと外部キーにRDBでインデックス張るのと変わらない。

Reduceで計算をする

ところで、Orderの金額の合計を出すには

SUM({OrderDetail}.num * OrderDetail.unit_price)

しなければなりません。これは簡単ですね。reduceに次のものを定義しておけばOK。

function(ks, vs, rr){
   if( rr ){
      return sum(vs);
   }else{
      var s = 0;
      for(var i in vs){
         var doc = vs[i];
         if( doc.type == "OrderDetail") { s += doc.unit_price * doc.num; }
      }
      return s;
   }
}

お得意さんには10%引きで

Orderドキュメントにお得意さんマークがついていたら10%OFFにしましょうか。というのは少し難しいです。これをCouchDBMapReduceに落とし込む、ができればあなたもRelax Goldです。
答えはまた後で。ヒントはReduceが交換律(訂正)結合律を満たす演算になるように演算子と式をモデル化する、a * (b + c + d + ...) != (a * b) + c + d + ... なのでどうしたらいいでしょうか、というのがヒントでしょうか。

でも。

JOINができるってそんなに重要なことだとは思わないんですよ....だから当分は非リレーショナルってことで。