この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
おつかれさまです。 最近、ドラマ「白い巨塔」を見て禁煙をはじめた新井@サーバーレス開発部です。
唐突ですが、みなさんはDynamoDBのLastEvaluatedKey
についてはご存知でしょうか?
※知らないという方はこちらをご覧ください。
簡単に言ってしまうと、DynamoDBのScanやQueryの操作で全件取得しきれなかった時に返却される、次点の開始位置を示すキーです。
DynamoDBのScanやQueryで全件取得しきれなかったり、最大取得件数(Limit
)を設定してベージネーションをしているときなど、よく見かける方も多いのではないでしょうか。
ちなみに、boto3のレスポンスだとこんな感じにLastEvaluatedKey
の情報が返却されます。
{
'Items': [{'id': Decimal('1'), 'name': 'charlie'}],
'Count': 1,
'ScannedCount': 1,
'LastEvaluatedKey': {'id': Decimal('1'), 'name': 'charlie'}
...
で、今回は
「ページネーションを行っている間に、このキーが指すデータポイントが削除/更新された場合の挙動ってどうなるんだろう?」
「データ取得時にエラーになるのでは?」
「不整合がおきる?」
と気になったため調べてみた際のメモになります。
先に結論
結論から言うと、レスポンスデータが返されて次のリクエストを送るまでの間に、lastEvaluatedKey
が指しているデータが削除、変更されたとしてもなんら問題ありません。 (※というか、そもそも変更はできません。詳しくは後述)
lastEvaluatedKey
はデータそのものを参照しているのではなく、あくまで開始位置を示しているだけだからです。
データが削除されようが、更新されようが、前後に新しいデータが追加されようが、開始位置は変わりません。更新タイミングによってはデータの取得漏れがありますが、そこは割り切りですね。
やってみた
下記のテーブルに対し、Scan操作を実施していきます。
- テーブル名:
arai-test-table
- HashKey:
id
(Number) - RangeKey:
name
(String)
id | name |
---|---|
1 | alice |
1 | bob |
1 | charlie |
2 | dave |
2 | eve |
3 | frank |
Scanした時の挙動
- Python コード
import boto3
import time
dynamodb = boto3.resource('dynamodb', region_name='ap-northeast-1')
table = dynamodb.Table('arai-test-table')
last_key = None
while True:
params = {'Limit':1}
if last_key:
params['ExclusiveStartKey'] = last_key
response = table.scan(**params)
if 'LastEvaluatedKey' not in response:
break
last_key = response['LastEvaluatedKey']
print('====================ITEMS====================')
print('Items', response['Items'], sep='=')
print('LastEvaluatedKey', last_key, sep='=')
time.sleep(3)
- 出力
python ddb_scan.py
====================ITEMS====================
Items=[{'id': Decimal('3'), 'name': 'frank'}]
LastEvaluatedKey={'id': Decimal('3'), 'name': 'frank'}
====================ITEMS====================
Items=[{'id': Decimal('2'), 'name': 'dave'}]
LastEvaluatedKey={'id': Decimal('2'), 'name': 'dave'}
====================ITEMS====================
Items=[{'id': Decimal('2'), 'name': 'eve'}]
LastEvaluatedKey={'id': Decimal('2'), 'name': 'eve'}
====================ITEMS====================
Items=[{'id': Decimal('1'), 'name': 'alice'}]
LastEvaluatedKey={'id': Decimal('1'), 'name': 'alice'}
====================ITEMS====================
Items=[{'id': Decimal('1'), 'name': 'bob'}]
LastEvaluatedKey={'id': Decimal('1'), 'name': 'bob'}
====================ITEMS====================
Items=[{'id': Decimal('1'), 'name': 'charlie'}]
LastEvaluatedKey={'id': Decimal('1'), 'name': 'charlie'}
Scanしたときの返却値の順番ってこんな感じになるんですね。。。
開始位置のItemを削除してみる
alice
まで読み込んだ後に、DynamoDBから{'id': Decimal('1'), 'name': 'alice'}
のレコードを削除してみます。
次点の検索では、ExclusiveStartKey
がalice
のレコードを指しています。
$ python ddb_scan.py
====================ITEMS====================
Items=[{'id': Decimal('3'), 'name': 'frank'}]
LastEvaluatedKey={'id': Decimal('3'), 'name': 'frank'}
====================ITEMS====================
Items=[{'id': Decimal('2'), 'name': 'dave'}]
LastEvaluatedKey={'id': Decimal('2'), 'name': 'dave'}
====================ITEMS====================
Items=[{'id': Decimal('2'), 'name': 'eve'}]
LastEvaluatedKey={'id': Decimal('2'), 'name': 'eve'}
====================ITEMS====================
Items=[{'id': Decimal('1'), 'name': 'alice'}]
LastEvaluatedKey={'id': Decimal('1'), 'name': 'alice'}
...
# ここで、{id:1, name: alice}のレコードを削除
...
====================ITEMS====================
Items=[{'id': Decimal('1'), 'name': 'bob'}]
LastEvaluatedKey={'id': Decimal('1'), 'name': 'bob'}
====================ITEMS====================
Items=[{'id': Decimal('1'), 'name': 'charlie'}]
LastEvaluatedKey={'id': Decimal('1'), 'name': 'charlie'}
なんら問題なく動作しました。
開始位置のItemを更新してみる
alice
まで読み込んだ後に、alice
の名前をarai
に変更して...
と考えていましたが、よく考えたらlastEvaluatedKey
にはユニークなプライマリーキーが返却されるので、プライマリーキーの変更はできません。
開始位置の前後にItem追加してみる
だいたい予想できるかとおもいますが、一応試してみます。
alice
まで読み込んだ後に、arai
とabe
というレコードを挿入してみます。
id | name |
---|---|
1 | abe |
1 | arai |
$ python ddb_scan.py
====================ITEMS====================
Items=[{'id': Decimal('3'), 'name': 'frank'}]
LastEvaluatedKey={'id': Decimal('3'), 'name': 'frank'}
====================ITEMS====================
Items=[{'id': Decimal('2'), 'name': 'dave'}]
LastEvaluatedKey={'id': Decimal('2'), 'name': 'dave'}
====================ITEMS====================
Items=[{'id': Decimal('2'), 'name': 'eve'}]
LastEvaluatedKey={'id': Decimal('2'), 'name': 'eve'}
====================ITEMS====================
Items=[{'id': Decimal('1'), 'name': 'alice'}]
LastEvaluatedKey={'id': Decimal('1'), 'name': 'alice'}
...
# ここで、{id:1, name: abe}と{id:1, name: arai}のレコードを挿入
...
====================ITEMS====================
Items=[{'id': Decimal('1'), 'name': 'arai'}]
LastEvaluatedKey={'id': Decimal('1'), 'name': 'arai'}
====================ITEMS====================
Items=[{'id': Decimal('1'), 'name': 'bob'}]
LastEvaluatedKey={'id': Decimal('1'), 'name': 'bob'}
====================ITEMS====================
Items=[{'id': Decimal('1'), 'name': 'charlie'}]
LastEvaluatedKey={'id': Decimal('1'), 'name': 'charlie'}
ソート順的に、alice
の後に来るarai
の方のレコードは取得できていますね。
まとめ
LastEvaluatedKey
やExclusiveStartKey
はあくまで、開始位置や終了位置を示しているだけで、直接データを参照しているわけではないということですね。
開始位置にデータがあろうがなかろうが関係ないということです。
ちなみに、Queryでのテーブル操作の場合も挙動は同じでした!
よく考えてみれば当たり前なのですが、普段意識しない部分なので、動きが確認できてよかったです。あらめてDynamoDBとお近づきになれた気がします。
以上、どなたかの役に立てば幸いです。お疲れ様でした!
余談
AWS CLIから利用した場合は、LastEvaluatedKey
ではなくNextToken
が返却されました。
$ aws dynamodb scan \
--table-name arai-test-table \
--max-items 1
{
"Items": [
{
"id": {
"N": "3"
},
"name": {
"S": "frank"
}
}
],
"Count": 6,
"ScannedCount": 6,
"ConsumedCapacity": null,
"NextToken": "eyJFeGNsdXNpdmVTdGFydEtleSI6IG51bGwsICJib3RvX3RydW5jYXRlX2Ftb3VudCI6IDF9"
}
$ echo -n "eyJFeGNsdXNpdmVTdGFydEtleSI6IG51bGwsICJib3RvX3RydW5jYXRlX2Ftb3VudCI6IDF9" | base64 -d
{"ExclusiveStartKey": null, "boto_truncate_amount": 1}
boto_truncate_amountこいつが何なのかちゃんと調べられていませんが、同じく開始位置を示しているっぽいのはわかります。ちなみにtrancated=「切り捨て」のことなので、普通に考えればoffset
のような扱いなんだと思います。
AWS CLIの場合は、コマンドに--starting-token
オプションを追加し、NextToken
の値をセットしてあげることで、途中からデータ取得を再開できます。