Terjemahan disediakan oleh mesin penerjemah. Jika konten terjemahan yang diberikan bertentangan dengan versi bahasa Inggris aslinya, utamakan versi bahasa Inggris.
Multi-attribute pola kunci
Gambaran umum
Multi-attribute kunci memungkinkan Anda untuk membuat partisi Global Secondary Index (GSI) dan mengurutkan kunci yang terdiri dari hingga empat atribut masing-masing. 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, di aplikasi game, untuk menanyakan pertandingan turnamen berdasarkan turnamen, wilayah, dan putaran, Anda dapat membuat kunci partisi GSI sintetis seperti TOURNAMENT #WINTER2024 #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.
Multi-attribute kunci memecahkan masalah ini untuk GSI. Anda menentukan kunci partisi GSI menggunakan beberapa atribut yang ada seperti TournamentID dan wilayah. DynamoDB menangani logika kunci komposit secara otomatis, menyatukan keduanya 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 pengisian 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 pembuatan sistem pelacakan pertandingan turnamen untuk platform esports. Platform perlu menanyakan pertandingan 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 dengan ID uniknya
-
Solusi: Tabel dasar dengan
matchIdkunci partisi
Pola akses 2: Ku eri 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 pertandingan WINTER2024 di NA-EAST wilayah” atau “Semua pertandingan SEMIFINAL dalam braket ATAS untuk” WINTER2024/NA-EAST
Pola akses 3: Ku eri 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 kueri: “Semua pertandingan untuk pemain 101" atau “Pertandingan Pemain 101 pada Januari 2024"
Perbedaan utama antara pendekatan tradisional dan multi-atribut menjadi jelas ketika 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 };
Multi-attribute Pendekatan Indeks Sekunder Global (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 mengindeksnya di beberapa GSI tanpa memerlukan kunci gabungan sintetis.
Skema tabel dasar:
-
Kunci partisi:
matchId(atribut 1)
Skema Indeks Sekunder Global (TournamentRegionIndex dengan kunci multi-atribut):
-
Kunci partisi:
tournamentId,region(atribut 2) -
Urutkan kunci:
round,bracket,matchId(3 atribut)
Skema Indeks Sekunder Global (PlayerMatchHistoryIndex dengan kunci multi-atribut):
-
Kunci partisi:
player1Id(atribut 1) -
Urutkan kunci:
matchDate,round(2 atribut)
Tabel dasar: TournamentMatches
| MatchID (PK) | TurnamenID | region | bulat | tanda kurung | Player1ID | Player2id | Tanggal pertandingan | pemenang | skor |
|---|---|---|---|---|---|---|---|---|---|
| cocok-001 | MUSIM DINGIN2024 | NA-EAST | FINAL | KEJUARAAN | 101 | 103 | 2024-01-20 | 101 | 3-1 |
| korek-002 | MUSIM DINGIN2024 | NA-EAST | SEMIFINAL | ATAS | 101 | 105 | 2024-01-18 | 101 | 3-2 |
| korek-003 | MUSIM DINGIN2024 | NA-EAST | SEMIFINAL | ATAS | 103 | 107 | 2024-01-18 | 103 | 3-0 |
| korek-004 | MUSIM DINGIN2024 | NA-EAST | PEREMPAT FINAL | ATAS | 101 | 109 | 2024-01-15 | 101 | 3-1 |
| korek-005 | MUSIM DINGIN2024 | NA-WEST | FINAL | KEJUARAAN | 102 | 104 | 2024-01-20 | 102 | 3-2 |
| korek-006 | MUSIM DINGIN2024 | NA-WEST | SEMIFINAL | ATAS | 102 | 106 | 2024-01-18 | 102 | 3-1 |
| korek-007 | MUSIM SEMI2024 | NA-EAST | PEREMPAT FINAL | ATAS | 101 | 108 | 2024-03-15 | 101 | 3-0 |
| korek-008 | MUSIM SEMI2024 | NA-EAST | 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 | ID Pemain | Tanggal pertandingan | pemenang | skor |
|---|---|---|---|---|---|---|---|---|---|
| MUSIM DINGIN2024 | NA-EAST | FINAL | KEJUARAAN | cocok-001 | 101 | 103 | 2024-01-20 | 101 | 3-1 |
| MUSIM DINGIN2024 | NA-EAST | PEREMPAT FINAL | ATAS | korek-004 | 101 | 109 | 2024-01-15 | 101 | 3-1 |
| MUSIM DINGIN2024 | NA-EAST | SEMIFINAL | ATAS | korek-002 | 101 | 105 | 2024-01-18 | 101 | 3-2 |
| MUSIM DINGIN2024 | NA-EAST | SEMIFINAL | ATAS | korek-003 | 103 | 107 | 2024-01-18 | 103 | 3-0 |
| MUSIM DINGIN2024 | NA-WEST | FINAL | KEJUARAAN | korek-005 | 102 | 104 | 2024-01-20 | 102 | 3-2 |
| MUSIM DINGIN2024 | NA-WEST | SEMIFINAL | ATAS | korek-006 | 102 | 106 | 2024-01-18 | 102 | 3-1 |
| MUSIM SEMI2024 | NA-EAST | PEREMPAT FINAL | LEBIH RENDAH | korek-008 | 103 | 110 | 2024-03-15 | 103 | 3-2 |
| MUSIM SEMI2024 | NA-EAST | PEREMPAT FINAL | ATAS | pertandingan-007 | 101 | 108 | 2024-03-15 | 101 | 3-0 |
GSI: PlayerMatchHistoryIndex (kunci multi-atribut)
| Player1ID (PK) | Tanggal Pertandingan (SK) | bulat (SK) | TurnamenID | region | tanda kurung | ID kecocokan | Player2id | pemenang | skor |
|---|---|---|---|---|---|---|---|---|---|
| 101 | 2024-01-15 | PEREMPAT FINAL | MUSIM DINGIN2024 | NA-EAST | ATAS | korek-004 | 109 | 101 | 3-1 |
| 101 | 2024-01-18 | SEMIFINAL | MUSIM DINGIN2024 | NA-EAST | ATAS | korek-002 | 105 | 101 | 3-2 |
| 101 | 2024-01-20 | FINAL | MUSIM DINGIN2024 | NA-EAST | KEJUARAAN | cocok-001 | 103 | 101 | 3-1 |
| 101 | 2024-03-15 | PEREMPAT FINAL | MUSIM SEMI2024 | NA-EAST | ATAS | pertandingan-007 | 108 | 101 | 3-0 |
| 102 | 2024-01-18 | SEMIFINAL | MUSIM DINGIN2024 | NA-WEST | ATAS | korek-006 | 106 | 102 | 3-1 |
| 102 | 2024-01-20 | FINAL | MUSIM DINGIN2024 | NA-WEST | KEJUARAAN | korek-005 | 104 | 102 | 3-2 |
| 103 | 2024-01-18 | SEMIFINAL | MUSIM DINGIN2024 | NA-EAST | ATAS | korek-003 | 107 | 103 | 3-0 |
| 103 | 2024-03-15 | PEREMPAT FINAL | MUSIM SEMI2024 | NA-EAST | LEBIH RENDAH | percakapan-008 | 110 | 103 | 3-2 |
Prasyarat
Sebelum memulai, 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 dengan hanya izin yang Anda butuhkan. Untuk tutorial ini, Anda dapat menggunakan kebijakan yang AWS dikelolaAmazonDynamoDBFullAccessV2.
Lingkungan Pengembangan
-
Node.js diinstal pada mesin Anda
-
AWS kredenSIAL dikonfigurasi menggunakan salah satu metode berikut:
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 GSI menggunakan kunci multi-atribut
Buat tabel dengan struktur kunci dasar sederhana dan GSI 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 sederhana sementara GSI menyediakan pola kueri yang kompleks.
TournamentRegionIndex Indeks Sekunder TournamentRegionIndex Global: Indeks Sekunder 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 PlayerMatchHistoryIndex Global: Indeks Sekunder Global mengatur ulang data oleh pemain yang digunakan player1Id sebagai 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, memberikan pemetaan langsung ke model domain.
Pengindeksan Indeks Sekunder Global Otom atis: GSI secara otomatis mengindeks item menggunakan atribut yang ada (tournamentId,region,round,bracket, matchId for TournamentRegionIndex dan player1IdmatchDate, round for 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 alaminya—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 menanyakan hanya dengan tournamentId sendiri 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 Multi-attribute partisi di-hash bersama, memberikan kinerja pencarian O (1) yang sama dengan kunci atribut tunggal.
Langkah 4: Kueri kunci pengurutan Indeks Sekunder Global dari kiri ke kanan
Atribut kunci pengurutan harus ditanyakan dari kiri ke kanan dalam urutan yang ditentukan dalam Indeks Sekunder Global. Contoh ini menunjukkan kueri TournamentRegionIndex pada tingkat hierarki yang berbeda: pemfilteran dengan hanyaround, dengan round +bracket, atau oleh ketiga atribut kunci pengurutan. Anda tidak dapat melewatkan atribut di tengah-tengah—misalnya, Anda tidak dapat menanyakan berdasarkan round dan matchId saat melewatkan. 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'(melewatkan putaran)Kueri tidak berurutan:
matchId = 'match-002' AND round = 'SEMIFINALS'Meninggalkan celah:
round = 'SEMIFINALS' AND matchId = 'match-002'(melewatkan braket)
catatan
Kiat desain: Urutkan atribut utama 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 penggunaan operator perbandingan (>=,BETWEEN) dan pencocokan awalan (begins_with()) pada atribut kunci pengurutan. Setelah 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'Ketidaksetaraan 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 pengurutan multi-atribut (matchDate+round). Ini memungkinkan analisis lintas turnamen dengan menanyakan semua pertandingan untuk pemain tertentu tanpa mengetahui ID turnamen — sedangkan tabel dasar akan memerlukan 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
Time-series data dengan kunci multi-atribut
Optimalkan untuk 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. Indeks Sekunder Global secara otomatis mengindeks semua pembacaan menggunakan atribut waktu alami mereka.
E-commerce pesanan 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
Tombol 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
Multi-penyewaan SaaS
Multi-tenant Platform SaaS 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 GSI
// 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 hingga 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' }