例で学ぶ DynamoDB の PartiQL
PartiQL は DynamoDB 向けの SQL 互換クエリ言語です。アドホックな作業では生の API より 親しみやすいものの、同じエンジン上で動作するため、おなじみの構文の下で同じキーのルール(そして 同じコスト)が適用されます。
DynamoDB 向けの PartiQL クエリはどう書くのか?
PartiQL は DynamoDB に 4 つの SQL 形式のステートメント — SELECT、INSERT、UPDATE、DELETE — を、
ExecuteStatement を通して提供します。それぞれが下層でネイティブ操作にコンパイルされるため、
パーティションキーでフィルタすれば Query のままですが、それを省くとテーブル全体の
Scan になります。書き込みは依然としてプライマリキーで 1 アイテムをターゲットにします。
リレーショナルな JOIN、GROUP BY、集計はありません。
どのステートメントも同じ ExecuteStatement API(またはそのバッチ/トランザクション版)を通ります。
テーブル名とインデックス名は、特殊文字を含む場合はダブルクォートで囲む必要があります。式の中の
属性名は、低レベル API と同じ引用ルールに従います。DynoTable の PartiQL ドキュメントは、
スキーマを認識した自動補完と、過去の実行を検索できる履歴とともに、ここでの例を再現しています。
SELECT
SELECT * FROM "AppData"
WHERE "PK" = 'CUSTOMER#42' AND begins_with("SK", 'ORDER#')パーティションキーでフィルタすれば、これは Query です。パーティションキーを省くと
PartiQL は暗黙のうちにテーブル全体の Scan を実行します — 同じ落とし穴が、
SELECT * の裏に隠れているだけです。
WHERE 句の形 | コンパイル後の操作 | 主なコスト要因 |
|---|---|---|
PK = ?(パーティション完全一致) | Query | そのパーティション内のアイテム(+フィルターの無駄) |
PK = ? AND begins_with(SK, ?) | Query | 1 パーティション内のソートキー範囲 |
FROM "Table"."GSI1" 経由の GSI1PK = ? | インデックスへの Query | インデックスのパーティション内のアイテム |
| テーブルにもインデックスにもパーティションキー等価なし | Scan | フィルター前の、対象内のすべてのアイテム |
Scan 側に数字を入れてみましょう。平均 1 KB のアイテムが 50 万件あるテーブルでは、SELECT * の
全走査はオンデマンド課金でおよそ 62,500 読み取りリクエストユニットを計測します(500,000 KB ÷
1 ユニットあたり 4 KB、結果整合性で 1 件あたり 0.5 RCU)。WHERE status = 'OPEN' を足しても請求額は
同じです — フィルターは、読み取りが計測されたあとに効きます。上の例のキーを使った SELECT が
触れるのは CUSTOMER#42 のパーティションだけで、そのコレクションが 1 KB の注文を 40 件持って
いるなら、同じ結果整合性の読み取りは 62,500 ではなく 5 ユニットです。
ExecuteStatement はネイティブ API と同じようにページングします。NextToken が空でなければ、
まだ行が残っています。トークンが返らなくなるまでループしてください(ページネーションのガイド)。
INSERT
INSERT INTO "AppData" VALUE {'PK': 'CUSTOMER#42', 'SK': 'PROFILE', 'plan': 'pro'}UPDATE
UPDATE "AppData" SET "plan" = 'enterprise'
WHERE "PK" = 'CUSTOMER#42' AND "SK" = 'PROFILE'DELETE
DELETE FROM "AppData"
WHERE "PK" = 'CUSTOMER#42' AND "SK" = 'ORDER#2026-001'インデックスをクエリする
FROM 句でインデックス名を使います。
SELECT * FROM "AppData"."GSI1" WHERE "GSI1PK" = 'STATUS#OPEN'WHERE は IN、contains()、begins_with() もサポートします。
SELECT * FROM "AppData"
WHERE "PK" = 'CUSTOMER#42' AND "SK" IN ['ORDER#1', 'ORDER#2']パラメータ化されたステートメント
値をインラインで埋め込む代わりに ? プレースホルダーを使います — 引用符付けやインジェクションの
問題を回避でき、SDK に型のマーシャリングを任せられます。
SELECT * FROM "AppData" WHERE "PK" = ? AND begins_with("SK", ?)Parameters: [{ S: 'CUSTOMER#42' }, { S: 'ORDER#' }] を ExecuteStatement に渡します。
アプリケーションのコードではパラメータを優先し、文字列の埋め込みはアドホックなコンソール操作に
とどめましょう。SDK が型(数値なら N、真偽値なら BOOL)をマーシャリングしてくれるので、
DynamoDB が数値属性を期待しているところに "42" という文字列を渡す、あの典型的な失敗を避けられます。
予約語の属性名は、FilterExpression とまったく同じように PartiQL でもエイリアスが必要です — 保存する
ステートメントに焼き込む前に、予約語チェッカーに名前を
通しておきましょう。
バッチとトランザクション
BatchExecuteStatement— 1 回の往復で最大 25 ステートメント。より高速ですが、アイテム間の アトミック性はありません(各ステートメントが個別に成功または失敗します)。ExecuteTransaction— 最大 100 ステートメント、オールオアナッシング。複数の書き込みを まとめてコミットしなければならないときに使います。
バッチとトランザクションの上限は、チューニング用のつまみではなく固定のサービス制限です。
30 明細に触れるカートの決済は、やはり分割が必要になります。BatchExecuteStatement を 2 回
(25 + 5)呼ぶか、30 件すべてにまたがるアトミック性が必要ならトランザクションを 1 回使います
(それでも各ステートメントは、完全なプライマリキーで単一のアイテムを対象にします)。
PartiQL と Workbench SQL
PartiQL は DynamoDB 上で 実行されます — 1 ステートメントにつきキーを使ったアクセスパターン
1 つです。DynoTable の SQL Workbench は、すでに取得した行に対して
クライアント側で 実行されるため、PartiQL があえて省いている JOIN、GROUP BY、集計が使えます。
この住み分けは見た目の話ではなく、運用上のものです。
| 必要なこと | 使うもの |
|---|---|
| プライマリキーによる単一パーティションの読み書き | PartiQL の SELECT / DML |
| GSI を設計しておいたパーティション横断のフィルター | インデックスに対する PartiQL |
| 2 つのエンティティ型をまたぐアドホックな結合 | 範囲を絞ったクエリの上での Workbench SQL |
| SKU 別の月次売上の集計 | Workbench の GROUP BY |
| 本番のホットパスにあるマイクロサービス | ネイティブの Query / GetItem API |
Workbench も DynamoDB のアクセスルールを守ります。パーティションはキーを使った読み取り(または 意識して受け入れた範囲限定の Scan)で取得し、そのうえで SQL が結果セットをローカルで整形します。 詳しい比較は PartiQL と SQL を参照してください。
PartiQL の落とし穴
PartiQL は SQL に_見えます_が DynamoDB のエンジン上で動作するため、SQL の習慣が裏目に出ます。
- 単一の
UPDATE/DELETEは完全なプライマリキーで1 つのアイテムをターゲットに しなければなりません —UPDATE … WHERE status = 'x'のような一括更新はありません(代わりにバッチで ループします)。 JOINもGROUP BYも集計(COUNT/SUM/AVG)もありません。 PartiQL vs SQL を参照してください。- パーティションキーを省くと、任意の
SELECTがテーブル全体の Scan になります — 制限は請求額だけです。
本当に JOIN、GROUP BY、集計が必要になったとき、DynoTable の SQL Workbench は、あなたが取得した
行に対してクライアント側でそれらを実行します — PartiQL には話せない SQL を、DynamoDB のアクセス
パターンのルール内で。
PartiQL は下層のデータ型を変えません — 値は依然として DynamoDB-JSON として ワイヤ上を送られ、コンバータで確認できます。
UPDATE と DELETE のステートメントでは、WHERE 句がちょうど 1 件のアイテムに解決しなければ
なりません。多数の行に一致する部分キーの WHERE 句は、一括更新になるのではなくコンパイル時か
実行時に失敗します — キーを使った Query でループし、アトミック性が必要なら
BatchExecuteStatement かトランザクションの中でアイテムごとに DML を発行してください。
複雑な KeyConditionExpression の形(入れ子の begins_with、AND と OR の混在)は、
DynamoDB Expression Builderで試作し、条件が正しくなってから
PartiQL の WHERE 構文に置き換えるほうが簡単です。
DynoTable を試すと、スキーマを認識した自動補完で PartiQL ステートメントを実行し、 ソート可能なテーブルビューで結果を閲覧できます — エンジンがプッシュダウンできない結合や集計が 必要になったら、同じデータセットをそのまま Workbench に持ち込めます。