こしあん
2024-12-22

DynamoDBのScanIndexForwardで昇順/降順ソートでクエリ


131{icon} {views}


DynamoDBでソートキーの昇順・降順を簡単に切り替えるために、ScanIndexForwardオプションを使う方法をTerraformとPythonのサンプルコードで検証してみました。 テーブル構造やインデックスを変えずに、ScanIndexForwardをTrue/Falseにするだけでソート順序が自在に切り替えられることが確認できました。

はじめに

DynamoDBをクエリする際、ソートキーに対してScanIndexForwardのオプションで簡単に昇順降順を切り替えると知ったので確かめてみた

テーブル作成

Terraformでテーブルを作成する。

resource "aws_dynamodb_table" "example" {
  name           = "example-table"
  billing_mode   = "PAY_PER_REQUEST"
  hash_key       = "PartitionKey"
  range_key      = "SortKey" # ソートキー

  attribute {
    name = "PartitionKey"
    type = "S" # S: String
  }

  attribute {
    name = "SortKey"
    type = "S" # ISO8601形式のタイムスタンプとして保存
  }
}

テーブル構造は以下の通りで、

  • PartitionKey:パーティションキーで、具体的にはユーザーIDを追加する。ここではソートの例を示すために固定でデータを登録する
  • SortKey:昇順/降順ソートを確認するためのソートキー、任意のタイムスタンプを登録する

アイテムを追加

以下のコードで10個ほどアイテムを登録する

import boto3
from botocore.exceptions import ClientError
from datetime import datetime, timedelta, timezone
import time

# DynamoDBリソースの取得
session = boto3.Session(profile_name='hogehoge')  # プロファイル名を指定
dynamodb = session.resource('dynamodb', region_name='ap-northeast-1')  # リージョンを指定

# テーブル名を指定
table = dynamodb.Table('example-table')

def generate_item(user_id, index):
    """
    アイテムを生成する関数
    同じPartitionKeyを使用し、SortKeyをタイムスタンプに設定
    """
    time.sleep(0.1)
    return {
        'PartitionKey': f'User#{user_id}',
        'SortKey': (datetime.now(timezone.utc) - timedelta(minutes=index)).isoformat(),  # タイムスタンプをソートキーに
        'Data': {
            'Attribute1': f'Value{index}',
            'Attribute2': index,
            'Attribute3': True
        },
        'CreatedAt': datetime.now(timezone.utc).isoformat()
    }

def batch_write_items(items):
    """
    バッチでアイテムを書き込む関数
    """
    try:
        with table.batch_writer() as batch:
            for item in items:
                batch.put_item(Item=item)
        print(f"Successfully added {len(items)} items.")
    except ClientError as e:
        print(f"Failed to add items: {e.response['Error']['Message']}")

if __name__ == "__main__":
    # 追加するアイテムのリストを生成
    items_to_add = [generate_item(user_id=1000, index=i) for i in range(10)] # 同じIDを使用

    # アイテムをバッチで書き込む
    batch_write_items(items_to_add)

マネジメントコンソールから確認すると以下のように登録されている。ソートキーには現在時刻からインデックスぶんの分数を引いているので、SortKeyCreatedAtの順番は逆になっている。

ScanIndexForwardを変えてクエリしてみる

ソートキーに対する昇順・降順の切り替えは、table.queryScanIndexForwardで切り替える。trueなら昇順、falseなら降順になる。

# query_items.py

import boto3
from botocore.exceptions import ClientError
from boto3.dynamodb.conditions import Key

# DynamoDBリソースの取得
session = boto3.Session(profile_name='hogehoge')  # プロファイル名を指定
dynamodb = session.resource('dynamodb', region_name='ap-northeast-1')  # リージョンを指定

# テーブル名を指定
table = dynamodb.Table('example-table')

def query_items(partition_key, scan_index_forward=True, limit=10):
    """
    パーティションキーに基づいてアイテムをクエリし、ソートキーの順序を指定する関数
    """
    try:
        response = table.query(
            KeyConditionExpression=Key('PartitionKey').eq(partition_key),
            ScanIndexForward=scan_index_forward,  # 昇順=True, 降順=False
            Limit=limit  # 必要に応じて制限
        )
        items = response.get('Items', [])
        order = '昇順' if scan_index_forward else '降順'
        print(f"\nクエリ結果({order}):")
        for item in items:
            print(item)
    except ClientError as e:
        print(f"クエリに失敗しました: {e.response['Error']['Message']}")

if __name__ == "__main__":
    # クエリするパーティションキーを指定
    partition_key = "User#1000"  # 例として追加したアイテムの一つを使用

    # 昇順でクエリ
    query_items(partition_key, scan_index_forward=True)

    # 降順でクエリ
    query_items(partition_key, scan_index_forward=False)

結果は以下の通り

クエリ結果(昇順):
{'PartitionKey': 'User#1000', 'SortKey': '2024-12-21T16:30:03.407969+00:00', 'Data': {'Attribute3': True, 'Attribute2': Decimal('9'), 'Attribute1': 'Value9'}, 'CreatedAt': '2024-12-21T16:39:03.407969+00:00'}
{'PartitionKey': 'User#1000', 'SortKey': '2024-12-21T16:31:03.298998+00:00', 'Data': {'Attribute3': True, 'Attribute2': Decimal('8'), 'Attribute1': 'Value8'}, 'CreatedAt': '2024-12-21T16:39:03.298998+00:00'}
{'PartitionKey': 'User#1000', 'SortKey': '2024-12-21T16:32:03.192171+00:00', 'Data': {'Attribute3': True, 'Attribute2': Decimal('7'), 'Attribute1': 'Value7'}, 'CreatedAt': '2024-12-21T16:39:03.192171+00:00'}
{'PartitionKey': 'User#1000', 'SortKey': '2024-12-21T16:33:03.081185+00:00', 'Data': {'Attribute3': True, 'Attribute2': Decimal('6'), 'Attribute1': 'Value6'}, 'CreatedAt': '2024-12-21T16:39:03.081185+00:00'}
{'PartitionKey': 'User#1000', 'SortKey': '2024-12-21T16:34:02.971412+00:00', 'Data': {'Attribute3': True, 'Attribute2': Decimal('5'), 'Attribute1': 'Value5'}, 'CreatedAt': '2024-12-21T16:39:02.971412+00:00'}
{'PartitionKey': 'User#1000', 'SortKey': '2024-12-21T16:35:02.863380+00:00', 'Data': {'Attribute3': True, 'Attribute2': Decimal('4'), 'Attribute1': 'Value4'}, 'CreatedAt': '2024-12-21T16:39:02.863380+00:00'}
{'PartitionKey': 'User#1000', 'SortKey': '2024-12-21T16:36:02.754544+00:00', 'Data': {'Attribute3': True, 'Attribute2': Decimal('3'), 'Attribute1': 'Value3'}, 'CreatedAt': '2024-12-21T16:39:02.754544+00:00'}
{'PartitionKey': 'User#1000', 'SortKey': '2024-12-21T16:37:02.646470+00:00', 'Data': {'Attribute3': True, 'Attribute2': Decimal('2'), 'Attribute1': 'Value2'}, 'CreatedAt': '2024-12-21T16:39:02.646470+00:00'}
{'PartitionKey': 'User#1000', 'SortKey': '2024-12-21T16:38:02.540057+00:00', 'Data': {'Attribute3': True, 'Attribute2': Decimal('1'), 'Attribute1': 'Value1'}, 'CreatedAt': '2024-12-21T16:39:02.540057+00:00'}
{'PartitionKey': 'User#1000', 'SortKey': '2024-12-21T16:39:02.431351+00:00', 'Data': {'Attribute3': True, 'Attribute2': Decimal('0'), 'Attribute1': 'Value0'}, 'CreatedAt': '2024-12-21T16:39:02.431351+00:00'}

クエリ結果(降順):
{'PartitionKey': 'User#1000', 'SortKey': '2024-12-21T16:39:02.431351+00:00', 'Data': {'Attribute3': True, 'Attribute2': Decimal('0'), 'Attribute1': 'Value0'}, 'CreatedAt': '2024-12-21T16:39:02.431351+00:00'}
{'PartitionKey': 'User#1000', 'SortKey': '2024-12-21T16:38:02.540057+00:00', 'Data': {'Attribute3': True, 'Attribute2': Decimal('1'), 'Attribute1': 'Value1'}, 'CreatedAt': '2024-12-21T16:39:02.540057+00:00'}
{'PartitionKey': 'User#1000', 'SortKey': '2024-12-21T16:37:02.646470+00:00', 'Data': {'Attribute3': True, 'Attribute2': Decimal('2'), 'Attribute1': 'Value2'}, 'CreatedAt': '2024-12-21T16:39:02.646470+00:00'}
{'PartitionKey': 'User#1000', 'SortKey': '2024-12-21T16:36:02.754544+00:00', 'Data': {'Attribute3': True, 'Attribute2': Decimal('3'), 'Attribute1': 'Value3'}, 'CreatedAt': '2024-12-21T16:39:02.754544+00:00'}
{'PartitionKey': 'User#1000', 'SortKey': '2024-12-21T16:35:02.863380+00:00', 'Data': {'Attribute3': True, 'Attribute2': Decimal('4'), 'Attribute1': 'Value4'}, 'CreatedAt': '2024-12-21T16:39:02.863380+00:00'}
{'PartitionKey': 'User#1000', 'SortKey': '2024-12-21T16:34:02.971412+00:00', 'Data': {'Attribute3': True, 'Attribute2': Decimal('5'), 'Attribute1': 'Value5'}, 'CreatedAt': '2024-12-21T16:39:02.971412+00:00'}
{'PartitionKey': 'User#1000', 'SortKey': '2024-12-21T16:33:03.081185+00:00', 'Data': {'Attribute3': True, 'Attribute2': Decimal('6'), 'Attribute1': 'Value6'}, 'CreatedAt': '2024-12-21T16:39:03.081185+00:00'}
{'PartitionKey': 'User#1000', 'SortKey': '2024-12-21T16:32:03.192171+00:00', 'Data': {'Attribute3': True, 'Attribute2': Decimal('7'), 'Attribute1': 'Value7'}, 'CreatedAt': '2024-12-21T16:39:03.192171+00:00'}
{'PartitionKey': 'User#1000', 'SortKey': '2024-12-21T16:31:03.298998+00:00', 'Data': {'Attribute3': True, 'Attribute2': Decimal('8'), 'Attribute1': 'Value8'}, 'CreatedAt': '2024-12-21T16:39:03.298998+00:00'}
{'PartitionKey': 'User#1000', 'SortKey': '2024-12-21T16:30:03.407969+00:00', 'Data': {'Attribute3': True, 'Attribute2': Decimal('9'), 'Attribute1': 'Value9'}, 'CreatedAt': '2024-12-21T16:39:03.407969+00:00'}

SortKeyに対して最初が昇順で、後が降順で、確かに想定された結果になった。

まとめ

  • クエリする際のオプションScanIndexForwardを変えてしまえば、追加のインデックスなし・テーブル構造を変えずに昇順/降順を切り替えられるので便利!


Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です