DynamoDB JOIN: Como Juntar Tabelas (e Por Que Você Geralmente Não Consegue)
Não há JOIN no DynamoDB. A API não tem operador de join, o modelo de dados não tem chaves
estrangeiras e — a parte que surpreende a maioria das pessoas — o PartiQL, a camada de consulta
com sabor de SQL, também não adiciona um. Um SELECT do PartiQL lê exatamente uma tabela.
Se você veio de um banco de dados relacional, essa é a primeira parede em que você bate. Este guia cobre por que a parede está ali, as quatro coisas que os desenvolvedores fazem no lugar, o único caso em que você genuinamente precisa de um join de verdade — e como rodar um.
O DynamoDB consegue fazer joins?
Não. O DynamoDB não consegue juntar tabelas — nem pela API de baixo nível (GetItem / Query /
Scan / BatchGetItem), nem pelo PartiQL, nem por qualquer planejador de consultas embutido, porque não há nenhum. Toda leitura mapeia para uma única tabela ou um de seus índices; combinar duas tabelas por uma chave correspondente é algo que você faz no seu app depois que o DynamoDB devolve os itens, nunca dentro dele.
- O DynamoDB não tem operador
JOIN. Nunca teve. - O
SELECTdo PartiQL é apenas de tabela única — a gramática é literalmenteSELECT … FROM {{table}}[.{{index}}], e apontá-lo para duas tabelas retornaValidationException: Only Select from a Single Table or index supported. - A solução que a AWS recomenda é não precisar de um join: desnormalize, ou use single-table design para que itens relacionados morem em uma partição que você busca em uma única requisição.
- Para o caso genuíno entre tabelas / ad-hoc, você faz o join fora do DynamoDB — no seu app, ou com uma ferramenta que faz isso por você.
Por que o DynamoDB não tem joins
Um JOIN de SQL pede ao banco de dados que leia múltiplas tabelas e as monte no momento da
consulta. O próprio
guia da AWS para modelar dados relacionais
detalha o custo: uma consulta como
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é flexível, mas "cada join na consulta aumenta a complexidade de runtime da consulta porque os dados de cada tabela precisam ser preparados e depois montados." Esse trabalho é ilimitado — seu custo depende dos dados, não da consulta — que é exatamente a propriedade que o DynamoDB se recusa a ter.
Então a AWS projetou a restrição. O DynamoDB é, nas palavras deles, "construído para minimizar
ambas as restrições [de CPU e rede] eliminando os JOINs (e incentivando a desnormalização de
dados) e otimizando a arquitetura do banco para responder totalmente a uma consulta da aplicação
com uma única requisição a um item." Essas são as qualidades que compram latência de milissegundos
de um dígito em qualquer escala: o custo de runtime de uma leitura do DynamoDB é constante
independentemente do tamanho da tabela. Não há motor de join nem conceito de chave estrangeira para
planejar — por design.
"Mas o PartiQL é SQL, com certeza faz join?"
Não. O PartiQL te dá sintaxe SELECT / INSERT / UPDATE / DELETE sobre o DynamoDB, mas é
SQL-compatível, não SQL. A
gramática oficial do SELECT
é:
SELECT {{expression}} [, ...]
FROM {{table}}[.{{index}}]
[ WHERE {{condition}} ]
[ ORDER BY {{key}} [DESC|ASC], ... ]O FROM recebe uma tabela (opcionalmente um de seus índices). Não há segunda tabela FROM, nem
JOIN, nem subconsulta, nem CTE. Aponte o PartiQL para duas tabelas e o DynamoDB rejeita
(relatado no AWS re:Post):
ValidationException: Only Select from a Single Table or index supportedSe você quer o raciocínio completo de por que o PartiQL parece SQL mas não consegue se comportar como tal, veja PartiQL vs SQL.
As 4 gambiarras que os devs de fato usam
1. Desnormalizar (copiar os dados para dentro)
Armazene os campos que você de outra forma juntaria diretamente no item. Um Order carrega um
snapshot do customerName e do shippingAddress em vez de um customerId que você resolveria
depois. Uma leitura, sem join.
O custo é o fan-out no momento da escrita: quando a fonte muda você atualiza cada cópia (tipicamente via um handler de DynamoDB Streams). Você está trocando complexidade de leitura por complexidade de escrita — geralmente uma boa troca para um app com muita leitura.
2. Single-table design (pré-join na partição)
Coloque entidades relacionadas em uma tabela sob uma partition key compartilhada para que uma
coleção de itens seja o resultado do join. Um cliente e todos os seus pedidos compartilham
PK = "CUSTOMER#42"; uma Query retorna o item do cliente mais cada item de pedido — o "join" já
aconteceu no momento da escrita.
Query PK = "CUSTOMER#42"
→ CUSTOMER#42 / PROFILE (o cliente)
→ CUSTOMER#42 / ORDER#1001 (um pedido)
→ CUSTOMER#42 / ORDER#1002 (um pedido)
Esta é a resposta canônica do DynamoDB para relacionamentos um-para-muitos. Passo a passo completo em single-table design.
3. Join no lado da aplicação (duas leituras, costura no código)
Leia da tabela A, pegue as chaves que recebeu de volta, leia da tabela B e mescle os dois conjuntos de resultados na sua aplicação. É a lógica de join relacional — só rodando no seu código em vez de no banco de dados:
// "Pegue cada pedido com o nome do cliente" — o join manual.
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
}));Tranquilo para fan-out pequeno. Com muitos pedidos vira um problema N+1 — uma leitura para
listar os pedidos, depois uma leitura por pedido — que é lento e queima capacidade de leitura. O
BatchGetItem (a seguir) colapsa essa segunda onda em um único round-trip.
4. BatchGetItem (um round-trip, múltiplas tabelas)
O BatchGetItem
é o mais perto que a API chega de "tocar duas tabelas de uma vez": uma requisição retorna "os
atributos de um ou mais itens de uma ou mais tabelas", até 100 itens ou 16 MB por chamada,
o que vier primeiro. Ele corta os round-trips de um join no lado do app — mas não é um join.
Você "identifica os itens solicitados pela chave primária"; não há condição ON nem correspondência
relacional. Você ainda tem que conhecer as chaves de antemão e costurar as respostas você mesmo.
Quando um JOIN de verdade é inevitável
As quatro gambiarras cobrem bem os caminhos de leitura de produção. Onde elas falham é na consulta ad-hoc, exploratória, analítica — aquela que você não modelou:
- "Quais clientes na UE fizeram um pedido acima de US$500 no mês passado?" entre uma tabela
Orderse uma tabelaCustomers. - Uma checagem pontual de qualidade de dados juntando dois tipos de entidade.
- Relatórios e agregações (
GROUP BY,SUM,COUNT) — para as quais o DynamoDB não tem operador algum.
Essas são exatamente as consultas que você não consegue pré-cozinhar numa partição, porque por
definição você não sabia que iria fazê-las. O instinto relacional — escrever um JOIN — é o certo
aqui. O DynamoDB simplesmente não consegue servi-lo nativamente, e o PartiQL também não.
A resposta pesada usual é exportar para o S3 e consultar com o Athena, ou canalizar para um data warehouse. Isso é correto para analytics de verdade em escala, mas é muito encanamento para uma pergunta que você quer respondida agora, contra sua tabela ao vivo.
Rodando um JOIN de verdade com o SQL Workbench do DynoTable
O DynoTable é um cliente desktop para DynamoDB cujo SQL Workbench roda SQL de
verdade — incluindo JOIN, GROUP BY e funções de agregação — sobre suas tabelas DynamoDB. Ele lê
os itens pela API normal do DynamoDB e depois executa as partes relacionais da consulta no cliente.
Então você pode escrever:
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— e obter um conjunto de resultados, contra tabelas que não têm relacionamento definido e um motor
de consulta que não tem palavra-chave JOIN.
A ressalva honesta — "dentro das regras de padrão de acesso do DynamoDB": o Workbench ainda lê
pelo DynamoDB, então um join ilimitado é uma leitura ilimitada. As consultas mais rápidas são
aquelas em que a cláusula WHERE (ou o atributo ON do join) atinge uma partition key ou um
GSI em pelo menos um lado, para que o DynamoDB rode uma Query em vez de um
scan de tabela inteira antes de o join executar. O Workbench não revoga as
restrições deste guia — ele só te deixa fazer a pergunta em SQL em vez de escrever a costura à
mão, e te diz o que está fazendo por baixo.
É o único "sim, você pode juntar" que é de fato verdade: o PartiQL e o próprio
NoSQL Workbench
da AWS — cujo construtor de operações é limitado a operações de plano de dados de tabela única
(Query / Scan / GetItem) — ambos param na parede de tabela única, assim como a maioria dos
outros clientes GUI. Veja como o DynoTable se compara como uma
GUI para DynamoDB.
FAQ
O PartiQL suporta JOIN?
Não. O SELECT do PartiQL lê uma única tabela (ou um de seus índices). Uma consulta multi-tabela
retorna ValidationException: Only Select from a Single Table or index supported. A mesma parede do
resto da API.
Você consegue juntar duas tabelas do DynamoDB em uma consulta?
Não nativamente. A API do DynamoDB não tem instrução que leia duas tabelas e as combine por uma
chave. O BatchGetItem consegue ler itens de múltiplas tabelas em uma requisição, mas não tem
condição ON — ele retorna os itens que você nomeou pela chave primária e deixa a correspondência
para você. Um JOIN … ON … de verdade só acontece fora do DynamoDB: no seu app, ou no SQL
Workbench do DynoTable.
Você consegue juntar uma tabela ao seu GSI?
Não — um Índice Secundário Global não é uma tabela separada à qual você se
junta; é uma visão alternativa por chave dos mesmos itens. Você dá Query ou na tabela ou no
índice em um dado SELECT, não nos dois juntos. Um GSI te deixa alcançar itens por uma chave
diferente, o que muitas vezes elimina a necessidade de um join em primeiro lugar.
Você consegue juntar entre duas contas AWS (ou duas tabelas em contas diferentes)?
Não nativamente, e nem com o BatchGetItem — uma única requisição não pode abranger credenciais, e
não há primitiva de join entre contas. Você leria cada tabela com as credenciais da sua própria
conta e juntaria os resultados na sua aplicação ou numa ferramenta como o Workbench do DynoTable.
A desnormalização é realmente melhor que um join? Para a carga de trabalho alvo do DynamoDB — leituras previsíveis e de alto volume — sim. Você move o custo para o momento da escrita (e aceita alguma duplicação de dados) em troca de leituras de requisição única que escalam de forma plana. O guia de single-table design cobre os trade-offs.
Montar as chaves e condições dessas leituras à mão é trabalhoso — o
construtor de expressões gera a sintaxe de
KeyConditionExpression / FilterExpression para você, e o DynoTable roda o SQL de
verdade quando uma gambiarra não resolve.