概要
筆者はエンジニアになって5年間ずっとRDB(リレーショナルデータベース)のMySQL(MariaDB)やPostgreSQLを使ってきました。
この度転職をきっかけにNoSQL(非リレーショナルデータベース)のDynamoDBを使う事になりました。
DB周り以外にも技術領域やプロダクトが大幅に変わりましたが、転職1か月弱でDB周りがかなりしんどい状態だったので、苦しみのおすそ分けと、同じ境遇の同志へのエールの意味も込めてRDB → NoSQLの差分やDynamoDBの入門的な知識をざっくばらんにまとめます。
サクッとDynamoDBについて学ぶなら本記事より下記の記事がおすすめです。
注意
記載内容はあくまで筆者の所感です。
間違えている箇所もあるかもしれません。ご注意ください。
RDBを知ってる人がNoSQLの雰囲気や概念を掴むために書きました。
厳密に言うと違う部分や正しい設計ではない場合も含まれます。
タイトルに「凝り固まった」なんて書き方をしましたが、RDBが悪いという意味ではないです。NoSQLの基礎を知った今、それぞれの良さを実感しています。本記事の中でも繰り返し書いていますが、優劣ではなくシステムに対して合った選択が大切です。何なら受け身でNoSQLに触れてこず自ら凝り固まりに行った筆者に問題があります。
NoSQLって?
リレーショナルデータベース(RDB)以外のデータベース管理システムを示す大まかな分類のことらしいです。
ざっくりいうとRDB以外のものが非リレーショナルデータベースってことみたいです。
筆者はこの説明をみて「RDBがデータの関係を用いたカチッとしたデータ保存、NoSQLがデータ項目名とデータのセットを放り込む柔軟なデータ保存」って感じがしました。
NoSQLって字面だけ見るとMySQLみたいな感じだからDBを制御する製品(RDBMS)の名前なのかな?って思ったけど違うっぽいです。データ構造や概念(キーバリューモデル)の名称です。対比を下記に簡単にまとめます。
データ保存の概念
├── リレーショナルDBという概念・データ構造の設計思想(RDB)
│ └── 製品例
│ ├── MySQL
│ ├── PostgreSQL
│ └── Oracle
│
└── 非リレーショナルDBという概念・データ構造の設計思想(NoSQL)
└── 製品例
├── DynamoDB ← AWSのサービス
├── MongoDB
├── Redis
└── Cassandra
RDBの軸とNoSQLの軸
調べた感じ、RDBとNoSQLでは大切にしている軸が違うみたいです。
RDBの軸
- データの整合性が優先
- 正規化をしてデータを細分化して保存
- システムの仕様変更にも強く、JOIN句を使えば柔軟にデータ取得可能
NoSQLの軸
- アクセスパターンの最適化と速度が優先
- 速度や効率重視のために非正規化してデータを冗長に保存することもある
- 実装の検索パターンに沿ったデータ保存の設計をする
といった感じで重要視しいている観点がまるで異なるらしいです。
優劣はなく、開発するシステムにマッチした選択が必要です。
ちょっと調べると「NoSQLは一つのテーブルにすべてのデータを格納(Single Table Design)が一般的です!」と出てくることがありますが、これは必ずしも絶対そういう作りになるってわけじゃないみたいです。
RDSほど正規化はしないながらもある程度の粒度でテーブルを分けることもあります。なので現場によっては「NoSQLなのにER図がある」なんてこともあります。ちなみにNoSQLで複数のテーブルに分けることを「Multi Table Design」というらしいです。アンチパターンでもなんでもなく、こちらもシステムの要件にあった選択が重要です。
NoSQLの場合とにかく早くデータを取得できることが重要なのでテーブルの作りも、それに合わせて最適化されます。
「整然とデータを並べる」のではなく「いかに早く返すか」が重要らしいです。
DynamoDBって?
DynamoDBはAWSのリソースであり、内部でNoSQLを用いたデータベースサービスです。
単にDynamoDBというとAWSのクライド上で提供されているNoSQLデータベースサービスを指しますが、ローカル開発環境用などで使うDynamoDB LocalというものもAWSの公式から提供されています。
RDBでいうところのRDSがNoSQLでいうところのDynamoDBです。(RDSはMySQLとかPostgreSQLとか選べますが、DynamoDBはDynamoDBしか使えない点は異なります。)わかりやすいように表にします。
| RDS系 | NoSQL系 | |
|---|---|---|
| 概念 | リレーショナルモデル | キーバリューモデル等 |
| 製品 | MySQL PostgreSQL等 | DynamoDB MongoDB等 |
| 代表AWSリソース | RDS | DynamoDB |
DynamoDBにおけるテーブル設計
RDBの場合、正規化を念頭に置きつつテーブルを設計します。RDBの軸の部分でもまとめましたが「正規化をしてデータを細分化して保存」しJOINを用いて様々なデータ取得をするためです。
キーバリュー型のNoSQLを用いたDynamoDBの場合も軸に沿ったテーブル設計を行います。「アクセスパターンの最適化と速度が優先」なのでそのデータをシステムがどうやって取得しにくるのか、そのために効率のいい構造は何かを考えて設計するイメージです。
RDBでユーザー情報を保存する場合下記のようなテーブル郡があるとします。(リレーションは1対1とし、外部キー制約等は正しく設定されているものとします。)
usersテーブル
| カラム名 | 備考 |
|---|---|
| id | |
| name |
emailsテーブル
| カラム名 | 備考 |
|---|---|
| id | |
| user_id | usersテーブルのid |
この様にデータが保存されていればシステム側からはいろいろなアプローチでデータ取得ができます。
- ユーザーのidを用いてユーザーを特定し、ユーザーのすべての情報を取得
- ユーザーのemailを用いてユーザーを特定し、ユーザーのnameを取得
等他にも複数あります。これこそRDBの真骨頂です。
ではDynamoDBの場合はどうなるでしょうか?軸は「アクセスパターンの最適化と速度が優先」です。
そのため「ユーザーのidを用いてユーザーを特定し、ユーザーのすべての情報を取得」や「ユーザーのemailを用いてユーザーを特定し、ユーザーのnameを取得」といったシステムからの要件に対し最速でデータを返せる様に設計します。
「ユーザーのidを用いてユーザーを特定し、ユーザーのすべての情報を取得」を用いて筆者の理解で説明します。
システムからの要件を考えたテーブル設計は下記です。
※「パーティションキー」や「属性」 という言葉が出てきますがあまり気にせず、今はそういう名前のカラムがあると思っておけば大丈夫です。
Userテーブル
| カラム名(属性名) | 備考 |
|---|---|
| パーティションキー | userId |
| 属性 | userName, userEmail |
ちょっとわかりにくいかもしれないので実際のデータを入れた例を書いてみます。
| userId (パーティションキー) |
属性 |
|---|---|
| 1 |
{userName: '太郎'userEmail: 'taro@example.com}
|
| 2 |
{userName: '次郎'userEmail: 'jiro@example.com}
|
| 3 |
{userName: '三郎'userEmail: 'saburo@example.com}
|
| 4 |
{userName: '四郎'userEmail: 'shiro@example.com}
|
これなら「userId(パーティションキー)が3のユーザーのすべての情報を取得」はシンプルです。
パーティションキーって?
パーティションキーとはなんでしょうか?
RDBでのプライマリーキー(主キー)とは皆さんご存知「レコードを一意に特定できるカラム」のことです。
説明の都合上、先程のUserテーブル(キーバリュー型DB)ではパーティションキーはあたかもRDB系のテーブルの主キーのような書き方をしました。これも間違いではありません。
ただパーティションキーが絶対に一意である必要は一切ありません。後から出てくるソートキーとパーティションキーの組み合わせで一意であればよいのです。
この「一意に特定できる属性・属性の組み合わせ」のことをNoSQLでは「プライマリーキー(主キー)」と呼びます。
プライマリーキー = 「一意に特定できる属性 or 属性の組み合わせ」となるので
プライマリーキー = パーティションキー
プライマリーキー = パーティションキー + ソートキー
どちらもOKということです。
RDBではプライマリーキー = それだけで一意に特定できるカラム
だったのでちょっとだけややこしいですね。
ちなみにRDBで「レコード」と呼んでいたものは、NoSQLでは「項目」と呼ぶようです。
少し話しを戻して「パーティションキー」という名前に着目してみます。
パーティションといえばHDD等の記憶領域の区切りや区切られた領域そのものを指したりします。
日常生活でいうと会議室を区切る「パーテーション」なども一般的です。
要は区切りそのものや区切られた場所を指します。
パーテーションキーのパーティションも例外ではなく、区切られた場所を指します。
どうやらDynamoDBはパーティションキーごとにデータを保存する場所が変わるらしいです。
イメージだけで言うなら、先ほどのUserテーブルのデータのuserId = 1の太郎さんのデータはAのデータセンター、userId = 2の二郎さんのデータはBのデータセンターみたいな感じ(実際は1つのアベイラビリティゾーンの中にパーティションがあるのでデータセンター単位で別の場所ってことな無いはず)
どこのパーティションにその項目の情報を保存するかはパーティションキーの値を用いてAWSが勝手に計算してデータを保持しているらしいです。
パーティションキーを指定してデータ取得する際、パーティションキーでパーティションを特定できるのでどこのパーティションにアクセスしたらいいかが明確になるのでなんとなく早そうですよね。
ソートキーって?
先ほどのUserテーブルではシステムからの要件がシンプルだったのでソートキーが不要でした。
ソートキーの説明のために「男性・女性の購入アイテムを取得」するためのテーブルUserBuyを定義してみます。
今回は説明のしやすさを重視したテーブル設計をしています。
このテーブル設計は「ホットパーティション問題」があります。(2パーティションしか使わないため)
実際はbuyIdをパーティションキーとしてデータをパーティションに分散させる設計が良いはずです。
何ならソートキーに設定しているbuyIdが一意な値となってしまっています。
あくまで雰囲気を掴む参考程度にご覧ください。
| カラム名(属性名) | 備考 |
|---|---|
| パーティションキー | gender |
| ソートキー | buyId |
| 属性 | age, items |
これだけだとイメージが湧きにくいのでいくつかデータを入れてみます。
| gender (パーティションキー) |
buyId (ソートキー) |
属性 |
|---|---|---|
| male | 1 |
{age: 20items: ['牛乳', '卵']}
|
| female | 2 |
{age: 43items: ['豚肉', '牛肉']}
|
| female | 3 |
{age: 22items: ['キャベツ', 'ニンジン']}
|
| male | 4 |
{age: 72items: ['マグロ', 'アジ']}
|
今回の場合はパーティションキーとソートキーを組み合わせることで項目を特定する事ができます。先に触れた「プライマリーキーがパーティションキーとソートキーの組み合わせ」のパターンです。(buyIdだけで一意な感じがしますがスルーしてください。経験が浅く、良い説明用データ構造が思いつかず、、、。)
このUserBuyテーブルはパーティションキーがgenderなのでmaleとfemaleという2つのパーティションに分かれて項目が格納されます。
男性のデータは男性の、女性のデータは女性のパーティションにそれぞれ入っているので「男性すべての購入情報」や「女性すべての購入情報」は簡単に取得できそうです。
ここまできてやっとソートキーについての説明です。
ソートキーとは
- その項目のパーティション内での並び順を決める
- パーティションキーと組み合わせて一意になるようにするための物
最初はこの認識だけあれば大丈夫かと思います。
筆者はここまでを理解して思いました。「男性・女性の購入アイテムを取得」以外の要件が出てきて、他のグルーピングや絞り込み項目でデータを絞って取得したいシステム要件が出てきた時どうするの?と
そんなときは「セカンダリインデックス」を使うみたいです。
セカンダリインデックスって?
すでに運用に乗っているシステムの要件の変更や追加開発はよくある話しです。
DynamoDBはシステムの要件に合わせてテーブル設計をします。それじゃテーブル作成語の要件変更や追加開発はできないのでは?と心配になりますよね。
ご安心ください。ちゃんと考えられていました。
「セカンダリインデックス」を使えば解決です。
セカンダリインデックスとは「元テーブルと自動同期される、別の検索の入口(インデックス)」ものです。
セカンダリインデックスには二種類あり「ローカルセカンダリインデックス(LSI)」と「グローバルセカンダリインデックス(GSI)」です。
ローカルセカンダリインデックス(LSI)って?
LSIは同じパーティション内でソートキーを別の属性で設定したインデックスを作成します。
同じパーティションを使って別の並び順を持つイメージです。「同じパーティション」なので"ローカル"セカンダリインデックスです。
例えばUserBuyテーブルでは
- パーティションキー:gender
- ソートキー:buyId
でしたが、LSIを使って
- パーティションキー:gender
- ソートキー:age
の並び順を作る感じです。変更できるのはソートキーのみで、パーティションキーは元テーブルと共通です。
実際のデータを用いると
| gender (パーティションキー) |
buyId (ソートキー) |
属性 |
|---|---|---|
| male | 1 |
{age: 20items: ['牛乳', '卵']}
|
| female | 2 |
{age: 43items: ['豚肉', '牛肉']}
|
| female | 3 |
{age: 22items: ['キャベツ', 'ニンジン']}
|
| male | 4 |
{age: 72items: ['マグロ', 'アジ']}
|
からLSIを使って
| gender (パーティションキー) |
age (ソートキー) |
属性 |
|---|---|---|
| male | 20 |
{buyId: 1items: ['牛乳', '卵']}
|
| female | 43 |
{buyId: 2items: ['豚肉', '牛肉']}
|
| female | 22 |
{buyId: 3items: ['キャベツ', 'ニンジン']}
|
| male | 72 |
{buyId: 4items: ['マグロ', 'アジ']}
|
を並び替えで作るイメージです。
この様に並び替えることで
- 男性の購入情報を年齢順で取得
- 女性の20歳以上の購入情報を取得
等の取得ができるようになります。
ちなみにセカンダリインデックスはこのLSIと後述するGSIの両方でパーティションキーとソートキーの組み合わせが一意になる必要はありません。
ここまで書いておいて本当に申し訳ないのですが、LSIは先に上げたような「テーブル追加後のシステムの要件の変更や追加開発」には使えません。
LSIの追加はテーブル作成時にのみ定義可能です。運用に乗った後に削除することもできません。
更に1テーブルにつき最大LSI数は5個です。
グローバルセカンダリインデックス(GSI)って?
LSIが同一パーティションで別の並び替えだけを持つ状態でした。
GSIは別パーティションに元テーブルの情報が自動同期される別テーブルを追加するイメージです。物理的に別でデータが保存されます。別パーティションに同期テーブルができるような状態になるのでLSIに比べてコストは高くなります。(LSIは同一パーティション内に並び替えデータを持つだけで実データは持たないので少しコストが安い)
例えばUserBuyテーブルでは
- パーティションキー:gender
- ソートキー:buyId
でしたが、GSIを使って
- パーティションキー:age
- ソートキー:gender
の並び順を作る感じです。パーティションキー、ソートキー両方が変更できます。
実際のデータを用いると
| gender (パーティションキー) |
buyId (ソートキー) |
属性 |
|---|---|---|
| male | 1 |
{age: 20items: ['牛乳', '卵']}
|
| female | 2 |
{age: 43items: ['豚肉', '牛肉']}
|
| female | 3 |
{age: 22items: ['キャベツ', 'ニンジン']}
|
| male | 4 |
{age: 72items: ['マグロ', 'アジ']}
|
からGSIを使って
| age (パーティションキー) |
gender (ソートキー) |
属性 |
|---|---|---|
| 20 | male |
{buyId: 1items: ['牛乳', '卵']}
|
| 43 | female |
{buyId: 2items: ['豚肉', '牛肉']}
|
| 22 | female |
{buyId: 3items: ['キャベツ', 'ニンジン']}
|
| 72 | male |
{buyId: 4items: ['マグロ', 'アジ']}
|
という自動同期するテーブルを別パーティションに作るイメージです。
この様に並び替えることで
- 20歳の購入情報を全部取得
- 20歳の男性の購入情報だけ取得
等の取得ができるようになります。(題材がイケてないのでLSIで取得できるようになるデータとほぼ同じ様に見ますが、検索の起点がちょっとだけ異なります。LSIが「性別の指定 → 年齢で絞り込む」、GSIが「年齢の指定 → 性別で絞り込む」)
LSIの方でも説明しましたが、パーティションキーとソートキーの組み合わせが一意になる必要はありません。
GSIはテーブル作成後も追加できますし、削除もできます。1テーブルの最大GSI数は20個です。
LSIの特性(後から追加・削除できず、上限も5個)から実務ではGSIが多様されているらしいです。LSIの利用タイミングはかなり限定的みたいです。