DynamoDBの基礎知識と使い方
 Author: 水卜

DynamoDB概要

AWSのキーバリューストア(NoSQL)であるdynamoDB。

amazonのカートで使われているらしい。

大量のセンサーデータを捌くようなIoT案件が増えていることもあり、スケーラブルなI/Oに対応する場面が増えていくことは想像に容易い。RDBに頼らない引き出しが出来るのは精神的にもありがたいので、ぜひ使いこなしたい。

下記の記事では軽く触れたが、真面目に使うための情報を記述する。

AWSの各種DBの使用感

特徴

  • スケーラブル
  • 低レイテンシ(低遅延)
  • 3つのAZにデータが置かれるので高可用性
  • 容量制限なし
  • 管理必要なし
  • プロビジョンドスループット

テーブルごとにRead/Writeのスループットキャパシティを設定可能。オートスケールも可能。

つまり、書き込みはたまにだけど読み込みが多いので「Read1000、write100」みたいな設定が可能。

DynamoDBとRDS(RDB)の比較

RDSを使うべき場合

  • 複雑な検索が必要
    • Joinなど
  • トランザクションが必要
    • データの一貫性を重視

DynamoDBを使うべき場合

  • スケーラブルなアクセスが来る
    • 大量のIoTセンサーデータやゲームのデータをとりあえずぶち込む等
  • 低レイテンシが期待される

DynamoDBのテーブル設計

テーブル

テーブルはPartition KeyとSortKeyを持つことができる。

  • Partition Key

いわゆるPrimary Key。同一テーブル内でユニーク。

  • SortKey

範囲検索ができるキー。任意で追加できる。

これを貼るときはPartition KeyとSort Keyの組み合わせで各レコードがユニークにならなければならない。

インデックス

  • ローカルセカンダリインデックス(LSI)

Partition KeyとSort Keyを持つテーブルに対し、Partition Keyが同じで、その他のAttributeで範囲検索したい時に貼るインデックス。

  • グローバルセカンダリインデックス(GSI)

任意の列でのイコール検索、それ以外の列での範囲検索をしたいときに貼るインデックス。

お金の話

リージョンはアジアパシフィック (東京)。

オンデマンドキャパシティの場合

読み込み/書き込み

料金タイプ 料金
書き込みリクエスト単位 書き込みリクエスト 100 万単位あたり 1.4269USD
読み込みリクエスト単位 読み込みリクエスト 100 万単位あたり 0.285USD

データストレージ

  • 毎月最初の 25 GB の保管は無料

  • それ以降は月額 0.285USD/GB

プロビジョンドキャパシティの場合

読み込み/書き込み

DynamoDB では、1 秒あたりの (1 KB までの) 書き込み 1 回ごとに WCU 1 個分、1 秒あたりのトランザクション書き込み 1 回ごとに WCU 2 個分の料金が発生します。DynamoDB の読み込みでは、強力な整合性のある読み込みなら 1 秒あたり 1 回ごとに RCU 1 個分、トランザクション読み込みなら 1 秒あたり 1 回ごとに RCU 2 個分、結果整合性のある読み込みなら 1 秒あたり 1 回ごとに RCU 0.5 個分の料金が発生します (4 KB まで)。

基本は以下の料金だが、EC2のリザーブドインスタンスのような、あらかじめ想定されるスループットに応じたキャパシティを前払いで購入することで受けられるディスカウントプランもある。(リザーブドキャパシティ)

プロビジョニングするスループットタイプ 時間あたりの料金
書き込みキャパシティーユニット (WCU) 0.000742USD/WCU
読み込みキャパシティーユニット (RCU) 0.0001484USD/RCU

データストレージ

  • 毎月最初の 25 GB の保管は無料

  • それ以降は月額 0.285USD/GB

無料枠

月ごとに以下を無料で利用可能。

  • 25 GB のデータストレージ
  • 2 つの AWS リージョンにデプロイされたグローバルテーブルに対して rWCU 25 個
  • DynamoDB ストリームからのストリーム読み込みリクエスト 250 万回
  • AWS のサービス全体での合計データ転送 (アウト) 1 GB

DynamoDB Accelerator (DAX)

読み書きのたびに金がかかるdynamoDBのデータをキャッシュし、DAXから返すことでコスト削減できる。

DAXインスタンスを立てて、稼働させた時間とインスタンスのサイズに応じて課金。

PythonからDynamoDBを操作

lambdaなどから使うことを想定し、PythonからDynamoDBを扱う時のサンプルコードを記述する。

データ追加

import boto3
dynamodb = boto3.resource('dynamodb',
                          region_name='us-west-2', # lambdaなら省略可
                          endpoint_url="http://localhost:8000") # lambdaなら省略可
table = dynamodb.Table('Movies')
title = "The Big New Movie"
year = 2015
response = table.put_item(
   Item={
        'year': year,
        'title': title,
        'info': {
            'plot':"Nothing happens at all.",
            'rating': decimal.Decimal(0)
        }
    }
)
print("PutItem succeeded:")
print(json.dumps(response))

たくさんのデータ追加

def push_sensor_data(data):
    dynamo_db = boto3.resource("dynamodb")
    table = dynamo_db.Table("Data")
    with table.batch_writer() as batch:
        for item in data:
            batch.put_item(Item=item)

データ更新

# 更新前
# {
#     year: 2015,
#     title: "The Big New Movie",
#     info: {
#         plot: "Nothing happens at all.",
#         rating: 0
#     }
# }
# 更新後
# {
#     year: 2015,
#     title: "The Big New Movie",
#     info: {
#         plot: "Everything happens all at once.",
#         rating: 5.5,
#         actors: ["Larry", "Moe", "Curly"]
#     }
# }

import boto3
dynamodb = boto3.resource('dynamodb',
                          region_name='us-west-2', # lambdaなら省略可
                          endpoint_url="http://localhost:8000") # lambdaなら省略可
table = dynamodb.Table('Movies')
title = "The Big New Movie"
year = 2015
response = table.update_item(
    Key={
        'year': year,
        'title': title
    },
    UpdateExpression="set info.rating = :r, info.plot=:p, info.actors=:a",
  	ConditionExpression="size(info.rating) == :num", # 条件付きの更新が可能
    ExpressionAttributeValues={
      	':num': 0, # ConditionExpressionで使う変数
        ':r': decimal.Decimal(5.5),
        ':p': "Everything happens all at once.",
        ':a': ["Larry", "Moe", "Curly"]
    },
    ReturnValues="UPDATED_NEW"
)

ReturnValuesに"UPDATED_NEW"とあるが、これで更新された属性のみがDynamoDBからのレスポンスとして返ってくるようになる。

アトミックカウンター

呼び出し部分以外は省略。呼ばれるたびに、他の書き込みリクエストを妨げることなく既存の属性値をインクリメントまたはデクリメントすることができる。

ブログのアクセスカウンターなどに便利。

response = table.update_item(
    Key={
        'year': year,
        'title': title
    },
    UpdateExpression="set info.rating = info.rating + :val",
    ExpressionAttributeValues={
        ':val': decimal.Decimal(1)
    },
    ReturnValues="UPDATED_NEW"
)

データ削除

import boto3
from botocore.exceptions import ClientError
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Movies')
title = "The Big New Movie"
year = 2015

try:
    response = table.delete_item(
        Key={
            'year': year,
            'title': title
        },
        ConditionExpression="info.rating <= :val",
        ExpressionAttributeValues= {
            ":val": decimal.Decimal(5)
        }
    )
except ClientError as e:
    if e.response['Error']['Code'] == "ConditionalCheckFailedException":
        print(e.response['Error']['Message'])
    else:
        raise
else:
    print("DeleteItem succeeded:")
    print(json.dumps(response))

条件付き削除に失敗した場合、ConditionalCheckFailedExceptionを吐く。

データ取得

title = "The Big New Movie"
year = 2015

try:
    response = table.get_item(
        Key={
            'year': year,
            'title': title
        }
    )
except ClientError as e:
    print(e.response['Error']['Message'])
else:
    item = response['Item']
    print("GetItem succeeded:")
    print(json.dumps(item))

クエリ

import boto3
from boto3.dynamodb.conditions import Key, Attr
import json
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Movies')

print("Movies from 1985")

response = table.query(
    KeyConditionExpression=Key('year').eq(1985)
)

for i in response['Items']:
    print(i['year'], ":", i['title'])

Amazon DynamoDB の使用可能な条件のリストについては、AWS SDK for Python (Boto 3) の使用開始の「DynamoDB の条件」を参照してください。

詳細については、「条件式」を参照してください。

複雑なクエリ

import boto3
from boto3.dynamodb.conditions import Key, Attr
import json

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Movies')

print("Movies from 1992 - titles A-L, with genres and lead actor")

response = table.query(
    ProjectionExpression="#yr, title, info.genres, info.actors[0]",
    ExpressionAttributeNames={ "#yr": "year" }, # Expression Attribute Names for Projection Expression only.
    KeyConditionExpression=Key('year').eq(1992) & Key('title').between('A', 'L')
)

year 1992 にリリースされたすべての映画のうちtitle が「A」~「L」で始まる映画を取得する。

スキャン

テーブルの全データを返す。フィルタリング(filter_expression)もできるが、テーブルの全データが読み込まれた後にフィルタリングされる。

import boto3
from boto3.dynamodb.conditions import Key, Attr
import json

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Movies')

fe = Key('year').between(1950, 1959)
pe = "#yr, title, info.rating"
# Expression Attribute Names for Projection Expression only.
ean = { "#yr": "year", }
esk = None

response = table.scan(
    FilterExpression=fe,
    ProjectionExpression=pe,
    ExpressionAttributeNames=ean
    )

for i in response['Items']:
    print(json.dumps(i, cls=DecimalEncoder))

while 'LastEvaluatedKey' in response:
    response = table.scan(
        ProjectionExpression=pe,
        FilterExpression=fe,
        ExpressionAttributeNames= ean,
        ExclusiveStartKey=response['LastEvaluatedKey']
        )

    for i in response['Items']:
        print(json.dumps(i))

Movies テーブル全体をスキャンし、1950 年代の映画のみ (約 100 項目) を取得して、残りはすべて破棄する。

参考

公式開発者ガイド