DynamoDB JOIN:如何 join 資料表(以及為什麼你通常不能)
DynamoDB 中沒有 JOIN。API 沒有 join 運算子、資料模型
沒有外鍵,而且 — 最讓多數人意外的部分 — PartiQL,這個
SQL 風味的查詢層,也沒有加上一個。 一個 PartiQL SELECT 只讀
恰好一張資料表。
如果你來自關聯式資料庫,這是你撞上的第一道牆。本 指南涵蓋這道牆為什麼在那裡、開發者改做的四件事、你 確實需要真正 join 的那一個情況 — 以及如何執行一個。
DynamoDB 能做 join 嗎?
不能。DynamoDB 無法 join 資料表——不透過低階 API(GetItem / Query / Scan / BatchGetItem)、不透過 PartiQL,也不透過任何內建的查詢規劃器,因為根本沒有。每次讀取都對應到單一資料表或它的某個索引;把兩張資料表依相符的鍵組合,是你在 DynamoDB 把項目交還給你之後才在應用程式中做的事,從不在它內部。
- DynamoDB 沒有
JOIN運算子。從來沒有。 - PartiQL 的
SELECT是僅單表 — 文法字面上就是SELECT … FROM {{table}}[.{{index}}],把它指向兩張資料表會回傳ValidationException: Only Select from a Single Table or index supported。 - AWS 推薦的修正是讓你不需要 join:去正規化,或使用 單表設計,讓相關項目住在 你能用單一請求取回的同一個分割區。
- 對於真正的跨表/臨時情況,你在 DynamoDB 之外 join — 在 你的應用程式中,或用一個替你做的工具。
DynamoDB 能做 join 嗎?
不能。DynamoDB 無法 join 資料表 — 不透過低階 API(GetItem /
Query / Scan / BatchGetItem)、不透過 PartiQL,也不透過任何
內建的查詢規劃器,因為根本沒有查詢規劃器。每次讀取都對應到
單一資料表或它的其中一個索引。把兩張資料表依相符的鍵組合,是你在
DynamoDB 把項目交回給你之後才做的事,從不在它內部。
那不是 AWS 忘了補的缺口。它是刻意的設計決策,而其 理由值得在你伸手找變通做法之前先理解。
為什麼 DynamoDB 沒有 join
一個 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很靈活,但「查詢中的每個 join 都會增加查詢的執行階段 複雜度,因為每張資料表的資料都必須暫存,然後組裝。」那 工作是無上界的 — 它的成本取決於資料,而非查詢 — 這 正是 DynamoDB 拒絕擁有的特性。
所以 AWS 把這個限制設計進去了。用他們的話來說,DynamoDB「藉由
消除 JOIN(並鼓勵資料去正規化)並優化資料庫架構,使其能
以對某個項目的單一請求完整回答一個應用程式查詢,來最小化
[CPU 與網路] 這兩種限制。」這些就是讓它在任何規模下都有
單位數毫秒延遲的特性:DynamoDB 一次讀取的執行階段成本不論
資料表大小都是常數。沒有 join 引擎、也沒有外鍵概念可供
規劃 — 出於設計。
「但 PartiQL 就是 SQL,它肯定能 join 吧?」
不能。PartiQL 給你 SELECT / INSERT / UPDATE / DELETE 語法以操作
DynamoDB,但它是 SQL相容,不是 SQL。
官方 SELECT 文法
是:
SELECT {{expression}} [, ...]
FROM {{table}}[.{{index}}]
[ WHERE {{condition}} ]
[ ORDER BY {{key}} [DESC|ASC], ... ]FROM 接受一張資料表(可選它的其中一個索引)。沒有第二張
FROM 資料表、沒有 JOIN、沒有子查詢、沒有 CTE。把 PartiQL 指向兩張資料表,
DynamoDB 就會拒絕它
(在 AWS re:Post 回報):
ValidationException: Only Select from a Single Table or index supported如果你想看 PartiQL 為什麼看起來像 SQL 卻無法表現得像 SQL 的 完整理由,請見 PartiQL vs SQL。
開發者實際採用的 4 種變通做法
1. 去正規化(把資料複製進來)
把你原本要 join 進來的欄位直接存到項目上。一個 Order 帶著
customerName 與 shippingAddress 的快照,而不是一個你之後要
解析的 customerId。一次讀取,沒有 join。
代價是寫入時的扇出:當來源變動時,你要更新每一份副本 (通常透過一個 DynamoDB Streams 處理器)。你在用讀取複雜度 換寫入複雜度 — 對讀取繁重的應用程式而言通常划算。
2. 單表設計(在分割區內預先 join)
把相關實體放進一張資料表、共用一個分割鍵,讓一個
項目集合就是 join 後的結果。一個客戶與他所有的訂單共用
PK = "CUSTOMER#42";一次 Query 回傳客戶項目外加每一個訂單
項目 — 那個「join」已經在寫入時發生了。
Query PK = "CUSTOMER#42"
→ CUSTOMER#42 / PROFILE (客戶)
→ CUSTOMER#42 / ORDER#1001 (一筆訂單)
→ CUSTOMER#42 / ORDER#1002 (一筆訂單)
這是 DynamoDB 對一對多關係的標準答案。完整 逐步說明請見單表設計。
3. 應用程式端 join(兩次讀取,在程式碼中縫合)
從資料表 A 讀取、拿你取回的鍵、從資料表 B 讀取,再在你的 應用程式中合併這兩個結果集。這就是關聯式 join 邏輯 — 只是 跑在你的程式碼中而非資料庫裡:
// 「為每筆訂單取得它的客戶名稱」 — 手動 join。
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 問題 — 一次讀取
列出訂單,然後每筆訂單一次讀取 — 既慢又燒讀取容量。
BatchGetItem(下一個)把第二波讀取縮成一次往返。
4. BatchGetItem(一次往返,多張資料表)
BatchGetItem
是 API 最接近「一次碰兩張資料表」的東西:一次
請求回傳「一個以上項目來自一張以上資料表」的屬性,每次
呼叫最多 100 個項目或 16 MB,以先達到者為準。它
削減了應用程式端 join 的往返 — 但它不是 join。你
「以主鍵識別所請求的項目」;沒有 ON 條件、也沒有
關聯式比對。你仍得事先知道鍵,並自己把回應
縫合起來。
當真正的 JOIN 無可避免時
這四種變通做法把生產讀取路徑涵蓋得不錯。它們失靈的地方是 臨時、探索性、分析性的查詢 — 那種你沒有為它建模的:
- 跨一張
Orders資料表與一張Customers資料表的 「上個月在歐盟下單超過 $500 的客戶有哪些?」 - 一次性的、join 兩種實體型別的資料品質檢查。
- 報表與彙總(
GROUP BY、SUM、COUNT) — 這些 DynamoDB 根本 沒有運算子能做。
這些正是你無法預先烘進分割區的查詢,因為根據
定義你不知道你會問它們。那個關聯式直覺 — 寫一個
JOIN — 在這裡是對的。DynamoDB 就是無法原生服務它,
PartiQL 也不行。
通常的重型答案是 匯出到 S3 並用 Athena 查詢, 或導入資料倉儲。對於真正的大規模分析那是對的,但對一個你想 現在、對你的即時資料表回答的問題而言,那是大量的 管線工程。
用 DynoTable 的 SQL Workbench 執行真正的 JOIN
DynoTable 是一款桌面版 DynamoDB 用戶端,其 SQL Workbench 能對你的
DynamoDB 資料表執行真正的 SQL — 包含 JOIN、GROUP BY 與彙總
函式。它透過正常的 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 讀取,所以一個無上界的 join 就是無上界的讀取。
最快的查詢是那些 WHERE 子句(或 join 的 ON
屬性)在至少一側命中分割鍵或 GSI 的,這樣
DynamoDB 就會在 join 執行前跑一個 Query 而非整表
掃描。Workbench 並未廢除
本指南中的限制 — 它只是讓你能問出那個 SQL 問題,
而非自己手寫縫合,並告訴你它在底層
做了什麼。
它是唯一一個真正成立的「可以,你能 join」:PartiQL 與 AWS 自家的
NoSQL Workbench
— 其操作建構器僅限於單表的資料平面操作
(Query / Scan / GetItem) — 兩者都止步於單表牆,多數
其他 GUI 用戶端也是。看看 DynoTable 作為
DynamoDB GUI 的比較。
常見問題
PartiQL 支援 JOIN 嗎?
不支援。PartiQL 的 SELECT 讀取單一資料表(或它的其中一個索引)。一個
多表查詢會回傳 ValidationException: Only Select from a Single Table or index supported。與 API 其餘部分是同一道牆。
你能在一個查詢中 join 兩張 DynamoDB 資料表嗎?
原生不行。DynamoDB API 沒有任何敘述能讀兩張資料表並依某個鍵
比對它們。BatchGetItem 能在一個請求中從多張資料表讀取項目,
但它沒有 ON 條件 — 它回傳你以主鍵指名的項目,
並把比對留給你。一個真正的 JOIN … ON … 只發生在 DynamoDB 之外:
在你的應用程式中,或在 DynoTable 的 SQL Workbench 中。
你能把一張資料表 join 到它的 GSI 嗎?
不能 — 全域次要索引不是一張你 join 過去的
獨立資料表;它是同一批項目的另一種鍵視圖。在某個
SELECT 中你 Query資料表或索引,不是把兩者 join 在一起。一個 GSI
讓你能用不同的鍵觸及項目,這往往一開始就消除了 join 的
需要。
你能跨兩個 AWS 帳號(或不同帳號的兩張資料表)join 嗎?
原生不行,用 BatchGetItem 也不行 — 單一請求無法橫跨
憑證,也沒有跨帳號 join 原語。你得用各自帳號的憑證讀取
每張資料表,再在你的應用程式或像 DynoTable 的 Workbench 這類工具中
join 結果。
去正規化真的比 join 好嗎? 對 DynamoDB 的目標工作負載 — 可預測、高量的讀取 — 是的。你把 成本移到寫入時(並接受一些資料重複),以換取 能平坦地擴展的單一請求讀取。 單表設計指南涵蓋了這些取捨。
手動為這些讀取建立鍵與條件很瑣碎 —
expression builder 替你產生
KeyConditionExpression / FilterExpression 語法,而
DynoTable 在變通做法行不通時執行真正的 SQL。