これは、2009年9月1日付の記事の翻訳であり、これを読む際に考慮する必要があります。 -約 あたり
この1、2か月で、Diggエンジニアリングチームは、
Cassandraの制作の調査、テスト、および完成にかなりの時間を費やしました。 とても楽しいプロジェクトでしたが、楽しみが始まる前に、Cassandraデータモデルが何であるかを理解するのにしばらく時間を費やさなければなりませんでした。「WTFは「スーパーコラム」です」 ? '')2回以上発音された。
以前にRDBMSを使用したことがある場合(これはほとんどすべての人に当てはまります)、Cassandraデータモデルを学習するときに、おそらく一部の名前に少しがっかりするでしょう。 それを手に入れるまでに、私とDiggチームは数日議論しました。 数週間前
、開発者のメーリングリストに、混乱を解決するためのまったく新しい命名スキームに関する
バイクシェッドプロセスがあり
ました 。 議論全体を通して、私は「おそらく、通常の例があれば、人々はその名前にそれほど混乱しないだろう」と考えました。 したがって、これはCassandraデータモデルを説明するための私の試みです。 慣れ親しむように設計されていますが、ジャングルには入らないように設計されています。これがいくつかのことを明らかにするのに役立つことを願っています。
個
まず、すべてがどのように連携するかを確認する前に、ビルディングブロックを見てみましょう。
コラム
列は最小限のデータ要素です。 これは、名前、値、およびタイムスタンプを含むトリプルです。 JSON表記で提供される列:
{
name: "emailAddress" , //
value: "arin@example.com" , //
timestamp: 123456789 //
}
以上です。 簡単にするために、タイムスタンプを省略できます。 列を名前/値のペアとして認識します。 また、名前と値の両方がバイナリ(技術的にはバイト[])であり、任意の長さにすることができることに注意してください。
スーパーコラム
SuperColumnは、バイナリ名と値の組み合わせです。これは基本的に、列名をキーとする無制限の数の列を含むテーブルです。 繰り返しますが、これをJSONとして想像してください。
{
name: "homeAddress" ,
// :
value: {
// -
street: {name: "street" , value: "1234 x street" , timestamp: 123456789},
city: {name: "city" , value: "san francisco" , timestamp: 123456789},
zip: {name: "zip" , value: "94107" , timestamp: 123456789},
}
}
カラムとスーパーカラム
列とスーパー列はどちらも名前と値のペアです。 主な違いは、通常の列の値が行であり、スーパー列の値が列の表であることです。 これが主な違いです。 それらの値には、さまざまなタイプのデータが含まれています。 もう1つの小さな違いは、スーパーカラムにタイムスタンプが含まれていないことです。
組み合わせる前に
先に進む前に、2つのことで表記を簡素化します。1)列のタイムスタンプに別れを告げ、2)列とスーパー列の名前を引き出して、キー/値のペアのようにします。 したがって、次から移動します。
{
name: "homeAddress" ,
value: {
street: {name: "street" , value: "1234 x street" , timestamp: 123456789},
city: {name: "city" , value: "san francisco" , timestamp: 123456789},
zip: {name: "zip" , value: "94107" , timestamp: 123456789},
}
}
に
homeAddress: {
street: "1234 x street" ,
city: "san francisco" ,
zip: "94107" ,
}
それらをグループ化する
列とスーパー列の両方をグループ化するために使用される構造があります。この構造はColumnFamilyファミリと呼ばれ、それぞれ通常とスーパーの2つのバリエーションがあります。
列ファミリー
列ファミリは、無制限の行を含む構造です。 うわー、あなたは線を言いましたか? はい-行:)頭に収まりやすくするために、RDBMSのテーブル行と考えてください。
したがって、各行にはクライアント(ユーザー)によって設定されたキーがあり、列のセットが含まれています。 繰り返しますが、セット内のキーは列名であり、値は列自体です。
UserProfile = {
phatduckk: { //
//
username: "phatduckk" ,
email: "phatduckk@example.com" ,
phone: "(900) 976-6666"
}, //
ieure: { //
//
username: "ieure" ,
email: "ieure@example.com" ,
phone: "(888) 555-1212"
age: "66" ,
gender: "undecided"
},
}
覚えておいてください:簡単にするために、列の値のみを表示しますが、実際にはセット内の値は列全体です。
ハッシュテーブル/辞書または連想配列と考えることができます。 そう考え始めたら、あなたは正しい軌道に乗っています。
このレベルでは必須のスキームはないという事実に注意を喚起したいと思います。 行には、含まれる列の定義済みリストがありません。 上記の例では、「ieure」キーのある行には「age」および「gender」という名前の列が含まれていますが、「phatduckk」キーで識別されている行には含まれていません。 これは100%の柔軟性です。1つの行には1989列を含めることができますが、他の行には2つしかありません。一方の行には「foo」という名前の列を含めることができます。 ここにあります-Cassandraの回路不足の見通し。
列ファミリはスーパーでもあります
そのため、カラムファミリのタイプはStandardまたはSuperになります。
上記で検討したのは、標準タイプの例です。 すべての行に通常の(スーパーではない)列のテーブルが含まれているため、これは標準です...スーパー列はありません。
列のファミリーがスーパータイプの場合、逆に各行にはスーパー列のセットが含まれます。 キーがスーパーカラムの名前であり、値がスーパーカラム自体であるセット。 また、わかりやすくするために、スーパーカラムファミリには通常のカラムは含まれていません。 以下に例を示します。
AddressBook = {
phatduckk: { //
// -
//
// -
// -
friend1: {street: "8th street" , zip: "90210" , city: "Beverley Hills" , state: "CA" },
// John' phatduckk'
John: {street: "Howard street" , zip: "94404" , city: "FC" , state: "CA" },
Kim: {street: "X street" , zip: "87876" , city: "Balls" , state: "VA" },
Tod: {street: "Jerry street" , zip: "54556" , city: "Cartoon" , state: "CO" },
Bob: {street: "Q Blvd" , zip: "24252" , city: "Nowhere" , state: "MN" },
...
//
}, //
ieure: { //
joey: {street: "A ave" , zip: "55485" , city: "Hell" , state: "NV" },
William: {street: "Armpit Dr" , zip: "93301" , city: "Bakersfield" , state: "CA" },
},
}
キースペース
キースペースは、すべてのデータを結合するものです。 すべての列ファミリはキースペースにあります。 キースペースはおそらくアプリケーションと一致します。
そのため、キースペースには複数のカラムファミリを含めることができますが、これはそれらが何らかの形で互いに依存することを意味するものではありません。 たとえば、MySQLのテーブルのようにJOINにすることはできません。 また、ColumnFamily_1にキー「phatduckk」の文字列が含まれているからといって、これはColumnFamily_2にもキーが含まれているという意味ではありません。
仕分け
そこで、どのデータコンテナが存在するかを把握しましたが、データモデルのもう1つの重要な要素は、データの並べ替え方法です。 Cassandraでは、SQLのようなクエリを作成することはできません-選択を行うときにデータをどのようにソートするかを指定することはできません(他の違いの中でも)。 データはクラスターに書き込むとすぐにソートされ、常にソートされたままになります。 これは読み取りパフォーマンスの大幅な改善ですが、この利点と引き換えに、アクセスパターンを満たすことができるようにデータモデルを計画する必要があります。
行内の列は常に列名でソートされます。 これは重要なので、繰り返します。列は常に名前でソートされます! 名前の比較方法は、列ファミリのCompareWithパラメーターによって異なります。 デフォルトでは、BytesType、UTF8Type、LexicalUUIDType、TimeUUIDType、AsciiType、およびLongTypeのオプションがあります。 これらの各オプションは、列名を異なるデータ型として扱い、ある程度の柔軟性を提供します。 たとえば、LongTypeを使用すると、列名が64ビット整数として扱われます。 ソートの前後にデータを見て、これを試して明確にしましょう。
// ,
// Cassandra "" .
// , -
{name: 123, value: "hello there" },
{name: 832416, value: "kjjkbcjkcbbd" },
{name: 3, value: "101010101010" },
{name: 976, value: "kjjkbcjkcbbd" }
したがって、LongTypeバリアントを使用しているとすると、これらの列は並べ替え後に次のようになります。
<!-- storage-conf.xml -->
< ColumnFamily CompareWith ="LongType" Name ="CF_NAME_HERE" />
// ,
// ,
{name: 3, value: "101010101010" },
{name: 123, value: "hello there" },
{name: 976, value: "kjjkbcjkcbbd" },
{name: 832416, value: "kjjkbcjkcbbd" }
ご覧のとおり、列名は64ビット整数であるかのように比較されました。 ここで別のCompareWithオプションを使用すると、異なる結果が得られます。 CompareWithをUTF8Typeとして設定すると、列名はUTF8エンコードされた文字列として扱われ、次の順序になります。
<!-- storage-conf.xml -->
< ColumnFamily CompareWith ="UTF8Type" Name ="CF_NAME_HERE" />
// UTF8
{name: 123, value: "hello there" },
{name: 3, value: "101010101010" },
{name: 832416, value: "kjjkbcjkcbbd" },
{name: 976, value: "kjjkbcjkcbbd" }
まったく異なる結果です!
この並べ替えの原則はスーパーカラムに適用されますが、別のディメンションがあります。スーパーカラムの並べ替え方法だけでなく、スーパーカラム内のカラムの並べ替え方法も決定します。 スーパー列内の列の並べ替えは、CompareSubcolumnsWithパラメーターの値によって決まります。 以下に例を示します。
//
//
{ //
name: "workAddress" ,
//
value: {
street: {name: "street" , value: "1234 x street" },
city: {name: "city" , value: "san francisco" },
zip: {name: "zip" , value: "94107" }
}
},
{ //
name: "homeAddress" ,
//
value: {
street: {name: "street" , value: "1234 x street" },
city: {name: "city" , value: "san francisco" },
zip: {name: "zip" , value: "94107" }
}
}
CompareSubcolumnsWithとCompareWithをUTF8Typeに設定すると、次の結果が得られます。
//
{
name: "homeAddress" ,
value: {
city: {name: "city" , value: "san francisco" },
street: {name: "street" , value: "1234 x street" },
zip: {name: "zip" , value: "94107" }
}
},
{
name: "workAddress" ,
value: {
city: {name: "city" , value: "san francisco" },
street: {name: "street" , value: "1234 x street" },
zip: {name: "zip" , value: "94107" }
}
}
最後の例では、CompareSubcolumnsWithとCompareWithは両方ともUTF8Typeに設定されていますが、これは必須ではありません。 CompareSubcolumnsWithとCompareWithの値は、必要に応じて組み合わせることができます。
そして、並べ替えに関して最後に言及したいのは、並べ替えを行う独自のクラスを作成できることです。 ソートメカニズムは独立して接続されています...このクラスがorg.apache.cassandra.db.marshal.ITypeインターフェースを実装するとすぐに、CompareSubcolumnsWithおよび/またはCompareWithに適切なクラス名を設定できます(つまり、ソート用の独自の比較スキームを作成できます) 。
回路例
さて、これでパズルのすべてのピースが揃ったので、それらをまとめて簡単なブログアプリケーションをモデル化しましょう。 次の仕様でアプリケーションをシミュレートします。
- 単一のブログのサポート
- 複数の著者が存在する可能性があります
- レコードには、タイトル、本文、一意のラベル、発行日が含まれます
- エントリは任意の数のタグに関連付けることができます
- 人々はコメントを残すことはできますが、登録することはできません:毎回自分自身に関する情報を入力します(単純化するだけです)
- コメントには、テキスト、コメントが残された時間、コメンテーターの名前が含まれます
- すべての投稿を新しい順に表示する必要があります(最新のものが一番上にあります)
- タグですべての投稿を新しい順に表示できる必要があります
以下の各セクションでは、アプリケーションのキースペースで定義する列ファミリについて説明し、xmlで定義を表示し、1つまたは別の並べ替えオプションを選択した理由を示し、列ファミリのデータをJSONとして表示します。
著者コラムファミリー
著者の列ファミリーのモデリングは非常に基本的です。 ここでは何もクールなことはしません。 各著者に行とキーを割り当てます。これが著者のフルネームになります。 行の各列は、特定の作成者プロファイルパラメーターを表します。
これは、オブジェクトとして文字列を使用する例です...この場合、Authorオブジェクト。 このアプローチでは、各列がオブジェクトのプロパティとして機能します。 とても簡単です。 行を列で表現する方法の定義がないため、この定義を自分で定義できることに注意してください。
キーを使用して列ファミリから行を受け取り、各行のすべての列を選択します(つまり、キー「foo」を持つ行から最初の3列のみを選択しません)。 これは、列がどのようにソートされるかは重要ではないことを意味します。したがって、列名の検証を必要としないため、BytesTypeソートを使用します。
<!--
ColumnFamily: Authors
.
=> (, )
: (email, bio ..)
:
: ( )
Authors : { //
Arin Sarkissian : { //
// ,
numPosts: 11,
twitter: phatduckk,
email: arin@example.com,
bio: "bla bla bla"
},
//
Author 2 {
...
}
}
-->
< ColumnFamily CompareWith ="BytesType" Name ="Authors" />
BlogEntries Columnファミリー
繰り返しますが、列ファミリは単純なキー/値ストアのように動作します。 1行に1レコードを保存します。 行の列は、ヘッダー、本文などの記録パラメーターとして機能します。 (前の例と同じ)。 少し最適化して、タグをコンマ区切りの文字列として単一の列に非正規化します。 出力では、列の値を分割してタグのリストを取得します。
各行のキーは一意のラベル(スラッグ)です。 したがって、1つのレコードを選択するには、このラベルで検索します。
<!--
ColumnFamily: BlogEntries
.
=> ( )
: (, , ..)
:
: ( )
: tags ... .
JSON, , ,
, ,
BlogEntries : { //
i-got-a-new-guitar : { // - (slug)
title: This is a blog entry about my new, awesome guitar,
body: this is a cool entry. etc etc yada yada
author: Arin Sarkissian // Authors
tags: life,guitar,music
pubDate: 1250558004 // unixtime
slug: i-got-a-new-guitar
},
//
another-cool-guitar : {
...
tags: guitar,
slug: another-cool-guitar
},
scream-is-the-best-movie-ever : {
...
tags: movie,horror,
slug: scream-is-the-best-movie-ever
}
}
-->
< ColumnFamily CompareWith ="BytesType" Name ="BlogEntries" />
TaggedPosts列ファミリー
最後に興味深いことがあります。 この一連の列には、新しいレベルが表示されます。 タグと投稿間のリンクの保存を担当します。 リンクを保存するだけでなく、特定のタグですべてのブログエントリをソートされた順序で選択することもできます(ソートについて知っていることはすべて覚えていますか?)。
私が注目したい解決策の特徴は、アプリケーションのロジックが各BlogEntry投稿にタグ「__notag__」を付加することです(私はそれを発明したばかりです)。 このようなタグにより、この列のファミリを使用して、すべてのブログエントリのリストをソートされた形式で保存できます。 これは、「最後の投稿をすべて表示」と「fooタグ付きの最後の投稿をすべて表示」の2つのサンプルで1つのファミリの列のみを使用できるようにする小さなトリックです。
このデータモデルによると、3つのタグを持つレコードは、4行の1列に対応します。 各タグに1つ、サービスタグ「__notag__」に1つ。
レコードのリストを時系列で表示することにしたので、TimeUUID型の列名を作成し、CompareWithパラメーターをTimeUUIDTypeに設定する必要があります。 これにより、時間で列が並べ替えられます。 したがって、「 `foo`タグで最後の10レコードを取得する」などのクエリを使用すると、非常に効率的な操作になります。
ここで、最後の10個のエントリ(メインなど)を表示する場合、次のものが必要になります。
- キー「__notag __」(タグ「すべての投稿」)で最後の10列を取得します
- この列のセットをループします
- ループでは、各列の値がBlogEntries列ファミリの行キーであることを知っています
- このキーを使用して、BlogEntries列ファミリからこのエントリの行を取得します。 そのため、レコードに関するすべてのデータを取得します
- BlogEntries行の列の1つはauthorという名前であり、その値はAuthors列ファミリのキーであり、これを使用して著者プロファイル情報を取得します
- 投稿データと著者データがあります
- 次に、タグを使用して列を分割し、タグのリストを取得します
- これで、この投稿を表示するすべてのものができました(これまでのところ、これは特定の投稿ではなく投稿のリストのページです)
任意のタグを使用してこの手順を実行できます。「すべてのレコード」と「 `foo`タグを持つレコード」に対して機能します。 悪くないようです。
<!--
ColumnFamily: TaggedPosts
, BlogEntries
=>
: TimeUUIDType
: BlogEntries
: "foo"
,
__notag__, " ".
...
, "- + 1" .
TaggedPosts : { //
// "guitar"
guitar : {
timeuuid_1 : i-got-a-new-guitar,
timeuuid_2 : another-cool-guitar,
},
//
__notag__ : {
timeuuid_1b : i-got-a-new-guitar,
// , "guitar"
timeuuid_2b : another-cool-guitar,
// - "movie"
timeuuid_2b : scream-is-the-best-movie-ever,
},
// "movie"
movie: {
timeuuid_1c: scream-is-the-best-movie-ever
}
}
-->
< ColumnFamily CompareWith ="TimeUUIDType" Name ="TaggedPosts" />
列ファミリのコメント
最後に確認する必要があるのは、コメントをモデル化する方法です。 そして最後に、スーパーカラムが必要です。
投稿ごとに1行あります。 キーとして、投稿に使用されたものと同じキーを使用します。 行には、コメントごとにスーパー列があります。 スーパーカラム名は、TimeUUIDTypeのような一意の識別子になります。 そのため、投稿に対するすべてのコメントが時系列でソートされることを保証します。 各スーパーカラムの列は、コメントパラメーター(コメンター名、コメント時間など)になります
したがって、これまでのところ非常に簡単です...超自然的なことは何もありません。
<!--
ColumnFamily: Comments
=> BlogEntries
: TimeUUIDType
:
Comments : {
// scream-is-the-best-movie-ever
scream-is-the-best-movie-ever : {
//
timeuuid_1 : { //
// -
commenter: Joe Blow,
email: joeb@example.com,
comment: you're a dumb douche, the godfather is the best movie ever
commentTime: 1250438004
},
... scream-is-the-best-movie-ever
// -
timeuuid_2 : {
commenter: Some Dude,
email: sd@example.com,
comment: be nice Joe Blow this isnt youtube
commentTime: 1250557004
},
},
// i-got-a-new-guitar
i-got-a-new-guitar : {
timeuuid_1 : {
commenter: Johnny Guitar,
email: guitardude@example.com,
comment: nice axe dawg...
commentTime: 1250438004
},
}
..
//
}
-->
< ColumnFamily CompareWith ="TimeUUIDType" ColumnType ="Super"
CompareSubcolumnsWith ="BytesType" Name ="Comments" />
すごい!
以上です。 私たちの小さなブログアプリケーションはシミュレートされており、すぐに使用できます。 かなり多くのダイジェストを実行すると、storage-conf.xmlに非常に小さなXMLが作成されます。
< Keyspace Name ="BloggyAppy" >
<!-- ... -->
<!-- CF definitions -->
< ColumnFamily CompareWith ="BytesType" Name ="Authors" />
< ColumnFamily CompareWith ="BytesType" Name ="BlogEntries" />
< ColumnFamily CompareWith ="TimeUUIDType" Name ="TaggedPosts" />
< ColumnFamily CompareWith ="TimeUUIDType" Name ="Comments"
CompareSubcolumnsWith ="BytesType" ColumnType ="Super" />
</ Keyspace >
あとは、Cassandraからデータを読み書きする方法を理解するだけです。 これは、
Thrift Interfaceを使用して実行できます。
Cassandra API wikiは 、それをどのように使用するかを説明するきちんとした仕事をしたので、これらすべての詳細については説明しません。 ただし、一般に、cassandra.thriftファイルをコンパイルし、生成されたコードを使用してAPIにアクセスするだけです。
Ruby クライアントまたは
Pythonクライアントを利用することもでき
ます 。
さて、このすべてがあなたにこのスーパーコラムが一体何なのかを感じさせて、あなたがクールなアプリケーションを作り始めることを願っています。
翻訳者から:できるだけオリジナルに近い翻訳を試みました。 建設的な批判を期待しています。
更新 : Honeymanの修正とリンクに感謝
2010年8月24日:NoSQLブログに移動