主にプログラミングに関して。Python, .NET Framework(C#), JavaScript, その他いくらか。
記事にあるサンプルやコードは要検証。使用に際しては責任を負いかねます

スポンサーサイト

                
tags:
上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

Cassandra, CQL: ブログのデータストアに使おうとしてみる

                
CQL v3の日本語訳を作った

 Cassandraは最初のリリースからだいぶ時間が経ち、実用段階に入っているらしい。
http://codezine.jp/article/detail/7548
で、その実用段階に入ったCassandraのことを知りたくても日本語で読めるドキュメントは豊富ではない。とくにCassandraのサーバとしての動作の仕方の部分から実際にインストールしてみようというところまで触れられたものはいくつか見つけられる。しかしそこらのリレーショナルデータベースから機能を減らしてあるデータ操作部分が実際のところどうなのかと触れているものはちょっと少ない。
 オライリーから出ている書籍『Cassandra』はバージョン0.8がメインで、今のバージョンは2.0だ。そのあいだのバージョンアップでCassandra自体がだいぶ変わってきている。大きなところではCQLというクエリ言語でデータ操作を行うようになったことだ。これがオライリーの『Cassandra』出版時にはまだほとんど使えるものではなかったため学べるほどの内容が書かれていない。今Cassandraを使うならCQLを学ぶことは必須なのに、日本語でそれを学べるものはなかなかない。
 サーバとしての振る舞いやデータの一貫性などは詳しいところに任せる。今回ここではWebアプリのデータストアとしてCassandraを使ってみようとすると、クエリがどうなるかを検証してみる。SQLライクなCQLではどの程度のことができるだろうか。


 環境としてCassandraにPythonドライバで接続している。接続方法については別の記事で。


 ブログ記事のデータとしてほしいのは、タイトル、本文、著者、投稿時間、タグあたりだ。記事一件につきこれらを一つの行にまとめて、それをさらにテーブルにまとめて格納する。公式ドキュメントのGettingStartedを参考にとりあえず適当にやってみる。記事にユニークな数値を与え、これをPRIMARY KEYにしてみる。
session.execute(
"""CREATE TABLE articles (id int PRIMARY KEY,
title text,
body text,
author text,
date timestamp,
tags set)
"""
)

 これでデータの保存はできるようになる。でもこれではブログのデータストアとして使うのにちょいとまずいことがある。誰かのブログ画面に飛んで行って表示されるのは、その誰かの記事が投稿時間の新しい順にならんだ画面だ。つまりサーバはデータストアから記事に著者名でフィルタをかけ、投稿時間で並べ替えを行っているということだ。Cassandraではパフォーマンスを出すために、フィルタや並べ替えに制限がある。上にある形式で行が定義されたテーブルでは、著者でフィルタをかけることはできる(でもそのためにセカンダリインデクスを作る必要がある)が、投稿時間での並べ替えができない。やろうとすれば、"ORDER BY with 2ndary indexes is not supported."というメッセージが返される。もう一度行定義を考え直さなければならない。

 ここでインデクスについて。行定義の際にプライマリキーを指定した。このプライマリキーに指定された列に従ってインデクスが自動で作られる。プライマリキーのための列は複数指定できるので、その組み合わせでユニーク性を確保してもOK。
 プライマリキーでない列のインデクスはセカンダリインデクスと呼ばれ、CREATE INDEXステートメントを使って手動で作らなければならない。セカンダリインデクスを扱うには、Cassandraがパフォーマンスを確保するための制限が設けられている。他のデータベースなどのようにこの列でフィルタをかけてあの列で並び替えを行ってと気軽にやることができない。

 著者名と投稿時間をプライマリキーの構成要素にする。これを踏まえて再度テーブルを定義する。
session.execute(
"""CREATE TABLE articles (title text,
body text,
author text,
date timestamp,
tags set,
PRIMARY KEY (author, date))
"""
)

上記の列を有するテーブルを定義することで、著者名でフィルタリングし、投稿日時で並び替えを行うクエリもかけられる。ためしにデータを入れてからクエリをかけてみる。
session.execute(
"""
INSERT INTO articles (title, body, author, date, tags)
VALUES (%s, %s, %s, %s, %s)
""",
("foo1", "", "john", "2011-02-03 00:00", {'DB', 'Cassandra'})
)

session.execute(
"""
INSERT INTO articles (title, body, author, date, tags)
VALUES (%s, %s, %s, %s, %s)
""",
("foo2", "", "john", "2011-02-06 00:00", {'Python'})
)

session.execute(
"""
INSERT INTO articles (title, body, author, date, tags)
VALUES (%s, %s, %s, %s, %s)
""",
("foo3", "", "john", "2011-02-08 00:00", {'Python', 'Flask'})
)

session.execute(
"""
INSERT INTO articles (title, body, author, date, tags)
VALUES (%s, %s, %s, %s, %s)
""",
("foo1", "", "alex", "2011-01-03 00:00", {'DB', 'MongoDB'})
)

session.execute(
"""
INSERT INTO articles (title, body, author, date, tags)
VALUES (%s, %s, %s, %s, %s)
""",
("foo2", "", "alex", "2011-01-06 00:00", {'Ruby'})
)

session.execute(
"""
INSERT INTO articles (title, body, author, date, tags)
VALUES (%s, %s, %s, %s, %s)
""",
("foo3", "", "alex", "2011-01-08 00:00", {'Ruby', 'Rails'})
)

rows = session.execute("SELECT * FROM articles WHERE author='john' ORDER BY date DESC")
for row in rows:
print row

最終行のprint文によって下記が出力された。
>>>
Row(author=u'john', date=datetime.datetime(2011, 2, 7, 15, 0), title=u'foo3', body=u'', tags=sortedset([u'Flask', u'Python']))
Row(author=u'john', date=datetime.datetime(2011, 2, 5, 15, 0), title=u'foo2', body=u'', tags=sortedset([u'Python']))
Row(author=u'john', date=datetime.datetime(2011, 2, 2, 15, 0), title=u'foo1', body=u'', tags=sortedset([u'Cassandra', u'DB']))
この結果をテンプレートエンジンを使うなりしてHTMLに成形すれば、ブログにできそうだ。

 続いて考えるのは記事のページ分割だ。だいたいのブログでは5件ほど記事をトップページに載せて、それ以降は2ページ目、3ページ目へと進んでいく。ここでは例として1ページ当たりの記事数を3件として進める。なのでまずはクエリ結果を3件に絞ることを考える。これはクエリにLIMITを設けるだけでいい。
rows = session.execute("""SELECT * FROM articles
WHERE author='john'
ORDER BY date DESC
LIMIT 3
""")


 上記のクエリは著者がjohnでdateを新しい順に並べた列集合から、最初の3件のみを取り出す。この結果はブログのトップページに使える。では4件目から6件目の行を取り出し、ブログの2ページ目に使いたい…使いたいけどそれがちょっとややこしい。OFFSETがCassandraにはないため、4件目から6件目のデータが欲しくても1件目から3件目のデータも取得しなければならない。なんらかの手段で3件目のプライマリキーとなっている列を得られるならやりようはあるが。たとえばTwitterのタイムラインをCassandraを使って再現したければ、それが10件ごとに非同期通信でツイートを取りにいくとして、10件目のキーを適当なhiddenフォームに入れておいて、リクエスト時にそれを送信すればいい。サーバ側ではそのキーを使ってSELECTステートメントを組めばTwitterは再現できる。Facebookのタイムラインも再現できる。しかしブログはどうしたものか。2ページ目、3ページ目、10ページ目が欲しいとリクエストを受けたときに効率的にそれを処理するにはどうしたものか。

 記事キーを著者ごとにまとめて管理するテーブルをつくって、そこの列にListコレクションを使って著者の記事キーを保存しておいて、そのキーで呼び出すという手がある。鍵が得られるのでLIMITを使う必要がなくなってしまったが。
session.execute("CREATE TABLE (author text PRIMARY KEY, ids list)")
***
keys = [ids[x:x+2] for x in xrange(0, len(ids), 2)]
key0 = keys[0]
row = session.execute("SELECT * FROM articles WHERE author=%s AND date=%s", (key0[0], key0[1],))[0]

これでブログ記事のページングはなんとかできるが、タグによる記事検索に対して返すレスポンスを作るのは難しい。Cassandraは列の値がn以上の行を取ってくるということなどはできるが、オフセットをかけてn件目以降の行を取ってくるということはできない。


 CassandraはMySQLなどに比べればクエリ部分は仕様が小さく、おそらく限定的ながらも簡単に使えるかなと考えていたが、検証してみたところなかなかクセが強い。このクセはそもそもCassandraが列指向であり、列の大規模集計に特化しているところに由来している。NoSQLってのは万能でなくて、使いどころを間違えるとかなり厄介なことを思い知った。



***実際のCassandra使用検討での注意
 技術系のメディアサイトか書籍で、NoSQLでなら最初に使用を検討するぐらいのデータベースだと書かれていた。この宣伝文句には「○○では」という条件付けが抜けている。
 Cassandraは列指向のデータベースである。列に着目した処理を高い頻度で行いたいならCassandraは適役だが、そうでないなら特段Cassandraがいいわけではないだろう。近年もてはやされているビッグデータを扱うなら、Cassandraは向いていると思う。
 Cassandra開発元のFacebookはメッセージ検索の機能でCassandraを使っていた。Twitterではトップツイートなどのリアルタイム分析に使っていた。集計処理が必要な一部のサービスの最適化のために使っていたのであって、汎用的なデータベースとして使っていたわけではないんだろう。
 Cassandraが採用している列指向は、複数の行から任意の列データをまとめて取り出したいときにパフォーマンスを発揮する。たとえばAさん、Bさん(行:A、B)の身長体重(列:身長、体重)をデータベースに保存する。多くのデータベースはA_height, A_weight, B_height, B_weightという順番でデータがならぶ。一方で列指向ではA_height, B_height, A_weight, B_weightとデータが並ぶので、身長データだけ取り出して平均などを出したいとき、同様に体重データだけ取り出して集計処理をしたいときに保存されている場所が連続しているので速くデータを呼んで処理ができる。逆にAさんの列データをすべて欲しいとかやる場合には、Aさんの列データは連続して保存されてはいないので向かない。
 Cassandraについて書かれた記事を見ると、たいていはSQLライクなCQLがあるから簡単に使えるという記事になっている。このブログでもそう書いたし、たしかになにかを作るだけならSQLをやったことがあればわりとすんなりいくと思う。だけどCassandraには列指向という特徴があって、それをわかって使わなければパフォーマンスは期待できないんじゃないだろうか。

参考:
http://www.datastax.com/documentation/cql/3.0/index.html
http://datastax.github.io/python-driver/index.html#
http://cassandra.apache.org/doc/cql3/CQL.html#createIndexStmt



メモ:プライマリキーで最初に指定した列をパーティションキーと呼び、パーティションキーもまた複数の列から構成することができる。
            

コメントの投稿

非公開コメント

プロフィール

hMatoba

Author:hMatoba
Github

最新記事
リンク
作ったものなど
月別アーカイブ
カテゴリ
タグリスト

検索フォーム
Amazon
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。