CLOVER🍀

That was when it all began.

Amazon DynamoDBローカル版とDocumentClientで、スキャンを試す

これは、なにをしたくて書いたもの?

前にAmazon DynamoDBのクエリーを試してみました。

Amazon DynamoDBローカル版とDocumentClientで、クエリーを試す - CLOVER🍀

今回は、スキャンを試してみようかなと思います。

Amazon DynamoDBのスキャン

Amazon DynamoDBのスキャンに関するドキュメントを見てみましょう。

まず、概要はこちら。

Amazon DynamoDB の Scan オペレーションでは、テーブルまたはセカンダリインデックスのすべての項目を読み込みます。デフォルトでは、Scan オペレーションはテーブルまたはインデックスのすべての項目のデータ属性を返します。ProjectionExpression パラメータを使用し、Scan がすべての属性ではなく一部のみを返すようにできます。

Scan は常に結果セットを返します。一致する項目がない場合、結果セットは空になります。

DynamoDB でのスキャンの操作 - Amazon DynamoDB

全データを読み込む、というのがすごくポイントな気がしますね…。

Amazon DynamoDB の Scan オペレーションでは、テーブルまたはセカンダリインデックスのすべての項目を読み込みます。デフォルトでは、Scan オペレーションはテーブルまたはインデックスのすべての項目のデータ属性を返します。

なお、大きなデータセットを扱えるようにも見えますが、1回のスキャンで扱えるデータ量は最大1MBのようです。

1 回の Scan リクエストで、最大 1 MB のデータを取得できます。

スキャンには、フィルター式を適用して結果のフィルタリングが可能なようです。

Scan 結果の絞り込みが必要な場合は、オプションでフィルタ式を指定できます。フィルタ式によって、Scan 結果の返される項目が決まります。他のすべての結果は破棄されます。

DynamoDB でのスキャンの操作 / Scan のフィルタ式

クエリーもそうでしたが、フィルターによる絞り込みはスキャン対象のデータ量を減らせる(検索対象を絞り込める)わけではなく、
あくまで結果を取得した際に呼び出し元に見せるデータを減らすだけですね。

フィルタ式は、Scan の完了後、結果が返される前に適用されます。そのため、Scan は、フィルタ式があるかどうかにかかわらず、同じ量の読み込みキャパシティーを消費します。

このため、フィルターを使用してもスキャン操作に関するデータ量の制限には影響しません。

1 回の Scan オペレーションで、最大 1 MB のデータを取得できます。この制限は、フィルタ式を評価する前に適用されます。

フィルターの書き方については、条件式のページを見なさい、と。

フィルタ式の構文は、条件式の構文と同じです。フィルタ式は、条件式と同じコンパレータ、関数および論理演算子を使用できます。

条件式 - Amazon DynamoDB

実際の比較演算子や、式の書き方の詳細はこちらです。

比較演算子および関数リファレンス - Amazon DynamoDB

式の属性値 - Amazon DynamoDB

式を使用する時の項目属性の指定 - Amazon DynamoDB

スキャン操作で返すアイテムの数は上限を設けることが可能なようです。

Scan オペレーションは、結果で返される項目数を制限することができます。​これを行うには、フィルタ式を評価する前に、Limit パラメータに、Scan オペレーションが返す項目の最大数を設定します。

DynamoDB でのスキャンの操作 / 結果セットの項目数の制限

説明を見ていると、Limitの指定はスキャンする幅自体を狭めるようですね。

たとえば、フィルタ式を使用せず、Scan 値を Limit として、テーブルを 6 するとします。Scan 結果には、テーブルの最初の 6 つの項目が含まれます。

ここで、Scan にフィルタ式を追加するとします。この場合、DynamoDB は返される 6 つの項目にフィルター式を適用し、一致しない項目を廃棄します。最終的な Scan 結果はフィルタリングされる項目の数に応じて、6 つ以下の項目を含みます。

スキャンはページングの機能も持っており、ページングを活用することで1MB以上のデータを取得することもできそうです。
※1つのページの大きさは1MB以下である必要がある

DynamoDB では、Scan オペレーションの結果をページ割りします。ページ割りを行うことで Scan 結果が 1 MB サイズ (またはそれ以下) のデータの「ページ」に分割されます。アプリケーションは結果の最初のページ、次に 2 ページと処理できます。

1 つの Scan は、サイズの制限である1 MB 以内の結果セットだけを返します。さらに結果があるかどうかを確認して、一度に 1 ページずつ結果を取り出すには、アプリケーションで次の操作を行う必要があります。

DynamoDB でのスキャンの操作 / ページ単位の出力件数を指定

読み込むデータの整合性は、デフォルトでは結果整合性です。

Scan オペレーションは、結果的に整合性のある読み込みをデフォルトで行います。つまり、Scan 結果が、最近完了した PutItem または UpdateItem オペレーションによる変更を反映しない場合があります。詳細については、「読み込み整合性」を参照してください。

DynamoDB でのスキャンの操作 / スキャンの読み込み整合性

強い整合性を求めることもできます。

強力な整合性のある読み込みが必要な場合は、Scan が開始する時に ConsistentRead パラメータを true リクエストで Scan に設定できます。これにより、Scan が開始する前に完了した書き込みオペレーションがすべて Scan 応答に含められます。

また、並列スキャンも可能なようです。並列スキャンを使用すると、スキャン操作を追加することで1MB以上のデータも取得できそうです。

デフォルトでは、Scan オペレーションは、データを順次処理します。Amazon DynamoDB はアプリケーションに 1 MB 単位でデータを返し、アプリケーションは追加の Scan オペレーションを使用して、次の 1 MB のデータを取得できます。

並列スキャン

Amazon DynamoDBのスキャンはデフォルトでシーケンシャルスキャンであり、スキャン操作は1回で1パーティションしか読み込むことが
できないそうです。

スキャンするテーブルまたはインデックスが大きいほど、Scan を完了するのに時間がかかります。さらに、シーケンシャル Scan は、プロビジョンされた読み込みスループット容量を常に完全に使用できるとは限りません。DynamoDB は大きなテーブルのデータを複数の物理パーティションに分散しますが、Scan オペレーションでは、一度に 1 つのパーティションしか読み込むことができません。このため、Scan のスループットは、単一のパーティションの最大スループットによって制約されます。

ここで、スキャン時にパラメーターを指定することで複数のワーカーに並列にスキャンさせることができるようです。

Scan オペレーションでは、テーブルまたはセカンダリインデックスを論理的に複数のセグメントに分割し、複数のアプリケーションワーカーがセグメントを並行してスキャンします。各ワーカーは、スレッド (マルチスレッドをサポートするプログラミング言語) またはオペレーティングシステムプロセスにすることができます。並列スキャンを実行するには、各ワーカーが独自の Scan リクエストを以下のパラメータで送信します。

ただ、並列スキャンを使用すると(当然ですが)スループットの消費が大きくなる可能性が高いため、注意する必要があります。

多数のワーカーを使用した並列スキャンでは、スキャン対象のテーブルまたはインデックスに対してプロビジョンされたスループットをすべて簡単に使用できます。テーブルまたはインデックスが他のアプリケーションから大量の読み込みまたは書き込みアクティビティが発生している場合は、このようなスキャンを避けることをお勧めします。

これについては、上限を設けてコントロールすることもできそうです。

リクエストごとに返されるデータの量を制御するには、Limit パラメータを使用します。これにより、1 人のワーカーがプロビジョンされたスループットをすべて消費し、他のすべてのワーカーが犠牲になる状況を防ぐことができます。

説明を見ていると、スキャンはそんなに使うものではなさそう…というか、特に大きなテーブルには使ってはいけなさそうに見えるのですが、
やっぱりそのようですね。

Scan アクションでは、FilterExpression パラメータも指定できます。これを使用して、結果に表示しない項目を破棄することができます。FilterExpression は、スキャンが実行された後で、結果が返される前に適用されます。(これは、大きなテーブルではお勧めしません。ごくわずかな一致する項目のみが返ってくる場合でも、Scan 全体に対して料金が請求されます。)

テーブルをスキャンする - Amazon DynamoDB

インデックスの話は、今回は置いておきます。

まあ、いったん雰囲気は確認してみましょうか。確認は、Amazon DynamoDBのローカル版を使います。

ページングと並列スキャンも、今回は対象外にします。

環境

今回の環境は、こちら。

$ aws --version
aws-cli/2.4.18 Python/3.8.8 Linux/5.4.0-99-generic exe/x86_64.ubuntu.20 prompt/off


$ java --version
openjdk 17.0.1 2021-10-19
OpenJDK Runtime Environment (build 17.0.1+12-Ubuntu-120.04)
OpenJDK 64-Bit Server VM (build 17.0.1+12-Ubuntu-120.04, mixed mode, sharing)

ローカル版のAmazon DynamoDBの情報。

$ grep -A 2 'Release Notes' README.txt
Release Notes
-----------------------------
2022-1-10 (1.18.0)

インメモリーで起動させておきます。

$ java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -inMemory

ローカル版のAmazon DynamoDBに、AWS CLIでアクセスするためのクレデンシャル。

$ export AWS_ACCESS_KEY_ID=fakeMyKeyId
$ export AWS_SECRET_ACCESS_KEY=fakeSecretAccessKey
$ export AWS_DEFAULT_REGION=ap-northeast-1

確認は、AWS SDK for JavaScriptを使って、Node.jsで行います。

$ node --version
v16.14.0


$ npm --version
8.3.1

準備

Node.jsプロジェクトを作成します。テストコードで確認することにしましょう。

$ npm init -y
$ npm i -D typescript
$ npm i -D -E prettier
$ npm i -D jest @types/jest
$ npm i -D esbuild esbuild-jest
$ mkdir src test

Node.jsの型宣言とAWS SDK for JavaScript v2をインストール。

$ npm i -D @types/node@v16
$ npm i aws-sdk

今回の依存関係は、こちら。

  "devDependencies": {
    "@types/jest": "^27.4.0",
    "@types/node": "^16.11.24",
    "esbuild": "^0.14.21",
    "esbuild-jest": "^0.5.0",
    "jest": "^27.5.1",
    "prettier": "2.5.1",
    "typescript": "^4.5.5"
  },
  "dependencies": {
    "aws-sdk": "^2.1073.0"
  }

設定はこちら。

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "lib": ["esnext"],
    "baseUrl": "./src",
    "outDir": "dist",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

tsconfig.typecheck.conf

{
  "extends": "./tsconfig",
  "compilerOptions": {
    "baseUrl": "./",
    "noEmit": true
  },
  "include": [
    "src", "test"
  ]
}

.prettierrc.json

{
  "singleQuote": true
}

jest.config.js

module.exports = {
  testEnvironment: 'node',
  transform: {
    "^.+\\.tsx?$": "esbuild-jest"
  }
};

今回も、ドキュメントインターフェースを使っていきます。

ドキュメントインターフェイス - Amazon DynamoDB

Class: AWS.DynamoDB.DocumentClient — AWS SDK for JavaScript

テーブル定義も前回と同じく、人をお題にします。

$ aws dynamodb create-table \
    --endpoint-url http://localhost:8000 \
    --table-name People \
    --attribute-definitions \
        AttributeName=familyId,AttributeType=N \
        AttributeName=firstName,AttributeType=S \
    --key-schema \
        AttributeName=familyId,KeyType=HASH \
        AttributeName=firstName,KeyType=RANGE \
    --provisioned-throughput \
        ReadCapacityUnits=10,WriteCapacityUnits=5 \
    --table-class STANDARD

今回は、ソートキーをfirstNameにしています。が、クエリーの時のようにスキャンでソートの昇順・降順を制御することはできなさそうです。

データのお題も、サザエさんで。

test/people.json

[
  {
    "familyId": 1,
    "lastName": "フグ田",
    "firstName": "サザエ",
    "age": 24
  },
  {
    "familyId": 1,
    "lastName": "フグ田",
    "firstName": "マスオ",
    "age": 28
  },
  {
    "familyId": 1,
    "lastName": "磯野",
    "firstName": "波平",
    "age": 54
  },
  {
    "familyId": 1,
    "lastName": "磯野",
    "firstName": "フネ",
    "age": 50
  },
  {
    "familyId": 1,
    "lastName": "磯野",
    "firstName": "カツオ",
    "age": 11
  },
  {
    "familyId": 1,
    "lastName": "磯野",
    "firstName": "ワカメ",
    "age": 9
  },
  {
    "familyId": 1,
    "lastName": "フグ田",
    "firstName": "タラオ",
    "age": 3
  },
  {
    "familyId": 2,
    "lastName": "波野",
    "firstName": "ノリスケ",
    "age": 26
  },
  {
    "familyId": 2,
    "lastName": "波野",
    "firstName": "タイコ",
    "age": 22
  },
  {
    "familyId": 2,
    "lastName": "波野",
    "firstName": "イクラ",
    "age": 1
  },
  {
    "familyId": 3,
    "lastName": "伊佐坂",
    "firstName": "難物",
    "age": 60
  },
  {
    "familyId": 3,
    "lastName": "伊佐坂",
    "firstName": "お軽",
    "age": 50
  },
  {
    "familyId": 3,
    "lastName": "伊佐坂",
    "firstName": "甚六",
    "age": 20
  },
  {
    "familyId": 3,
    "lastName": "伊佐坂",
    "firstName": "浮江",
    "age": 16
  }
]

プログラムを作成する

では、スキャンを使うプログラムを作成します。

データにマッピングするクラスを作成。

src/person.ts

export class Person {
  familyId: number;
  lastName: string;
  firstName: string;
  age: number;

  constructor(
    familyId: number,
    lastName: string,
    firstName: string,
    age: number
  ) {
    this.familyId = familyId;
    this.lastName = lastName;
    this.firstName = firstName;
    this.age = age;
  }
}

テストコードのimport部分と、DocumentClientの作成。

test/scan.test.ts

import { DocumentClient } from 'aws-sdk/clients/dynamodb';
import file from 'fs';
import { Person } from '../src/person';

const dynamodb = new DocumentClient({
  credentials: {
    accessKeyId: 'fakeMyKeyId',
    secretAccessKey: 'fakeSecretAccessKey',
  },
  region: 'ap-northeast-1',
  endpoint: 'http://localhost:8000',
});

// ここに、テストを書く!

テストデータのロードと、削除を最後に入れておきます。

test('load people', async () => {
  const people = JSON.parse(
    await file.promises.readFile(`${__dirname}/people.json`, 'utf8')
  ) as Person[];

  const params: DocumentClient.BatchWriteItemInput = {
    RequestItems: {
      People: people.map((p) => ({ PutRequest: { Item: p } })),
    },
  };

  const writed = await dynamodb.batchWrite(params).promise();
  expect(writed.UnprocessedItems).toEqual({});
});

// ここに、テストを書く!

test('delete people', async () => {
  const people = JSON.parse(
    await file.promises.readFile(`${__dirname}/people.json`, 'utf8')
  ) as Person[];

  for (const person of people) {
    const params: DocumentClient.DeleteItemInput = {
      TableName: 'People',
      Key: {
        familyId: person.familyId,
        firstName: person.firstName,
      },
      ReturnValues: 'ALL_OLD',
    };

    try {
      const deleted = await dynamodb.delete(params).promise();

      if (deleted.Attributes) {
        expect(deleted.Attributes['familyId']).toBe(person.familyId);
        expect(deleted.Attributes['lastName']).toBe(person.lastName);
        expect(deleted.Attributes['firstName']).toBe(person.firstName);
        expect(deleted.Attributes['age']).toBe(person.age);
      } else {
        throw new Error('fail');
      }
    } catch (e) {
      throw e;
    }
  }

  return people;
});

では、スキャンを使っていきます。

DocumentClient#scanのAPIドキュメントはこちら。

Class: AWS.DynamoDB.DocumentClient / scan

Amazon DynamoDB自体のScanのAPIはこちら。

Scan - Amazon DynamoDB

まずは条件なしでシンプルに。

test('scan simply', async () => {
  const params: DocumentClient.ScanInput = {
    TableName: 'People',
    ConsistentRead: true,
  };

  const result = await dynamodb.scan(params).promise();
  expect(result.Count).toBe(14);
  expect(result.ScannedCount).toBe(14);

  if (result.Items) {
    const names = result.Items.map((v) => {
      const p = v as Person;
      return `${p.lastName}${p.firstName}`;
    });
    expect(names).toEqual([
      '波野イクラ',
      '波野タイコ',
      '波野ノリスケ',
      '磯野カツオ',
      'フグ田サザエ',
      'フグ田タラオ',
      '磯野フネ',
      'フグ田マスオ',
      '磯野ワカメ',
      '磯野波平',
      '伊佐坂お軽',
      '伊佐坂浮江',
      '伊佐坂甚六',
      '伊佐坂難物',
    ]);
  } else {
    throw new Error('fail');
  }
});

特に絞り込みなくパラメーター設定して

  const params: DocumentClient.ScanInput = {
    TableName: 'People',
    ConsistentRead: true,
  };

実行すると、全件を取得、スキャンしていることが確認できます(今回のデータは14件です)。

  const result = await dynamodb.scan(params).promise();
  expect(result.Count).toBe(14);
  expect(result.ScannedCount).toBe(14);

実際、返ってきているデータも全件ですね。

    const names = result.Items.map((v) => {
      const p = v as Person;
      return `${p.lastName}${p.firstName}`;
    });
    expect(names).toEqual([
      '波野イクラ',
      '波野タイコ',
      '波野ノリスケ',
      '磯野カツオ',
      'フグ田サザエ',
      'フグ田タラオ',
      '磯野フネ',
      'フグ田マスオ',
      '磯野ワカメ',
      '磯野波平',
      '伊佐坂お軽',
      '伊佐坂浮江',
      '伊佐坂甚六',
      '伊佐坂難物',
    ]);

次に、フィルターを使ってみます。

test('scan with filter', async () => {
  const params: DocumentClient.ScanInput = {
    TableName: 'People',
    FilterExpression:
      '(lastName = :lastName1 or lastName = :lastName2) and age <= :age',
    ExpressionAttributeValues: {
      ':lastName1': '磯野',
      ':lastName2': '伊佐坂',
      ':age': 20,
    },
    ConsistentRead: true,
  };

  const result = await dynamodb.scan(params).promise();
  expect(result.Count).toBe(4); // スキャンの戻り値となるデータ数
  expect(result.ScannedCount).toBe(14); // データ数

  if (result.Items) {
    const katsuo = result.Items[0] as Person;
    expect(katsuo.familyId).toBe(1);
    expect(katsuo.lastName).toBe('磯野');
    expect(katsuo.firstName).toBe('カツオ');
    expect(katsuo.age).toBe(11);

    const wakame = result.Items[1] as Person;
    expect(wakame.familyId).toBe(1);
    expect(wakame.lastName).toBe('磯野');
    expect(wakame.firstName).toBe('ワカメ');
    expect(wakame.age).toBe(9);

    const ukie = result.Items[2] as Person;
    expect(ukie.familyId).toBe(3);
    expect(ukie.lastName).toBe('伊佐坂');
    expect(ukie.firstName).toBe('浮江');
    expect(ukie.age).toBe(16);

    const jinroku = result.Items[3] as Person;
    expect(jinroku.familyId).toBe(3);
    expect(jinroku.lastName).toBe('伊佐坂');
    expect(jinroku.firstName).toBe('甚六');
    expect(jinroku.age).toBe(20);
  } else {
    throw new Error('fail');
  }
});

FilterExpressionで条件を指定して、値をバインド。

  const params: DocumentClient.ScanInput = {
    TableName: 'People',
    FilterExpression:
      '(lastName = :lastName1 or lastName = :lastName2) and age <= :age',
    ExpressionAttributeValues: {
      ':lastName1': '磯野',
      ':lastName2': '伊佐坂',
      ':age': 20,
    },
    ConsistentRead: true,
  };

条件式の書き方は、こちら。

Comparison Operator and Function Reference - Amazon DynamoDB

ここで、返却されるデータ数とスキャンしたデータ数に差が現れます。

  const result = await dynamodb.scan(params).promise();
  expect(result.Count).toBe(4); // スキャンの戻り値となるデータ数
  expect(result.ScannedCount).toBe(14); // データ数

ドキュメントにも、フィルターは返ってくる件数が変わるだけで、スキャン対象のデータ数が減るわけではない、ということが
書かれていましたからね。

プロジェクション。取得する属性を絞ることができます。

test('scan with filter, projection', async () => {
  const params: DocumentClient.ScanInput = {
    TableName: 'People',
    FilterExpression:
      '(lastName = :lastName1 or lastName = :lastName2) and age <= :age',
    ExpressionAttributeValues: {
      ':lastName1': '磯野',
      ':lastName2': '伊佐坂',
      ':age': 20,
    },
    ProjectionExpression: 'firstName,age',
    ConsistentRead: true,
  };

  const result = await dynamodb.scan(params).promise();
  expect(result.Count).toBe(4); // スキャンの戻り値となるデータ数
  expect(result.ScannedCount).toBe(14); // データ数

  if (result.Items) {
    const katsuo = result.Items[0] as Person;
    expect(katsuo.familyId).toBeUndefined();
    expect(katsuo.lastName).toBeUndefined();
    expect(katsuo.firstName).toBe('カツオ');
    expect(katsuo.age).toBe(11);

    const wakame = result.Items[1] as Person;
    expect(wakame.familyId).toBeUndefined();
    expect(wakame.lastName).toBeUndefined();
    expect(wakame.firstName).toBe('ワカメ');
    expect(wakame.age).toBe(9);

    const ukie = result.Items[2] as Person;
    expect(ukie.familyId).toBeUndefined();
    expect(ukie.lastName).toBeUndefined();
    expect(ukie.firstName).toBe('浮江');
    expect(ukie.age).toBe(16);

    const jinroku = result.Items[3] as Person;
    expect(jinroku.familyId).toBeUndefined();
    expect(jinroku.lastName).toBeUndefined();
    expect(jinroku.firstName).toBe('甚六');
    expect(jinroku.age).toBe(20);
  } else {
    throw new Error('fail');
  }
});

ProjectionExpressionで指定しますが、複数の属性を対象とする場合は,区切りで指定します。

    ProjectionExpression: 'firstName,age',

A string that identifies one or more attributes to retrieve from the specified table or index. These attributes can include scalars, sets, or elements of a JSON document. The attributes in the expression must be separated by commas.

Scan - Amazon DynamoDB

こうすると、指定した属性だけが返ってくるようになります。

    const katsuo = result.Items[0] as Person;
    expect(katsuo.familyId).toBeUndefined();
    expect(katsuo.lastName).toBeUndefined();
    expect(katsuo.firstName).toBe('カツオ');
    expect(katsuo.age).toBe(11);

    const wakame = result.Items[1] as Person;
    expect(wakame.familyId).toBeUndefined();
    expect(wakame.lastName).toBeUndefined();
    expect(wakame.firstName).toBe('ワカメ');
    expect(wakame.age).toBe(9);

    const ukie = result.Items[2] as Person;
    expect(ukie.familyId).toBeUndefined();
    expect(ukie.lastName).toBeUndefined();
    expect(ukie.firstName).toBe('浮江');
    expect(ukie.age).toBe(16);

    const jinroku = result.Items[3] as Person;
    expect(jinroku.familyId).toBeUndefined();
    expect(jinroku.lastName).toBeUndefined();
    expect(jinroku.firstName).toBe('甚六');
    expect(jinroku.age).toBe(20);

次は、件数制限をしてみましょう。

test('scan with limit', async () => {
  const params: DocumentClient.ScanInput = {
    TableName: 'People',
    Limit: 5,
    ConsistentRead: true,
  };

  const result = await dynamodb.scan(params).promise();
  expect(result.Count).toBe(5);
  expect(result.ScannedCount).toBe(5);

  if (result.Items) {
    const names = result.Items.map((v) => {
      const p = v as Person;
      return `${p.lastName}${p.firstName}`;
    });
    expect(names).toEqual([
      '波野イクラ',
      '波野タイコ',
      '波野ノリスケ',
      '磯野カツオ',
      'フグ田サザエ',
    ]);
  } else {
    throw new Error('fail');
  }
});

今回は5件にしてみました。

  const params: DocumentClient.ScanInput = {
    TableName: 'People',
    Limit: 5,
    ConsistentRead: true,
  };

取得件数も5件ですね。

  const result = await dynamodb.scan(params).promise();
  expect(result.Count).toBe(5);
  expect(result.ScannedCount).toBe(5);

  if (result.Items) {
    const names = result.Items.map((v) => {
      const p = v as Person;
      return `${p.lastName}${p.firstName}`;
    });
    expect(names).toEqual([
      '波野イクラ',
      '波野タイコ',
      '波野ノリスケ',
      '磯野カツオ',
      'フグ田サザエ',
    ]);

そして、取得件数制限とフィルターを組み合わせてみます。

test('scan with filter, limit', async () => {
  const params: DocumentClient.ScanInput = {
    TableName: 'People',
    FilterExpression:
      '(lastName = :lastName1 or lastName = :lastName2) and age <= :age',
    ExpressionAttributeValues: {
      ':lastName1': '磯野',
      ':lastName2': '伊佐坂',
      ':age': 20,
    },
    Limit: 5,
    ConsistentRead: true,
  };

  const result = await dynamodb.scan(params).promise();
  expect(result.Count).toBe(1); // スキャンの戻り値となるデータ数
  expect(result.ScannedCount).toBe(5); // データ数

  if (result.Items) {
    const katsuo = result.Items[0] as Person;
    expect(katsuo.familyId).toBe(1);
    expect(katsuo.lastName).toBe('磯野');
    expect(katsuo.firstName).toBe('カツオ');
    expect(katsuo.age).toBe(11);
  } else {
    throw new Error('fail');
  }
});

こうすると、Limitでスキャン対象のデータを絞ったあとにフィルターがかかっていることがよくわかります。

全件スキャンした後であれば4件返ってくる条件を指定していますが、Limitを5にすると今回のテーブル定義、データでは1件になりました。

  const result = await dynamodb.scan(params).promise();
  expect(result.Count).toBe(1); // スキャンの戻り値となるデータ数
  expect(result.ScannedCount).toBe(5); // データ数

  if (result.Items) {
    const katsuo = result.Items[0] as Person;
    expect(katsuo.familyId).toBe(1);
    expect(katsuo.lastName).toBe('磯野');
    expect(katsuo.firstName).toBe('カツオ');
    expect(katsuo.age).toBe(11);

スキャンはこんなところでしょうか。

ページングなどは扱っていませんが。使う場合は、こちらの記載に従ってLastEvaluatedKeyの結果を見て、次に実行するスキャンの
ExclusiveStartKeyに指定する、という感じになるようです。

DynamoDB でのスキャンの操作 / ページ単位の出力件数を指定

まとめ

今回は、Amazon DynamoDBのスキャンを簡単に試してみました。

プライマリーキーに関わらずデータを取得できますが、その特性には注意した方が良さそうですね。