아장아장 개발 일기

DynamoDB 기본기 (PrimaryKey, SecondaryIndex, FilterExpression 본문

개발/AWS

DynamoDB 기본기 (PrimaryKey, SecondaryIndex, FilterExpression

빨간머리 마녀 🍒 2023. 3. 10. 15:53

DynamoDB의 기본적인 특징

  • 프라이머리키를 제외하고 나머지는 스키마가 없다
    ⇒ 속성이나 데이터 타입이 미리 지정될 필요 없고, 각각 고유의 속성을 가질 수 있다
  • 대부분의 Attributes속성값들은 scalar 값을 가진다 ex) String, numbers
  • 속성값은 nested attributes(포함된 속성 즉, 중첩속성)을 가질 수도 있다 ex) json, list, map

Primary Key

  • 테이블을 만들때는 각각의 아이템을 구분하기 위해 프라이머리 키를 필수적으로 정해야한다. 아래와 같이 두가지 종류의 프라이머리 키 구성이 가능하다.
    1. Partition Key
    2. Partition Key + Sort Key
  1. Partition Key
    - 두개 이상의 아이템은 동일한 파티션 키 값을 가질 수 없다.
    - 파티션 키 값으로 바로 해당 아이템에 접근할 수 있다.
    - 아래 People 테이블은 PersonID를 단독 Primary Key로 사용한다.
People

{
    "PersonID": 101,
    "LastName": "Smith",
    "FirstName": "Fred",
    "Phone": "555-4321"
}

{
    "PersonID": 102,
    "LastName": "Jones",
    "FirstName": "Mary",
    "Address": {
                "Street": "123 Main",
                "City": "Anytown",
                "State": "OH",
                "ZIPCode": 12345
    }
}

{
    "PersonID": 103,
    "LastName": "Stephens",
    "FirstName": "Howard",
    "Address": {
                "Street": "123 Main",
                "City": "London",                                    
                "PostalCode": "ER3 5K8"
    },
    "FavoriteColor": "Blue"
}

 2. Partition Key + Range Key
    - composite primary key라고도 불린다.
    - partition key를 내부 해시기능을 위한 값으로 사용하고, 그 결과값으로 아이템이 어디에 저장될지 결정된다.
    - 같은 partition key값을 가진 아이템들은 같이 저장되는데, sort key 값으로 정렬된다.
    - 두개 이상의 아이템이 같은 partition key 값을 가질 수도 있으나 sort key 값은 달라야 한다.

 

아래 Music 테이블은 Artist를 partition key로, songTitle을 range key로 가진다.

(-> 즉 Artist와 songTitle 값으로 아이템에 접근할 수 있다.)

Artist 값만 제공하면 해당 아티스트의 모든 아이템이 조회되고, Artist와 SongTitle 값을 함께 제공하면 해당 아티스트의 특정 아이템이 조회된다.

Music

{
    "Artist": "No One You Know",
    "SongTitle": "My Dog Spot",
    "AlbumTitle": "Hey Now",
    "Price": 1.98,
    "Genre": "Country",
    "CriticRating": 8.4
}

{
    "Artist": "No One You Know",
    "SongTitle": "Somewhere Down The Road",
    "AlbumTitle": "Somewhat Famous",
    "Genre": "Country",
    "CriticRating": 8.4,
    "Year": 1984
}

{
    "Artist": "The Acme Band",
    "SongTitle": "Still in Love",
    "AlbumTitle": "The Buck Starts Here",
    "Price": 2.47,
    "Genre": "Rock",
    "PromotionInfo": {
        "RadioStationsPlaying": {
            "KHCR",
            "KQBX",
            "WTNR",
            "WJJH"
        },
        "TourDates": {
            "Seattle": "20150622",
            "Cleveland": "20150630"
        },
        "Rotation": "Heavy"
    }
}

{
    "Artist": "The Acme Band",
    "SongTitle": "Look Out, World",
    "AlbumTitle": "The Buck Starts Here",
    "Price": 0.99,
    "Genre": "Rock"
}

Secondary Indexes

  • 한개 이상의 세컨더리 인덱스를 만들 수 있다.
  • 세컨더리 인덱스는 대체 키를 사용해 프라이머리 키와 더불어 추가적으로 데이터를 쿼리할 수 있도록 한다.
  • 세컨더리 인덱스 사용은 필수가 아니지만, 사용할 경우 데이터를 쿼리하는데에 있어 더 유연해진다.
  • 세컨더리 인덱스는 기존 테이블의 데이터를 새로운 프라이머리 스키마를 사용해 새로운 구조로 복사한다.
  • 세컨더리 인덱스를 만들때 해당 인덱스의 키 스키마를 명시해야하며 세컨더리 인덱스 역시 메인 테이블과 같이 Query API를 사용할 수 있음
  • DynamoDB는 아이템이 세컨더리 인덱스의 주요 스키마에 해당하는 요소를 모두 가지고 있을때만 세컨더리 인덱스로 포함시킨다. → 아이템에 특정 요소가 있는지 확인하는 글로벌 필터로서의 역할을 한다

예시) 한 레코드안의 노래중에 platinum(백만부 이상 판매된 노래)를 찾고 싶을때
        → SalesCount와 동일한 값을 가지는 SongPlatinumSalesCount 속성을 추가한다.
        (단, 해당 아이템의 판매수가 100만 이상일 경우에만)

세컨더리 인덱스는 메인 테이블의 모든 아이템을 가지고 있지는 않다.(=sparse)

SongPlatinumSalesCount 속성값이 없는 Album 아이템이나 백만건 미만의 판매량을 가지는 Song 아이템은 제외되었다.

 

이처럼 세컨더리 인덱스를 사용해 레코드 라벨로 100만건 이상의 판매량을 가진 노래를 가져올 수 있다.

아래는 RecordLabel을 파티션 키로 사용해 플래티넘 송을 가져오는 쿼리문이다.(아이템들은 판매량을 기준으로 소팅된다)

const AWS = require('aws-sdk');
const client = new AWS.DynamoDB.DocumentClient({apiVersion: '2012-08-10'});

const results = await client.query({
  TableName: 'MusicTable',
  IndexName: 'PlatinumSongsByLabel',
  KeyConditionExpression: 'PK = :pk',
  ExpressionAttributeValues: {
    ':pk': 'Capitol'
  }
})

위의 쿼리문은 아래의 SQL 문과 동일한 의미를 가진다.

-- Fetch all platinum songs from Capital records
SELECT songs.*
FROM songs
JOIN albums ON songs.album_id = albums.id
JOIN labels ON albums.label_id = labels.id
WHERE labels.name = 'Capitol'
AND songs.copies_sold >= 1000000
ORDER BY songs.copies_sold DESC

Filter Expression

Filtering Expression이란?

주어진 표현식에 부합하지 않는 값을 Query 혹은 Scan에서 제외시켜주는 방법이다.

Filtering을 지양해야하는 이유가 있는데 바로 먼저 아이템을 모두 읽은 후 Filtering이 진행되기 때문에 정확하지 않기 때문이다.

아래는 FilterExpression을 사용하여 아이템을 선별하려고할때 일어날 수 있는 문제점과 그 원인에 대한 설명이다.

  1. DynamoDB 아이템 조회는 1MB까지만 진행됨
  2. 예를들어 1 GB 사이즈의 Music 테이블에서 Platinum 에 해당하는 음악만 조회한다고했을때
  3. Music 테이블을 1000번 조회해야하며, 그중 많은 경우 빈값이 반환됨(platinum에 해당하지 않는 아이템이 많기 때문)
  4. DynamoDB를 쓸경우 테이블의 사이즈가 1GB보다는 클경우가 많은데, filtering으로 특정 아이템을 골라낼때 오류 발생할 확률 매우 높음

⇒ 그렇다면 어떻게 query해야할까?

     Primary Key 혹은 Secondary Index를 통한 Sparse Indexing을 통해 쿼리하는 것이 보다 안전한 방식이다.

 

하지만 FilteringExpression을 항상 피해야하는 것은 아니다.
아래에 어떤 경우에 Filtering이 사용되면 알맞을지 예시를 들어놨다.


Filtering이 사용되는 경우

  1. 결과 페이로드 사이즈를 줄이고 싶을때
    → DynamoDB의 결과 페이로드 사이즈는 최대 1MB인데 이는 꽤 큰 데이터 전송이다. 만약 애플리케이션에 데이터가 도달한 이후 그중 많은 수를 버려야한다면, 이 필터링을 클라이언트가 아닌 서버에서 처리하는 게 나을 수 있다.
  2. 더 쉽게 어플리케이션을 필터링하고 싶을 때
    → 쿼리 결과값에서 많은 수를 즉시 필터링해야한다면 앱내의 코드보다는 filter expression을 사용하는게 낫다. 한 앨범의 모든 곡중에 50만건 이상의 판매를 올린 곡만 얻고싶다고 하자. 이를 구현하는 한가지 방법은 앨범의 모든 곡을 조회한 후, 그중 50만건 미만의 판매를 낸 곡을 필터링하는 것이다.아니면 filter expression 을 사용해 클라이언트에서 필터링할 필요를 없앨 수도 있다.
const AWS = require('aws-sdk');
const client = new AWS.DynamoDB.DocumentClient({apiVersion: '2012-08-10'});

const results = await client.query({
  TableName: 'MusicTable',
  KeyConditionExpression: 'PK = :pk',
  ExpressionAttributeValues: {
    ':pk': 'ALBUM#PAUL MCCARTNEY#FLAMING PIE'
  }
})

const filteredResults = results.Items.filter(item => item.Sales >= 500000)

 

const AWS = require('aws-sdk');
const client = new AWS.DynamoDB.DocumentClient({apiVersion: '2012-08-10'});

const results = await client.query({
  TableName: 'MusicTable',
  KeyConditionExpression: 'PK = :pk',
  FilterExpression: 'Sales >= :threshold',
  ExpressionAttributeValues: {
    ':pk': 'ALBUM#PAUL MCCARTNEY#FLAMING PIE',
    ':threshold': 500000
  }
})

3. time-to-live 만료시간에 대해서 더 정확한 검증을 하고싶을 때
    DynamoDB를 사용하면 테이블에 time-to-live 속성을 명시할 수 있다.
    DynamoDB는 주기적으로 아이템들을 리뷰해서 TTL 속성이 현재보다 이전인 아이템을 삭제한다.
    이는 기본적으로 애플리케이션의 인증(로그인)을 위한 세션 저장에 사용될 수 있다.
     → 지정한 시간이 지나면 세션이 만료되어 사용자는 다시 인증(로그인)해야한다.

위의 테이블에서 Partition Key 는 SessionId 이며, ExpiresAt 속성도 존재한다. 해당 속성에 TTL 을 설정하면 해당 시간이 지났을때 DynamoDB는 해당 아이템을 삭제한다. 대부분의 경우 DynamoDB의 TTL은 주어진 시간에 매우 근접해 작동하지만, DynamoDB 문서에는 보통 만료시점으로부터 48시간 내에 아이템을 삭제한다고 쓰여있다. 즉, 앱이 이미 만료된 세션을 읽어들일 가능성이 있다. 이를 방지하기위해 이미 만료된 세션은 필터링하는 filter expression을 사용할 수 있다.

const AWS = require('aws-sdk');
const client = new AWS.DynamoDB.DocumentClient({apiVersion: '2012-08-10'});

// Find the current timestamp.
const time = Date.now() / 1000

client.query({
  TableName: 'SessionStore',
  KeyConditionExpression: 'SessionId = :session',
  FilterExpression: 'ExpiresAt >= :currentTime,
  ExpressionAttributeValues: {
    ':session': 'd96d4fa6-2a20-48e0-a6cf-676397597a81'
    ':currentTime': time
  }
})

 

참고

https://huilife.tistory.com/entry/DynamoDB%EC%9D%98-Hash-key%EC%99%80-Sort-key

https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/SecondaryIndexes.html

https://www.alexdebrie.com/posts/dynamodb-filter-expressions/

Comments