Patrón de claves de varios atributos - Amazon DynamoDB

Patrón de claves de varios atributos

Descripción general

Las claves de varios atributos le permiten crear claves de partición y clasificación del índice secundario global (GSI) compuestas por un máximo de cuatro atributos cada una. Esto reduce el código del cliente y facilita el modelado inicial de los datos y la posterior agregación de nuevos patrones de acceso.

Pensemos en un escenario común: para crear un GSI que consulte los elementos por varios atributos jerárquicos, tradicionalmente se necesitaría crear claves sintéticas concatenando valores. Por ejemplo, en una aplicación de juegos, para consultar las partidas de un torneo por torneo, región y ronda, puede crear una clave de partición GSI sintética, como TOURNAMENT#WINTER2024#REGION#NA-EAST y una clave de clasificación sintética, como ROUND#SEMIFINALS#BRACKET#UPPER. Este enfoque funciona, pero requiere la concatenación de cadenas al escribir datos, el análisis al leer y el relleno de claves sintéticas en todos los elementos existentes si va a agregar el GSI a una tabla existente. Esto hace que el código sea más desordenado y difícil de mantener la seguridad tipográfica en los componentes clave individuales.

Las claves de varios atributos resuelven este problema para los GSI. La clave de partición de GSI se define mediante varios atributos existentes, como el tournamentId y la región. DynamoDB gestiona automáticamente la lógica de claves compuestas y las agrupa para la distribución de los datos. Los elementos se escriben con los atributos naturales del modelo de dominio y el GSI los indexa automáticamente. Sin concatenación, sin análisis, sin relleno. El código se mantiene limpio, los datos se mantienen escritos y las consultas se mantienen sencillas. Este enfoque resulta especialmente útil cuando se dispone de datos jerárquicos con agrupaciones de atributos naturales (como torneo → región → ronda u organización → departamento → equipo).

Ejemplo de aplicación

Esta guía explica cómo crear un sistema de seguimiento de partidos de torneos para una plataforma de deportes electrónicos. La plataforma necesita consultar los partidos de manera eficiente en múltiples dimensiones: por torneo y región para administrar los grupos, por jugador para ver el historial de partidos y por fecha para programarlos.

Modelo de datos

En este tutorial, el sistema de seguimiento de los partidos del torneo admite tres patrones de acceso principales, cada uno de los cuales requiere una estructura de clave diferente:

Patrón de acceso 1: busque un partido específico por su ID único

  • Solución: tabla base con matchId como clave de partición

Patrón de acceso 2: consulte todos los partidos de un torneo y una región específicos y, si lo prefiere, filtre por ronda, cuadro o partido

  • Solución: índice secundario global con clave de partición de varios atributos (tournamentId + region) y clave de clasificación de varios atributos (round + bracket + matchId)

  • Consultas de ejemplo: “Todos los partidos de WINTER2024 en la región NA-EAST” o “Todos los partidos de SEMIFINALES en el corchete SUPERIOR para WINTER2024/NA-EAST”

Patrón de acceso 3: consulte el historial de partidos de un jugador o, si lo prefiere, filtre por intervalo de fechas o por ronda del torneo

  • Solución: índice secundario global con clave de partición única (player1Id) y clave de clasificación de varios atributos (matchDate + round)

  • Consultas de ejemplo: “Todos los partidos del jugador 101” o “Los partidos del jugador 101 en enero de 2024”

La diferencia clave entre el enfoque tradicional y el de varios atributos queda clara al examinar la estructura de los elementos:

Enfoque de índice secundario global tradicional (claves concatenadas):

// Manual concatenation required for GSI keys const item = { matchId: 'match-001', // Base table PK tournamentId: 'WINTER2024', region: 'NA-EAST', round: 'SEMIFINALS', bracket: 'UPPER', player1Id: '101', // Synthetic keys needed for GSI GSI_PK: `TOURNAMENT#${tournamentId}#REGION#${region}`, // Must concatenate GSI_SK: `${round}#${bracket}#${matchId}`, // Must concatenate // ... other attributes };

Enfoque de índice secundario global de varios atributos (claves nativas):

// Use existing attributes directly - no concatenation needed const item = { matchId: 'match-001', // Base table PK tournamentId: 'WINTER2024', region: 'NA-EAST', round: 'SEMIFINALS', bracket: 'UPPER', player1Id: '101', matchDate: '2024-01-18', // No synthetic keys needed - GSI uses existing attributes directly // ... other attributes };

Con las claves de varios atributos, los elementos se escriben una sola vez con atributos de dominio naturales. DynamoDB los indexa automáticamente en varios GSI sin necesidad de claves concatenadas sintéticas.

Esquema de tabla base:

  • Clave de partición: matchId (1 atributo)

Esquema de índice secundario global (TournamentRegionIndex con claves de varios atributos):

  • Clave de partición: tournamentId, region (2 atributos)

  • Clave de clasificación: round, bracket, matchId (3 atributos)

Esquema de índice secundario global (PlayerMatchHistoryIndex con claves de varios atributos):

  • Clave de partición: player1Id (1 atributo)

  • Clave de clasificación: matchDate, round (2 atributos)

Tabla base: TournamentMatches

matchId (PK) tournamentId region round soporte player1Id player2Id matchDate ganador puntuación
match-001 WINTER2024 NA-EAST FINALES CAMPEONATO MUNDIAL 101 103 2024-01-20 101 3-1
match-002 WINTER2024 NA-EAST SEMIFINALES UPPER 101 105 2024-01-18 101 3-2
match-003 WINTER2024 NA-EAST SEMIFINALES UPPER 103 107 2024-01-18 103 3-0
match-004 WINTER2024 NA-EAST CUARTOS DE FINAL UPPER 101 109 2024-01-15 101 3-1
match-005 WINTER2024 NA-WEST FINALES CAMPEONATO MUNDIAL 102 104 2024-01-20 102 3-2
match-006 WINTER2024 NA-WEST SEMIFINALES UPPER 102 106 2024-01-18 102 3-1
match-007 SPRING2024 NA-EAST CUARTOS DE FINAL UPPER 101 108 2024-03-15 101 3-0
match-008 SPRING2024 NA-EAST CUARTOS DE FINAL LOWER 103 110 2024-03-15 103 3-2

GSI: TournamentRegionIndex (claves de varios atributos)

tournamentId (PK) región (PK) ronda (SK) soporte (SK) matchId (SK) player1Id player2Id matchDate ganador puntuación
WINTER2024 NA-EAST FINALES CAMPEONATO MUNDIAL match-001 101 103 2024-01-20 101 3-1
WINTER2024 NA-EAST CUARTOS DE FINAL UPPER match-004 101 109 2024-01-15 101 3-1
WINTER2024 NA-EAST SEMIFINALES UPPER match-002 101 105 2024-01-18 101 3-2
WINTER2024 NA-EAST SEMIFINALES UPPER match-003 103 107 2024-01-18 103 3-0
WINTER2024 NA-WEST FINALES CAMPEONATO MUNDIAL match-005 102 104 2024-01-20 102 3-2
WINTER2024 NA-WEST SEMIFINALES UPPER match-006 102 106 2024-01-18 102 3-1
SPRING2024 NA-EAST CUARTOS DE FINAL LOWER match-008 103 110 2024-03-15 103 3-2
SPRING2024 NA-EAST CUARTOS DE FINAL UPPER match-007 101 108 2024-03-15 101 3-0

GSI: PlayerMatchHistoryIndex (claves de varios atributos)

player1Id (PK) matchDate (SK) ronda (SK) tournamentId region soporte matchId player2Id ganador puntuación
101 2024-01-15 CUARTOS DE FINAL WINTER2024 NA-EAST UPPER match-004 109 101 3-1
101 2024-01-18 SEMIFINALES WINTER2024 NA-EAST UPPER match-002 105 101 3-2
101 2024-01-20 FINALES WINTER2024 NA-EAST CAMPEONATO MUNDIAL match-001 103 101 3-1
101 2024-03-15 CUARTOS DE FINAL SPRING2024 NA-EAST UPPER match-007 108 101 3-0
102 2024-01-18 SEMIFINALES WINTER2024 NA-WEST UPPER match-006 106 102 3-1
102 2024-01-20 FINALES WINTER2024 NA-WEST CAMPEONATO MUNDIAL match-005 104 102 3-2
103 2024-01-18 SEMIFINALES WINTER2024 NA-EAST UPPER match-003 107 103 3-0
103 2024-03-15 CUARTOS DE FINAL SPRING2024 NA-EAST LOWER match-008 110 103 3-2

Requisitos previos

Antes de comenzar, asegúrese de que dispone de lo siguiente:

Permisos y cuenta

  • Una cuenta de AWS activa (cree una aquí si es necesario)

  • Permisos de IAM para las operaciones de DynamoDB:

    • dynamodb:CreateTable

    • dynamodb:DeleteTable

    • dynamodb:DescribeTable

    • dynamodb:PutItem

    • dynamodb:Query

    • dynamodb:BatchWriteItem

nota

Nota de seguridad: para uso de producción, cree una política de IAM personalizada con solo los permisos que necesite. Para este tutorial, puede usar la política administrada de AWS AmazonDynamoDBFullAccessV2.

Entorno de desarrollo

  • Node.js se ha instalado en el equipo

  • Credenciales de AWS configuradas mediante uno de estos métodos:

Opción 1: CLI de AWS

aws configure

Opción 2: Variables de entorno

export AWS_ACCESS_KEY_ID=your_access_key_here export AWS_SECRET_ACCESS_KEY=your_secret_key_here export AWS_DEFAULT_REGION=us-east-1

Instalación de los paquetes obligatorios

npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb

Implementación

Paso 1: creación de una tabla con los GSI mediante claves de varios atributos

Cree una tabla con una estructura de clave básica simple y con GSI que utilicen claves de varios atributos.

import { DynamoDBClient, CreateTableCommand } from "@aws-sdk/client-dynamodb"; const client = new DynamoDBClient({ region: 'us-west-2' }); const response = await client.send(new CreateTableCommand({ TableName: 'TournamentMatches', // Base table: Simple partition key KeySchema: [ { AttributeName: 'matchId', KeyType: 'HASH' } // Simple PK ], AttributeDefinitions: [ { AttributeName: 'matchId', AttributeType: 'S' }, { AttributeName: 'tournamentId', AttributeType: 'S' }, { AttributeName: 'region', AttributeType: 'S' }, { AttributeName: 'round', AttributeType: 'S' }, { AttributeName: 'bracket', AttributeType: 'S' }, { AttributeName: 'player1Id', AttributeType: 'S' }, { AttributeName: 'matchDate', AttributeType: 'S' } ], // GSIs with multi-attribute keys GlobalSecondaryIndexes: [ { IndexName: 'TournamentRegionIndex', KeySchema: [ { AttributeName: 'tournamentId', KeyType: 'HASH' }, // GSI PK attribute 1 { AttributeName: 'region', KeyType: 'HASH' }, // GSI PK attribute 2 { AttributeName: 'round', KeyType: 'RANGE' }, // GSI SK attribute 1 { AttributeName: 'bracket', KeyType: 'RANGE' }, // GSI SK attribute 2 { AttributeName: 'matchId', KeyType: 'RANGE' } // GSI SK attribute 3 ], Projection: { ProjectionType: 'ALL' } }, { IndexName: 'PlayerMatchHistoryIndex', KeySchema: [ { AttributeName: 'player1Id', KeyType: 'HASH' }, // GSI PK { AttributeName: 'matchDate', KeyType: 'RANGE' }, // GSI SK attribute 1 { AttributeName: 'round', KeyType: 'RANGE' } // GSI SK attribute 2 ], Projection: { ProjectionType: 'ALL' } } ], BillingMode: 'PAY_PER_REQUEST' })); console.log("Table with multi-attribute GSI keys created successfully");

Decisiones de diseño de clave:

Tabla base: la tabla base utiliza una clave de partición de matchId simple para las búsquedas de coincidencias directas, lo que mantiene la estructura de la tabla base sencilla, mientras que los GSI proporcionan patrones de consulta complejos.

Índice secundario global TournamentRegionIndex: el índice secundario global TournamentRegionIndex utiliza tournamentId + region como clave de partición con varios atributos, lo que aísla la región del torneo, ya que los datos se distribuyen mediante el hash de ambos atributos combinados, lo que permite realizar consultas eficaces dentro de un contexto específico de una región o torneo. La clave de clasificación de varios atributos (round + bracket + matchId) proporciona una clasificación jerárquica que permite realizar consultas en cualquier nivel de la jerarquía con un orden natural, desde general (ronda) hasta específico (ID de partido).

Índice secundario global PlayerMatchHistoryIndex: el índice secundario global PlayerMatchHistoryIndex reorganiza los datos por jugador usando player1Id como clave de partición, lo que permite realizar consultas entre torneos para un jugador específico. La clave de clasificación con varios atributos (matchDate + round) proporciona un orden cronológico y permite filtrar por intervalos de fechas o rondas específicas del torneo.

Paso 2: insertar datos con atributos nativos

Agrega los datos de los partidos del torneo con atributos naturales. El GSI indexará automáticamente estos atributos sin necesidad de claves sintéticas.

import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb"; const client = new DynamoDBClient({ region: 'us-west-2' }); const docClient = DynamoDBDocumentClient.from(client); // Tournament match data - no synthetic keys needed for GSIs const matches = [ // Winter 2024 Tournament, NA-EAST region { matchId: 'match-001', tournamentId: 'WINTER2024', region: 'NA-EAST', round: 'FINALS', bracket: 'CHAMPIONSHIP', player1Id: '101', player2Id: '103', matchDate: '2024-01-20', winner: '101', score: '3-1' }, { matchId: 'match-002', tournamentId: 'WINTER2024', region: 'NA-EAST', round: 'SEMIFINALS', bracket: 'UPPER', player1Id: '101', player2Id: '105', matchDate: '2024-01-18', winner: '101', score: '3-2' }, { matchId: 'match-003', tournamentId: 'WINTER2024', region: 'NA-EAST', round: 'SEMIFINALS', bracket: 'UPPER', player1Id: '103', player2Id: '107', matchDate: '2024-01-18', winner: '103', score: '3-0' }, { matchId: 'match-004', tournamentId: 'WINTER2024', region: 'NA-EAST', round: 'QUARTERFINALS', bracket: 'UPPER', player1Id: '101', player2Id: '109', matchDate: '2024-01-15', winner: '101', score: '3-1' }, // Winter 2024 Tournament, NA-WEST region { matchId: 'match-005', tournamentId: 'WINTER2024', region: 'NA-WEST', round: 'FINALS', bracket: 'CHAMPIONSHIP', player1Id: '102', player2Id: '104', matchDate: '2024-01-20', winner: '102', score: '3-2' }, { matchId: 'match-006', tournamentId: 'WINTER2024', region: 'NA-WEST', round: 'SEMIFINALS', bracket: 'UPPER', player1Id: '102', player2Id: '106', matchDate: '2024-01-18', winner: '102', score: '3-1' }, // Spring 2024 Tournament, NA-EAST region { matchId: 'match-007', tournamentId: 'SPRING2024', region: 'NA-EAST', round: 'QUARTERFINALS', bracket: 'UPPER', player1Id: '101', player2Id: '108', matchDate: '2024-03-15', winner: '101', score: '3-0' }, { matchId: 'match-008', tournamentId: 'SPRING2024', region: 'NA-EAST', round: 'QUARTERFINALS', bracket: 'LOWER', player1Id: '103', player2Id: '110', matchDate: '2024-03-15', winner: '103', score: '3-2' } ]; // Insert all matches for (const match of matches) { await docClient.send(new PutCommand({ TableName: 'TournamentMatches', Item: match })); console.log(`Added: ${match.matchId} - ${match.tournamentId}/${match.region} - ${match.round} ${match.bracket}`); } console.log(`\nInserted ${matches.length} tournament matches`); console.log("No synthetic keys created - GSIs use native attributes automatically");

Explicación de la estructura de datos:

Uso de atributos naturales: cada atributo representa un concepto de torneo real sin necesidad de concatenar cadenas ni analizar, lo que proporciona una asignación directa al modelo de dominio.

Indexación automática del índice secundario global: los GSI indexan automáticamente los elementos utilizando los atributos existentes (tournamentId, region, round, bracket, matchId para TournamentRegionIndex y player1Id, matchDate, round para PlayerMatchHistoryIndex) sin necesidad de claves concatenadas sintéticas.

No es necesario rellenar: al agregar un nuevo índice secundario global con claves de varios atributos a una tabla existente, DynamoDB indexa automáticamente todos los elementos existentes con sus atributos naturales, sin necesidad de actualizar los elementos con claves sintéticas.

Paso 3: consulte el índice secundario global de TournamentRegionIndex con todos los atributos de las claves de partición

En este ejemplo, se consulta el índice secundario global TournamentRegionIndex, que tiene una clave de partición de varios atributos (tournamentId + region). Todos los atributos de la clave de partición se deben especificar con condiciones de igualdad en las consultas; no se pueden consultar solo con tournamentId ni utilizar operadores de desigualdad en los atributos de la clave de partición.

import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, QueryCommand } from "@aws-sdk/lib-dynamodb"; const client = new DynamoDBClient({ region: 'us-west-2' }); const docClient = DynamoDBDocumentClient.from(client); // Query GSI: All matches for WINTER2024 tournament in NA-EAST region const response = await docClient.send(new QueryCommand({ TableName: 'TournamentMatches', IndexName: 'TournamentRegionIndex', KeyConditionExpression: 'tournamentId = :tournament AND #region = :region', ExpressionAttributeNames: { '#region': 'region', // 'region' is a reserved keyword '#tournament': 'tournament' }, ExpressionAttributeValues: { ':tournament': 'WINTER2024', ':region': 'NA-EAST' } })); console.log(`Found ${response.Items.length} matches for WINTER2024/NA-EAST:\n`); response.Items.forEach(match => { console.log(` ${match.round} | ${match.bracket} | ${match.matchId}`); console.log(` Players: ${match.player1Id} vs ${match.player2Id}`); console.log(` Winner: ${match.winner}, Score: ${match.score}\n`); });

Resultado previsto:

Found 4 matches for WINTER2024/NA-EAST:

  FINALS | CHAMPIONSHIP | match-001
    Players: 101 vs 103
    Winner: 101, Score: 3-1

  QUARTERFINALS | UPPER | match-004
    Players: 101 vs 109
    Winner: 101, Score: 3-1

  SEMIFINALS | UPPER | match-002
    Players: 101 vs 105
    Winner: 101, Score: 3-2

  SEMIFINALS | UPPER | match-003
    Players: 103 vs 107
    Winner: 103, Score: 3-0

Consultas no válidas:

// Missing region attribute KeyConditionExpression: 'tournamentId = :tournament' // Using inequality on partition key attribute KeyConditionExpression: 'tournamentId = :tournament AND #region > :region'

Rendimiento: las claves de partición de varios atributos se codifican entre sí, lo que proporciona el mismo rendimiento de búsqueda O(1) que las claves de un solo atributo.

Paso 4: consulte las claves de clasificación del índice secundario global de izquierda a derecha

Los atributos de clave de clasificación se deben consultar de izquierda a derecha en el orden en que están definidos en el índice secundario global. En este ejemplo, se muestra cómo consultar el TournamentRegionIndex en diferentes niveles jerárquicos: filtrando solo por round, por round + bracket, o por los tres atributos de clave de clasificación. No puede omitir los atributos que están en el centro; por ejemplo, no puede consultar round y matchId mientras se omite bracket.

import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, QueryCommand } from "@aws-sdk/lib-dynamodb"; const client = new DynamoDBClient({ region: 'us-west-2' }); const docClient = DynamoDBDocumentClient.from(client); // Query 1: Filter by first sort key attribute (round) console.log("Query 1: All SEMIFINALS matches"); const query1 = await docClient.send(new QueryCommand({ TableName: 'TournamentMatches', IndexName: 'TournamentRegionIndex', KeyConditionExpression: 'tournamentId = :tournament AND #region = :region AND round = :round', ExpressionAttributeNames: { '#region': 'region' // 'region' is a reserved keyword }, ExpressionAttributeValues: { ':tournament': 'WINTER2024', ':region': 'NA-EAST', ':round': 'SEMIFINALS' } })); console.log(` Found ${query1.Items.length} matches\n`); // Query 2: Filter by first two sort key attributes (round + bracket) console.log("Query 2: SEMIFINALS UPPER bracket matches"); const query2 = await docClient.send(new QueryCommand({ TableName: 'TournamentMatches', IndexName: 'TournamentRegionIndex', KeyConditionExpression: 'tournamentId = :tournament AND #region = :region AND round = :round AND bracket = :bracket', ExpressionAttributeNames: { '#region': 'region' // 'region' is a reserved keyword }, ExpressionAttributeValues: { ':tournament': 'WINTER2024', ':region': 'NA-EAST', ':round': 'SEMIFINALS', ':bracket': 'UPPER' } })); console.log(` Found ${query2.Items.length} matches\n`); // Query 3: Filter by all three sort key attributes (round + bracket + matchId) console.log("Query 3: Specific match in SEMIFINALS UPPER bracket"); const query3 = await docClient.send(new QueryCommand({ TableName: 'TournamentMatches', IndexName: 'TournamentRegionIndex', KeyConditionExpression: 'tournamentId = :tournament AND #region = :region AND round = :round AND bracket = :bracket AND matchId = :matchId', ExpressionAttributeNames: { '#region': 'region' // 'region' is a reserved keyword }, ExpressionAttributeValues: { ':tournament': 'WINTER2024', ':region': 'NA-EAST', ':round': 'SEMIFINALS', ':bracket': 'UPPER', ':matchId': 'match-002' } })); console.log(` Found ${query3.Items.length} matches\n`); // Query 4: INVALID - skipping round console.log("Query 4: Attempting to skip first sort key attribute (WILL FAIL)"); try { const query4 = await docClient.send(new QueryCommand({ TableName: 'TournamentMatches', IndexName: 'TournamentRegionIndex', KeyConditionExpression: 'tournamentId = :tournament AND #region = :region AND bracket = :bracket', ExpressionAttributeNames: { '#region': 'region' // 'region' is a reserved keyword }, ExpressionAttributeValues: { ':tournament': 'WINTER2024', ':region': 'NA-EAST', ':bracket': 'UPPER' } })); } catch (error) { console.log(` Error: ${error.message}`); console.log(` Cannot skip sort key attributes - must query left-to-right\n`); }

Resultado previsto:

Query 1: All SEMIFINALS matches
  Found 2 matches

Query 2: SEMIFINALS UPPER bracket matches
  Found 2 matches

Query 3: Specific match in SEMIFINALS UPPER bracket
  Found 1 matches

Query 4: Attempting to skip first sort key attribute (WILL FAIL)
  Error: Query key condition not supported
  Cannot skip sort key attributes - must query left-to-right

Reglas de consulta de izquierda a derecha: debe consultar los atributos en orden de izquierda a derecha, sin omitir ninguno.

Patrones validos:

  • Solo el primer atributo: round = 'SEMIFINALS'

  • Los dos primeros atributos: round = 'SEMIFINALS' AND bracket = 'UPPER'

  • Los tres atributos: round = 'SEMIFINALS' AND bracket = 'UPPER' AND matchId = 'match-002'

Patrones no válidos:

  • Omisión del primer atributo: bracket = 'UPPER' (se salta)

  • La consulta está fuera de orden: matchId = 'match-002' AND round = 'SEMIFINALS'

  • Dejar huecos: round = 'SEMIFINALS' AND matchId = 'match-002' (se salta el corchete)

nota

Consejo de diseño: ordene los atributos de la clave de clasificación desde los más generales hasta los más específicos para maximizar la flexibilidad de las consultas.

Paso 5: uso de las condiciones de desigualdad en las claves de clasificación del índice secundario global

Las condiciones de desigualdad deben ser la última condición de la consulta. En este ejemplo, se muestra el uso de los operadores de comparación (>=, BETWEEN) y la coincidencia de prefijos (begins_with()) en los atributos de las claves de clasificación. Una vez que utilice un operador de desigualdad, no podrá agregar ninguna condición de clave de clasificación adicional después de este; la desigualdad debe ser la condición final de la expresión de condición de la clave.

import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, QueryCommand } from "@aws-sdk/lib-dynamodb"; const client = new DynamoDBClient({ region: 'us-west-2' }); const docClient = DynamoDBDocumentClient.from(client); // Query 1: Round comparison (inequality on first sort key attribute) console.log("Query 1: Matches from QUARTERFINALS onwards"); const query1 = await docClient.send(new QueryCommand({ TableName: 'TournamentMatches', IndexName: 'TournamentRegionIndex', KeyConditionExpression: 'tournamentId = :tournament AND #region = :region AND round >= :round', ExpressionAttributeNames: { '#region': 'region' // 'region' is a reserved keyword }, ExpressionAttributeValues: { ':tournament': 'WINTER2024', ':region': 'NA-EAST', ':round': 'QUARTERFINALS' } })); console.log(` Found ${query1.Items.length} matches\n`); // Query 2: Round range with BETWEEN console.log("Query 2: Matches between QUARTERFINALS and SEMIFINALS"); const query2 = await docClient.send(new QueryCommand({ TableName: 'TournamentMatches', IndexName: 'TournamentRegionIndex', KeyConditionExpression: 'tournamentId = :tournament AND #region = :region AND round BETWEEN :start AND :end', ExpressionAttributeNames: { '#region': 'region' // 'region' is a reserved keyword }, ExpressionAttributeValues: { ':tournament': 'WINTER2024', ':region': 'NA-EAST', ':start': 'QUARTERFINALS', ':end': 'SEMIFINALS' } })); console.log(` Found ${query2.Items.length} matches\n`); // Query 3: Prefix matching with begins_with (treated as inequality) console.log("Query 3: Matches in brackets starting with 'U'"); const query3 = await docClient.send(new QueryCommand({ TableName: 'TournamentMatches', IndexName: 'TournamentRegionIndex', KeyConditionExpression: 'tournamentId = :tournament AND #region = :region AND round = :round AND begins_with(bracket, :prefix)', ExpressionAttributeNames: { '#region': 'region' // 'region' is a reserved keyword }, ExpressionAttributeValues: { ':tournament': 'WINTER2024', ':region': 'NA-EAST', ':round': 'SEMIFINALS', ':prefix': 'U' } })); console.log(` Found ${query3.Items.length} matches\n`); // Query 4: INVALID - condition after inequality console.log("Query 4: Attempting condition after inequality (WILL FAIL)"); try { const query4 = await docClient.send(new QueryCommand({ TableName: 'TournamentMatches', IndexName: 'TournamentRegionIndex', KeyConditionExpression: 'tournamentId = :tournament AND #region = :region AND round > :round AND bracket = :bracket', ExpressionAttributeNames: { '#region': 'region' // 'region' is a reserved keyword }, ExpressionAttributeValues: { ':tournament': 'WINTER2024', ':region': 'NA-EAST', ':round': 'QUARTERFINALS', ':bracket': 'UPPER' } })); } catch (error) { console.log(` Error: ${error.message}`); console.log(` Cannot add conditions after inequality - it must be last\n`); }

Reglas de operadores de desigualdad: puede usar operadores de comparación (>, >=, <, <=) BETWEEN para consultas de rango y begins_with() para hacer coincidir prefijos. La desigualdad deben ser la última condición de la consulta.

Patrones validos:

  • Condiciones de igualdad seguidas de desigualdad: round = 'SEMIFINALS' AND bracket = 'UPPER' AND matchId > 'match-001'

  • Desigualdad en el primer atributo: round BETWEEN 'QUARTERFINALS' AND 'SEMIFINALS'

  • La coincidencia de prefijos como condición final: round = 'SEMIFINALS' AND begins_with(bracket, 'U')

Patrones no válidos:

  • Agregación de condiciones después de una desigualdad: round > 'QUARTERFINALS' AND bracket = 'UPPER'

  • Uso de múltiples desigualdades: round > 'QUARTERFINALS' AND bracket > 'L'

importante

begins_with() se trata como una condición de desigualdad, por lo que no puede seguirla ninguna condición de clave de clasificación adicional.

Paso 6: consulta del índice secundario global PlayerMatchHistoryIndex con la clave de clasificación de varios atributos

En este ejemplo, se consulta el PlayerMatchHistoryIndex, que tiene una clave de partición única (player1Id) y una clave de clasificación de varios atributos (matchDate + round). Esto permite analizar todos los torneos consultando todos los partidos de un jugador específico sin conocer los ID del torneo, mientras que la tabla base requeriría consultas independientes por combinación de torneo y región.

import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, QueryCommand } from "@aws-sdk/lib-dynamodb"; const client = new DynamoDBClient({ region: 'us-west-2' }); const docClient = DynamoDBDocumentClient.from(client); // Query 1: All matches for Player 101 across all tournaments console.log("Query 1: All matches for Player 101"); const query1 = await docClient.send(new QueryCommand({ TableName: 'TournamentMatches', IndexName: 'PlayerMatchHistoryIndex', KeyConditionExpression: 'player1Id = :player', ExpressionAttributeValues: { ':player': '101' } })); console.log(` Found ${query1.Items.length} matches for Player 101:`); query1.Items.forEach(match => { console.log(` ${match.tournamentId}/${match.region} - ${match.matchDate} - ${match.round}`); }); console.log(); // Query 2: Player 101 matches on specific date console.log("Query 2: Player 101 matches on 2024-01-18"); const query2 = await docClient.send(new QueryCommand({ TableName: 'TournamentMatches', IndexName: 'PlayerMatchHistoryIndex', KeyConditionExpression: 'player1Id = :player AND matchDate = :date', ExpressionAttributeValues: { ':player': '101', ':date': '2024-01-18' } })); console.log(` Found ${query2.Items.length} matches\n`); // Query 3: Player 101 SEMIFINALS matches on specific date console.log("Query 3: Player 101 SEMIFINALS matches on 2024-01-18"); const query3 = await docClient.send(new QueryCommand({ TableName: 'TournamentMatches', IndexName: 'PlayerMatchHistoryIndex', KeyConditionExpression: 'player1Id = :player AND matchDate = :date AND round = :round', ExpressionAttributeValues: { ':player': '101', ':date': '2024-01-18', ':round': 'SEMIFINALS' } })); console.log(` Found ${query3.Items.length} matches\n`); // Query 4: Player 101 matches in date range console.log("Query 4: Player 101 matches in January 2024"); const query4 = await docClient.send(new QueryCommand({ TableName: 'TournamentMatches', IndexName: 'PlayerMatchHistoryIndex', KeyConditionExpression: 'player1Id = :player AND matchDate BETWEEN :start AND :end', ExpressionAttributeValues: { ':player': '101', ':start': '2024-01-01', ':end': '2024-01-31' } })); console.log(` Found ${query4.Items.length} matches\n`);

Variaciones de patrones

Datos de series temporales con claves de varios atributos

Optimización de las consultas de series temporales con atributos temporales jerárquicos

{ TableName: 'IoTReadings', // Base table: Simple partition key KeySchema: [ { AttributeName: 'readingId', KeyType: 'HASH' } ], AttributeDefinitions: [ { AttributeName: 'readingId', AttributeType: 'S' }, { AttributeName: 'deviceId', AttributeType: 'S' }, { AttributeName: 'locationId', AttributeType: 'S' }, { AttributeName: 'year', AttributeType: 'S' }, { AttributeName: 'month', AttributeType: 'S' }, { AttributeName: 'day', AttributeType: 'S' }, { AttributeName: 'timestamp', AttributeType: 'S' } ], // GSI with multi-attribute keys for time-series queries GlobalSecondaryIndexes: [{ IndexName: 'DeviceLocationTimeIndex', KeySchema: [ { AttributeName: 'deviceId', KeyType: 'HASH' }, { AttributeName: 'locationId', KeyType: 'HASH' }, { AttributeName: 'year', KeyType: 'RANGE' }, { AttributeName: 'month', KeyType: 'RANGE' }, { AttributeName: 'day', KeyType: 'RANGE' }, { AttributeName: 'timestamp', KeyType: 'RANGE' } ], Projection: { ProjectionType: 'ALL' } }], BillingMode: 'PAY_PER_REQUEST' } // Query patterns enabled via GSI: // - All readings for device in location // - Readings for specific year // - Readings for specific month in year // - Readings for specific day // - Readings in time range

Beneficios: la jerarquía temporal natural (año → mes → día → marca temporal) permite realizar consultas eficientes con una granularidad en cualquier momento sin necesidad de analizar ni manipular la fecha. El índice secundario global indexa automáticamente todas las lecturas con sus atributos de tiempo natural.

Pedidos de comercio electrónico con claves de varios atributos

Realización de un seguimiento de los pedidos con múltiples dimensiones

{ TableName: 'Orders', // Base table: Simple partition key KeySchema: [ { AttributeName: 'orderId', KeyType: 'HASH' } ], AttributeDefinitions: [ { AttributeName: 'orderId', AttributeType: 'S' }, { AttributeName: 'sellerId', AttributeType: 'S' }, { AttributeName: 'region', AttributeType: 'S' }, { AttributeName: 'orderDate', AttributeType: 'S' }, { AttributeName: 'category', AttributeType: 'S' }, { AttributeName: 'customerId', AttributeType: 'S' }, { AttributeName: 'orderStatus', AttributeType: 'S' } ], GlobalSecondaryIndexes: [ { IndexName: 'SellerRegionIndex', KeySchema: [ { AttributeName: 'sellerId', KeyType: 'HASH' }, { AttributeName: 'region', KeyType: 'HASH' }, { AttributeName: 'orderDate', KeyType: 'RANGE' }, { AttributeName: 'category', KeyType: 'RANGE' }, { AttributeName: 'orderId', KeyType: 'RANGE' } ], Projection: { ProjectionType: 'ALL' } }, { IndexName: 'CustomerOrdersIndex', KeySchema: [ { AttributeName: 'customerId', KeyType: 'HASH' }, { AttributeName: 'orderDate', KeyType: 'RANGE' }, { AttributeName: 'orderStatus', KeyType: 'RANGE' } ], Projection: { ProjectionType: 'ALL' } } ], BillingMode: 'PAY_PER_REQUEST' } // SellerRegionIndex GSI queries: // - Orders by seller and region // - Orders by seller, region, and date // - Orders by seller, region, date, and category // CustomerOrdersIndex GSI queries: // - Customer's orders // - Customer's orders by date // - Customer's orders by date and status

Datos de organización jerárquica

Jerarquías organizativas de modelos

{ TableName: 'Employees', // Base table: Simple partition key KeySchema: [ { AttributeName: 'employeeId', KeyType: 'HASH' } ], AttributeDefinitions: [ { AttributeName: 'employeeId', AttributeType: 'S' }, { AttributeName: 'companyId', AttributeType: 'S' }, { AttributeName: 'divisionId', AttributeType: 'S' }, { AttributeName: 'departmentId', AttributeType: 'S' }, { AttributeName: 'teamId', AttributeType: 'S' }, { AttributeName: 'skillCategory', AttributeType: 'S' }, { AttributeName: 'skillLevel', AttributeType: 'S' }, { AttributeName: 'yearsExperience', AttributeType: 'N' } ], GlobalSecondaryIndexes: [ { IndexName: 'OrganizationIndex', KeySchema: [ { AttributeName: 'companyId', KeyType: 'HASH' }, { AttributeName: 'divisionId', KeyType: 'HASH' }, { AttributeName: 'departmentId', KeyType: 'RANGE' }, { AttributeName: 'teamId', KeyType: 'RANGE' }, { AttributeName: 'employeeId', KeyType: 'RANGE' } ], Projection: { ProjectionType: 'ALL' } }, { IndexName: 'SkillsIndex', KeySchema: [ { AttributeName: 'skillCategory', KeyType: 'HASH' }, { AttributeName: 'skillLevel', KeyType: 'RANGE' }, { AttributeName: 'yearsExperience', KeyType: 'RANGE' } ], Projection: { ProjectionType: 'INCLUDE', NonKeyAttributes: ['employeeId', 'name'] } } ], BillingMode: 'PAY_PER_REQUEST' } // OrganizationIndex GSI query patterns: // - All employees in company/division // - Employees in specific department // - Employees in specific team // SkillsIndex GSI query patterns: // - Employees by skill and experience level

Claves dispersas de varios atributos

Combinación de claves de varios atributos para crear un GSI disperso

{ TableName: 'Products', // Base table: Simple partition key KeySchema: [ { AttributeName: 'productId', KeyType: 'HASH' } ], AttributeDefinitions: [ { AttributeName: 'productId', AttributeType: 'S' }, { AttributeName: 'categoryId', AttributeType: 'S' }, { AttributeName: 'subcategoryId', AttributeType: 'S' }, { AttributeName: 'averageRating', AttributeType: 'N' }, { AttributeName: 'reviewCount', AttributeType: 'N' } ], GlobalSecondaryIndexes: [ { IndexName: 'CategoryIndex', KeySchema: [ { AttributeName: 'categoryId', KeyType: 'HASH' }, { AttributeName: 'subcategoryId', KeyType: 'HASH' }, { AttributeName: 'productId', KeyType: 'RANGE' } ], Projection: { ProjectionType: 'ALL' } }, { IndexName: 'ReviewedProductsIndex', KeySchema: [ { AttributeName: 'categoryId', KeyType: 'HASH' }, { AttributeName: 'averageRating', KeyType: 'RANGE' }, // Optional attribute { AttributeName: 'reviewCount', KeyType: 'RANGE' } // Optional attribute ], Projection: { ProjectionType: 'ALL' } } ], BillingMode: 'PAY_PER_REQUEST' } // Only products with reviews appear in ReviewedProductsIndex GSI // Automatic filtering without application logic // Multi-attribute sort key enables rating and count queries

Multitenencia de SaaS

Plataforma SaaS multiusuario con aislamiento de clientes

// Table design { TableName: 'SaasData', // Base table: Simple partition key KeySchema: [ { AttributeName: 'resourceId', KeyType: 'HASH' } ], AttributeDefinitions: [ { AttributeName: 'resourceId', AttributeType: 'S' }, { AttributeName: 'tenantId', AttributeType: 'S' }, { AttributeName: 'customerId', AttributeType: 'S' }, { AttributeName: 'resourceType', AttributeType: 'S' } ], // GSI with multi-attribute keys for tenant-customer isolation GlobalSecondaryIndexes: [{ IndexName: 'TenantCustomerIndex', KeySchema: [ { AttributeName: 'tenantId', KeyType: 'HASH' }, { AttributeName: 'customerId', KeyType: 'HASH' }, { AttributeName: 'resourceType', KeyType: 'RANGE' }, { AttributeName: 'resourceId', KeyType: 'RANGE' } ], Projection: { ProjectionType: 'ALL' } }], BillingMode: 'PAY_PER_REQUEST' } // Query GSI: All resources for tenant T001, customer C001 const resources = await docClient.send(new QueryCommand({ TableName: 'SaasData', IndexName: 'TenantCustomerIndex', KeyConditionExpression: 'tenantId = :tenant AND customerId = :customer', ExpressionAttributeValues: { ':tenant': 'T001', ':customer': 'C001' } })); // Query GSI: Specific resource type for tenant/customer const documents = await docClient.send(new QueryCommand({ TableName: 'SaasData', IndexName: 'TenantCustomerIndex', KeyConditionExpression: 'tenantId = :tenant AND customerId = :customer AND resourceType = :type', ExpressionAttributeValues: { ':tenant': 'T001', ':customer': 'C001', ':type': 'document' } }));

Beneficios: consultas eficientes en el contexto de inquilino-cliente y organización natural de los datos.

Transacciones financieras

Sistema bancario que rastrea las transacciones de las cuentas mediante GSI

// Table design { TableName: 'BankTransactions', // Base table: Simple partition key KeySchema: [ { AttributeName: 'transactionId', KeyType: 'HASH' } ], AttributeDefinitions: [ { AttributeName: 'transactionId', AttributeType: 'S' }, { AttributeName: 'accountId', AttributeType: 'S' }, { AttributeName: 'year', AttributeType: 'S' }, { AttributeName: 'month', AttributeType: 'S' }, { AttributeName: 'day', AttributeType: 'S' }, { AttributeName: 'transactionType', AttributeType: 'S' } ], GlobalSecondaryIndexes: [ { IndexName: 'AccountTimeIndex', KeySchema: [ { AttributeName: 'accountId', KeyType: 'HASH' }, { AttributeName: 'year', KeyType: 'RANGE' }, { AttributeName: 'month', KeyType: 'RANGE' }, { AttributeName: 'day', KeyType: 'RANGE' }, { AttributeName: 'transactionId', KeyType: 'RANGE' } ], Projection: { ProjectionType: 'ALL' } }, { IndexName: 'TransactionTypeIndex', KeySchema: [ { AttributeName: 'accountId', KeyType: 'HASH' }, { AttributeName: 'transactionType', KeyType: 'RANGE' }, { AttributeName: 'year', KeyType: 'RANGE' }, { AttributeName: 'month', KeyType: 'RANGE' } ], Projection: { ProjectionType: 'ALL' } } ], BillingMode: 'PAY_PER_REQUEST' } // Query AccountTimeIndex GSI: All transactions for account in 2023 const yearTransactions = await docClient.send(new QueryCommand({ TableName: 'BankTransactions', IndexName: 'AccountTimeIndex', KeyConditionExpression: 'accountId = :account AND #year = :year', ExpressionAttributeNames: { '#year': 'year' }, ExpressionAttributeValues: { ':account': 'ACC-12345', ':year': '2023' } })); // Query AccountTimeIndex GSI: Transactions in specific month const monthTransactions = await docClient.send(new QueryCommand({ TableName: 'BankTransactions', IndexName: 'AccountTimeIndex', KeyConditionExpression: 'accountId = :account AND #year = :year AND #month = :month', ExpressionAttributeNames: { '#year': 'year', '#month': 'month' }, ExpressionAttributeValues: { ':account': 'ACC-12345', ':year': '2023', ':month': '11' } })); // Query TransactionTypeIndex GSI: Deposits in 2023 const deposits = await docClient.send(new QueryCommand({ TableName: 'BankTransactions', IndexName: 'TransactionTypeIndex', KeyConditionExpression: 'accountId = :account AND transactionType = :type AND #year = :year', ExpressionAttributeNames: { '#year': 'year' }, ExpressionAttributeValues: { ':account': 'ACC-12345', ':type': 'deposit', ':year': '2023' } }));

Ejemplo completo

El siguiente ejemplo muestra las claves de varios atributos desde la configuración hasta la limpieza:

import { DynamoDBClient, CreateTableCommand, DeleteTableCommand, waitUntilTableExists } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, PutCommand, QueryCommand } from "@aws-sdk/lib-dynamodb"; const client = new DynamoDBClient({ region: 'us-west-2' }); const docClient = DynamoDBDocumentClient.from(client); async function multiAttributeKeysDemo() { console.log("Starting Multi-Attribute GSI Keys Demo\n"); // Step 1: Create table with GSIs using multi-attribute keys console.log("1. Creating table with multi-attribute GSI keys..."); await client.send(new CreateTableCommand({ TableName: 'TournamentMatches', KeySchema: [ { AttributeName: 'matchId', KeyType: 'HASH' } ], AttributeDefinitions: [ { AttributeName: 'matchId', AttributeType: 'S' }, { AttributeName: 'tournamentId', AttributeType: 'S' }, { AttributeName: 'region', AttributeType: 'S' }, { AttributeName: 'round', AttributeType: 'S' }, { AttributeName: 'bracket', AttributeType: 'S' }, { AttributeName: 'player1Id', AttributeType: 'S' }, { AttributeName: 'matchDate', AttributeType: 'S' } ], GlobalSecondaryIndexes: [ { IndexName: 'TournamentRegionIndex', KeySchema: [ { AttributeName: 'tournamentId', KeyType: 'HASH' }, { AttributeName: 'region', KeyType: 'HASH' }, { AttributeName: 'round', KeyType: 'RANGE' }, { AttributeName: 'bracket', KeyType: 'RANGE' }, { AttributeName: 'matchId', KeyType: 'RANGE' } ], Projection: { ProjectionType: 'ALL' } }, { IndexName: 'PlayerMatchHistoryIndex', KeySchema: [ { AttributeName: 'player1Id', KeyType: 'HASH' }, { AttributeName: 'matchDate', KeyType: 'RANGE' }, { AttributeName: 'round', KeyType: 'RANGE' } ], Projection: { ProjectionType: 'ALL' } } ], BillingMode: 'PAY_PER_REQUEST' })); await waitUntilTableExists({ client, maxWaitTime: 120 }, { TableName: 'TournamentMatches' }); console.log("Table created\n"); // Step 2: Insert tournament matches console.log("2. Inserting tournament matches..."); const matches = [ { matchId: 'match-001', tournamentId: 'WINTER2024', region: 'NA-EAST', round: 'FINALS', bracket: 'CHAMPIONSHIP', player1Id: '101', player2Id: '103', matchDate: '2024-01-20', winner: '101', score: '3-1' }, { matchId: 'match-002', tournamentId: 'WINTER2024', region: 'NA-EAST', round: 'SEMIFINALS', bracket: 'UPPER', player1Id: '101', player2Id: '105', matchDate: '2024-01-18', winner: '101', score: '3-2' }, { matchId: 'match-003', tournamentId: 'WINTER2024', region: 'NA-WEST', round: 'FINALS', bracket: 'CHAMPIONSHIP', player1Id: '102', player2Id: '104', matchDate: '2024-01-20', winner: '102', score: '3-2' }, { matchId: 'match-004', tournamentId: 'SPRING2024', region: 'NA-EAST', round: 'QUARTERFINALS', bracket: 'UPPER', player1Id: '101', player2Id: '108', matchDate: '2024-03-15', winner: '101', score: '3-0' } ]; for (const match of matches) { await docClient.send(new PutCommand({ TableName: 'TournamentMatches', Item: match })); } console.log(`Inserted ${matches.length} tournament matches\n`); // Step 3: Query GSI with multi-attribute partition key console.log("3. Query TournamentRegionIndex GSI: WINTER2024/NA-EAST matches"); const gsiQuery1 = await docClient.send(new QueryCommand({ TableName: 'TournamentMatches', IndexName: 'TournamentRegionIndex', KeyConditionExpression: 'tournamentId = :tournament AND #region = :region', ExpressionAttributeNames: { '#region': 'region' }, ExpressionAttributeValues: { ':tournament': 'WINTER2024', ':region': 'NA-EAST' } })); console.log(` Found ${gsiQuery1.Items.length} matches:`); gsiQuery1.Items.forEach(match => { console.log(` ${match.round} - ${match.bracket} - ${match.winner} won`); }); // Step 4: Query GSI with multi-attribute sort key console.log("\n4. Query PlayerMatchHistoryIndex GSI: All matches for Player 101"); const gsiQuery2 = await docClient.send(new QueryCommand({ TableName: 'TournamentMatches', IndexName: 'PlayerMatchHistoryIndex', KeyConditionExpression: 'player1Id = :player', ExpressionAttributeValues: { ':player': '101' } })); console.log(` Found ${gsiQuery2.Items.length} matches for Player 101:`); gsiQuery2.Items.forEach(match => { console.log(` ${match.tournamentId}/${match.region} - ${match.matchDate} - ${match.round}`); }); console.log("\nDemo complete"); console.log("No synthetic keys needed - GSIs use native attributes automatically"); } async function cleanup() { console.log("Deleting table..."); await client.send(new DeleteTableCommand({ TableName: 'TournamentMatches' })); console.log("Table deleted"); } // Run demo multiAttributeKeysDemo().catch(console.error); // Uncomment to cleanup: // cleanup().catch(console.error);

Estructura de código mínima

// 1. Create table with GSI using multi-attribute keys await client.send(new CreateTableCommand({ TableName: 'MyTable', KeySchema: [ { AttributeName: 'id', KeyType: 'HASH' } // Simple base table PK ], AttributeDefinitions: [ { AttributeName: 'id', AttributeType: 'S' }, { AttributeName: 'attr1', AttributeType: 'S' }, { AttributeName: 'attr2', AttributeType: 'S' }, { AttributeName: 'attr3', AttributeType: 'S' }, { AttributeName: 'attr4', AttributeType: 'S' } ], GlobalSecondaryIndexes: [{ IndexName: 'MyGSI', KeySchema: [ { AttributeName: 'attr1', KeyType: 'HASH' }, // GSI PK attribute 1 { AttributeName: 'attr2', KeyType: 'HASH' }, // GSI PK attribute 2 { AttributeName: 'attr3', KeyType: 'RANGE' }, // GSI SK attribute 1 { AttributeName: 'attr4', KeyType: 'RANGE' } // GSI SK attribute 2 ], Projection: { ProjectionType: 'ALL' } }], BillingMode: 'PAY_PER_REQUEST' })); // 2. Insert items with native attributes (no concatenation needed for GSI) await docClient.send(new PutCommand({ TableName: 'MyTable', Item: { id: 'item-001', attr1: 'value1', attr2: 'value2', attr3: 'value3', attr4: 'value4', // ... other attributes } })); // 3. Query GSI with all partition key attributes await docClient.send(new QueryCommand({ TableName: 'MyTable', IndexName: 'MyGSI', KeyConditionExpression: 'attr1 = :v1 AND attr2 = :v2', ExpressionAttributeValues: { ':v1': 'value1', ':v2': 'value2' } })); // 4. Query GSI with sort key attributes (left-to-right) await docClient.send(new QueryCommand({ TableName: 'MyTable', IndexName: 'MyGSI', KeyConditionExpression: 'attr1 = :v1 AND attr2 = :v2 AND attr3 = :v3', ExpressionAttributeValues: { ':v1': 'value1', ':v2': 'value2', ':v3': 'value3' } })); // Note: If any attribute name is a DynamoDB reserved keyword, use ExpressionAttributeNames: // KeyConditionExpression: 'attr1 = :v1 AND #attr2 = :v2' // ExpressionAttributeNames: { '#attr2': 'attr2' }

Recursos adicionales