初級読了 3 分

例で学ぶ DynamoDB の 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, ?)Query1 パーティション内のソートキー範囲
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 ドキュメントで説明しています。

著者: Dmitriy · 更新日