

Terjemahan disediakan oleh mesin penerjemah. Jika konten terjemahan yang diberikan bertentangan dengan versi bahasa Inggris aslinya, utamakan versi bahasa Inggris.

# Pola desain
<a name="GSI.DesignPatterns"></a>

Pola desain memberikan solusi yang terbukti untuk tantangan umum saat bekerja dengan indeks sekunder global. Pola-pola ini membantu Anda membangun aplikasi yang efisien dan dapat diskalakan dengan menunjukkan kepada Anda cara menyusun indeks Anda untuk kasus penggunaan tertentu.

Setiap pola menyertakan panduan implementasi lengkap dengan contoh kode, praktik terbaik, dan kasus penggunaan dunia nyata untuk membantu Anda menerapkan pola ke aplikasi Anda sendiri.

**Topics**
+ [Kunci multi-atribut](GSI.DesignPattern.MultiAttributeKeys.md)

# Pola kunci multi-atribut
<a name="GSI.DesignPattern.MultiAttributeKeys"></a>

## Gambaran umum
<a name="GSI.DesignPattern.MultiAttributeKeys.Overview"></a>

Kunci multi-atribut memungkinkan Anda membuat partisi Global Secondary Index (GSI) dan mengurutkan kunci masing-masing terdiri dari hingga empat atribut. Ini mengurangi kode sisi klien dan membuatnya lebih mudah untuk awalnya memodelkan data dan menambahkan pola akses baru nanti.

Pertimbangkan skenario umum: untuk membuat GSI yang menanyakan item dengan beberapa atribut hierarkis, Anda secara tradisional perlu membuat kunci sintetis dengan menggabungkan nilai. Misalnya, dalam aplikasi game, untuk menanyakan pertandingan turnamen berdasarkan turnamen, wilayah, dan putaran, Anda dapat membuat kunci partisi GSI sintetis seperti TURNAMEN \$1 WINTER2 024 \$1REGION \$1NA -EAST dan kunci pengurutan sintetis seperti ROUND \$1SEMIFINALS \$1BRACKET \$1UPPER. Pendekatan ini berfungsi, tetapi memerlukan penggabungan string saat menulis data, mengurai saat membaca, dan mengisi kembali kunci sintetis di semua item yang ada jika Anda menambahkan GSI ke tabel yang ada. Hal ini membuat kode lebih berantakan dan menantang untuk menjaga keamanan tipe pada masing-masing komponen kunci.

Kunci multi-atribut memecahkan masalah ini untuk GSIs. Anda menentukan kunci partisi GSI Anda menggunakan beberapa atribut yang ada seperti TournamentID dan wilayah. DynamoDB menangani logika kunci komposit secara otomatis, hashing mereka bersama-sama untuk distribusi data. Anda menulis item menggunakan atribut alami dari model domain Anda, dan GSI secara otomatis mengindeksnya. Tidak ada penggabungan, tidak ada penguraian, tidak ada penimbunan ulang. Kode Anda tetap bersih, data Anda tetap diketik, dan kueri Anda tetap sederhana. Pendekatan ini sangat berguna ketika Anda memiliki data hierarkis dengan pengelompokan atribut alami (seperti turnamen → wilayah → putaran, atau organisasi → departemen → tim).

## Contoh aplikasi
<a name="GSI.DesignPattern.MultiAttributeKeys.ApplicationExample"></a>

Panduan ini berjalan melalui pembangunan sistem pelacakan pertandingan turnamen untuk platform esports. Platform perlu menanyakan kecocokan secara efisien di berbagai dimensi: berdasarkan turnamen dan wilayah untuk manajemen braket, oleh pemain untuk riwayat pertandingan, dan berdasarkan tanggal untuk penjadwalan.

## Model Data
<a name="GSI.DesignPattern.MultiAttributeKeys.DataModel"></a>

Dalam panduan ini, sistem pelacakan pertandingan turnamen mendukung tiga pola akses utama, masing-masing membutuhkan struktur kunci yang berbeda:

**Pola akses 1:** Cari kecocokan tertentu berdasarkan ID uniknya
+ **Solusi:** Tabel dasar dengan `matchId` kunci partisi

**Pola akses 2:** Kueri semua pertandingan untuk turnamen dan wilayah tertentu, secara opsional memfilter berdasarkan putaran, braket, atau pertandingan
+ **Solusi:** Indeks Sekunder Global dengan kunci partisi multi-atribut (`tournamentId`\$1`region`) dan kunci pengurutan multi-atribut (`round`\$1 \$1`bracket`) `matchId`
+ **Contoh kueri:** “Semua WINTER2 024 pertandingan di wilayah NA-EAST” atau “Semua Semifinal cocok di braket UPPER untuk 024/NA-EAST” WINTER2

**Pola akses 3:** Menanyakan riwayat pertandingan pemain, secara opsional memfilter berdasarkan rentang tanggal atau putaran turnamen
+ **Solusi:** Indeks Sekunder Global dengan kunci partisi tunggal (`player1Id`) dan kunci pengurutan multi-atribut (`matchDate`\$1`round`)
+ **Contoh pertanyaan:** “Semua pertandingan untuk pemain 101" atau “Pertandingan Pemain 101 pada Januari 2024"

Perbedaan utama antara pendekatan tradisional dan multi-atribut menjadi jelas saat memeriksa struktur item:

**Pendekatan Indeks Sekunder Global Tradisional (kunci gabungan):**

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

**Pendekatan Indeks Sekunder Global multi-atribut (kunci asli):**

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

Dengan kunci multi-atribut, Anda menulis item sekali dengan atribut domain alami. DynamoDB secara otomatis mengindeks mereka di GSIs beberapa tanpa memerlukan kunci gabungan sintetis.

**Skema tabel dasar:**
+ Kunci partisi: `matchId` (1 atribut)

**Skema Indeks Sekunder Global (TournamentRegionIndex dengan kunci multi-atribut):**
+ Kunci partisi:`tournamentId`, `region` (2 atribut)
+ Kunci sortir:`round`,`bracket`, `matchId` (3 atribut)

**Skema Indeks Sekunder Global (PlayerMatchHistoryIndex dengan kunci multi-atribut):**
+ Kunci partisi: `player1Id` (1 atribut)
+ Kunci sortir:`matchDate`, `round` (2 atribut)

### Tabel dasar: TournamentMatches
<a name="GSI.DesignPattern.MultiAttributeKeys.BaseTable"></a>


| MatchID (PK) | TurnamenID | region | bulat | tanda kurung | Player1id | Player2id | MatchDate | pemenang | skor | 
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | 
| pertandingan-001 | WINTER2024 | NA-TIMUR | FINAL | KEJUARAAN | 101 | 103 | 2024-01-20 | 101 | 3-1 | 
| pertandingan-002 | WINTER2024 | NA-TIMUR | SEMIFINAL | ATAS | 101 | 105 | 2024-01-18 | 101 | 3-2 | 
| pertandingan-003 | WINTER2024 | NA-TIMUR | SEMIFINAL | ATAS | 103 | 107 | 2024-01-18 | 103 | 3-0 | 
| pertandingan-004 | WINTER2024 | NA-TIMUR | PEREMPAT FINAL | ATAS | 101 | 109 | 2024-01-15 | 101 | 3-1 | 
| pertandingan-005 | WINTER2024 | NA-BARAT | FINAL | KEJUARAAN | 102 | 104 | 2024-01-20 | 102 | 3-2 | 
| pertandingan-006 | WINTER2024 | NA-BARAT | SEMIFINAL | ATAS | 102 | 106 | 2024-01-18 | 102 | 3-1 | 
| pertandingan-007 | SPRING2024 | NA-TIMUR | PEREMPAT FINAL | ATAS | 101 | 108 | 2024-03-15 | 101 | 3-0 | 
| pertandingan-008 | SPRING2024 | NA-TIMUR | PEREMPAT FINAL | LEBIH RENDAH | 103 | 110 | 2024-03-15 | 103 | 3-2 | 

### GSI: TournamentRegionIndex (kunci multi-atribut)
<a name="GSI.DesignPattern.MultiAttributeKeys.TournamentRegionIndexTable"></a>


| TurnamenID (PK) | wilayah (PK) | bulat (SK) | braket (SK) | MatchID (SK) | Player1id | Player2id | MatchDate | pemenang | skor | 
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | 
| WINTER2024 | NA-TIMUR | FINAL | KEJUARAAN | pertandingan-001 | 101 | 103 | 2024-01-20 | 101 | 3-1 | 
| WINTER2024 | NA-TIMUR | PEREMPAT FINAL | ATAS | pertandingan-004 | 101 | 109 | 2024-01-15 | 101 | 3-1 | 
| WINTER2024 | NA-TIMUR | SEMIFINAL | ATAS | pertandingan-002 | 101 | 105 | 2024-01-18 | 101 | 3-2 | 
| WINTER2024 | NA-TIMUR | SEMIFINAL | ATAS | pertandingan-003 | 103 | 107 | 2024-01-18 | 103 | 3-0 | 
| WINTER2024 | NA-BARAT | FINAL | KEJUARAAN | pertandingan-005 | 102 | 104 | 2024-01-20 | 102 | 3-2 | 
| WINTER2024 | NA-BARAT | SEMIFINAL | ATAS | pertandingan-006 | 102 | 106 | 2024-01-18 | 102 | 3-1 | 
| SPRING2024 | NA-TIMUR | PEREMPAT FINAL | LEBIH RENDAH | pertandingan-008 | 103 | 110 | 2024-03-15 | 103 | 3-2 | 
| SPRING2024 | NA-TIMUR | PEREMPAT FINAL | ATAS | pertandingan-007 | 101 | 108 | 2024-03-15 | 101 | 3-0 | 

### GSI: PlayerMatchHistoryIndex (kunci multi-atribut)
<a name="GSI.DesignPattern.MultiAttributeKeys.PlayerMatchHistoryIndexTable"></a>


| Player1ID (PK) | MatchDate (SK) | bulat (SK) | TurnamenID | region | tanda kurung | MatchID | Player2id | pemenang | skor | 
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | 
| 101 | 2024-01-15 | PEREMPAT FINAL | WINTER2024 | NA-TIMUR | ATAS | pertandingan-004 | 109 | 101 | 3-1 | 
| 101 | 2024-01-18 | SEMIFINAL | WINTER2024 | NA-TIMUR | ATAS | pertandingan-002 | 105 | 101 | 3-2 | 
| 101 | 2024-01-20 | FINAL | WINTER2024 | NA-TIMUR | KEJUARAAN | pertandingan-001 | 103 | 101 | 3-1 | 
| 101 | 2024-03-15 | PEREMPAT FINAL | SPRING2024 | NA-TIMUR | ATAS | pertandingan-007 | 108 | 101 | 3-0 | 
| 102 | 2024-01-18 | SEMIFINAL | WINTER2024 | NA-BARAT | ATAS | pertandingan-006 | 106 | 102 | 3-1 | 
| 102 | 2024-01-20 | FINAL | WINTER2024 | NA-BARAT | KEJUARAAN | pertandingan-005 | 104 | 102 | 3-2 | 
| 103 | 2024-01-18 | SEMIFINAL | WINTER2024 | NA-TIMUR | ATAS | pertandingan-003 | 107 | 103 | 3-0 | 
| 103 | 2024-03-15 | PEREMPAT FINAL | SPRING2024 | NA-TIMUR | LEBIH RENDAH | pertandingan-008 | 110 | 103 | 3-2 | 

## Prasyarat
<a name="GSI.DesignPattern.MultiAttributeKeys.Prerequisites"></a>

Sebelum Anda mulai, pastikan Anda memiliki:

### Akun dan izin
<a name="GSI.DesignPattern.MultiAttributeKeys.Prerequisites.AWSAccount"></a>
+  AWS Akun aktif ([buat satu di sini](https://aws.amazon.com/free/) jika diperlukan)
+ Izin IAM untuk operasi DynamoDB:
  + `dynamodb:CreateTable`
  + `dynamodb:DeleteTable`
  + `dynamodb:DescribeTable`
  + `dynamodb:PutItem`
  + `dynamodb:Query`
  + `dynamodb:BatchWriteItem`

**catatan**  
**Catatan Keamanan:** Untuk penggunaan produksi, buat kebijakan IAM khusus hanya dengan izin yang Anda butuhkan. Untuk tutorial ini, Anda dapat menggunakan kebijakan AWS terkelola`AmazonDynamoDBFullAccessV2`.

### Lingkungan Pengembangan
<a name="GSI.DesignPattern.MultiAttributeKeys.Prerequisites.DevEnvironment"></a>
+ Node.js diinstal pada mesin Anda
+ AWS kredensil yang dikonfigurasi menggunakan salah satu metode ini:

**Opsi 1: AWS CLI**

```
aws configure
```

**Opsi 2: Variabel Lingkungan**

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

### Instal Paket yang Diperlukan
<a name="GSI.DesignPattern.MultiAttributeKeys.Prerequisites.InstallPackages"></a>

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

## Implementasi
<a name="GSI.DesignPattern.MultiAttributeKeys.Implementation"></a>

### Langkah 1: Buat tabel dengan GSIs menggunakan tombol multi-atribut
<a name="GSI.DesignPattern.MultiAttributeKeys.CreateTable"></a>

Buat tabel dengan struktur kunci dasar sederhana dan GSIs yang menggunakan kunci multi-atribut.

#### Contoh kode
<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");
```

**Keputusan desain utama:**

**Tabel dasar:** Tabel dasar menggunakan kunci `matchId` partisi sederhana untuk pencarian kecocokan langsung, menjaga struktur tabel dasar tetap mudah sementara GSIs menyediakan pola kueri yang kompleks.

**TournamentRegionIndex Indeks Sekunder Global: Indeks** Sekunder `TournamentRegionIndex` Global menggunakan `tournamentId` \$1 `region` sebagai kunci partisi multi-atribut, menciptakan isolasi wilayah turnamen di mana data didistribusikan oleh hash dari kedua atribut yang digabungkan, memungkinkan kueri yang efisien dalam konteks wilayah turnamen tertentu. Kunci pengurutan multi-atribut (`round`\$1 `bracket` \$1`matchId`) menyediakan penyortiran hierarkis yang mendukung kueri di setiap tingkat hierarki dengan urutan alami dari umum (putaran) ke spesifik (ID kecocokan).

**PlayerMatchHistoryIndex Indeks Sekunder Global: Indeks** Sekunder `PlayerMatchHistoryIndex` Global mengatur ulang data berdasarkan pemain menggunakan `player1Id` kunci partisi, memungkinkan kueri lintas-turnamen untuk pemain tertentu. Kunci pengurutan multi-atribut (`matchDate`\$1`round`) menyediakan urutan kronologis dengan kemampuan untuk memfilter berdasarkan rentang tanggal atau putaran turnamen tertentu.

### Langkah 2: Masukkan data dengan atribut asli
<a name="GSI.DesignPattern.MultiAttributeKeys.InsertData"></a>

Tambahkan data pertandingan turnamen menggunakan atribut alami. GSI akan secara otomatis mengindeks atribut ini tanpa memerlukan kunci sintetis.

#### Contoh kode
<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");
```

**Struktur data dijelaskan:**

**Penggunaan atribut alami:** Setiap atribut mewakili konsep turnamen nyata tanpa penggabungan string atau penguraian yang diperlukan, menyediakan pemetaan langsung ke model domain.

**Pengindeksan Indeks Sekunder Global Otomatis:** GSIs Secara otomatis mengindeks item menggunakan atribut yang ada (`tournamentId``region`,,`round`,`bracket`, `matchId` untuk TournamentRegionIndex dan`player1Id`,`matchDate`, `round` untuk PlayerMatchHistoryIndex) tanpa memerlukan kunci gabungan sintetis.

**Tidak perlu pengisian ulang:** Saat Anda menambahkan Indeks Sekunder Global baru dengan kunci multi-atribut ke tabel yang ada, DynamoDB secara otomatis mengindeks semua item yang ada menggunakan atribut alamiahnya—tidak perlu memperbarui item dengan kunci sintetis.

### Langkah 3: Kueri Indeks Sekunder TournamentRegionIndex Global dengan semua atribut kunci partisi
<a name="GSI.DesignPattern.MultiAttributeKeys.QueryAllPartitionKeys"></a>

Contoh ini menanyakan Indeks Sekunder TournamentRegionIndex Global yang memiliki kunci partisi multi-atribut (`tournamentId`\$1`region`). Semua atribut kunci partisi harus ditentukan dengan kondisi kesetaraan dalam kueri—Anda tidak dapat melakukan kueri hanya dengan `tournamentId` sendirian atau menggunakan operator ketidaksetaraan pada atribut kunci partisi.

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

**Output yang diharapkan:**

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

**Kueri tidak valid:**

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

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

**Kinerja:** Kunci partisi multi-atribut di-hash bersama, memberikan kinerja pencarian O (1) yang sama dengan kunci atribut tunggal.

### Langkah 4: Kueri kunci pengurutan Indeks Sekunder Global left-to-right
<a name="GSI.DesignPattern.MultiAttributeKeys.QuerySortKeysLeftToRight"></a>

Atribut kunci sortir harus ditanyakan left-to-right dalam urutan yang ditentukan dalam Indeks Sekunder Global. Contoh ini menunjukkan kueri TournamentRegionIndex pada tingkat hierarki yang berbeda: memfilter hanya dengan, dengan `round` \$1 `round``bracket`, atau dengan ketiga atribut kunci pengurutan. Anda tidak dapat melewati atribut di tengah—misalnya, Anda tidak dapat melakukan kueri dengan `round` dan `matchId` saat melompati. `bracket`

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

**Output yang diharapkan:**

```
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 aturan kueri:** Anda harus menanyakan atribut secara berurutan dari kiri ke kanan, tanpa melewatkan apa pun.

**Pola yang valid:**
+ Atribut pertama saja: `round = 'SEMIFINALS'`
+ Dua atribut pertama: `round = 'SEMIFINALS' AND bracket = 'UPPER'`
+ Ketiga atribut: `round = 'SEMIFINALS' AND bracket = 'UPPER' AND matchId = 'match-002'`

**Pola tidak valid:**
+ Melewatkan atribut pertama: `bracket = 'UPPER'` (melompat-lompat)
+ Menanyakan di luar pesanan: `matchId = 'match-002' AND round = 'SEMIFINALS'`
+ Meninggalkan celah: `round = 'SEMIFINALS' AND matchId = 'match-002'` (melompati braket)

**catatan**  
**Tip desain:** Urutkan atribut kunci urutan dari yang paling umum hingga yang paling spesifik untuk memaksimalkan fleksibilitas kueri.

### Langkah 5: Gunakan kondisi ketidaksetaraan pada kunci pengurutan Indeks Sekunder Global
<a name="GSI.DesignPattern.MultiAttributeKeys.InequalityConditions"></a>

Kondisi ketidaksetaraan harus menjadi kondisi terakhir dalam kueri Anda. Contoh ini menunjukkan menggunakan operator perbandingan (`>=`,`BETWEEN`) dan awalan pencocokan (`begins_with()`) pada atribut kunci sortir. Setelah Anda menggunakan operator ketidaksetaraan, Anda tidak dapat menambahkan kondisi kunci pengurutan tambahan setelahnya — ketidaksetaraan harus menjadi kondisi akhir dalam ekspresi kondisi kunci Anda.

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

**Aturan operator ketidaksetaraan:** Anda dapat menggunakan operator perbandingan (`>`,,`>=`,`<=`)`<`, `BETWEEN` untuk kueri rentang, dan `begins_with()` untuk pencocokan awalan. Ketidaksetaraan harus menjadi kondisi terakhir dalam kueri Anda.

**Pola yang valid:**
+ Kondisi kesetaraan diikuti oleh ketidaksetaraan: `round = 'SEMIFINALS' AND bracket = 'UPPER' AND matchId > 'match-001'`
+ Ketimpangan pada atribut pertama: `round BETWEEN 'QUARTERFINALS' AND 'SEMIFINALS'`
+ Pencocokan awalan sebagai kondisi akhir: `round = 'SEMIFINALS' AND begins_with(bracket, 'U')`

**Pola tidak valid:**
+ Menambahkan kondisi setelah ketidaksetaraan: `round > 'QUARTERFINALS' AND bracket = 'UPPER'`
+ Menggunakan beberapa ketidaksetaraan: `round > 'QUARTERFINALS' AND bracket > 'L'`

**penting**  
`begins_with()`diperlakukan sebagai kondisi ketidaksetaraan, jadi tidak ada kondisi kunci pengurutan tambahan yang dapat mengikutinya.

### Langkah 6: Kueri Indeks Sekunder PlayerMatchHistoryIndex Global dengan kunci pengurutan multi-atribut
<a name="GSI.DesignPattern.MultiAttributeKeys.QueryPlayerHistory"></a>

Contoh ini menanyakan PlayerMatchHistoryIndex yang memiliki kunci partisi tunggal (`player1Id`) dan kunci sortir multi-atribut (`matchDate`\$1`round`). Hal ini memungkinkan analisis lintas-turnamen dengan menanyakan semua pertandingan untuk pemain tertentu tanpa mengetahui turnamen IDs — sedangkan tabel dasar akan membutuhkan kueri terpisah per kombinasi turnamen-wilayah.

#### Contoh kode
<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`);
```

## Variasi pola
<a name="GSI.DesignPattern.MultiAttributeKeys.PatternVariations"></a>

### Data deret waktu dengan kunci multi-atribut
<a name="GSI.DesignPattern.MultiAttributeKeys.TimeSeries"></a>

Optimalkan kueri deret waktu dengan atribut waktu hierarkis

#### Contoh kode
<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
```

**Manfaat:** Hirarki waktu alami (tahun → bulan → hari → stempel waktu) memungkinkan kueri yang efisien setiap saat perincian tanpa penguraian atau manipulasi tanggal. Global Secondary Index secara otomatis mengindeks semua bacaan menggunakan atribut waktu alaminya.

### Pesanan e-niaga dengan kunci multi-atribut
<a name="GSI.DesignPattern.MultiAttributeKeys.ECommerce"></a>

Lacak pesanan dengan berbagai dimensi

#### Contoh kode
<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
```

### Data organisasi hierarkis
<a name="GSI.DesignPattern.MultiAttributeKeys.Hierarchical"></a>

Model hierarki organisasi

#### Contoh kode
<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
```

### Kunci multi-atribut jarang
<a name="GSI.DesignPattern.MultiAttributeKeys.Sparse"></a>

Gabungkan kunci multi-atribut untuk membuat GSI yang jarang

#### Contoh kode
<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-tenancy
<a name="GSI.DesignPattern.MultiAttributeKeys.SaaS"></a>

Platform SaaS multi-penyewa dengan isolasi pelanggan

#### Contoh kode
<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'
    }
}));
```

**Manfaat:** Kueri yang efisien dalam konteks penyewa-pelanggan dan organisasi data alami.

### Transaksi keuangan
<a name="GSI.DesignPattern.MultiAttributeKeys.Financial"></a>

Sistem perbankan melacak transaksi rekening menggunakan GSIs

#### Contoh kode
<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'
    }
}));
```

## Contoh lengkap
<a name="GSI.DesignPattern.MultiAttributeKeys.CompleteExample"></a>

Contoh berikut menunjukkan kunci multi-atribut dari penyiapan ke pembersihan:

### Contoh kode
<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);
```

**Perancah kode minimal**

### Contoh kode
<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' }
```

## Sumber daya tambahan
<a name="GSI.DesignPattern.MultiAttributeKeys.AdditionalResources"></a>
+ [Praktik Terbaik DynamoDB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/best-practices.html)
+ [Bekerja dengan Tabel dan Data](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithTables.html)
+ [Indeks Sekunder Global](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html)
+ [Operasi Kueri dan Pemindaian](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html)