Padrão de chaves de vários atributos - Amazon DynamoDB

Padrão de chaves de vários atributos

Visão geral

As chaves de vários atributos permitem criar partições de Índice Secundário Global (GSI) e chaves de classificação compostas por até quatro atributos cada. Isso reduz o código do lado do cliente e facilita a modelagem inicial dos dados e a adição posterior de novos padrões de acesso.

Pense em um cenário comum: para criar um GSI que consulte itens por vários atributos hierárquicos, você tradicionalmente precisaria criar chaves sintéticas concatenando valores. Por exemplo, em uma aplicação de jogos, para consultar partidas de torneios por torneio, região e rodada, você pode criar uma chave de partição de GSI sintética, como TOURNAMENT#WINTER2024#REGION#NA-EAST, e uma chave de classificação sintética, como ROUND#SEMIFINALS#BRACKET#UPPER. Essa abordagem funciona, mas requer concatenação de strings ao gravar dados, analisar durante a leitura e preencher chaves sintéticas em todos os itens existentes se você estiver adicionando o GSI a uma tabela existente. Isso torna o código mais confuso e desafiador para manter a segurança de tipos em componentes de chave individuais.

As chaves de vários atributos resolvem esse problema para GSIs. Você define sua chave de partição de GSI usando vários atributos existentes, como tournamentId e region. O DynamoDB processa a lógica da chave composta automaticamente, juntando-as para distribuição de dados. Você grava itens usando atributos naturais do seu modelo de domínio e o GSI os indexa automaticamente. Sem concatenação, sem análise, sem preenchimento. Seu código permanece limpo, seus dados permanecem tipados e suas consultas permanecem simples. Essa abordagem é particularmente útil quando você tem dados hierárquicos com agrupamentos de atributos naturais (como torneio → região → rodada ou organização → departamento → equipe).

Aplicação de exemplo

Este guia explica a criação de um sistema de rastreamento de partidas de torneios para uma plataforma de esportes eletrônicos. A plataforma precisa consultar partidas de forma eficiente em várias dimensões: por torneio e região para gerenciamento de chaves, por jogador para o histórico de partidas e por data para agendamento.

Modelo de dados

Neste passo a passo, o sistema de rastreamento de partidas do torneio comporta três padrões de acesso primários, cada um exigindo uma estrutura de chave diferente:

Padrão de acesso 1: procure uma correspondência específica por seu ID exclusivo.

  • Solução: tabela base com matchId como chave de partição.

Padrão de acesso 2: consulte todas as partidas de um torneio e região específicos, opcionalmente filtrando por rodada, chave ou partida.

  • Solução: índice secundário global com chave de partição de vários atributos (tournamentId + region) e chave de classificação de vários atributos (round + bracket + matchId).

  • Consultas de exemplo: “Todas as partidas WINTER2024 na região NA-EAST” ou “Todas as partidas de SEMIFINALS na chave UPPER de WINTER2024/NA-EAST”.

Padrão de acesso 3: consulte o histórico de partidas de um jogador, opcionalmente filtrando por intervalo de datas ou rodada do torneio.

  • Solução: índice secundário global com uma chave de partição (player1Id) e uma chave de classificação de vários atributos (matchDate + round).

  • Consultas de exemplo: “Todas as partidas do jogador 101" ou “Partidas do jogador 101 em janeiro de 2024".

A principal diferença entre as abordagens tradicional e de vários atributos fica clara ao examinar a estrutura do item:

Abordagem tradicional do Índice Secundário Global (chaves 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 };

Abordagem de Índice Secundário Global de vários atributos (chaves 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 };

Com chaves de vários atributos, você grava itens uma vez com atributos de domínio naturais. O DynamoDB os indexa automaticamente em vários GSIs sem exigir chaves concatenadas sintéticas.

Esquema da tabela base:

  • Chave de partição: matchId (1 atributo)

Esquema de Índice Secundário Global (TournamentRegionIndex com chaves de vários atributos):

  • Chave de partição: tournamentId, region (2 atributos)

  • Chave de classificação: round, bracket, matchId (3 atributos)

Esquema de Índice Secundário Global (PlayerMatchHistoryIndex com chaves de vários atributos):

  • Chave de partição: player1Id (1 atributo)

  • Chave de classificação: matchDate, round (2 atributos)

Tabela base: TournamentMatches

matchId (PK) tournamentId região round bracket player1Id player2Id matchDate winner pontuação
match-001 WINTER2024 NA-EAST FINALS CHAMPIONSHIP 101 103 2024-01-20 101 3-1
match-002 WINTER2024 NA-EAST SEMIFINALS UPPER 101 105 2024-01-18 101 3-2
match-003 WINTER2024 NA-EAST SEMIFINALS UPPER 103 107 2024-01-18 103 3-0
match-004 WINTER2024 NA-EAST QUARTERFINALS UPPER 101 109 2024-01-15 101 3-1
match-005 WINTER2024 NA-WEST FINALS CHAMPIONSHIP 102 104 2024-01-20 102 3-2
match-006 WINTER2024 NA-WEST SEMIFINALS UPPER 102 106 2024-01-18 102 3-1
match-007 SPRING2024 NA-EAST QUARTERFINALS UPPER 101 108 2024-03-15 101 3-0
match-008 SPRING2024 NA-EAST QUARTERFINALS LOWER 103 110 2024-03-15 103 3-2

GSI: TournamentRegionIndex (chaves de vários atributos)

tournamentId (PK) region (PK) round (SK) bracket (SK) matchId (SK) player1Id player2Id matchDate winner pontuação
WINTER2024 NA-EAST FINALS CHAMPIONSHIP match-001 101 103 2024-01-20 101 3-1
WINTER2024 NA-EAST QUARTERFINALS UPPER match-004 101 109 2024-01-15 101 3-1
WINTER2024 NA-EAST SEMIFINALS UPPER match-002 101 105 2024-01-18 101 3-2
WINTER2024 NA-EAST SEMIFINALS UPPER match-003 103 107 2024-01-18 103 3-0
WINTER2024 NA-WEST FINALS CHAMPIONSHIP match-005 102 104 2024-01-20 102 3-2
WINTER2024 NA-WEST SEMIFINALS UPPER match-006 102 106 2024-01-18 102 3-1
SPRING2024 NA-EAST QUARTERFINALS LOWER match-008 103 110 2024-03-15 103 3-2
SPRING2024 NA-EAST QUARTERFINALS UPPER match-007 101 108 2024-03-15 101 3-0

GSI: PlayerMatchHistoryIndex (chaves de vários atributos)

player1Id (PK) matchDate (SK) round (SK) tournamentId região bracket matchId player2Id winner pontuação
101 2024-01-15 QUARTERFINALS WINTER2024 NA-EAST UPPER match-004 109 101 3-1
101 2024-01-18 SEMIFINALS WINTER2024 NA-EAST UPPER match-002 105 101 3-2
101 2024-01-20 FINALS WINTER2024 NA-EAST CHAMPIONSHIP match-001 103 101 3-1
101 2024-03-15 QUARTERFINALS SPRING2024 NA-EAST UPPER match-007 108 101 3-0
102 2024-01-18 SEMIFINALS WINTER2024 NA-WEST UPPER match-006 106 102 3-1
102 2024-01-20 FINALS WINTER2024 NA-WEST CHAMPIONSHIP match-005 104 102 3-2
103 2024-01-18 SEMIFINALS WINTER2024 NA-EAST UPPER match-003 107 103 3-0
103 2024-03-15 QUARTERFINALS SPRING2024 NA-EAST LOWER match-008 110 103 3-2

Pré-requisitos

Antes de começar, verifique se você tem:

Conta e permissões

  • Uma conta da AWS ativa (crie uma aqui, se necessário)

  • Permissões do IAM para operações do DynamoDB:

    • dynamodb:CreateTable

    • dynamodb:DeleteTable

    • dynamodb:DescribeTable

    • dynamodb:PutItem

    • dynamodb:Query

    • dynamodb:BatchWriteItem

nota

Nota de segurança: para uso em produção, crie uma política do IAM personalizada com apenas as permissões necessárias. Para este tutorial, você pode usar a política gerenciada pela AWS AmazonDynamoDBFullAccessV2.

Ambiente de desenvolvimento

  • Node.js instalado em sua máquina

  • Credenciais da AWS configuradas usando um dos seguintes métodos:

Opção 1: AWS CLI

aws configure

Opção 2: Variáveis de ambiente

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

Instalar os pacotes obrigatórios

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

Implementação

Etapa 1: criar tabela com GSIs usando chaves de vários atributos

Crie uma tabela com uma estrutura de chave básica simples e GSIs que usam chaves de vários 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");

Principais decisões de design:

Tabela base: a tabela base usa uma chave de partição matchId simples para pesquisas diretas de correspondência, mantendo a estrutura da tabela base simples, enquanto os GSIs fornecem padrões de consulta complexos.

Índice Secundário Global TournamentRegionIndex: o Índice Secundário Global TournamentRegionIndex usa tournamentId + region como uma chave de partição de vários atributos, criando um isolamento da região do torneio em que os dados são distribuídos pelo hash de ambos os atributos combinados, permitindo consultas eficientes dentro de um contexto específico da região do torneio. A chave de classificação de vários atributos (round + bracket +matchId) fornece classificação hierárquica que comporta consultas em qualquer nível da hierarquia com ordenação natural do geral (redondo) ao específico (ID de correspondência).

Índice secundário global PlayerMatchHistoryIndex: o Índice Secundário Global PlayerMatchHistoryIndex reorganiza os dados por jogador usando player1Id como chave de partição, permitindo consultas entre torneios para um jogador específico. A chave de classificação de vários atributos (matchDate + round) fornece ordem cronológica com a capacidade de filtrar por intervalos de datas ou rodadas específicas do torneio.

Etapa 2: inserir dados com atributos nativos

Adicione dados da partida do torneio usando atributos naturais. O GSI indexará automaticamente esses atributos sem exigir chaves 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");

Estrutura de dados explicada:

Uso de atributos naturais: cada atributo representa um conceito real de torneio sem necessidade de concatenação ou análise de strings, fornecendo mapeamento direto para o modelo de domínio.

Indexação automática do Índice Secundário Global: os GSIs indexam itens automaticamente usando os atributos existentes (tournamentId, region, round, bracket, matchId para TournamentRegionIndex e player1Id, matchDate, round para PlayerMatchHistoryIndex) sem exigir chaves concatenadas sintéticas.

Sem necessidade de preenchimento: quando você adiciona um novo Índice Secundário Global com chaves de vários atributos a uma tabela existente, o DynamoDB indexa automaticamente todos os itens existentes usando seus atributos naturais, sem a necessidade de atualizar os itens com chaves sintéticas.

Etapa 3: consultar o Índice Secundário Global TournamentRegionIndex com todos os atributos da chave de partição

Este exemplo consulta o Índice Secundário Global TournamentRegionIndex, que tem uma chave de partição de vários atributos (tournamentId + region). Todos os atributos da chave de partição devem ser especificados com condições de igualdade nas consultas, ou seja, você não pode fazer consultas com apenas tournamentId sozinho ou usar operadores de desigualdade nos atributos da chave de partição.

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`); });

Saída esperada:

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 inválidas:

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

Performance: as chaves de partição de vários atributos são combinadas por meio de uma função hash, fornecendo a mesma performance de pesquisa O(1) como chaves de atributo único.

Etapa 4: consultar as chaves de classificação do Índice Secundário Global da esquerda para a direita

Os atributos da chave de classificação devem ser consultados da esquerda para a direita na ordem em que estão definidos no Índice Secundário Global. Este exemplo demonstra a consulta do TournamentRegionIndex em diferentes níveis hierárquicos: filtrando por apenas round, por round + bracket ou por todos os três atributos da chave de classificação. Você não pode ignorar atributos no meio, por exemplo, você não pode consultar por round e matchId e ignorar 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`); }

Saída esperada:

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

Regras de consulta da esquerda para a direita: você deve consultar os atributos na ordem da esquerda para a direita, sem ignorar nenhum.

Padrões válidos:

  • Somente o primeiro atributo: round = 'SEMIFINALS'

  • Primeiros dois atributos: round = 'SEMIFINALS' AND bracket = 'UPPER'

  • Todos os três atributos: round = 'SEMIFINALS' AND bracket = 'UPPER' AND matchId = 'match-002'

Padrões inválidos:

  • Ignorando o primeiro atributo: bracket = 'UPPER' (ignora uma rodada)

  • Consulta fora de ordem: matchId = 'match-002' AND round = 'SEMIFINALS'

  • Deixando lacunas: round = 'SEMIFINALS' AND matchId = 'match-002' (ignora a chave)

nota

Dica de design: ordene os atributos da chave de classificação do mais geral para o mais específico para maximizar a flexibilidade da consulta.

Etapa 5: usar condições de desigualdade nas chaves de classificação do Índice Secundário Global

Condições de desigualdade devem ser a última condição em sua consulta. Este exemplo demonstra o uso de operadores de comparação (>=, BETWEEN) e correspondência de prefixo (begins_with()) nos atributos da chave de classificação. Depois de usar um operador de desigualdade, você não pode adicionar nenhuma condição adicional de chave de classificação depois dele, ou seja, a desigualdade deve ser a condição final em sua expressão de condição de chave.

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`); }

Regras do operador de desigualdade: você pode usar operadores de comparação (>, >=, <, <=) BETWEEN para consultas de intervalo e begins_with() para correspondência de prefixos. A desigualdade deve ser a última condição em sua consulta.

Padrões válidos:

  • Condições de igualdade seguidas de desigualdade: round = 'SEMIFINALS' AND bracket = 'UPPER' AND matchId > 'match-001'

  • Desigualdade no primeiro atributo: round BETWEEN 'QUARTERFINALS' AND 'SEMIFINALS'

  • Correspondência de prefixo como condição final: round = 'SEMIFINALS' AND begins_with(bracket, 'U')

Padrões inválidos:

  • Adicionando condições após uma desigualdade: round > 'QUARTERFINALS' AND bracket = 'UPPER'

  • Usando várias desigualdades: round > 'QUARTERFINALS' AND bracket > 'L'

Importante

begins_with() é tratada como uma condição de desigualdade, portanto, nenhuma condição adicional de chave de classificação pode segui-la.

Etapa 6: consultar o Índice Secundário Global PlayerMatchHistoryIndex com chaves de classificação vários atributos

Este exemplo consulta o PlayerMatchHistoryIndex, que tem uma única chave de partição (player1Id) e uma chave de classificação de vários atributos (matchDate + round). Isso permite a análise de vários torneios consultando todas as partidas de um jogador específico sem saber os IDs do torneio, enquanto a tabela base exigiria consultas separadas por combinação de torneio e região.

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`);

Variações de padrões

Dados de séries temporais com chaves de vários atributos

Otimizar para consultas de séries temporais com atributos de tempo hierá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

Benefícios: a hierarquia de tempo natural (ano → mês → dia → carimbo de data/hora) permite consultas eficientes em qualquer momento, granularidade, sem análise ou manipulação de datas. O Índice Secundário Global indexa automaticamente todas as leituras usando seus atributos de tempo natural.

Pedidos de comércio eletrônico com chaves de vários atributos

Monitorar pedidos com várias dimensões

{ 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

Dados de organizações hierárquicas

Hierarquias organizacionais 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

Chaves esparsas de vários atributos

Combinar chaves de vários atributos para criar um GSI esparso

{ 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

SaaS e multilocação

Plataforma SaaS multilocatário com isolamento 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' } }));

Benefícios: consultas eficientes no contexto locatário-cliente e na organização natural dos dados.

Transações financeiras

Sistema bancário monitorando transações de contas usando GSIs

// 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' } }));

Exemplo completo

O exemplo a seguir demonstra chaves de vários atributos, da configuração à limpeza:

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);

Estrutura 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 adicionais