例で学ぶ DynamoDB の PartiQL
PartiQL は DynamoDB 向けの SQL 互換クエリ言語です。アドホックな作業では生の API より親しみやすいものの、同じエンジン上で動作するため、おなじみの構文の下で同じキーのルール(そして同じコスト)が適用されます。
DynamoDB 向けの PartiQL クエリはどう書くのか?
PartiQL は DynamoDB に 4 つの SQL 形式のステートメント(SELECT、INSERT、UPDATE、
DELETE)を提供し、ExecuteStatement を通して実行します。パーティションキーでフィルタすればキーを使った読み取りになり、省けばテーブル全体の
Scan になります。
書き込みは完全なプライマリキーで 1 アイテムをターゲットにします。リレーショナルな
JOIN、GROUP BY、集計はありません。
引用符は SQL と同じです。文字列の値はシングルクォート、識別子はダブルクォートで囲みます。特殊文字を含むテーブル名はダブルクォートで囲み、インデックスをクエリするときは必ず両方の名前をダブルクォートで囲みます("AppData"."GSI1")。
SELECT
SELECT * FROM "AppData"
WHERE "PK" = 'ACCOUNT#acme' AND begins_with("SK", 'TICKET#')パーティションキーを = または IN で固定すれば、DynamoDB はそのパーティションだけを読み取ります。パーティションキーを省くと、PartiQL は暗黙のうちにテーブル全体の
Scan を実行します。同じ落とし穴が、SELECT * の裏に隠れているだけです。
WHERE 句の形 | DynamoDB の動作 | 主なコスト要因 |
|---|---|---|
PK = ?(パーティション完全一致) | Query | そのパーティション内のアイテム(+フィルターの無駄) |
PK = ? AND begins_with(SK, ?) | Query | 1 パーティション内のソートキー範囲 |
PK IN [?, ?] または PK = ? OR PK = ? | キーを使った読み取り、Scan なし | 列挙した各キー配下のアイテム |
FROM "Table"."GSI1" 経由の GSI1PK = ? | インデックスへの Query | インデックスのパーティション内のアイテム |
PK > ?、または PK = ? OR attr = ?(非キー) | Scan | フィルター前の、対象内のすべてのアイテム |
| テーブルにもインデックスにもパーティションキーの等価条件なし | Scan | フィルター前の、対象内のすべてのアイテム |
このルールは AWS の SELECT リファレンスによるものです。パーティションキーに対する等価条件または
IN なら全件スキャンを避けられます。パーティションキーに対する範囲条件や、もう一方の分岐が非キー属性の
OR はスキャンになります。
Scan 側に数字を入れてみましょう。平均 1 KB のアイテムが 500,000 件あるテーブルでは、SELECT * の全走査はオンデマンドでおよそ 62,500 読み取りリクエストユニットを計測します(結果整合性のある読み取りで
500,000 KB ÷ 4 KB × 0.5)。
WHERE status = 'OPEN' を足しても請求額は同じです。フィルターは、読み取りが計測されたあとに効きます。上のキーを使った SELECT が読むのは ACCOUNT#acme だけです。1 KB のチケットが 40 件なら、コストは
62,500 ではなく 5 ユニットです(40 KB ÷ 4 KB × 0.5)。
ExecuteStatement はネイティブ API と同じようにページングします。レスポンスに NextToken が含まれている間は、それを付けて再度呼び出します。空のページで止めないでください。フィルター付きの読み取りは、
0 件のアイテムとトークンを返すことがあります(ページネーションのガイド)。
INSERT
INSERT INTO "AppData" VALUE {'PK': 'ACCOUNT#acme', 'SK': 'PROFILE', 'plan': 'team'}INSERT は決して上書きしません。そのキーのアイテムがすでに存在すると、ステートメントは
DuplicateItemException で失敗します。アイテムを置き換えるには、デフォルトで上書きするネイティブの
PutItem(aws dynamodb put-item)を使います。
UPDATE
UPDATE "AppData" SET "plan" = 'enterprise'
WHERE "PK" = 'ACCOUNT#acme' AND "SK" = 'PROFILE'UPDATE は決して作成しません。WHERE は完全なプライマリキーを指定する必要があり、そのキーのアイテムが存在しなければ、ステートメントは ConditionalCheckFailedException で失敗します。
アップサートには、キーが存在しないときにアイテムを作成するネイティブの UpdateItem
(aws dynamodb update-item)を使います。
DELETE
DELETE FROM "AppData"
WHERE "PK" = 'ACCOUNT#acme' AND "SK" = 'TICKET#1001'DELETE も完全なキーが必要です。存在しないキーの削除は、削除件数 0 で成功します。何が削除されたかを知りたいときは RETURNING ALL OLD * を付けます。削除されたアイテムが返り、なければ何も返りません。
インデックスをクエリする
FROM 句でインデックス名を指定し、両方の名前をダブルクォートで囲みます。
SELECT * FROM "AppData"."GSI1" WHERE "GSI1PK" = 'STATUS#OPEN'WHERE は IN、contains()、begins_with() もサポートします。
SELECT * FROM "AppData"
WHERE "PK" = 'ACCOUNT#acme' AND "SK" IN ['TICKET#1001', 'TICKET#1002']パラメータ化されたステートメント
値をインラインで埋め込む代わりに ? プレースホルダーを使います。引用符付けやインジェクションの問題を回避できます。
SELECT * FROM "AppData" WHERE "PK" = ? AND begins_with("SK", ?)低レベルクライアントでは、各値を型付きで渡します:
Parameters: [{ S: 'ACCOUNT#acme' }, { S: 'TICKET#' }]。型を変換してくれる仕組みはないため、慎重に指定してください。
数値を { S: '100' } として送ると、キーの比較は Key attribute's data type should match its data type in table's schema で失敗します。非キーの比較は単に何にも一致しません。
アプリケーションのコードではパラメータを優先し、文字列の埋め込みはアドホックなコンソール操作にとどめましょう。
PartiQL には ExpressionAttributeNames のエイリアスがありません。属性名が PartiQL のキーワードと衝突する場合はダブルクォートで囲みます。count = 1 は Statement wasn't well formed, can't be processed で失敗しますが、"count" = 1 は実行できます。
予約語チェッカーが調べるのは式 API のリスト(FilterExpression など)で、PartiQL のリストではありません。status はそのリストに含まれていますが、ここでは引用符なしで実行できます。すべての識別子をダブルクォートで囲めば、常に安全です。
逆の間違いはもっと気づきにくいものです。WHERE "PK" = "ACCOUNT#acme" はエラーになりません。
DynamoDB は "ACCOUNT#acme" を属性名として読み、2 つの属性を比較して 0 行を返します。文字列の値には常にシングルクォートを使います。
バッチとトランザクション
BatchExecuteStatement: 1 回の往復で最大 25 ステートメント。各ステートメントが個別に成功または失敗するため、Responsesのすべてのエントリを確認してください。バッチは読み取りだけか書き込みだけで構成し、バッチ内の各SELECTは完全なプライマリキーを指定する必要があります。ExecuteTransaction: 最大 100 ステートメント、オールオアナッシング。複数の書き込みをまとめてコミットしなければならないときに使います。
バッチとトランザクションの上限は、チューニング用のつまみではなく固定のサービス制限です。
30 件のチケットを一度にクローズするにも、やはり分割が必要です。BatchExecuteStatement を 2 回(25 + 5)呼ぶか、30 件すべてにまたがるアトミック性が必要ならトランザクションを 1 回使います。
PartiQL と Workbench SQL
PartiQL は DynamoDB 上で 実行され、1 ステートメントにつき 1 テーブルです。DynoTable の
SQL Workbench は、DynamoDB 自身の操作(ベーステーブルには
Query/Scan、結合される側にはキーによる参照)で読み取った行に対して クライアント側で 実行され、
JOIN、GROUP BY、集計を加えます。用途で選びましょう。
| 必要なこと | 使うもの |
|---|---|
| プライマリキーによる単一パーティションの読み書き | PartiQL の SELECT / DML |
| GSI を設計しておいたパーティション横断のフィルター | インデックスに対する PartiQL |
| 2 つのエンティティ型をまたぐアドホックな結合 | キーを使った読み取りをコードでマージ、Athena コネクタ、または Workbench SQL |
| アカウント別の月次サポート時間の集計 | 事前計算した集計値、Athena、または Workbench の GROUP BY |
| 本番のホットパスにあるマイクロサービス | ネイティブの Query / GetItem API |
Workbench も DynamoDB のアクセスルールに従って読み取ります。キーを使った読み取りか、意識して受け入れた Scan です。JOIN のルールは DynamoDB のための SQL に、機能ごとの比較は PartiQL と SQL にあります。
PartiQL の落とし穴
PartiQL は SQL に見えますが DynamoDB のエンジン上で動作するため、SQL の習慣が裏目に出ます。
UPDATE/DELETEは完全なプライマリキーで 1 つのアイテムを変更します。 部分的なキーはValidationExceptionで失敗します。WHERE句はすべてのキー属性を等号で指定する必要があります。一括更新するには、テーブルか GSI をクエリしてキーを取得し、アイテムごとのステートメントをBatchExecuteStatementで、アトミック性が必要ならExecuteTransactionで送ります。JOINもGROUP BYも集計(COUNT/SUM/AVG)もありません。 結合は各側をキーでクエリしてコードでマージします(JOIN のガイド)。合計は書き込み時に最新に保つか(GROUP BY、COUNT と SUM)、アドホックな SQL を Athena で実行します(DynamoDB のための SQL)。- キーのない
SELECTはテーブル全体の Scan です。 これをエラーにするには、呼び出し元の IAM ポリシーでdynamodb:FullTableScanがtrueのときにdynamodb:PartiQLSelectを拒否します。
PartiQL は下層のデータ型を変えません。値は依然として DynamoDB-JSON としてワイヤ上を送られ、コンバータで確認できます。
同じアクセスをネイティブ API で書くなら、
DynamoDB Expression Builder が KeyConditionExpression
(パーティションキーの等価条件 1 つと、最大 1 つのソートキー条件。OR は不可)と
FilterExpression を組み立ててくれます。
DynoTable を試すと、スキーマを認識した自動補完、ダブルクォートの値・LIKE・IN (…) のクイックフィックス、検索できる実行履歴、実行前の読み取り見積もりとともに PartiQL を実行できます。
結合や集計が必要なときは Workbench の Tab を開きましょう。エディタについては PartiQL ドキュメントで説明しています。