DynamoDB JOIN: cómo unir tablas (y por qué normalmente no puedes)
No hay JOIN en DynamoDB. La API no tiene operador de join, el modelo de
datos no tiene claves foráneas y — la parte que sorprende a la mayoría — PartiQL, la
capa de consulta con sabor a SQL, tampoco añade uno. Un SELECT de PartiQL lee
exactamente una tabla.
Si vienes de una base de datos relacional, este es el primer muro contra el que chocas. Esta guía cubre por qué está ahí el muro, las cuatro cosas que los desarrolladores hacen en su lugar, el único caso en el que realmente necesitas un join de verdad — y cómo ejecutar uno.
¿Puede DynamoDB hacer joins?
No. DynamoDB no puede unir tablas — ni a través de la API de bajo nivel (GetItem / Query / Scan / BatchGetItem), ni a través de PartiQL, ni a través de ningún planificador de consultas integrado, porque no hay ninguno. Cada lectura mapea a una sola tabla o a uno de sus índices; combinar dos tablas por una clave coincidente es algo que haces en tu aplicación después de que DynamoDB devuelva los items, nunca dentro de ella.
- DynamoDB no tiene operador
JOIN. Nunca lo ha tenido. - El
SELECTde PartiQL es solo de una tabla — la gramática es literalmenteSELECT … FROM {{table}}[.{{index}}], y apuntarlo a dos tablas devuelveValidationException: Only Select from a Single Table or index supported. - La solución que AWS recomienda es no necesitar un join: desnormaliza, o usa diseño de tabla única para que los items relacionados vivan en una partición que recuperas en una sola petición.
- Para el caso genuino entre tablas / ad-hoc, unes fuera de DynamoDB — en tu aplicación, o con una herramienta que lo haga por ti.
Por qué DynamoDB no tiene joins
Un JOIN de SQL pide a la base de datos leer múltiples tablas y ensamblarlas en tiempo de
consulta. La propia
guía de AWS para modelar datos relacionales
deletrea el coste: una 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 DESCes flexible, pero «cada join en la consulta aumenta la complejidad en tiempo de ejecución de la consulta porque los datos de cada tabla deben prepararse y luego ensamblarse». Ese trabajo es ilimitado — su coste depende de los datos, no de la consulta — que es exactamente la propiedad que DynamoDB se niega a tener.
Así que AWS diseñó la restricción dentro. DynamoDB está, en sus palabras, «construido para
minimizar ambas restricciones [de CPU y red] eliminando los JOIN (y
fomentando la desnormalización de datos) y optimizando la arquitectura de la base de datos para
responder completamente a una consulta de aplicación con una sola petición a un item». Esas son
las cualidades que compran latencia de un dígito de milisegundo a cualquier escala: el
coste en tiempo de ejecución de una lectura de DynamoDB es constante independientemente del tamaño de la tabla. No hay
motor de join ni concepto de clave foránea contra el que planificar — por diseño.
«Pero PartiQL es SQL, ¿seguro que une?»
No. PartiQL te da sintaxis SELECT / INSERT / UPDATE / DELETE sobre
DynamoDB, pero es compatible con SQL, no SQL. La
gramática oficial de SELECT
es:
SELECT {{expression}} [, ...]
FROM {{table}}[.{{index}}]
[ WHERE {{condition}} ]
[ ORDER BY {{key}} [DESC|ASC], ... ]FROM toma una tabla (opcionalmente uno de sus índices). No hay una segunda
tabla FROM, ni JOIN, ni subconsulta, ni CTE. Apunta PartiQL a dos tablas y
DynamoDB lo rechaza
(reportado en AWS re:Post):
ValidationException: Only Select from a Single Table or index supportedSi quieres el razonamiento completo de por qué PartiQL parece SQL pero no puede comportarse como tal, mira PartiQL frente a SQL.
Las 4 soluciones que los devs realmente usan
1. Desnormalizar (copiar los datos dentro)
Almacena los campos que de otro modo unirías directamente en el item. Un Order lleva
una instantánea del customerName y la shippingAddress en lugar de un
customerId que resolverías después. Una lectura, sin join.
El coste es el fan-out en tiempo de escritura: cuando la fuente cambia actualizas cada copia (normalmente vía un manejador de DynamoDB Streams). Estás cambiando complejidad de lectura por complejidad de escritura — normalmente un buen trato para una aplicación con muchas lecturas.
2. Diseño de tabla única (pre-unir en la partición)
Pon entidades relacionadas en una tabla bajo una clave de partición compartida para que una
colección de items sea el resultado unido. Un cliente y todos sus pedidos comparten
PK = "CUSTOMER#42"; un Query devuelve el item del cliente más cada item de
pedido — el «join» ya ocurrió en tiempo de escritura.
Query PK = "CUSTOMER#42"
→ CUSTOMER#42 / PROFILE (el cliente)
→ CUSTOMER#42 / ORDER#1001 (un pedido)
→ CUSTOMER#42 / ORDER#1002 (un pedido)
Esta es la respuesta canónica de DynamoDB a las relaciones uno-a-muchos. Recorrido completo en diseño de tabla única.
3. Join del lado de la aplicación (dos lecturas, cosido en código)
Lee de la tabla A, toma las claves que obtuviste, lee de la tabla B, y fusiona los dos conjuntos de resultados en tu aplicación. Es la lógica de join relacional — solo ejecutándose en tu código en lugar de en la base de datos:
// "Obtener cada pedido con el nombre de su cliente" — el 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
}));Bien para fan-out pequeño. Con muchos pedidos se convierte en un problema N+1 — una lectura
para listar pedidos, luego una lectura por pedido — que es lento y quema capacidad de lectura.
BatchGetItem (siguiente) colapsa esa segunda oleada en un solo viaje de ida y vuelta.
4. BatchGetItem (un viaje de ida y vuelta, múltiples tablas)
BatchGetItem
es lo más cerca que llega la API de «tocar dos tablas a la vez»: una
petición devuelve «los atributos de uno o más items de una o más
tablas», hasta 100 items o 16 MB por llamada, lo que golpee primero.
Recorta los viajes de ida y vuelta de un join del lado de la aplicación — pero no es un join. Tú
«identificas los items solicitados por clave primaria»; no hay condición ON ni
emparejamiento relacional. Aún tienes que conocer las claves por adelantado y coser las
respuestas tú mismo.
Cuándo un JOIN real es inevitable
Las cuatro soluciones cubren bien las rutas de lectura de producción. Donde fallan es en la consulta ad-hoc, exploratoria, analítica — la que no modelaste:
- «¿Qué clientes de la UE hicieron un pedido de más de 500 $ el mes pasado?» entre una
tabla
Ordersy una tablaCustomers. - Una comprobación puntual de calidad de datos uniendo dos tipos de entidad.
- Informes y agregados (
GROUP BY,SUM,COUNT) — para los que DynamoDB no tiene operador en absoluto.
Estas son exactamente las consultas que no puedes precocinar en una partición, porque por
definición no sabías que las harías. El instinto relacional — escribir un
JOIN — es el correcto aquí. DynamoDB simplemente no puede servirlo de forma nativa, y
PartiQL tampoco.
La respuesta pesada habitual es exportar a S3 y consultar con Athena, o canalizar a un almacén. Eso es correcto para análisis reales a escala, pero es mucha fontanería para una pregunta que quieres respondida ahora, contra tu tabla en vivo.
Ejecutar un JOIN real con el SQL Workbench de DynoTable
DynoTable es un cliente de escritorio de DynamoDB cuyo SQL Workbench ejecuta
SQL real — incluido JOIN, GROUP BY y funciones de agregación — sobre tus
tablas de DynamoDB. Lee los items a través de la API normal de DynamoDB, y luego
ejecuta las partes relacionales de la consulta en el cliente. Así que puedes escribir:
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— y obtener un conjunto de resultados, contra tablas que no tienen relación definida y un
motor de consulta que no tiene palabra clave JOIN.
El aviso honesto — «dentro de las reglas de patrones de acceso de DynamoDB»: el Workbench
aún lee a través de DynamoDB, así que un join ilimitado es una lectura ilimitada. Las
consultas más rápidas son aquellas en las que la cláusula WHERE (o el atributo ON del
join) golpea una clave de partición o un GSI en al menos un
lado, para que DynamoDB ejecute un Query en lugar de un
scan de tabla completa antes de que se ejecute el join. El Workbench no
deroga las restricciones de esta guía — solo te deja hacer la pregunta SQL
en lugar de escribir el cosido a mano tú mismo, y te dice qué está haciendo
por debajo.
Es el único «sí, puedes unir» que es realmente cierto: PartiQL y el propio
NoSQL Workbench de AWS
— cuyo constructor de operaciones está limitado a operaciones del plano de datos de una sola tabla
(Query / Scan / GetItem) — se detienen ambos en el muro de una tabla, como hace la mayoría de
otros clientes con GUI. Mira cómo se compara DynoTable como
GUI para DynamoDB.
Preguntas frecuentes
¿Soporta PartiQL JOIN?
No. El SELECT de PartiQL lee una sola tabla (o uno de sus índices). Una
consulta multi-tabla devuelve ValidationException: Only Select from a Single Table or index supported. El mismo muro que el resto de la API.
¿Puedes unir dos tablas de DynamoDB en una consulta?
No de forma nativa. La API de DynamoDB no tiene sentencia que lea dos tablas y las empareje
por una clave. BatchGetItem puede leer items de múltiples tablas en una petición,
pero no tiene condición ON — devuelve los items que nombraste por clave primaria y
deja el emparejamiento a ti. Un JOIN … ON … real solo ocurre fuera de DynamoDB:
en tu aplicación, o en el SQL Workbench de DynoTable.
¿Puedes unir una tabla con su GSI?
No — un Índice secundario global no es una tabla separada a la que te
unas; es una vista de clave alternativa de los mismos items. Consultas o bien
la tabla o bien el índice en un SELECT dado, no ambos unidos. Un GSI
te deja alcanzar items por una clave diferente, lo que a menudo elimina la necesidad de un
join en primer lugar.
¿Puedes unir entre dos cuentas de AWS (o dos tablas en cuentas diferentes)?
No de forma nativa, y tampoco con BatchGetItem — una sola petición no puede abarcar
credenciales, y no hay primitiva de join entre cuentas. Leerías cada tabla
con las credenciales de su propia cuenta y unirías los resultados en tu aplicación o en
una herramienta como el Workbench de DynoTable.
¿Es la desnormalización realmente mejor que un join? Para la carga de trabajo objetivo de DynamoDB — lecturas predecibles de alto volumen — sí. Mueves el coste al tiempo de escritura (y aceptas algo de duplicación de datos) a cambio de lecturas de una sola petición que escalan de forma plana. La guía de diseño de tabla única cubre los compromisos.
Construir las claves y condiciones para estas lecturas a mano es engorroso — el
constructor de expresiones genera la
sintaxis de KeyConditionExpression / FilterExpression por ti, y
DynoTable ejecuta el SQL real cuando una solución alternativa no basta.