
DynamoDBのLastEvaluatedKeyが示すレコードを削除した時の挙動について調べてみた
この記事は公開されてから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の値をセットしてあげることで、途中からデータ取得を再開できます。







