中級読了 4 分

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], ... ]

FROM1つの テーブル(オプションでそのインデックスの1つ)を取ります。2つ目の FROM テーブルも、JOIN も、サブクエリも、CTE もありません。それぞれがどう失敗するかを 正確に見るため、3つすべてを DynamoDB に対して実行しました。

明示的な JOIN:

SELECT o.pk FROM "Orders" o JOIN "Customers" c ON o.customerId = c.pk
ValidationException: 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 の代わりに、customerNameshippingAddress のスナップショットを持ちます。 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 BYSUMCOUNT)— DynamoDB にはそもそも演算子がありません。

これらはまさに、パーティションに事前焼き付けできないクエリです。定義上、あなたはそれを 尋ねると知らなかったからです。リレーショナルの直感 — JOIN を書く — が、ここでは正しい のです。DynamoDB はそれをネイティブにさばけず、PartiQL も同様です。

いつもの重量級の答えは、 S3 にエクスポートして Athena でクエリする (または Athena のフェデレーテッドクエリコネクタで、ライブテーブルを JOIN する)か、 ウェアハウスに流し込むことです。真の大規模分析にはそれが正解ですが、今すぐ、ライブ テーブルに対して答えが欲しい問いには、かなりの配管仕事です。

DynoTable の SQL Workbench で本物の JOIN を実行する

DynoTable は、SQL Workbench が実際の SQL — JOINGROUP 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 BuilderKeyConditionExpression / FilterExpression の構文を代わりに生成し、回避策では 足りないときは DynoTable が本物の SQL を実行します。

PartiQL の拒否メッセージは 2026-08-11 に DynamoDB Local(us-east-1、 @aws-sdk/client-dynamodb v3.1096.0)に対して再現し、ValidationException.message から逐語的に引用したものです。

更新日