

# Patrones de diseño
<a name="GSI.DesignPatterns"></a>

Los patrones de diseño ofrecen soluciones probadas a los desafíos más comunes cuando se trabaja con índices secundarios globales. Estos patrones le ayudan a crear aplicaciones eficientes y escalables, ya que le muestran cómo estructurar los índices para casos de uso específicos.

Cada patrón incluye una guía de implementación completa con ejemplos de código, prácticas recomendadas y casos de uso reales que le ayudarán a aplicar el patrón a sus propias aplicaciones.

**Topics**
+ [Claves de varios atributos](GSI.DesignPattern.MultiAttributeKeys.md)

# Patrón de claves de varios atributos
<a name="GSI.DesignPattern.MultiAttributeKeys"></a>

## Descripción general
<a name="GSI.DesignPattern.MultiAttributeKeys.Overview"></a>

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\$1WINTER2024\$1REGION\$1NA-EAST y una clave de clasificación sintética, como ROUND\$1SEMIFINALS\$1BRACKET\$1UPPER. 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
<a name="GSI.DesignPattern.MultiAttributeKeys.ApplicationExample"></a>

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
<a name="GSI.DesignPattern.MultiAttributeKeys.DataModel"></a>

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` \$1 `region`) y clave de clasificación de varios atributos (`round` \$1 `bracket` \$1 `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` \$1 `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
<a name="GSI.DesignPattern.MultiAttributeKeys.BaseTable"></a>


| 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)
<a name="GSI.DesignPattern.MultiAttributeKeys.TournamentRegionIndexTable"></a>


| 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)
<a name="GSI.DesignPattern.MultiAttributeKeys.PlayerMatchHistoryIndexTable"></a>


| 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
<a name="GSI.DesignPattern.MultiAttributeKeys.Prerequisites"></a>

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

### Permisos y cuenta
<a name="GSI.DesignPattern.MultiAttributeKeys.Prerequisites.AWSAccount"></a>
+ Una cuenta de AWS activa ([cree una aquí](https://aws.amazon.com/free/) 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
<a name="GSI.DesignPattern.MultiAttributeKeys.Prerequisites.DevEnvironment"></a>
+ 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
<a name="GSI.DesignPattern.MultiAttributeKeys.Prerequisites.InstallPackages"></a>

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

## Implementación
<a name="GSI.DesignPattern.MultiAttributeKeys.Implementation"></a>

### Paso 1: creación de una tabla con los GSI mediante claves de varios atributos
<a name="GSI.DesignPattern.MultiAttributeKeys.CreateTable"></a>

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

#### Ejemplo de código
<a name="w2aac19c13c45c23b9c11b3b5b1"></a>

```
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` \$1 `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` \$1 `bracket` \$1 `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` \$1 `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
<a name="GSI.DesignPattern.MultiAttributeKeys.InsertData"></a>

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

#### Ejemplo de código
<a name="w2aac19c13c45c23b9c11b5b5b1"></a>

```
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
<a name="GSI.DesignPattern.MultiAttributeKeys.QueryAllPartitionKeys"></a>

En este ejemplo, se consulta el índice secundario global TournamentRegionIndex, que tiene una clave de partición de varios atributos (`tournamentId` \$1 `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.

#### Ejemplo de código
<a name="w2aac19c13c45c23b9c11b7b5b1"></a>

```
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
<a name="GSI.DesignPattern.MultiAttributeKeys.QuerySortKeysLeftToRight"></a>

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` \$1 `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`.

#### Ejemplo de código
<a name="w2aac19c13c45c23b9c11b9b5b1"></a>

```
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
<a name="GSI.DesignPattern.MultiAttributeKeys.InequalityConditions"></a>

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.

#### Ejemplo de código
<a name="w2aac19c13c45c23b9c11c11b5b1"></a>

```
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
<a name="GSI.DesignPattern.MultiAttributeKeys.QueryPlayerHistory"></a>

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` \$1 `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.

#### Ejemplo de código
<a name="w2aac19c13c45c23b9c11c13b5b1"></a>

```
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
<a name="GSI.DesignPattern.MultiAttributeKeys.PatternVariations"></a>

### Datos de series temporales con claves de varios atributos
<a name="GSI.DesignPattern.MultiAttributeKeys.TimeSeries"></a>

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

#### Ejemplo de código
<a name="w2aac19c13c45c23b9c13b3b5b1"></a>

```
{
    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
<a name="GSI.DesignPattern.MultiAttributeKeys.ECommerce"></a>

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

#### Ejemplo de código
<a name="w2aac19c13c45c23b9c13b5b5b1"></a>

```
{
    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
<a name="GSI.DesignPattern.MultiAttributeKeys.Hierarchical"></a>

Jerarquías organizativas de modelos

#### Ejemplo de código
<a name="w2aac19c13c45c23b9c13b7b5b1"></a>

```
{
    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
<a name="GSI.DesignPattern.MultiAttributeKeys.Sparse"></a>

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

#### Ejemplo de código
<a name="w2aac19c13c45c23b9c13b9b5b1"></a>

```
{
    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
<a name="GSI.DesignPattern.MultiAttributeKeys.SaaS"></a>

Plataforma SaaS multiusuario con aislamiento de clientes

#### Ejemplo de código
<a name="w2aac19c13c45c23b9c13c11b5b1"></a>

```
// 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
<a name="GSI.DesignPattern.MultiAttributeKeys.Financial"></a>

Sistema bancario que rastrea las transacciones de las cuentas mediante GSI

#### Ejemplo de código
<a name="w2aac19c13c45c23b9c13c13b5b1"></a>

```
// 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
<a name="GSI.DesignPattern.MultiAttributeKeys.CompleteExample"></a>

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

### Ejemplo de código
<a name="w2aac19c13c45c23b9c15b5b1"></a>

```
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**

### Ejemplo de código
<a name="w2aac19c13c45c23b9c15b9b1"></a>

```
// 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
<a name="GSI.DesignPattern.MultiAttributeKeys.AdditionalResources"></a>
+ [Prácticas recomendadas de DynamoDB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/best-practices.html)
+ [Uso de tablas y datos](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithTables.html)
+ [Índices secundarios globales](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html)
+ [Operaciones de consulta y análisis](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html)