目的
- 任意の属性値に対して部分一致検索(
contains
)を行う - 無料利用枠内に必ず収める
- 速度は気にしない
結論
-
ExecuteStatementCommand
を使う -
ReturnConsumedCapacity
で消費キャパシティを確認する - 最適な
Limit
をかけて、取得処理をループさせる
はじめに
無料利用枠の難しさ
そんな魅力的なDynamoDBだが、無料利用枠内に必ず収めようとするとさまざまな制約が生じてくる。オートスケーリングはoff、読み込み/書き込みキャパシティの割り当ては低めに設定...。この場合に、レコード数の多いテーブルに対してScanCommand
(全件取得)を実行すると、読み込みキャパシティが足りずにコケる場合がある。消費するキャパシティの手綱を完全に掌握しつつ、意図した動作をさせるには工夫が要る。
部分一致検索ゆえの難しさ
AWS DynamoDBで部分一致検索をしようとすると、FilterExpression
でcontains
を使うしかない(他にあったら教えてください...)。
FilterExpression
による絞り込みは、取得されたレコードに対して行われる。そのため、全レコードに対して部分位置検索をかけようとすると、Query
でもScan
でも内部的には一度全件取得をする必要がある。この点において、Query
の旨味である「消費キャパシティを抑えられる」ことを活かすことができない。
解決策
「消費キャパシティを抑えつつ全件取得することが難しい」「contains
を使うなら全件取得が要る」双方を解決するのが、ExecuteStatementCommand
だ。最大の特徴は、ScanCommand
やQueryCommand
ではできない「ループして全件取得戦法」が使える点で、Limit
で溢れたレコードを再取得するためのtokenが返り値に含まれる。このtokenがundefined
になるまで再取得すれば、条件にあうレコードを全件取得することができる。
また、ReturnConsumedCapacity
を設定することで、そのリクエストで消費したキャパシティの量が表示される。この値はLimit
に依存するので、その環境に合ったLimit
を経験的に設定し、最大消費キャパシティを管理することができる。
実装
条件
- ローカル環境で実行
- 実行ユーザはMFAなしでリソースにアクセスできる
- アクセスキーなどは環境変数から読み込み
- 対象テーブル名は
tablename
- 検索対象キー
target_key
に対してsearching_string
で部分一致検索をする
import { fromEnv } from "@aws-sdk/credential-providers";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
DynamoDBDocumentClient,
ExecuteStatementCommand,
ExecuteStatementCommandInput
} from "@aws-sdk/lib-dynamodb";
// クライアント定義
const client = new DynamoDBClient({
region: process.env.AWS_REGION,
credentials: fromEnv(),
});
const docClient = DynamoDBDocumentClient.from(client);
// DynamoDBアクセス
const accessor = async (statement: string, nextToken?: string) => {
const inputs: ExecuteStatementCommandInput = {
Statement: statement,
ReturnConsumedCapacity: "TOTAL",
Limit: 10// ここの値は、目標のキャパシティ数に応じて調整する
};
if (nextToken) inputs.NextToken = nextToken;
const command = new ExecuteStatementCommand(inputs);
const response = await docClient.send(command);
return response;
};
// ループして取得
const main = async () => {
const statement = `SELECT * FROM "tablename" WHERE contains(target_key, 'searching_string')`;
const responses: Array<object> = [];
let nextToken = undefined;
while (true) {
const res = await accessor(statement, nextToken);
res.Items?.map((item) => responses.push(item));
if (res.NextToken) {
nextToken = res.NextToken;
} else {
break;
}
};
};
main();
テスト
- DynamoDBの生responseの例
{
'$metadata': {
httpStatusCode: 200,
requestId: 'hogehgoehogehogehogehogehogehogehogehoge',
extendedRequestId: undefined,
cfId: undefined,
attempts: 1,
totalRetryDelay: 0
},
ConsumedCapacity: { CapacityUnits: 0.5, TableName: 'tablename' },
Items: [
{
id: 1,
name: 'ほげげ'
}
],
LastEvaluatedKey: { id: 23 },
NextToken: 'kokonitottemonagaitokengahairuyo'
}
おわりに
DynamoDBで部分一致検索をしつつ、必ず無料利用枠内におさめるには、
-
ExecuteStatementCommand
を使う -
ReturnConsumedCapacity
で消費キャパシティを確認する - 最適な
Limit
をかけて、取得処理をループさせる
おまけ
ぼやき
- こんな感じでループするのRedisぽい
LIKE
は使えない
このドキュメントでは用例としてLIKE
表現を用いた検索方法が紹介されているが、DynamoDBではLIKE
表現が使えなかった。
よく調べてみると、このドキュメントにはLIKE
がなかった。
(わかりづらいよッ...!!!)
検索対象文字列のクオテーション
次のPartiQL文は期待した動作をしない
SELECT * FROM "table_name" WHERE contain(target_key, "searching_string");
正しいのはこっち
SELECT * FROM "table_name" WHERE contain(target_key, 'searching_string');
おそらくこのあたりが原因だと思われる(SQL初心者)。