다중 속성 키 패턴 - Amazon DynamoDB

다중 속성 키 패턴

개요

다중 속성 키를 사용하면 각각 최대 4개의 속성으로 구성된 글로벌 보조 인덱스(GSI) 파티션 및 정렬 키를 생성할 수 있습니다. 이렇게 하면 클라이언트 측 코드가 줄어들며, 처음에 데이터를 모델링하고 나중에 새 액세스 패턴을 더 쉽게 추가할 수 있습니다.

다음과 같은 일반적인 시나리오를 고려해 보세요. 여러 계층 속성으로 항목을 쿼리하는 GSI를 생성하려면 일반적으로 값을 연결하여 합성 키를 생성해야 합니다. 예를 들어 게임 앱에서 토너먼트, 리전 및 라운드별로 토너먼트 매치를 쿼리하려면 TOURNAMENT#WINTER2024#REGION#NA-EAST와 같은 합성 GSI 파티션 키와 ROUND#SEMIFINALS#BRACKET#UPPER와 같은 합성 정렬 키를 생성하면 됩니다. 이 접근 방식은 잘 작동하지만 기존 테이블에 GSI를 추가하는 상황에서 데이터를 쓰고, 읽을 때 구문 분석하고, 모든 기존 항목에서 합성 키를 채우는 경우에는 문자열 연결이 필요합니다. 따라서 코드가 더 복잡해지고 개별 키 구성 요소에서 유형 안전을 유지하기가 어렵습니다.

다중 속성 키는 GSI에 대해 이 문제를 해결합니다. tournamentId 및 리전과 같은 여러 기존 속성을 사용하여 GSI 파티션 키를 정의합니다. DynamoDB는 복합 키 로직을 자동으로 처리하여 데이터 배포를 위해 함께 해시합니다. 도메인 모델의 자연 속성을 사용하여 항목을 작성하면 GSI가 자동으로 항목을 인덱싱합니다. 연결 없음, 구문 분석 없음, 채우기 없음. 코드는 깔끔하게 유지되고, 데이터는 형식으로 유지되며, 쿼리는 간단하게 유지됩니다. 이 접근 방식은 자연 속성 그룹(예: 토너먼트 → 리전 → 라운드 또는 조직 → 부서 → 팀)이 있는 계층적 데이터가 있는 경우 특히 유용합니다.

애플리케이션 예제

이 가이드에서는 e스포츠 플랫폼을 위한 토너먼트 매치 추적 시스템을 구축하는 방법을 안내합니다. 플랫폼은 그룹 관리를 위한 토너먼트 및 리전별, 매치 기록을 위한 플레이어별, 일정 예약을 위한 날짜별 등 여러 차원에서 매치를 효율적으로 쿼리해야 합니다.

데이터 모델

이 연습에서는 토너먼트 매치 추적 시스템이 세 가지 기본 액세스 패턴을 지원하며, 각 패턴에는 다른 키 구조가 필요합니다.

액세스 패턴 1: 고유 ID로 특정 매치 조회

  • 솔루션: 파티션 키가 matchId인 기본 테이블

액세스 패턴 2: 특정 토너먼트 및 리전에 대한 모든 매치 쿼리, 선택적으로 라운드, 그룹 또는 매치별로 필터링

  • 솔루션: 다중 속성 파티션 키(tournamentId + region) 및 다중 속성 정렬 키(round + bracket + matchId)가 있는 글로벌 보조 인덱스

  • 쿼리 예: "NA-EAST 리전의 모든 WINTER2024 매치" 또는 "WINTER2024/NA-EAST의 모든 SEMIFINALS 매치"

액세스 패턴 3: 플레이어의 매치 기록을 쿼리하여 선택적으로 날짜 범위 또는 토너먼트 라운드별로 필터링

  • 솔루션: 단일 파티션 키(player1Id) 및 다중 속성 정렬 키(matchDate + round)가 있는 글로벌 보조 인덱스

  • 쿼리 예: "2024년 1월 플레이어 101의 모든 매치" 또는 "플레이어 101의 매치"

항목 구조를 검사할 때 기존 접근 방식과 다중 속성 접근 방식의 주요 차이점은 다음과 같습니다.

기존 글로벌 보조 인덱스 접근 방식(연결 키):

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

다중 속성 글로벌 보조 인덱스 접근 방식(기본 키):

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

다중 속성 키를 사용하면 자연 도메인 속성으로 항목을 한 번 작성합니다. DynamoDB는 합성 연결 키 없이 여러 GSI에서 자동으로 인덱싱합니다.

기본 테이블 스키마:

  • 파티션 키: matchId(1개 속성)

글로벌 보조 인덱스 스키마(다중 속성 키가 있는 TournamentRegionIndex):

  • 파티션 키: tournamentId, region(2개 속성)

  • 정렬 키: round, bracket, matchId(3개 속성)

글로벌 보조 인덱스 스키마(다중 속성 키가 있는 PlayerMatchHistoryIndex):

  • 파티션 키: player1Id(1개 속성)

  • 정렬 키: matchDate, round(2개 속성)

기본 테이블: TournamentMatches

matchId(PK) tournamentId 리전 round 그룹 player1Id player2Id matchDate 승자 점수
match-001 WINTER2024 NA-EAST FINALS CHAMPIONSHIP 101 103 2024-01-20 101 3-1
match-002 WINTER2024 NA-EAST SEMIFINALS UPPER 101 105 2024-01-18 101 3-2
match-003 WINTER2024 NA-EAST SEMIFINALS UPPER 103 107 2024-01-18 103 3-0
match-004 WINTER2024 NA-EAST QUARTERFINALS UPPER 101 109 2024-01-15 101 3-1
match-005 WINTER2024 NA-WEST FINALS CHAMPIONSHIP 102 104 2024-01-20 102 3-2
match-006 WINTER2024 NA-WEST SEMIFINALS UPPER 102 106 2024-01-18 102 3-1
match-007 SPRING2024 NA-EAST QUARTERFINALS UPPER 101 108 2024-03-15 101 3-0
match-008 SPRING2024 NA-EAST QUARTERFINALS LOWER 103 110 2024-03-15 103 3-2

GSI: TournamentRegionIndex(다중 속성 키)

tournamentId(PK) 리전(PK) 라운드(SK) 그룹(SK) matchId(SK) player1Id player2Id matchDate 승자 점수
WINTER2024 NA-EAST FINALS CHAMPIONSHIP match-001 101 103 2024-01-20 101 3-1
WINTER2024 NA-EAST QUARTERFINALS UPPER match-004 101 109 2024-01-15 101 3-1
WINTER2024 NA-EAST SEMIFINALS UPPER match-002 101 105 2024-01-18 101 3-2
WINTER2024 NA-EAST SEMIFINALS UPPER match-003 103 107 2024-01-18 103 3-0
WINTER2024 NA-WEST FINALS CHAMPIONSHIP match-005 102 104 2024-01-20 102 3-2
WINTER2024 NA-WEST SEMIFINALS UPPER match-006 102 106 2024-01-18 102 3-1
SPRING2024 NA-EAST QUARTERFINALS LOWER match-008 103 110 2024-03-15 103 3-2
SPRING2024 NA-EAST QUARTERFINALS UPPER match-007 101 108 2024-03-15 101 3-0

GSI: PlayerMatchHistoryIndex(다중 속성 키)

player1Id(PK) matchDate(SK) 라운드(SK) tournamentId 리전 그룹 matchId player2Id 승자 점수
101 2024-01-15 QUARTERFINALS WINTER2024 NA-EAST UPPER match-004 109 101 3-1
101 2024-01-18 SEMIFINALS WINTER2024 NA-EAST UPPER match-002 105 101 3-2
101 2024-01-20 FINALS WINTER2024 NA-EAST CHAMPIONSHIP match-001 103 101 3-1
101 2024-03-15 QUARTERFINALS SPRING2024 NA-EAST UPPER match-007 108 101 3-0
102 2024-01-18 SEMIFINALS WINTER2024 NA-WEST UPPER match-006 106 102 3-1
102 2024-01-20 FINALS WINTER2024 NA-WEST CHAMPIONSHIP match-005 104 102 3-2
103 2024-01-18 SEMIFINALS WINTER2024 NA-EAST UPPER match-003 107 103 3-0
103 2024-03-15 QUARTERFINALS SPRING2024 NA-EAST LOWER match-008 110 103 3-2

사전 조건

시작하기 전에 다음을 갖추었는지 확인하세요.

계정 및 권한

  • 활성 AWS 계정(필요한 경우 여기에서 생성)

  • DynamoDB 작업에 대한 IAM 권한:

    • dynamodb:CreateTable

    • dynamodb:DeleteTable

    • dynamodb:DescribeTable

    • dynamodb:PutItem

    • dynamodb:Query

    • dynamodb:BatchWriteItem

참고

보안 참고: 프로덕션용으로 필요한 권한만 사용하여 사용자 지정 IAM 정책을 생성합니다. 이 자습서에서는 AWS 관리형 정책 AmazonDynamoDBFullAccessV2를 사용할 수 있습니다.

개발 환경

  • 시스템에 설치된 Node.js

  • 다음 방법 중 하나를 사용하여 구성된 AWS 자격 증명:

옵션 1: AWS CLI

aws configure

옵션 2: 환경 변수

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

필수 패키지 설치

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

구현

1단계: 다중 속성 키를 사용하여 GSI로 테이블 생성

다중 속성 키를 사용하는 간단한 기본 키 구조와 GSI로 테이블을 생성합니다.

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");

주요 설계 결정:

기본 테이블: 기본 테이블은 직접 일치 조회에 간단한 matchId 파티션 키를 사용하여 기본 테이블 구조를 단순하게 유지하면서 GSI는 복잡한 쿼리 패턴을 제공합니다.

TournamentRegionIndex 글로벌 보조 인덱스: TournamentRegionIndex 글로벌 보조 인덱스는 tournamentId + region을 다중 속성 파티션 키로 사용해 데이터를 두 속성의 해시로 분산하는 토너먼트 리전 격리를 생성하여, 특정 토너먼트 리전 컨텍스트 내에서 효율적인 쿼리를 활성화합니다. 다중 속성 정렬 키(round + bracket + matchId)는 일반(라운드)에서 특정(매치 ID)으로 자연스럽게 정렬하여 계층 구조의 모든 수준에서 쿼리를 지원하는 계층적 정렬을 제공합니다.

PlayerMatchHistoryIndex 글로벌 보조 인덱스: PlayerMatchHistoryIndex 글로벌 보조 인덱스는 player1Id를 파티션 키로 사용하여 플레이어별로 데이터를 재구성하고 특정 플레이어에 대한 교차 토너먼트 쿼리를 활성화합니다. 다중 속성 정렬 키(matchDate + round)는 날짜 범위 또는 특정 토너먼트 라운드를 기준으로 필터링할 수 있는 시간 순서를 제공합니다.

2단계: 네이티브 속성이 있는 데이터 삽입

자연 속성을 사용하여 토너먼트 매치 데이터를 추가합니다. GSI는 합성 키 없이 이러한 속성을 자동으로 인덱싱합니다.

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");

데이터 구조 설명:

자연 속성 사용: 각 속성은 문자열 연결 또는 구문 분석이 필요하지 않은 실제 토너먼트 개념을 나타내며 도메인 모델에 직접 매핑됩니다.

자동 글로벌 보조 인덱스 인덱싱: GSI는 합성 연결 키를 필요로 하지 않고 기존 속성(TournamentRegionIndex에는 tournamentId, region, round, bracket, matchId 및 PlayerMatchHistoryIndex에는 player1Id, matchDate, round)을 사용하여 항목을 자동으로 인덱싱합니다.

채우기 불필요: 다중 속성 키가 있는 새 글로벌 보조 인덱스를 기존 테이블에 추가하면 DynamoDB는 합성 키로 항목을 업데이트할 필요 없이 자연 속성을 사용하여 기존 항목을 모두 자동으로 인덱싱합니다.

3단계: 모든 파티션 키 속성을 사용하여 TournamentRegionIndex 글로벌 보조 인덱스 쿼리

이 예제에서는 다중 속성 파티션 키(tournamentId + region)가 있는 TournamentRegionIndex 글로벌 보조 인덱스를 쿼리합니다. 모든 파티션 키 속성은 쿼리에서 등식 조건으로 지정해야 합니다. tournamentId를 단독으로 쿼리하거나 파티션 키 속성에 부등식 연산자를 사용할 수 없습니다.

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

예상 결과:

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

잘못된 쿼리:

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

성능: 다중 속성 파티션 키는 함께 해시되어 단일 속성 키와 동일한 O(1) 조회 성능을 제공합니다.

4단계: 글로벌 보조 인덱스 정렬 키를 왼쪽에서 오른쪽으로 쿼리

정렬 키 속성은 글로벌 보조 인덱스에 정의된 순서에 따라 왼쪽에서 오른쪽으로 쿼리해야 합니다. 이 예제는 서로 다른 계층 수준에서 TournamentRegionIndex를 쿼리하는 방법을 보여줍니다. round만으로 필터링하거나, round + bracket으로 필터링하거나, 세 가지 정렬 키 속성 모두로 필터링할 수 있습니다. 중간에는 속성을 건너뛸 수 없습니다. 예를 들어 bracket을 건너뛰는 동안 roundmatchId를 기준으로 쿼리할 수 없습니다.

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

예상 결과:

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

왼쪽에서 오른쪽으로 쿼리 규칙: 속성을 건너뛰지 않고 왼쪽에서 오른쪽으로 속성을 쿼리해야 합니다.

유효한 패턴:

  • 첫 번째 속성만: round = 'SEMIFINALS'

  • 처음 두 속성: round = 'SEMIFINALS' AND bracket = 'UPPER'

  • 세 가지 속성 모두: round = 'SEMIFINALS' AND bracket = 'UPPER' AND matchId = 'match-002'

잘못된 패턴:

  • 첫 번째 속성 건너뛰기: bracket = 'UPPER'(라운드 건너뛰기)

  • 순서에 맞지 않는 쿼리: matchId = 'match-002' AND round = 'SEMIFINALS'

  • 공백 남기기: round = 'SEMIFINALS' AND matchId = 'match-002'(괄호 건너뛰기)

참고

설계 팁: 쿼리 유연성을 극대화하기 위해 정렬 키 속성을 가장 일반적인 속성부터 가장 구체적인 속성까지 정렬합니다.

5단계: 글로벌 보조 인덱스 정렬 키에 부등 조건 사용

부등 조건은 쿼리의 마지막 조건이어야 합니다. 이 예제에서는 정렬 키 속성에서 비교 연산자(>=, BETWEEN) 및 접두사 일치(begins_with())를 사용하는 방법을 보여줍니다. 부등식 연산자를 사용하면 이후에 정렬 키 조건을 추가할 수 없습니다. 부등식은 키 조건 표현식의 최종 조건이어야 합니다.

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

부등 연산자 규칙: 비교 연산자(>, >=, <, <=), BETWEEN 범위 쿼리, 접두사 일치용 begins_with()를 사용할 수 있습니다. 부등식은 쿼리의 마지막 조건이어야 합니다.

유효한 패턴:

  • 평등 조건 뒤에 부등이 오는 경우: round = 'SEMIFINALS' AND bracket = 'UPPER' AND matchId > 'match-001'

  • 첫 번째 속성에 대한 부등: round BETWEEN 'QUARTERFINALS' AND 'SEMIFINALS'

  • 최종 조건으로 접두사 일치: round = 'SEMIFINALS' AND begins_with(bracket, 'U')

잘못된 패턴:

  • 부등식 후 조건 추가: round > 'QUARTERFINALS' AND bracket = 'UPPER'

  • 여러 부등 사용: round > 'QUARTERFINALS' AND bracket > 'L'

중요

begins_with()는 부등 조건으로 취급되므로 이를 따를 수 있는 추가 정렬 키 조건은 없습니다.

6단계: 다중 속성 정렬 키를 사용하여 PlayerMatchHistoryIndex 글로벌 보조 인덱스 쿼리

이 예제에서는 단일 파티션 키(player1Id)와 다중 속성 정렬 키(matchDate + round)가 있는 PlayerMatchHistoryIndex를 쿼리합니다. 이렇게 하면 토너먼트 ID를 모르고 특정 플레이어의 모든 매치를 쿼리하여 교차 토너먼트 분석을 수행할 수 있습니다. 기본 테이블에는 토너먼트 리전 조합당 별도의 쿼리가 필요합니다.

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

패턴 변형

다중 속성 키가 있는 시계열 데이터

계층적 시간 속성을 사용하여 시계열 쿼리에 최적화

{ 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

이점: 자연 시간 계층 구조(년 → 월 → 일 → 타임스탬프)를 사용하면 날짜 구문 분석 또는 조작 없이 언제든지 효율적인 쿼리를 수행할 수 있습니다. 글로벌 보조 인덱스는 자연 시간 속성을 사용하여 모든 판독값을 자동으로 인덱싱합니다.

다중 속성 키를 사용한 전자 상거래 주문

여러 차원으로 주문 추적

{ 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

계층적 조직 데이터

조직 계층 구조 모델링

{ 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

희소 다중 속성 키

다중 속성 키를 결합하여 희소 GSI 생성

{ 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 및 멀티테넌시

고객 격리 기능이 있는 다중 테넌트 SaaS 플랫폼

// 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' } }));

이점: 테넌트-고객 컨텍스트 및 자연 데이터 조직 내에서 효율적인 쿼리.

금융 트랜잭션

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' } }));

전체 예제

다음 예제에서는 설정부터 정리까지 다중 속성 키를 보여줍니다.

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);

미니멀 코드 스캐폴드

// 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' }

추가 리소스