DynamoDB の JOIN: テーブルを結合する方法
DynamoDB に JOIN はありません。API に結合演算子はなく、データモデルに外部キーはなく —
そしてほとんどの人を驚かせる部分ですが — SQL 風のクエリ層である も、
それを追加しません。 PartiQL の SELECT はちょうど1つのテーブルを読みます。
リレーショナルデータベースから来たなら、これが最初にぶつかる壁です。このガイドでは、 なぜその壁があるのか、開発者が代わりに行う4つのこと、本物の結合が本当に必要になる1つの ケース — そしてその実行方法を扱います。
DynamoDB は結合できますか?
いいえ。DynamoDB はテーブルを結合できません — 低レベル API(GetItem / Query /
Scan / BatchGetItem)を通しても、 を通しても、組み込みの
クエリプランナを通してもです。そもそもプランナが存在しないからです。すべての読み取りは
1つのテーブルか、そのインデックスの1つに対応します。一致するキーで2つのテーブルを結合
することは、DynamoDB がアイテムを返した 後 にアプリで行うことであって、決して内部では
行いません。
- DynamoDB に
JOIN演算子はありません。一度もありませんでした。 - PartiQL の
SELECTは 単一テーブル専用 です — 文法は文字どおりSELECT … FROM {{table}}[.{{index}}]であり、2つのテーブルを指すとValidationException: Only select from a single table or index is supported.を返します。 - AWS が推奨する対処は、結合を必要としない ことです。 するか、 シングルテーブル設計 を使って、関連するアイテムを1回の リクエストで取得できる1つのパーティションに置きます。
- 本物のクロステーブルやアドホックなケースでは、DynamoDB の 外 で結合します — アプリで、または代わりにやってくれるツールで。
なぜ 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
INNER JOIN Inventories ON Products.Product_ID = Inventories.Product_ID
ORDER BY Quantity_on_Hand DESC柔軟ですが、「クエリ内の各結合は、各テーブルのデータをステージングしてから組み立てなければ ならないため、クエリの実行時複雑性を増やす」のです。その作業は無界です — コストはクエリ ではなくデータに依存します — が、これはまさに DynamoDB が持つことを拒否する性質です。
そこで AWS はその制約を設計に組み込みました。DynamoDB は、彼らの言葉を借りれば、
「JOIN を排除(そしてデータの非正規化を奨励)し、アプリケーションのクエリを
アイテムへの単一リクエストで完全に答えられるようデータベースアーキテクチャを最適化する
ことで、[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 もありません。それぞれがどう失敗するかを
正確に見るため、3つすべてを DynamoDB に対して実行しました。
明示的な JOIN:
SELECT o.pk FROM "Orders" o JOIN "Customers" c ON o.customerId = c.pkValidationException: Only select from a single table or index is supported.FROM に2つのテーブル — 拒否は同じです。つまりエンジンが拒んでいるのは JOIN
キーワードではなく、2つ目のテーブルです。
SELECT * FROM "Orders", "Customers"ValidationException: Only select from a single table or index is supported.サブクエリは 異なる形 で失敗します。デバッグするなら知っておく価値があります。
PartiQL は複数テーブルのチェックにそもそも到達しません — その前に IN のオペランドを
拒否するので、テーブルに一切触れないメッセージが返ります。
SELECT * FROM "Orders" WHERE customerId IN (SELECT pk FROM "Customers")ValidationException: IN operator must have a left hand argument of type Variable
Reference and right hand argument of type Seq with at least one member実務上の帰結: 結合を得られる書き方は存在しません。最初の2つは2つ目のテーブルで死に、 3つ目はさらに手前の、オペランドで死にます。
PartiQL が SQL に見えるのに SQL のように振る舞えない理由の完全な説明は、 PartiQL と SQL を参照してください。
開発者が実際に使う4つの回避策
1. 非正規化する(データをコピーして入れる)
結合するはずだったフィールドを、アイテムに直接格納します。Order は、後で解決する
customerId の代わりに、customerName と shippingAddress のスナップショットを持ちます。
1回の読み取り、結合なしです。
コストは書き込み時のファンアウトです。元データが変わると、すべてのコピーを更新します (通常は のハンドラ経由で)。読み取りの複雑さを書き込みの 複雑さと引き換えているのです — 読み取りが多いアプリでは、たいてい良いトレードです。
2. シングルテーブル設計(パーティション内で事前結合する)
関連するエンティティを、共有したパーティションキーの下の 1つのテーブル に置き、
そのもの が結合結果になるようにします。ある顧客と
その全注文が PK = "CUSTOMER#42" を共有し、1回の Query が顧客アイテムとすべての注文
アイテムを返します — 「結合」は書き込み時にすでに起きています。
Query PK = "CUSTOMER#42"
→ CUSTOMER#42 / PROFILE (the customer)
→ CUSTOMER#42 / ORDER#1001 (an order)
→ CUSTOMER#42 / ORDER#1002 (an order)
これが一対多の関係に対する DynamoDB の定番の答えです。完全なウォークスルーは シングルテーブル設計 にあります。
3. アプリケーション側の結合(2回読み、コードで縫い合わせる)
テーブル A から読み、返ってきたキーを取り、テーブル B から読み、2つの結果セットを アプリケーションでマージします。リレーショナルの結合ロジックそのものです — ただ、 データベースではなくコードで動くだけです。
// "Get each order with its customer name" — the manual join.
const {Items: orders} = await ddb.query({TableName: 'Orders' /* … */});
const customers = await Promise.all(
orders.map((o) => ddb.get({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つの回避策は、実運用の読み取りパスをうまくカバーします。それらが破綻するのは、アドホックで、 探索的で、分析的な クエリ — モデル化しなかったもの — です。
- 「先月 500 ドルを超える注文をした EU の顧客は誰か?」 を
OrdersテーブルとCustomersテーブルにまたがって。 - 2つのエンティティ型を結合する、1回限りのデータ品質チェック。
- レポートと集計(
GROUP BY、SUM、COUNT)— DynamoDB にはそもそも演算子がありません。
これらはまさに、パーティションに事前焼き付けできないクエリです。定義上、あなたはそれを
尋ねると知らなかったからです。リレーショナルの直感 — JOIN を書く — が、ここでは正しい
のです。DynamoDB はそれをネイティブにさばけず、PartiQL も同様です。
いつもの重量級の答えは、
S3 にエクスポートして Athena でクエリする
(または Athena のフェデレーテッドクエリコネクタで、ライブテーブルを JOIN する)か、
ウェアハウスに流し込むことです。真の大規模分析にはそれが正解ですが、今すぐ、ライブ
テーブルに対して答えが欲しい問いには、かなりの配管仕事です。
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 の問いを尋ねられる ようにし、内部で何をしているかを教えてくれるだけです。
GUI クライアントの中で、これが唯一の本当に正しい「はい、結合できます」です。PartiQL と AWS 自身の NoSQL Workbench — そのオペレーションビルダーは単一テーブルの操作と PartiQL 文(JOIN なし、複数テーブル SELECT なし)を実行します — は、どちらも単一テーブルの壁で止まり、他のほとんどの GUI クライアントも同じです。DynoTable が DynamoDB GUI としてどう比較されるかを見てください。
よくある質問
PartiQL は JOIN をサポートしていますか?
いいえ。PartiQL の SELECT は単一のテーブル(またはそのインデックスの1つ)を読みます。
複数テーブルのクエリは
ValidationException: Only select from a single table or index is supported. を返します。
API の他の部分と同じ壁です。
2つの DynamoDB テーブルを1回のクエリで結合できますか?
ネイティブにはできません。DynamoDB API には、2つのテーブルを読んでキーで一致させる文が
ありません。BatchGetItem は1回のリクエストで複数のテーブルからアイテムを読めますが、
ON 条件がありません — プライマリキーで指定したアイテムを返し、一致はあなたに任せます。
本物の JOIN … ON … は DynamoDB の外でしか起きません。アプリで、または DynoTable の
SQL Workbench で。
テーブルとその GSI を結合できますか?
いいえ — グローバルセカンダリインデックス は、結合する別のテーブル
ではありません。それは同じアイテムに対する代替のキービューです。ある SELECT の中で、
テーブル か インデックスの どちらか に Query するのであって、両方を結合するのでは
ありません。GSI は異なるキーでアイテムに 到達 できるようにするので、そもそも結合の必要を
なくすことがよくあります。
2つの AWS アカウント(または別々のアカウントの2つのテーブル)にまたがって結合できますか?
ネイティブにはできません — クロスアカウントの結合プリミティブはありません。BatchGetItem
は、そのテーブルのリソースベースポリシーが呼び出し元にアクセスを許可していれば(2024年3月
以降)、別アカウントのテーブルに到達できますが、依然として ON 条件がないので、結合では
なく複数テーブルの読み取りです。各側を読んで、結果をアプリケーションで、または DynoTable の
Workbench のようなツールで結合することになります。
非正規化は本当に結合より優れているのですか? DynamoDB の対象ワークロード — 予測可能で大量の読み取り — では、はい。コストを書き込み時に 移し(そして多少のデータ重複を受け入れ)、その代わりにフラットにスケールする単一リクエストの 読み取りを得ます。シングルテーブル設計 のガイドが トレードオフを扱います。
これらの読み取りのためにキーと条件を手で組み立てるのは面倒です —
Expression Builder が
KeyConditionExpression / FilterExpression の構文を代わりに生成し、回避策では
足りないときは DynoTable が本物の SQL を実行します。
PartiQL の拒否メッセージは 2026-08-11 に DynamoDB Local(us-east-1、
@aws-sdk/client-dynamodb v3.1096.0)に対して再現し、ValidationException.message
から逐語的に引用したものです。