DynamoDB の JOIN: テーブルを結合する方法(と、たいてい結合できない理由)
DynamoDB に JOIN はありません。API には結合演算子がなく、データモデルには
外部キーがなく、そして — ほとんどの人を驚かせる部分ですが — SQL 風のクエリ層
である PartiQL でも結合は追加されません。 PartiQL の SELECT は、
ちょうど 1 つのテーブルだけを読み取ります。
リレーショナルデータベースから来た人にとって、これは最初にぶつかる壁です。本 ガイドでは、なぜその壁が存在するのか、開発者が代わりに使う 4 つの手法、本当に 本物の結合が必要になる唯一のケース、そしてそれをどう実行するかを扱います。
DynamoDB で JOIN はできますか?
いいえ。DynamoDB はテーブルを結合できません — 低レベル API(GetItem / Query / Scan / BatchGetItem)でも、PartiQL でも、組み込みのクエリプランナーでもできません。クエリプランナーが存在しないからです。すべての読み取りは単一テーブルまたはそのインデックスの 1 つにマッピングされます。一致するキーで 2 つのテーブルを組み合わせるのは、DynamoDB がアイテムを返した 後 にアプリ側で行うことであり、DynamoDB の内部で行うことは決してありません。
- DynamoDB には
JOIN演算子がありません。これまで一度もありませんでした。 - PartiQL の
SELECTは 単一テーブル専用 です — 文法は文字どおりSELECT … FROM {{table}}[.{{index}}]であり、2 つのテーブルを指定するとValidationException: Only Select from a Single Table or index supportedが 返ります。 - AWS が推奨する解決策は 結合を必要としないこと です。非正規化するか、 シングルテーブル設計を使って、関連するアイテムを 1 回のリクエストで取得できる 1 つのパーティションにまとめます。
- 本物のクロステーブル / アドホックなケースでは、DynamoDB の 外側で 結合します — アプリ内で、あるいは代わりに結合してくれるツールで。
DynamoDB は結合できる?
いいえ。DynamoDB はテーブルを結合できません — 低レベル API(GetItem /
Query / Scan / BatchGetItem)でも、PartiQL でも、組み込みのクエリプランナーでも
できません。クエリプランナーが存在しないからです。すべての読み取りは単一テーブル
またはそのインデックスの 1 つにマッピングされます。一致するキーで 2 つのテーブルを
組み合わせるのは、DynamoDB がアイテムを返した 後 に行うことであり、その内部で
行うことは決してありません。
これは AWS が埋め忘れたギャップではありません。意図的な設計上の決定であり、回避策に 手を伸ばす前にその理由を理解しておく価値があります。
DynamoDB に結合がない理由
SQL の JOIN は、複数のテーブルを読み取り、クエリ実行時にそれらを組み立てるよう
データベースに要求します。AWS 自身の
リレーショナルデータのモデリングガイド
は、そのコストを明確に述べています。次のようなクエリは
SELECT * FROM Orders
INNER JOIN Order_Items ON Orders.Order_ID = Order_Items.Order_ID
INNER JOIN Products ON Products.Product_ID = Order_Items.Product_ID
ORDER BY Quantity_on_Hand DESC柔軟ですが、「各テーブルのデータをステージしてから組み立てなければならないため、 クエリ内の各結合はクエリの実行時複雑度を増大させる」とあります。その作業は無制限 です — そのコストはクエリではなくデータに依存します — これはまさに DynamoDB が 持つことを拒否する性質です。
そこで AWS はその制約を設計に組み込みました。DynamoDB は、彼らの言葉を借りれば、
「JOIN を排除し(そしてデータの非正規化を推奨し)、1 回のアイテムへのリクエストで
アプリケーションのクエリに完全に応答するようデータベースアーキテクチャを最適化する
ことで、[CPU とネットワークの両方の]制約を最小化するよう構築されている」のです。
これらは、どんな規模でも 1 桁ミリ秒のレイテンシをもたらす性質です。DynamoDB の
読み取りの実行時コストは、テーブルサイズに関わらず一定です。設計上、計画すべき
結合エンジンも外部キーの概念も存在しません。
「でも PartiQL は SQL でしょう、当然結合できるのでは?」
いいえ。PartiQL は DynamoDB に対して SELECT / INSERT / UPDATE / DELETE の
構文を提供しますが、SQL 互換 であって SQL ではありません。
公式の SELECT 文法
は次のとおりです。
SELECT {{expression}} [, ...]
FROM {{table}}[.{{index}}]
[ WHERE {{condition}} ]
[ ORDER BY {{key}} [DESC|ASC], ... ]FROM は 1 つの テーブル(任意でそのインデックスの 1 つ)を取ります。2 つ目の
FROM テーブルも、JOIN も、サブクエリも、CTE もありません。PartiQL を 2 つの
テーブルに向けると DynamoDB は拒否します
(AWS re:Post で報告)。
ValidationException: Only Select from a Single Table or index supportedPartiQL が SQL のように見えてなぜ SQL のように振る舞えないのか、その完全な理由は PartiQL vs SQL を参照してください。
開発者が実際に使う 4 つの回避策
1. 非正規化する(データをコピーして入れる)
本来結合するフィールドを、アイテムに直接保存します。Order は、後で解決する
customerId の代わりに、customerName と shippingAddress のスナップショットを
持ちます。1 回の読み取り、結合なし。
そのコストは書き込み時のファンアウトです。ソースが変わったらすべてのコピーを更新します (通常は DynamoDB Streams のハンドラ経由)。読み取りの複雑さを書き込みの複雑さと 引き換えにしているわけです — 読み取りの多いアプリでは、たいてい良い取引です。
2. シングルテーブル設計(パーティション内で事前結合する)
関連するエンティティを、共有のパーティションキーの下で 1 つのテーブル にまとめ、
アイテムコレクションが結合済みの結果 そのもの になるようにします。顧客とその
すべての注文が PK = "CUSTOMER#42" を共有し、1 回の Query で顧客アイテムと
すべての注文アイテムが返ります — 「結合」は書き込み時にすでに行われていたのです。
Query PK = "CUSTOMER#42"
→ CUSTOMER#42 / PROFILE (顧客)
→ CUSTOMER#42 / ORDER#1001 (注文)
→ CUSTOMER#42 / ORDER#1002 (注文)
これは一対多の関係に対する DynamoDB の定番の答えです。詳しい手順は シングルテーブル設計を参照してください。
3. アプリケーション側の結合(2 回読んでコードで繋ぐ)
テーブル A から読み取り、得られたキーを取り、テーブル B から読み取り、2 つの結果 セットをアプリケーション内でマージします。リレーショナルな結合ロジックそのものを、 データベースの代わりにあなたのコードで実行するだけです。
// 「各注文を顧客名つきで取得」— 手動の結合。
const {Items: orders} = await ddb.query({TableName: 'Orders' /* … */});
const customers = await Promise.all(
orders.map((o) => ddb.getItem({TableName: 'Customers', Key: {id: o.customerId}}))
);
const joined = orders.map((o, i) => ({
...o,
customerName: customers[i].Item?.name
}));ファンアウトが小さければ問題ありません。注文が多いと N+1 問題 になります —
注文を一覧する 1 回の読み取り、その後に注文ごとに 1 回の読み取り — これは遅く、
読み取りキャパシティを消費します。BatchGetItem(次項)は、その第 2 波を 1 回の
往復にまとめます。
4. BatchGetItem(1 回の往復、複数のテーブル)
BatchGetItem
は、API が「2 つのテーブルに同時に触れる」ことに最も近いものです。1 回のリクエストで
「1 つ以上のテーブルから 1 つ以上のアイテムの属性」を返し、1 回の呼び出しあたり
最大 100 アイテムまたは 16 MB(先に達したほう)まで対応します。アプリ側結合の
往復を削減しますが、結合では ありません。「リクエストするアイテムをプライマリキーで
識別する」のであり、ON 条件もリレーショナルなマッチングもありません。事前にキーを
知っておき、レスポンスを自分で繋ぎ合わせる必要があります。
本物の JOIN が避けられないとき
4 つの回避策は本番の読み取りパスをよくカバーします。それらが力尽きるのは、 アドホックで探索的、分析的な クエリ — モデリングしなかったクエリ — です。
OrdersテーブルとCustomersテーブルにまたがる 「先月 500 ドルを超える注文を した EU の顧客は誰か?」。- 2 つのエンティティ型を結合する一度きりのデータ品質チェック。
- レポーティングと集計(
GROUP BY、SUM、COUNT)— DynamoDB にはこれらの演算子が まったくありません。
これらはまさにパーティションに事前焼き込みできないクエリです。定義上、自分がそれを
尋ねると知らなかったからです。リレーショナルな本能 — JOIN を書く — はここでは
正しいのです。DynamoDB はそれをネイティブには提供できず、PartiQL も同様です。
通常の重量級の答えは、 S3 にエクスポートして Athena でクエリするか、 データウェアハウスに流し込むことです。大規模な真の分析には正しいですが、ライブテーブルに 対して 今 答えが欲しい質問には、かなりの配管作業です。
DynoTable の SQL Workbench で本物の JOIN を実行する
DynoTable は、SQL Workbench が実際の SQL — JOIN、GROUP BY、
集計関数を含む — を DynamoDB テーブルに対して実行するデスクトップ DynamoDB
クライアントです。通常の DynamoDB API を通してアイテムを読み取り、クエリの
リレーショナルな部分をクライアント内で実行します。つまり、次のように書けます。
SELECT c.name, SUM(o.total) AS spend
FROM Customers c
JOIN Orders o ON o.customerId = c.id
WHERE c.region = 'EU'
GROUP BY c.name
HAVING SUM(o.total) > 500— そして、関係が定義されていないテーブルと JOIN キーワードを持たないクエリ
エンジンに対して、結果セットが返ります。
正直な注意点 — 「DynamoDB のアクセスパターンのルール内で」: Workbench は依然として
DynamoDB を通して読み取るため、無制限の結合は無制限の読み取りです。最速のクエリは、
WHERE 句(または結合の ON 属性)が少なくとも片側でパーティションキーや
GSI にヒットするものです。そうすると DynamoDB は、結合の実行前に
フルテーブルスキャンではなく Query を実行します。Workbench は
本ガイドの制約を撤廃するわけではありません — ただ、繋ぎ合わせを手で書く代わりに
SQL の質問を尋ねられる ようにし、内部で何をしているかを教えてくれるだけです。
これが唯一、本当に「はい、結合できます」と言える答えです。PartiQL も AWS 自身の
NoSQL Workbench
— その操作ビルダーは単一テーブルのデータプレーン操作(Query / Scan / GetItem)に
限定されています — も、他のほとんどの GUI クライアントと同様、単一テーブルの壁で
止まります。DynoTable が
DynamoDB GUI としてどう比較されるかをご覧ください。
FAQ
PartiQL は JOIN をサポートしている?
いいえ。PartiQL の SELECT は単一テーブル(またはそのインデックスの 1 つ)を読み取り
ます。複数テーブルのクエリは ValidationException: Only Select from a Single Table or index supported を返します。API の他の部分と同じ壁です。
1 つのクエリで 2 つの DynamoDB テーブルを結合できる?
ネイティブにはできません。DynamoDB API には、2 つのテーブルを読み取りキーでマッチング
する文がありません。BatchGetItem は 1 回のリクエストで複数のテーブルからアイテムを
読み取れますが、ON 条件はありません — プライマリキーで指定したアイテムを返し、
マッチングはあなたに委ねます。本物の JOIN … ON … は DynamoDB の外側でのみ起こります。
あなたのアプリ内か、DynoTable の SQL Workbench で。
テーブルをその GSI に結合できる?
いいえ — グローバルセカンダリインデックスは、結合する別個の
テーブルではありません。同じアイテムの代替キービューです。ある SELECT の中で、
テーブル または インデックスのどちらかを Query するのであって、両方を結合する
ことはありません。GSI は別のキーでアイテムに 到達する ことを可能にし、それが
そもそも結合の必要性をなくすことがよくあります。
2 つの AWS アカウントにまたがって(または異なるアカウントの 2 つのテーブルを)結合できる?
ネイティブにはできず、BatchGetItem でもできません — 1 回のリクエストは認証情報を
またげず、クロスアカウントの結合プリミティブもありません。各テーブルをそれぞれの
アカウントの認証情報で読み取り、結果をアプリケーションや DynoTable の Workbench の
ようなツールで結合することになります。
非正規化は本当に結合より良い? DynamoDB のターゲットワークロード — 予測可能で大量の読み取り — にとっては、はい。 コストを書き込み時に移し(そしてある程度のデータ重複を受け入れ)、代わりに平坦に スケールする単一リクエストの読み取りを得ます。 シングルテーブル設計ガイドがトレードオフを扱います。
これらの読み取り用のキーと条件を手で組み立てるのは面倒です —
式ビルダーがあなたの代わりに
KeyConditionExpression / FilterExpression の構文を生成し、回避策では足りないときは
DynoTable が本物の SQL を実行します。