Terjemahan disediakan oleh mesin penerjemah. Jika konten terjemahan yang diberikan bertentangan dengan versi bahasa Inggris aslinya, utamakan versi bahasa Inggris.
Pola kunci multi-atribut
Gambaran umum
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 # WINTER2 024 #REGION #NA -EAST dan kunci pengurutan sintetis seperti ROUND #SEMIFINALS #BRACKET #UPPER. 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
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
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
matchIdkunci 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+region) dan kunci pengurutan multi-atribut (round+ +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+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
| 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)
| 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)
| 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
Sebelum Anda mulai, pastikan Anda memiliki:
Akun dan izin
-
AWS Akun aktif (buat satu di sini
jika diperlukan) -
Izin IAM untuk operasi DynamoDB:
dynamodb:CreateTabledynamodb:DeleteTabledynamodb:DescribeTabledynamodb:PutItemdynamodb:Querydynamodb: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 terkelolaAmazonDynamoDBFullAccessV2.
Lingkungan Pengembangan
-
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
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
Implementasi
Langkah 1: Buat tabel dengan GSIs menggunakan tombol multi-atribut
Buat tabel dengan struktur kunci dasar sederhana dan GSIs yang menggunakan kunci multi-atribut.
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 + 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+ bracket +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+round) menyediakan urutan kronologis dengan kemampuan untuk memfilter berdasarkan rentang tanggal atau putaran turnamen tertentu.
Langkah 2: Masukkan data dengan atribut asli
Tambahkan data pertandingan turnamen menggunakan atribut alami. GSI akan secara otomatis mengindeks atribut ini tanpa memerlukan kunci sintetis.
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 (tournamentIdregion,,round,bracket, matchId untuk TournamentRegionIndex danplayer1Id,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
Contoh ini menanyakan Indeks Sekunder TournamentRegionIndex Global yang memiliki kunci partisi multi-atribut (tournamentId+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.
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
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 + roundbracket, 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
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
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.
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
Contoh ini menanyakan PlayerMatchHistoryIndex yang memiliki kunci partisi tunggal (player1Id) dan kunci sortir multi-atribut (matchDate+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.
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
Data deret waktu dengan kunci multi-atribut
Optimalkan kueri deret waktu dengan atribut waktu hierarkis
{ 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
Lacak pesanan dengan berbagai dimensi
{ 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
Model hierarki organisasi
{ 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
Gabungkan kunci multi-atribut untuk membuat GSI yang jarang
{ 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
Platform SaaS multi-penyewa dengan isolasi pelanggan
// 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
Sistem perbankan melacak transaksi rekening menggunakan GSIs
// Table design { TableName: 'BankTransactions', // Base table: Simple partition key KeySchema: [ { AttributeName: 'transactionId', KeyType: 'HASH' } ], AttributeDefinitions: [ { AttributeName: 'transactionId', AttributeType: 'S' }, { AttributeName: 'accountId', AttributeType: 'S' }, { AttributeName: 'year', AttributeType: 'S' }, { AttributeName: 'month', AttributeType: 'S' }, { AttributeName: 'day', AttributeType: 'S' }, { AttributeName: 'transactionType', AttributeType: 'S' } ], GlobalSecondaryIndexes: [ { IndexName: 'AccountTimeIndex', KeySchema: [ { AttributeName: 'accountId', KeyType: 'HASH' }, { AttributeName: 'year', KeyType: 'RANGE' }, { AttributeName: 'month', KeyType: 'RANGE' }, { AttributeName: 'day', KeyType: 'RANGE' }, { AttributeName: 'transactionId', KeyType: 'RANGE' } ], Projection: { ProjectionType: 'ALL' } }, { IndexName: 'TransactionTypeIndex', KeySchema: [ { AttributeName: 'accountId', KeyType: 'HASH' }, { AttributeName: 'transactionType', KeyType: 'RANGE' }, { AttributeName: 'year', KeyType: 'RANGE' }, { AttributeName: 'month', KeyType: 'RANGE' } ], Projection: { ProjectionType: 'ALL' } } ], BillingMode: 'PAY_PER_REQUEST' } // Query AccountTimeIndex GSI: All transactions for account in 2023 const yearTransactions = await docClient.send(new QueryCommand({ TableName: 'BankTransactions', IndexName: 'AccountTimeIndex', KeyConditionExpression: 'accountId = :account AND #year = :year', ExpressionAttributeNames: { '#year': 'year' }, ExpressionAttributeValues: { ':account': 'ACC-12345', ':year': '2023' } })); // Query AccountTimeIndex GSI: Transactions in specific month const monthTransactions = await docClient.send(new QueryCommand({ TableName: 'BankTransactions', IndexName: 'AccountTimeIndex', KeyConditionExpression: 'accountId = :account AND #year = :year AND #month = :month', ExpressionAttributeNames: { '#year': 'year', '#month': 'month' }, ExpressionAttributeValues: { ':account': 'ACC-12345', ':year': '2023', ':month': '11' } })); // Query TransactionTypeIndex GSI: Deposits in 2023 const deposits = await docClient.send(new QueryCommand({ TableName: 'BankTransactions', IndexName: 'TransactionTypeIndex', KeyConditionExpression: 'accountId = :account AND transactionType = :type AND #year = :year', ExpressionAttributeNames: { '#year': 'year' }, ExpressionAttributeValues: { ':account': 'ACC-12345', ':type': 'deposit', ':year': '2023' } }));
Contoh lengkap
Contoh berikut menunjukkan kunci multi-atribut dari penyiapan ke pembersihan:
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
// 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' }