

Les traductions sont fournies par des outils de traduction automatique. En cas de conflit entre le contenu d'une traduction et celui de la version originale en anglais, la version anglaise prévaudra.

# Motifs de design
<a name="GSI.DesignPatterns"></a>

Les modèles de conception fournissent des solutions éprouvées aux défis courants liés à l'utilisation d'index secondaires globaux. Ces modèles vous aident à créer des applications efficaces et évolutives en vous montrant comment structurer vos index pour des cas d'utilisation spécifiques.

Chaque modèle inclut un guide d'implémentation complet avec des exemples de code, les meilleures pratiques et des cas d'utilisation réels pour vous aider à appliquer le modèle à vos propres applications.

**Topics**
+ [Clés à attributs multiples](GSI.DesignPattern.MultiAttributeKeys.md)

# Modèle de clés à attributs multiples
<a name="GSI.DesignPattern.MultiAttributeKeys"></a>

## Aperçu
<a name="GSI.DesignPattern.MultiAttributeKeys.Overview"></a>

Les clés à attributs multiples vous permettent de créer une partition d'index secondaire global (GSI) et de trier les clés composées d'un maximum de quatre attributs chacune. Cela réduit le code côté client et facilite la modélisation initiale des données et l'ajout de nouveaux modèles d'accès ultérieurement.

Imaginons un scénario courant : pour créer un GSI qui interroge des éléments selon plusieurs attributs hiérarchiques, vous devez traditionnellement créer des clés synthétiques en concaténant des valeurs. Par exemple, dans une application de jeu, pour interroger les matchs d'un tournoi par tournoi, région et manche, vous pouvez créer une clé de partition GSI synthétique telle que TOURNAMENT\$1 WINTER2 024 \$1REGION \$1NA -EAST et une clé de tri synthétique telle que ROUND \$1SEMIFINALS \$1BRACKET \$1UPPER. Cette approche fonctionne, mais nécessite la concaténation de chaînes lors de l'écriture de données, l'analyse lors de la lecture et le remplissage de clés synthétiques sur tous les éléments existants si vous ajoutez le GSI à une table existante. Cela rend le code plus encombré et il est difficile de maintenir la sécurité des types sur les composants clés individuels.

Les clés à attributs multiples résolvent ce problème pour GSIs. Vous définissez votre clé de partition GSI à l'aide de plusieurs attributs existants tels que TournamentID et region. DynamoDB gère automatiquement la logique des clés composites, en les hachant ensemble pour la distribution des données. Vous écrivez des éléments en utilisant les attributs naturels de votre modèle de domaine, et le GSI les indexe automatiquement. Pas de concaténation, pas d'analyse syntaxique, pas de remblayage. Votre code reste propre, vos données restent saisies et vos requêtes restent simples. Cette approche est particulièrement utile lorsque vous disposez de données hiérarchiques avec des groupements d'attributs naturels (par exemple tournoi → région → manche, ou organisation → département → équipe).

## Exemple d'application
<a name="GSI.DesignPattern.MultiAttributeKeys.ApplicationExample"></a>

Ce guide explique comment créer un système de suivi des matchs de tournoi pour une plateforme d'e-sport. La plateforme doit interroger efficacement les matchs selon plusieurs critères : par tournoi et par région pour la gestion des brackets, par joueur pour l'historique des matchs et par date pour la programmation.

## Modèle de données
<a name="GSI.DesignPattern.MultiAttributeKeys.DataModel"></a>

Dans cette présentation, le système de suivi des matchs de tournoi prend en charge trois modèles d'accès principaux, chacun nécessitant une structure de clé différente :

**Modèle d'accès 1 :** recherchez une correspondance spécifique à l'aide de son identifiant unique
+ **Solution :** table de base avec `matchId` comme clé de partition

**Modèle d'accès 2 :** recherchez tous les matchs pour un tournoi et une région spécifiques, en filtrant éventuellement par tour, bracket ou match
+ **Solution :** index secondaire global avec clé de partition multi-attributs (`tournamentId`\$1`region`) et clé de tri multi-attributs (`round`\$1 \$1`bracket`) `matchId`
+ **Exemples de requêtes :** « Tous les WINTER2 024 matchs dans la région NA-EAST » ou « Tous les matchs des DEMI-FINALES dans la tranche SUPÉRIEURE pour 024/NA-EAST » WINTER2

**Modèle d'accès 3 : recherchez** l'historique des matchs d'un joueur, en filtrant éventuellement par plage de dates ou par tour de tournoi
+ **Solution :** index secondaire global avec clé de partition unique (`player1Id`) et clé de tri multi-attributs (`matchDate`\$1`round`)
+ **Exemples de requêtes :** « Tous les matchs du joueur 101 » ou « Les matchs du joueur 101 en janvier 2024 »

La principale différence entre les approches traditionnelles et les approches à attributs multiples apparaît clairement lorsque l'on examine la structure des articles :

**Approche traditionnelle de l'index secondaire global (clés concaténées) :**

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

**Approche de l'index secondaire global à attributs multiples (clés natives) :**

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

Avec les clés à attributs multiples, vous pouvez écrire des éléments une seule fois avec des attributs de domaine naturels. DynamoDB les indexe automatiquement sur GSIs plusieurs sans nécessiter de clés concaténées synthétiques.

**Schéma de table de base :**
+ Clé de partition : `matchId` (1 attribut)

**Schéma d'index secondaire global (TournamentRegionIndex avec clés à attributs multiples) :**
+ Clé de partition :`tournamentId`, `region` (2 attributs)
+ Clé de tri : `round``bracket`,, `matchId` (3 attributs)

**Schéma d'index secondaire global (PlayerMatchHistoryIndex avec clés à attributs multiples) :**
+ Clé de partition : `player1Id` (1 attribut)
+ Clé de tri :`matchDate`, `round` (2 attributs)

### Table de base : TournamentMatches
<a name="GSI.DesignPattern.MultiAttributeKeys.BaseTable"></a>


| MatchID (PK) | ID du tournoi | region | round | crochet | ID du joueur 1 | ID du joueur 2 | Date du match | vainqueur | score | 
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | 
| match-001 | WINTER2024 | NA-EAST | FINALES | CHAMPIONNAT | 101 | 103 | 20-01-2024 | 101 | 3-1 | 
| match-002 | WINTER2024 | NA-EAST | DEMI-FINALES | UPPER | 101 | 105 | 18-01-2024 | 101 | 3-2 | 
| match-003 | WINTER2024 | NA-EAST | DEMI-FINALES | UPPER | 103 | 107 | 18-01-2024 | 103 | 3-0 | 
| match-004 | WINTER2024 | NA-EAST | QUARTS DE FINALE | UPPER | 101 | 109 | 15-01-2024 | 101 | 3-1 | 
| match-005 | WINTER2024 | NA-WEST | FINALES | CHAMPIONNAT | 102 | 104 | 20-01-2024 | 102 | 3-2 | 
| match-006 | WINTER2024 | NA-WEST | DEMI-FINALES | UPPER | 102 | 106 | 18-01-2024 | 102 | 3-1 | 
| match-007 | SPRING2024 | NA-EAST | QUARTS DE FINALE | UPPER | 101 | 108 | 15/03/2024 | 101 | 3-0 | 
| match-008 | SPRING2024 | NA-EAST | QUARTS DE FINALE | LOWER | 103 | 110 | 15/03/2024 | 103 | 3-2 | 

### GSI : TournamentRegionIndex (clés à attributs multiples)
<a name="GSI.DesignPattern.MultiAttributeKeys.TournamentRegionIndexTable"></a>


| ID du tournoi (PK) | région (PK) | rond (SK) | support (SK) | MatchID (SK) | ID du joueur 1 | ID du joueur 2 | Date du match | vainqueur | score | 
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | 
| WINTER2024 | NA-EAST | FINALES | CHAMPIONNAT | match-001 | 101 | 103 | 20-01-2024 | 101 | 3-1 | 
| WINTER2024 | NA-EAST | QUARTS DE FINALE | UPPER | match-004 | 101 | 109 | 15-01-2024 | 101 | 3-1 | 
| WINTER2024 | NA-EAST | DEMI-FINALES | UPPER | match-002 | 101 | 105 | 18-01-2024 | 101 | 3-2 | 
| WINTER2024 | NA-EAST | DEMI-FINALES | UPPER | match-003 | 103 | 107 | 18-01-2024 | 103 | 3-0 | 
| WINTER2024 | NA-WEST | FINALES | CHAMPIONNAT | match-005 | 102 | 104 | 20-01-2024 | 102 | 3-2 | 
| WINTER2024 | NA-WEST | DEMI-FINALES | UPPER | match-006 | 102 | 106 | 18-01-2024 | 102 | 3-1 | 
| SPRING2024 | NA-EAST | QUARTS DE FINALE | LOWER | match-008 | 103 | 110 | 15/03/2024 | 103 | 3-2 | 
| SPRING2024 | NA-EAST | QUARTS DE FINALE | UPPER | match-007 | 101 | 108 | 15/03/2024 | 101 | 3-0 | 

### GSI : PlayerMatchHistoryIndex (clés à attributs multiples)
<a name="GSI.DesignPattern.MultiAttributeKeys.PlayerMatchHistoryIndexTable"></a>


| ID du joueur 1 (PK) | MatchDate (SK) | rond (SK) | ID du tournoi | region | crochet | Identifiant du match | ID du joueur 2 | vainqueur | score | 
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | 
| 101 | 15-01-2024 | QUARTS DE FINALE | WINTER2024 | NA-EAST | UPPER | match-004 | 109 | 101 | 3-1 | 
| 101 | 18-01-2024 | DEMI-FINALES | WINTER2024 | NA-EAST | UPPER | match-002 | 105 | 101 | 3-2 | 
| 101 | 20-01-2024 | FINALES | WINTER2024 | NA-EAST | CHAMPIONNAT | match-001 | 103 | 101 | 3-1 | 
| 101 | 15/03/2024 | QUARTS DE FINALE | SPRING2024 | NA-EAST | UPPER | match-007 | 108 | 101 | 3-0 | 
| 102 | 18-01-2024 | DEMI-FINALES | WINTER2024 | NA-WEST | UPPER | match-006 | 106 | 102 | 3-1 | 
| 102 | 20-01-2024 | FINALES | WINTER2024 | NA-WEST | CHAMPIONNAT | match-005 | 104 | 102 | 3-2 | 
| 103 | 18-01-2024 | DEMI-FINALES | WINTER2024 | NA-EAST | UPPER | match-003 | 107 | 103 | 3-0 | 
| 103 | 15/03/2024 | QUARTS DE FINALE | SPRING2024 | NA-EAST | LOWER | match-008 | 110 | 103 | 3-2 | 

## Prérequis
<a name="GSI.DesignPattern.MultiAttributeKeys.Prerequisites"></a>

Avant de commencer, assurez-vous de disposer des éléments suivants :

### Compte et autorisations
<a name="GSI.DesignPattern.MultiAttributeKeys.Prerequisites.AWSAccount"></a>
+ Un AWS compte actif ([créez-en un ici](https://aws.amazon.com/free/) si nécessaire)
+ Autorisations IAM pour les opérations DynamoDB :
  + `dynamodb:CreateTable`
  + `dynamodb:DeleteTable`
  + `dynamodb:DescribeTable`
  + `dynamodb:PutItem`
  + `dynamodb:Query`
  + `dynamodb:BatchWriteItem`

**Note**  
**Note de sécurité :** Pour une utilisation en production, créez une politique IAM personnalisée avec uniquement les autorisations dont vous avez besoin. Pour ce didacticiel, vous pouvez utiliser la politique AWS gérée`AmazonDynamoDBFullAccessV2`.

### Environnement de développement
<a name="GSI.DesignPattern.MultiAttributeKeys.Prerequisites.DevEnvironment"></a>
+ Node.js installé sur votre machine
+ AWS informations d'identification configurées à l'aide de l'une des méthodes suivantes :

**Option 1 : AWS CLI**

```
aws configure
```

**Option 2 : variables d’environnement**

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

### Installation des packages obligatoires
<a name="GSI.DesignPattern.MultiAttributeKeys.Prerequisites.InstallPackages"></a>

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

## Mise en œuvre
<a name="GSI.DesignPattern.MultiAttributeKeys.Implementation"></a>

### Étape 1 : Création d'une table à GSIs l'aide de clés à attributs multiples
<a name="GSI.DesignPattern.MultiAttributeKeys.CreateTable"></a>

Créez une table avec une structure de clés de base simple et utilisant GSIs des clés à attributs multiples.

#### Exemple de code
<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");
```

**Principales décisions de conception :**

**Table de base :** La table de base utilise une clé de `matchId` partition simple pour les recherches par correspondance directe, ce qui permet de maintenir la structure de la table de base simple tout en GSIs fournissant des modèles de requête complexes.

**TournamentRegionIndex Index secondaire global : L'index** secondaire `TournamentRegionIndex` mondial utilise `tournamentId` \$1 `region` comme clé de partition à attributs multiples, ce qui crée une isolation entre les régions de tournois, les données étant distribuées par le hachage des deux attributs combinés, ce qui permet d'effectuer des requêtes efficaces dans un contexte de région de tournoi spécifique. La clé de tri à attributs multiples (`round`\$1 `bracket` \$1`matchId`) permet un tri hiérarchique qui prend en charge les requêtes à tous les niveaux de la hiérarchie avec un ordre naturel, du général (rond) au spécifique (identifiant de correspondance).

**PlayerMatchHistoryIndex Index secondaire mondial : L'index** secondaire `PlayerMatchHistoryIndex` mondial réorganise les données par joueur en les utilisant `player1Id` comme clé de partition, ce qui permet d'effectuer des requêtes entre tournois pour un joueur spécifique. La clé de tri à attributs multiples (`matchDate`\$1`round`) fournit un ordre chronologique avec la possibilité de filtrer par plages de dates ou par tours de tournoi spécifiques.

### Étape 2 : Insérer des données avec des attributs natifs
<a name="GSI.DesignPattern.MultiAttributeKeys.InsertData"></a>

Ajoutez les données des matchs du tournoi à l'aide d'attributs naturels. Le GSI indexera automatiquement ces attributs sans nécessiter de clés synthétiques.

#### Exemple de code
<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");
```

**Structure des données expliquée :**

**Utilisation naturelle des attributs :** chaque attribut représente un véritable concept de tournoi sans aucune concaténation ou analyse de chaînes requise, fournissant ainsi un mappage direct avec le modèle de domaine.

Indexation **automatique de l'index secondaire global : Indexation** GSIs automatique des éléments à l'aide des attributs existants (`tournamentId``region`,`round`,`bracket`, `matchId` pour TournamentRegionIndex et `player1Id``matchDate`, `round` pour PlayerMatchHistoryIndex) sans nécessiter de clés concaténées synthétiques.

**Aucun remplissage supplémentaire n'est nécessaire :** lorsque vous ajoutez un nouvel index secondaire global avec des clés à attributs multiples à une table existante, DynamoDB indexe automatiquement tous les éléments existants à l'aide de leurs attributs naturels. Il n'est pas nécessaire de mettre à jour les éléments à l'aide de clés synthétiques.

### Étape 3 : Interrogez TournamentRegionIndex l'index secondaire global avec tous les attributs de clé de partition
<a name="GSI.DesignPattern.MultiAttributeKeys.QueryAllPartitionKeys"></a>

Cet exemple interroge l'index secondaire TournamentRegionIndex global qui possède une clé de partition à attributs multiples (`tournamentId`\$1`region`). Tous les attributs de clé de partition doivent être spécifiés avec des conditions d'égalité dans les requêtes. Vous ne pouvez pas effectuer de requête `tournamentId` uniquement ou utiliser des opérateurs d'inégalité sur les attributs de clé de partition.

#### Exemple de code
<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`);
});
```

**Résultat attendu :**

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

**Requêtes non valides :**

```
// Missing region attribute
KeyConditionExpression: 'tournamentId = :tournament'

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

**Performances :** les clés de partition à attributs multiples sont hachées ensemble, offrant les mêmes performances de recherche O (1) que les clés à attribut unique.

### Étape 4 : demander les clés de tri de l'index secondaire global left-to-right
<a name="GSI.DesignPattern.MultiAttributeKeys.QuerySortKeysLeftToRight"></a>

Les attributs clés de tri doivent être interrogés left-to-right dans l'ordre dans lequel ils sont définis dans l'index secondaire global. Cet exemple montre comment interroger les différents TournamentRegionIndex niveaux hiérarchiques : filtrage par simple`round`, par `round` \$1 `bracket` ou par les trois attributs clés de tri. Vous ne pouvez pas ignorer les attributs au milieu. Par exemple, vous ne pouvez pas effectuer d'interrogation par `round` et `matchId` pendant les sauts. `bracket`

#### Exemple de code
<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`);
}
```

**Résultat attendu :**

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

**Left-to-right règles de requête :** vous devez interroger les attributs dans l'ordre de gauche à droite, sans en sauter aucun.

**Modèles valides :**
+ Premier attribut uniquement : `round = 'SEMIFINALS'`
+ Les deux premiers attributs : `round = 'SEMIFINALS' AND bracket = 'UPPER'`
+ Les trois attributs : `round = 'SEMIFINALS' AND bracket = 'UPPER' AND matchId = 'match-002'`

**Modèles non valides :**
+ Ignorer le premier attribut : `bracket = 'UPPER'` (saute le tour)
+ Interrogation hors ordre : `matchId = 'match-002' AND round = 'SEMIFINALS'`
+ Laisser des espaces : `round = 'SEMIFINALS' AND matchId = 'match-002'` (ne tient pas compte du crochet)

**Note**  
**Conseil de conception :** Triez les attributs clés du plus général au plus spécifique afin d'optimiser la flexibilité des requêtes.

### Étape 5 : Utiliser les conditions d'inégalité sur les clés de tri de l'indice secondaire global
<a name="GSI.DesignPattern.MultiAttributeKeys.InequalityConditions"></a>

Les conditions d'inégalité doivent être la dernière condition de votre requête. Cet exemple montre comment utiliser les opérateurs de comparaison (`>=`,`BETWEEN`) et la correspondance de préfixes (`begins_with()`) sur les attributs clés de tri. Une fois que vous avez utilisé un opérateur d'inégalité, vous ne pouvez pas ajouter de conditions clés de tri supplémentaires après celui-ci. L'inégalité doit être la condition finale de votre expression de condition clé.

#### Exemple de code
<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`);
}
```

**Règles relatives aux opérateurs d'inégalité :** vous pouvez utiliser des opérateurs de comparaison (`>``>=`,`<`,,`<=`) `BETWEEN` pour les requêtes de plage et `begins_with()` pour la correspondance de préfixes. L'inégalité doit être la dernière condition de votre requête.

**Modèles valides :**
+ Conditions d'égalité suivies d'inégalités : `round = 'SEMIFINALS' AND bracket = 'UPPER' AND matchId > 'match-001'`
+ Inégalité sur le premier attribut : `round BETWEEN 'QUARTERFINALS' AND 'SEMIFINALS'`
+ Correspondance du préfixe comme condition finale : `round = 'SEMIFINALS' AND begins_with(bracket, 'U')`

**Modèles non valides :**
+ Ajouter des conditions après une inégalité : `round > 'QUARTERFINALS' AND bracket = 'UPPER'`
+ En utilisant de multiples inégalités : `round > 'QUARTERFINALS' AND bracket > 'L'`

**Important**  
`begins_with()`est traitée comme une condition d'inégalité, de sorte qu'aucune condition clé de tri supplémentaire ne peut la suivre.

### Étape 6 : Interroger l'index secondaire PlayerMatchHistoryIndex global avec une clé de tri à attributs multiples
<a name="GSI.DesignPattern.MultiAttributeKeys.QueryPlayerHistory"></a>

Cet exemple interroge celui PlayerMatchHistoryIndex qui possède une clé de partition unique (`player1Id`) et une clé de tri multi-attributs (`matchDate`\$1`round`). Cela permet d'effectuer une analyse entre tournois en interrogeant tous les matchs pour un joueur spécifique sans connaître le tournoi, IDs alors que le tableau de base nécessiterait des requêtes distinctes par combinaison tournien-région.

#### Exemple de code
<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`);
```

## Variations de motifs
<a name="GSI.DesignPattern.MultiAttributeKeys.PatternVariations"></a>

### Données chronologiques avec clés à attributs multiples
<a name="GSI.DesignPattern.MultiAttributeKeys.TimeSeries"></a>

Optimisation pour les requêtes de séries chronologiques avec des attributs temporels hiérarchiques

#### Exemple de code
<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
```

**Avantages :** La hiérarchie temporelle naturelle (année → mois → jour → horodatage) permet des requêtes efficaces à tout moment avec une granularité sans analyse ou manipulation de dates. L'index secondaire global indexe automatiquement toutes les lectures en utilisant leurs attributs temporels naturels.

### Commandes de commerce électronique avec clés à attributs multiples
<a name="GSI.DesignPattern.MultiAttributeKeys.ECommerce"></a>

Suivez les commandes avec plusieurs dimensions

#### Exemple de code
<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
```

### Données d'organisation hiérarchiques
<a name="GSI.DesignPattern.MultiAttributeKeys.Hierarchical"></a>

Hiérarchies organisationnelles modélisées

#### Exemple de code
<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
```

### Clés à attributs multiples éparses
<a name="GSI.DesignPattern.MultiAttributeKeys.Sparse"></a>

Combinez des clés à attributs multiples pour créer un GSI clairsemé

#### Exemple de code
<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
```

### SaaS multi-tenant
<a name="GSI.DesignPattern.MultiAttributeKeys.SaaS"></a>

Plateforme SaaS multi-locataires avec isolation du client

#### Exemple de code
<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'
    }
}));
```

**Avantages :** requêtes efficaces dans le contexte locataire-client et organisation naturelle des données.

### Transactions financières
<a name="GSI.DesignPattern.MultiAttributeKeys.Financial"></a>

Le système bancaire suit les transactions du compte en utilisant GSIs

#### Exemple de code
<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'
    }
}));
```

## Exemple complet
<a name="GSI.DesignPattern.MultiAttributeKeys.CompleteExample"></a>

L'exemple suivant illustre les clés à attributs multiples, de la configuration au nettoyage :

### Exemple de code
<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);
```

**Échafaudage de code minimal**

### Exemple de code
<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' }
```

## Ressources supplémentaires
<a name="GSI.DesignPattern.MultiAttributeKeys.AdditionalResources"></a>
+ [Meilleures pratiques DynamoDB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/best-practices.html)
+ [Utilisation de tables et de données](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithTables.html)
+ [Index secondaires globaux](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html)
+ [Opérations de requête et de numérisation](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html)