Relaciones muchos a muchos en DynamoDB
Un estudiante se matricula en muchos cursos; un curso tiene muchos estudiantes. En SQL vas
a una tabla de join y un JOIN de tres tablas.
DynamoDB no tiene joins, así que la relación tiene que vivir en las claves — y el
truco es guardar cada edge de matrícula en una forma que ambos lados puedan Query
directamente.
Esta guía recorre el problema estudiantes ↔ cursos de punta a punta: los patrones de acceso, el patrón que los resuelve, un key schema original que puedes copiar y cómo leer ambas direcciones de vuelta sin escanear nunca la tabla.
¿Cómo modelas una relación muchos a muchos en DynamoDB?
DynamoDB no tiene joins, así que modelas una relación muchos a muchos con el patrón : guarda cada enlace como su propio item edge claveado por un lado, luego añade un GSI invertido que intercambia las claves. Un solo edge, escrito una vez, responde luego a queries de ambas direcciones de forma barata.
- Guarda cada matrícula como su propio item edge, no como un atributo lista en ninguno de los lados.
- Clavea el edge por el estudiante (
PK = STU#…,SK = ENROLL#CRS#…) para que un soloQuerydevuelva toda la lista de cursos del estudiante. - Añade un invertido que intercambia los roles (
GSI1PK = CRS#…) para que el mismo edge también responda «¿quién está en este curso?». - Un edge, escrito una vez, se lee barato en ambos sentidos — ese es todo el juego.
Enmarca primero los patrones de acceso
El modelado en DynamoDB es access-pattern-first: decides las lecturas antes de elegir un solo nombre de atributo. Una relación muchos a muchos casi siempre tiene dos lecturas simétricas más los lookups de entidad:
- Traer el perfil de un estudiante, y listar cada curso en el que ese estudiante está matriculado.
- Traer los metadatos de un curso, y listar cada estudiante matriculado en ese curso.
- Buscar un solo edge de matrícula — para actualizar una nota o abandonar el curso.
Las dos lecturas de lista apuntan en direcciones opuestas a través del mismo conjunto de
edges. Un diseño ingenuo sirve una barata y fuerza un Scan para la otra — el pie
exacto cubierto en Query vs Scan.
El trabajo es hacer que ambas direcciones sean un solo Query.
Usa el patrón adjacency-list
La propia guía de DynamoDB para relaciones es la adjacency list: modela cada relación como un item cuya clave de partición es un endpoint y cuya clave de ordenación es el otro.
AWS lo documenta en la página Best Practices for Managing Many-to-Many Relationships del DynamoDB Developer Guide.
¿Por qué claves y no una segunda tabla? Porque la primitiva que te da DynamoDB es un Query
contra una sola partición.
Un Query lee un rango contiguo de valores de sort-key bajo una clave de partición en una
sola operación facturada — ese es el único "join" que ofrece el motor.
Para obtener una relación que se lea barata desde ambos lados, duplicas el edge: escríbelo una vez claveado por el estudiante, luego usa un índice secundario para proyectar el mismo edge claveado por el curso.
Es el pensamiento de claves sobrecargadas de Single-Table Design, aplicado a una relación en vez de a una jerarquía padre-hijo.
La forma son dos vistas apiladas del mismo edge — la tabla base claveada por estudiante, el GSI invertido claveado por curso:
Cada edge se escribe una vez en la tabla base y se proyecta al GSI con sus claves
intercambiadas, así que un Query contra cualquiera de las particiones lee la relación barato.
El linaje vuelve al paper Amazon Dynamo de 2007: la clave de partición es la unidad de distribución, y el acceso por clave única es el camino rápido.
Las relaciones en DynamoDB son un ejercicio de doblar lecturas muchos-a-muchos hacia ese camino rápido.
Trabaja el ejemplo: estudiantes ↔ cursos
Usa una tabla con claves genéricas, PK y SK, y codifica el tipo de entidad en el
valor. El edge de matrícula es el corazón:
| PK | SK | attributes |
|---|---|---|
| STU#a91 | PROFILE | name, year, major |
| STU#a91 | ENROLL#CRS#math204 enrolledOn, grade | |
| STU#a91 | ENROLL#CRS#cs101 | enrolledOn, grade |
| CRS#math204 | METADATA | title, credits, term |
| CRS#cs101 | METADATA | title, credits, term |
Un solo Query PK = "STU#a91" devuelve el perfil del estudiante y cada matrícula
en una lectura. Estrecha con SK begins_with "ENROLL#" para obtener solo los edges de curso.
Eso resuelve "listar los cursos de un estudiante".
Pero "listar los estudiantes de un curso" apunta al otro lado — y la tabla base no puede responderlo, porque el id del estudiante está en la clave de partición, no en la de ordenación.
Añade un índice secundario global invertido que intercambia los roles. Dale a los items edge un
par genérico GSI1PK/GSI1SK con el curso en el lado de partición y el estudiante
en el de ordenación:
| PK | SK | GSI1PK | GSI1SK |
|---|---|---|---|
| STU#a91 | ENROLL#CRS#math204 | CRS#math204 | STU#a91 |
| STU#b30 | ENROLL#CRS#math204 | CRS#math204 | STU#b30 |
| STU#a91 | ENROLL#CRS#cs101 | CRS#cs101 | STU#a91 |
Ahora Query GSI1 WHERE GSI1PK = "CRS#math204" lista cada estudiante de ese curso — la
lectura que la tabla base no podía servir. Un item edge, escrito una vez, responde ambas
direcciones.
Tiene que ser un GSI, no un LSI: la partición del curso es totalmente distinta de la partición del estudiante, y un LSI comparte la clave de partición de la tabla base.
El índice abarca varias particiones, así que debe ser global — mira GSI vs LSI.
Los GSI en DynamoDB se rellenan de forma asíncrona. Una matrícula recién creada puede tardar un
momento en aparecer en la dirección CRS#….
Trata la lectura del roster del curso como — lo que el Developer Guide señala explícitamente para los índices secundarios globales.
Escríbelo y léelo en DynoTable
Escribir la matrícula significa poner cuatro atributos de clave más los datos propios del edge. La
condición que evita que un estudiante se matricule dos veces en el mismo curso es una guarda
attribute_not_exists(PK) sobre la clave compuesta.
Ese es exactamente el tipo de condición que puedes montar visualmente con el
DynamoDB Expression Builder en vez de
escribir a mano los ExpressionAttributeNames y los valores placeholder.
En DynoTable apuntas un Query a GSI1, pones GSI1PK = "CRS#math204", y el
roster vuelve como una tabla que puedes leer, ordenar y editar in place — ambas direcciones de
la relación explorables desde un solo schema.

Escollos y próximos pasos
- No guardes un lado como atributo lista. Un array
courseIdsen el item estudiante parece ordenado hasta que un curso necesita su roster, el array choca el techo de 400 KB del item, o dos matrículas compiten y se pisan. Items edge discretos escalan y se actualizan de forma independiente. - Mantén los datos del edge en el edge. El
gradey elenrolledOnde la matrícula pertenecen al item edge, no duplicados en el estudiante o el curso — hay exactamente una fila por par (estudiante, curso) que actualizar. - Cuida la propagación del GSI. La dirección del índice invertido es de consistencia eventual, así que una lectura justo después de una matrícula puede ir por detrás una fracción de segundo.
- Proyecta solo lo que el roster necesita. Una proyección
KEYS_ONLYo estrecha mantiene el GSI pequeño cuando la vista del roster solo necesita ids.
Para ir más a fondo en los patrones de alrededor, lee Single-Table Design para claves sobrecargadas y GSI vs LSI para cuándo el índice invertido tiene que ser global. Y para partir de tus propias relaciones, la herramienta de Single-Table Design gratis convierte una lista de patrones de acceso como "listar los cursos de un estudiante / listar los estudiantes de un curso" en un plan PK/SK/GSI con items de ejemplo.
Luego descarga DynoTable para modelar el schema estudiantes ↔ cursos de verdad —
escribe los edges, construye la condición con el Expression Builder, y consulta ambas
direcciones de la relación sin un solo scan. Y cuando quieras la
vista clásica de JOIN de tres tablas de todos modos, el SQL Workbench de
DynoTable la ejecuta sobre tus tablas en vivo.


