Pola kunci multi-atribut - Amazon DynamoDB

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 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+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: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 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' }

Sumber daya tambahan