Python(boto3)で DynamoDB の GSI をクエリする

GSI へのクエリは、通常の queryIndexName を足しただけのものです。AlbumTitle-index はアルバム別の曲を返してくれます。これは Artist + SongTitle というテーブルキーでは満たせないアクセスパターンです。Python で変わるのはエラー処理です。インデックス周りでもっともよくある 2 つの間違いは、boto3 の別々の層で失敗し、例外クラスで捕まえられるのはそのうち一方だけです。

コード

import boto3

client = boto3.client("dynamodb")

paginator = client.get_paginator("query")

items = []
for page in paginator.paginate(
    TableName="Music",
    IndexName="AlbumTitle-index",
    KeyConditionExpression="#hashKey = :hashKeyValue",
    ExpressionAttributeNames={"#hashKey": "AlbumTitle"},
    ExpressionAttributeValues={":hashKeyValue": {"S": "Danzon"}},
):
    items.extend(page["Items"])

print(f"Found {len(items)} songs on the album")

except ValidationException は捕まえる以前にコンパイルすら通らない

上のクエリに ConsistentRead=True を足すと、クライアント API でもリソース API でも、boto3 はこれを送出します。

botocore.exceptions.ClientError: An error occurred (ValidationException) when
calling the Query operation: Consistent reads are not supported on global
secondary indexes

素直なハンドラは except client.exceptions.ValidationException でしょう。それは存在しません。

AttributeError: <botocore.errorfactory.DynamoDBExceptions object> has no
attribute ValidationException. Valid exceptions are: BackupInUseException,
... IndexNotFoundException, ... ProvisionedThroughputExceededException, ...

botocore は例外クラスをサービスモデルから生成し、DynamoDB はそのうち 33 個をモデル化しています。ValidationException はプロトコルレベルのエラーでそこには含まれないため、確実に分岐できるのはコードだけです。

except ClientError as exc:
    if exc.response["Error"]["Code"] == "ValidationException":
        ...

この非対称性は現実のものです。インデックス名を打ち間違えると IndexNotFoundException が返り、これは モデル化されていて クラスで捕捉できます。整合性フラグを誤用すると、返ってくるのは文字列比較です。どちらもインデックスのエラーですが、型を持つのは一方だけです。

カーソルはテーブルのキーも運ぶ

ページネータは LastEvaluatedKey を隠しますが、インデックス上でそれが何を保持しているかは知っておく価値があります。1 枚のアルバムに 300 曲ある場合です。

page 1: Count 271  capacity 128.5  LastEvaluatedKey ['AlbumTitle', 'Artist', 'SongTitle']
page 2: Count  29  capacity  14.0  LastEvaluatedKey []

GSI のキーは一意ではないため、インデックスのキーだけでは読み取りを再開できません。DynamoDB はインデックスのキーとベーステーブルのキーをまとめて返します。インデックスのキーしか保存しない自作のページ送りは、アイテムを重複させるか取りこぼします。

2026-07-28 に、CPython 3.14.6 上の boto3 1.43.58 で DynamoDB Local(amazon/dynamodb-local)に対して再現しました。エラーテキストとキーの一覧はライブラリ自身の出力です。

解説

  • IndexNameTableName の代わりではありません。両方を同じ呼び出しに入れ、KeyConditionExpressionインデックスの パーティションキー(AlbumTitle)を、テーブルへのクエリと同じ演算子セットで指定します。
  • 得られるのは射影された分だけです。インデックスが返すのは射影したもの(ALLKEYS_ONLY、または INCLUDE のリスト)だけです。API リファレンスによれば「global secondary index queries cannot fetch attributes from the parent table」。属性が足りなければ、ベースキーへの追加の get_item か、より広い射影を持つ新しいインデックスが必要になります。
  • インデックスキーを持たないアイテムは決して現れません — スパースインデックスのパターンです。status = "OPEN" に対するインデックスを小さく保てる一方、GSI へのクエリが期待より少なく返しつつ何も送出しない理由でもあります。
  • リソース API も同じ IndexName を取りますtable.query(IndexName="AlbumTitle-index", KeyConditionExpression=Key("AlbumTitle").eq("Danzon")) のように、ネイティブな Python の値を渡し、Decimal を受け取ります。
  • GSI への書き込みはテーブルへの書き込みの後に着地します。レプリケーションは非同期なので、インデックスに対する read-after-write の経路はときどき取りこぼします。それをタイトなループでリトライしても、レプリケーションが速くなることはなく、キャパシティを燃やすだけです。

ビジュアルに行う

DynamoDB 式ビルダーは、インデックスのキー条件と型付きの値マップを、boto3 にそのまま渡せる Python として書き出します。クライアント API が要求し、リソース API が禁じる {"S": ...} のラッパー付きです。

同じインデックスへのクエリを、フォームから自分のテーブルに向けて実行し、結果をページ送り付きのグリッドで読むには、DynoTable をダウンロードしてください。

関連する例

参考資料

このリクエストをビジュアルに組み立てる

この操作を無料の DynamoDB クエリビルダーで組み立て — キー条件、フィルタ、インデックス、Limit、ソート順、ページネーションループ — 実行可能な SDK v3・CLI・boto3 のプログラムとしてコピーして戻れます。

DynamoDB クエリビルダーを開く

Console なしで DynamoDB を扱う

DynamoDB では実行できない本物の SQL(JOINs、GROUP BY、集計)を実行する高速な DynamoDB デスクトップクライアント。ビジュアル編集と、あなた自身の Bedrock キーで動く AI エージェントを備えています。

30日間無料トライアル、クレジットカード不要 — その後は期限のない Free プラン。