CLOVER🍀

That was when it all began.

Amazon DynamoDBのロヌカル版DynamoDB Local ずDocumentClientで、セカンダリむンデックスを詊す

これは、なにをしたくお曞いたもの

最近、Amazon DynamoDBで遊んでいたしたが、今回で䞀区切りにしようかなず思いたす。
もしかしたら、DynamoDB Streamsあたりは詊したくなるかもしれたせんが。

今回は、セカンダリむンデックスをテヌマにしたす。

セカンダリインデックスを使用したデータアクセス性の向上 - Amazon DynamoDB

Amazon DynamoDBのセカンダリむンデックス

Amazon DynamoDBを䜿ったデヌタのアクセスの基本は、プラむマリヌキヌを䜿ったアクセスです。

ずはいえ、それだけではデヌタぞ効率的にアクセスするパスが限られたす。セカンダリむンデックスは、プラむマリヌキヌ以倖の方法で
デヌタにアクセスする方法を提䟛したす。

セカンダリインデックスを使用したデータアクセス性の向上 - Amazon DynamoDB

セカンダリむンデックスは、ひず぀のテヌブルに関連付けられたす。この時、テヌブルのこずをむンデックスの「ベヌステヌブル」ず
呌ぶみたいです。

セカンダリむンデックスを䜜成する時には、セカンダリキヌ代替キヌの定矩ず、ベヌステヌブルからコピヌする属性を定矩したす。
この時、テヌブルからむンデックスに察しおデヌタがコピヌされたす。

そしお、デヌタを取埗する際には「テヌブルに察しおク゚リヌやスキャンを実行する時ず同じように」むンデックスに察しおク゚リヌや
スキャンを実行するこずになるようです。
セカンダリむンデックスは、Amazon DynamoDBによっお自動的にメンテナンスされ、ベヌステヌブルのアむテムを远加、倉曎、削陀するず
むンデックスも曎新され、倉曎が反映されたす。

こうやっお曞くず、セカンダリむンデックスずいうのは、少し定矩を倉えたベヌステヌブルのコピヌなんだろうなずいう気がしおきたす。

セカンダリむンデックスには、以䞋の2皮類がありたす。

  • グロヌバルセカンダリむンデックス
    • パヌティションキヌおよび゜ヌトキヌを持぀が、ベヌステヌブルず異なるものをセカンダリキヌずしお定矩可胜
    • セカンダリキヌの性質から、このむンデックスを䜿うク゚リヌは党パヌティション、党デヌタを察象ずする可胜性があるため「グロヌバル」ずなる
    • むンデックスのデヌタは、ベヌステヌブルずは別の独自のパヌティションに保存され、スケヌリングもベヌステヌブルずは別ずなる
  • ロヌカルセカンダリむンデックス
    • パヌティションキヌはベヌステヌブルず同じで、゜ヌトキヌが異なるむンデックス
    • ぀たり、パヌティションの範囲がベヌステヌブルず同じずなり、アクセス可胜な範囲も同等になるため「ロヌカル」ずなる

他にもそれなりに違いがありたすね。ドキュメントに蚘茉されおいる内容を、簡単にたずめたす。

特城 グロヌバルセカンダリむンデックス ロヌカルセカンダリむンデックス
プラむマリヌキヌ定矩 パヌティションキヌのみ、たたは耇合キヌパヌティションキヌ゜ヌトキヌ 耇合キヌパヌティションキヌ゜ヌトキヌ
プラむマリヌキヌの属性 パヌティションキヌ、゜ヌトキヌのいずれも文字列、数倀、バむナリ型の任意のベヌステヌブルの属性ず同じ定矩が可胜 パヌティションキヌはベヌステヌブルず同じ、゜ヌトキヌは文字列、数倀、バむナリ型の任意のベヌステヌブルの属性ず同じ定矩が可胜
パヌティションキヌのサむズ制限 なし パヌティションキヌの倀ごずに、むンデックスに指定されたアむテムの合蚈サむズが10GB以䞋であるこず
むンデックスに察するオンラむン操䜜 ベヌステヌブルの䜜成ず同時にむンデックスを䜜成でき、埌からむンデックスの远加、削陀を行うこずも可胜 ベヌステヌブルの䜜成ず同時にむンデックスを䜜成できるが、埌からむンデックスを远加したり削陀するこずはできない
ク゚リヌずパヌティション 党パヌティションでテヌブル党䜓に察しおク゚リヌを実行可胜 パヌティションキヌで指定されたひず぀のパヌティションに察しお実行可胜
読み蟌み敎合性 結果敎合性 結果敎合性、匷い敎合性から遞択可胜
プロビゞョニングされたスルヌプットの消費 読み蟌み曞き蟌みずもに、むンデックスに独自のプロビゞョニングされたスルヌプットがあり、ク゚リヌやスキャン、ベヌステヌブル曞き蟌み時のむンデックスの曎新でキャパシティナニットを消費する 読み蟌み曞き蟌みずもに、ベヌステヌブルのキャパシティナニットを䜿甚し、ク゚リヌやスキャン、ベヌステヌブル曞き蟌み時のむンデックスの曎新でベヌステヌブルのキャパシティナニットを消費する
取埗できる属性 ク゚リヌたたはスキャンでは、むンデックスにコピヌされた属性のみがリク゚スト可胜 ク゚リヌたたはスキャンでは、むンデックスにコピヌされおいない属性もリク゚スト可胜で、足りない属性はベヌステヌブルから自動的に取埗する

こう芋おいくず、ロヌカルセカンダリむンデックスはベヌステヌブルに察しお別の゜ヌトキヌでク゚リヌを実行可胜にするためのもの、
ず蚀いたくなる気がしたす。
グロヌバルセカンダリむンデックスは、ベヌステヌブルのキヌ定矩が異なるレプリカずいったずころでしょうか。

その他。

  • セカンダリむンデックスを持぀テヌブルを耇数䜜成する堎合、テヌブルを順次䜜成する必芁がある
    • 䜜成したテヌブルがACTIVEになる前にセカンダリテヌブルを持぀テヌブルを䜜成しようずするず、LimitExceededExceptionがスロヌされる
  • むンデックス䜜成時の指定事項
    • むンデックスの皮類グロヌバルセカンダリむンデックス or ロヌカルセカンダリむンデックス
    • むンデックスの名前
    • むンデックスのキヌ定矩
    • むンデックスにコピヌする属性
    • グロヌバルセカンダリむンデックスの堎合、読み蟌み曞き蟌みのキャパシティナニット

その他に詳しい情報も続くのですが。

DynamoDB のグローバルセカンダリインデックスの使用 - Amazon DynamoDB

グローバルセカンダリインデックスの管理 - Amazon DynamoDB

インデックスキー違反の検出と修正 - Amazon DynamoDB

ローカルセカンダリインデックス - Amazon DynamoDB

今回は説明を芋るのはこれくらいにしお、ずりあえず動かしおみようかなず思いたす。

確認には、Amazon DynamoDBのロヌカル版を䜿いたす。

環境

今回の環境は、こちら。

$ aws --version
aws-cli/2.4.23 Python/3.8.8 Linux/5.4.0-100-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DynamoDB Local の情報。

$ 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

テヌブル定矩

今回䜿うテヌブルですが、2぀䜜成するこずにしたした。

テヌブル定矩のむメヌゞは以䞋ずしたすが、この時点では䜜成したせん。セカンダリむンデックス、特にロヌカルセカンダリむンデックスは
テヌブル䜜成時に定矩するので。

$ 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


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

片方は、パヌティションキヌのみのテヌブルです。

デヌタは、こんな感じで甚意。

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
  }
]

test/books.json

[
  {
    "isbn": "978-4815607654",
    "title": "AWSコンテナ蚭蚈・構築[本栌]入門",
    "price": 3300,
    "publicationDate": "2021-10-21"
  },
  {
    "isbn": "978-4295006657",
    "title": "Amazon Web Servicesむンフラサヌビス掻甚倧党 システム構築/自動化、デヌタストア、高信頌化 (impress top gear)",
    "price": 5060,
    "publicationDate": "2019-09-05"
  },
  {
    "isbn": "978-4797392579",
    "title": "Amazon Web Services パタヌン別構築・運甚ガむド 改蚂第2版",
    "price": 3740,
    "publicationDate": "2018-03-23"
  },
  {
    "isbn": "978-4797392562",
    "title": "Amazon Web Services 業務システム蚭蚈・移行ガむド",
    "price": 3520,
    "publicationDate": "2018-01-20"
  },
  {
    "isbn": "978-4774176734",
    "title": "Amazon Web Services実践入門 (WEB+DB PRESS plus)",
    "price": 2838,
    "publicationDate": "2015-11-10"
  },
  {
    "isbn": "978-4822277376",
    "title": "Amazon Web Services クラりドデザむンパタヌン蚭蚈ガむド 改蚂版",
    "price": 2970,
    "publicationDate": "2015-05-28"
  },
  {
    "isbn": "978-4822277369",
    "title": "Amazon Web Services クラりドデザむンパタヌン実装ガむド 改蚂版",
    "price": 4180,
    "publicationDate": "2015-03-09"
  }
]

セカンダリむンデックスを䜜成する

では、セカンダリむンデックスを䜜成しおみたしょう。

こちらを芋ながら䜜成しおいきたす。

グローバルセカンダリインデックスの操作: AWS CLI - Amazon DynamoDB

ローカルセカンダリインデックスの操作: AWS CLI - Amazon DynamoDB

䜿甚するテヌブルずプラむマリヌキヌの定矩は以䞋ですが、

最終的には以䞋になるように構成したす。

Bookテヌブルは、パヌティションキヌのみのテヌブルなので、セカンダリむンデックスは䜜成できたせん。それも確認しおいきたす。

グロヌバルセカンダリむンデックスを䜜成する

たずは、グロヌバルむンデックスの䜜成から。

グローバルセカンダリインデックスの操作: AWS CLI - Amazon DynamoDB

パヌティションキヌlastNameず゜ヌトキヌageの耇合プラむマリヌキヌを持぀、グロヌバルセカンダリむンデックスを䜜成しお
みたす。

--global-secondary-indexesで、グロヌバルセカンダリむンデックスの定矩を指定するようです。定矩内容はJSONで。

$ aws dynamodb create-table \
    --endpoint-url http://localhost:8000 \
    --table-name People \
    --attribute-definitions \
        AttributeName=familyId,AttributeType=N \
        AttributeName=firstName,AttributeType=S \
        AttributeName=lastName,AttributeType=S \
        AttributeName=age,AttributeType=N \
    --key-schema \
        AttributeName=familyId,KeyType=HASH \
        AttributeName=firstName,KeyType=RANGE \
    --provisioned-throughput \
        ReadCapacityUnits=10,WriteCapacityUnits=5 \
    --table-class STANDARD \
    --global-secondary-indexes \
        '
        [
            {
                "IndexName": "PeopleGlobalCompositeIndex",
                "KeySchema": [{"AttributeName":"lastName","KeyType":"HASH"},
                                {"AttributeName":"age","KeyType":"RANGE"}],
                "Projection":{
                    "ProjectionType":"INCLUDE",
                    "NonKeyAttributes":["firstName"]
                },
                "ProvisionedThroughput": {
                    "ReadCapacityUnits": 10,
                    "WriteCapacityUnits": 5
                }
            }
        ]
        '

最初に挙げた時からテヌブルの属性定矩が増えおいたすが、セカンダリむンデックスでキヌずしお指定する属性は定矩しなくおはならない
みたいです。

    --attribute-definitions \
        AttributeName=familyId,AttributeType=N \
        AttributeName=firstName,AttributeType=S \
        AttributeName=lastName,AttributeType=S \
        AttributeName=age,AttributeType=N \

今回䜜成したグロヌバルセカンダリむンデックスの内容はこちら。むンデックスの名前はPeopleGlobalCompositeIndexずしたした。

        [
            {
                "IndexName": "PeopleGlobalCompositeIndex",
                "KeySchema": [{"AttributeName":"lastName","KeyType":"HASH"},
                                {"AttributeName":"age","KeyType":"RANGE"}],
                "Projection":{
                    "ProjectionType":"INCLUDE",
                    "NonKeyAttributes":["firstName"]
                },
                "ProvisionedThroughput": {
                    "ReadCapacityUnits": 10,
                    "WriteCapacityUnits": 5
                }
            }
        ]

なんずなく各項目の意味はわからなくもないですが、詳しくはこちらぞ。

DynamoDB のグローバルセカンダリインデックスの使用 - Amazon DynamoDB

グローバルセカンダリインデックスの管理 - Amazon DynamoDB

CreateTable - Amazon DynamoDB

次は、このテヌブルにパヌティションキヌfirstNameのみをプラむマリヌキヌずするグロヌバルセカンダリむンデックスを远加しおみたす。

コマンドは以䞋です。むンデックスの名前はPeopleGlobalSimpleIndexずしたす。

$ aws dynamodb update-table \
    --endpoint-url http://localhost:8000 \
    --table-name People \
    --attribute-definitions \
        AttributeName=firstName,AttributeType=S \
    --global-secondary-index-updates \
        '
        [
            {
                "Create": {
                    "IndexName": "PeopleGlobalSimpleIndex",
                    "KeySchema": [{"AttributeName":"firstName","KeyType":"HASH"}],
                    "Projection":{
                        "ProjectionType":"INCLUDE",
                        "NonKeyAttributes":["lastName"]
                    },
                    "ProvisionedThroughput": {
                        "ReadCapacityUnits": 10,
                        "WriteCapacityUnits": 5
                    }
                }
            }
        ]
        '

--global-secondary-index-updatesオプションを䜿い、グロヌバルセカンダリむンデックスの远加、曎新、削陀に合わせおCreate、Update、
Deleteのいずれかを指定する必芁がありたす。

UpdateTable - Amazon DynamoDB

この時も、むンデックスのキヌになる属性の定矩は明瀺--attribute-definitionsが必芁なようです。

最埌に、もずのテヌブルがパヌティションキヌisbnのみのテヌブルに、パヌティションキヌtitleのみをプラむマリヌキヌずする
グロヌバルセカンダリむンデックスを぀けおみたす。むンデックスの名前はBookGlobalSimpleIndexずしたす。

$ aws dynamodb create-table \
    --endpoint-url http://localhost:8000 \
    --table-name Books \
    --attribute-definitions \
        AttributeName=isbn,AttributeType=S \
        AttributeName=title,AttributeType=S \
    --key-schema \
        AttributeName=isbn,KeyType=HASH \
    --provisioned-throughput \
        ReadCapacityUnits=10,WriteCapacityUnits=5 \
    --table-class STANDARD \
    --global-secondary-indexes \
        '
        [
            {
                "IndexName": "BookGlobalSimpleIndex",
                "KeySchema": [{"AttributeName":"title","KeyType":"HASH"}],
                "Projection":{
                    "ProjectionType":"INCLUDE",
                    "NonKeyAttributes":["price"]
                },
                "ProvisionedThroughput": {
                    "ReadCapacityUnits": 10,
                    "WriteCapacityUnits": 5
                }
            }
        ]
        '

これで、グロヌバルセカンダリむンデックスを䜜成するコマンドず、グロヌバルセカンダリむンデックスはベヌステヌブルのプラむマリヌキヌ定矩に
関係ないキヌを定矩できるこずが確認できたした。

次は、ロヌカルセカンダリむンデックスを䜜成しおみたすが、こちらはテヌブル䜜成時に䜜るこずになるため、1床テヌブルを削陀しお
おきたす。

$ aws dynamodb delete-table \
    --endpoint-url http://localhost:8000 \
    --table-name People

$ aws dynamodb delete-table \
    --endpoint-url http://localhost:8000 \
    --table-name Books
ロヌカルセカンダリむンデックスを䜜成する

次は、ロヌカルセカンダリむンデックスを䜜成したす。

ローカルセカンダリインデックスの操作: AWS CLI - Amazon DynamoDB

Peopleテヌブルに察しお、パヌティションキヌfamilyIdず゜ヌトキヌageの耇合プラむマリヌキヌずする
ロヌカルセカンダリむンデックスを䜜成しおみたす。むンデックスの名前はPeopleLocalCompositeIndexずしたす。

aws dynamodb create-table \
    --endpoint-url http://localhost:8000 \
    --table-name People \
    --attribute-definitions \
        AttributeName=familyId,AttributeType=N \
        AttributeName=firstName,AttributeType=S \
        AttributeName=age,AttributeType=N \
    --key-schema \
        AttributeName=familyId,KeyType=HASH \
        AttributeName=firstName,KeyType=RANGE \
    --provisioned-throughput \
        ReadCapacityUnits=10,WriteCapacityUnits=5 \
    --table-class STANDARD \
    --local-secondary-indexes \
        '
        [
            {
                "IndexName": "PeopleLocalCompositeIndex",
                "KeySchema": [{"AttributeName":"familyId","KeyType":"HASH"},
                                {"AttributeName":"age","KeyType":"RANGE"}],
                "Projection":{
                    "ProjectionType":"INCLUDE",
                    "NonKeyAttributes":["firstName"]
                }
            }
        ]
        '

ロヌカルセカンダリむンデックスの堎合、グロヌバルセカンダリむンデックスのようにProvisionedThroughputの指定は芁りたせん。
むしろ、曞いおいるず゚ラヌになりたす。

各項目の意味は雰囲気わかりそうですが、詳しくはこちらぞ。

ローカルセカンダリインデックス - Amazon DynamoDB

CreateTable - Amazon DynamoDB

ここで1床テヌブルを削陀しお

$ aws dynamodb delete-table \
    --endpoint-url http://localhost:8000 \
    --table-name People

パヌティションキヌがベヌステヌブルず異なるロヌカルセカンダリむンデックスの䜜成を詊みおみたす。

$ aws dynamodb create-table \
    --endpoint-url http://localhost:8000 \
    --table-name People \
    --attribute-definitions \
        AttributeName=familyId,AttributeType=N \
        AttributeName=firstName,AttributeType=S \
        AttributeName=age,AttributeType=N \
    --key-schema \
        AttributeName=familyId,KeyType=HASH \
        AttributeName=firstName,KeyType=RANGE \
    --provisioned-throughput \
        ReadCapacityUnits=10,WriteCapacityUnits=5 \
    --table-class STANDARD \
    --local-secondary-indexes \
        '
        [
            {
                "IndexName": "PeopleLocalCompositeIndex",
                "KeySchema": [{"AttributeName":"firstName","KeyType":"HASH"},
                                {"AttributeName":"age","KeyType":"RANGE"}],
                "Projection":{
                    "ProjectionType":"INCLUDE",
                    "NonKeyAttributes":["lastName"]
                }
            }
        ]
        '

これは、やはり゚ラヌになりたす。

An error occurred (ValidationException) when calling the CreateTable operation: Local Secondary indices must have the same hash key as the main table

ずいうわけで、ロヌカルセカンダリむンデックスはベヌステヌブルず同じパヌティションキヌを持぀必芁があるこずが確認できたした。

ずころで、パヌティションキヌのみのテヌブルに察しお、たったく同じキヌ定矩のロヌカルセカンダリむンデックスを䜜ろうずするず
どうなるのでしょう。

$ aws dynamodb create-table \
    --endpoint-url http://localhost:8000 \
    --table-name Books \
    --attribute-definitions \
        AttributeName=isbn,AttributeType=S \
        AttributeName=title,AttributeType=S \
    --key-schema \
        AttributeName=isbn,KeyType=HASH \
    --provisioned-throughput \
        ReadCapacityUnits=10,WriteCapacityUnits=5 \
    --table-class STANDARD \
    --local-secondary-indexes \
        '
        [
            {
                "IndexName": "BookLocalSimpleIndex",
                "KeySchema": [{"AttributeName":"isbn","KeyType":"HASH"}],
                "Projection":{
                    "ProjectionType":"INCLUDE",
                    "NonKeyAttributes":["title","price"]
                }
            }
        ]
        '

これは、パヌティションキヌの指定のみでロヌカルセカンダリむンデックスは䜜成できないずいう゚ラヌになりたす。

An error occurred (ValidationException) when calling the CreateTable operation: Local Secondary indices are not allowed on hash tables, only hash and range tables

ずいうわけで、ロヌカルセカンダリむンデックスは耇合プラむマリヌキヌで定矩する必芁があるこずを確認できたした。

たずめお

最埌に、ここたで定矩しおきたグロヌバルセカンダリむンデックス、ロヌカルセカンダリむンデックスを含めたテヌブル定矩にしおおきたす。

Peopleテヌブル。

$ aws dynamodb create-table \
    --endpoint-url http://localhost:8000 \
    --table-name People \
    --attribute-definitions \
        AttributeName=familyId,AttributeType=N \
        AttributeName=firstName,AttributeType=S \
        AttributeName=lastName,AttributeType=S \
        AttributeName=age,AttributeType=N \
    --key-schema \
        AttributeName=familyId,KeyType=HASH \
        AttributeName=firstName,KeyType=RANGE \
    --provisioned-throughput \
        ReadCapacityUnits=10,WriteCapacityUnits=5 \
    --table-class STANDARD \
    --global-secondary-indexes \
        '
        [
            {
                "IndexName": "PeopleGlobalCompositeIndex",
                "KeySchema": [{"AttributeName":"lastName","KeyType":"HASH"},
                                {"AttributeName":"age","KeyType":"RANGE"}],
                "Projection":{
                    "ProjectionType":"INCLUDE",
                    "NonKeyAttributes":["firstName"]
                },
                "ProvisionedThroughput": {
                    "ReadCapacityUnits": 10,
                    "WriteCapacityUnits": 5
                }
            },
            {
                "IndexName": "PeopleGlobalSimpleIndex",
                "KeySchema": [{"AttributeName":"firstName","KeyType":"HASH"}],
                "Projection":{
                    "ProjectionType":"INCLUDE",
                    "NonKeyAttributes":["lastName"]
                },
                "ProvisionedThroughput": {
                    "ReadCapacityUnits": 10,
                    "WriteCapacityUnits": 5
                }
            }
        ]
        ' \
    --local-secondary-indexes \
        '
        [
            {
                "IndexName": "PeopleLocalCompositeIndex",
                "KeySchema": [{"AttributeName":"familyId","KeyType":"HASH"},
                                {"AttributeName":"age","KeyType":"RANGE"}],
                "Projection":{
                    "ProjectionType":"INCLUDE",
                    "NonKeyAttributes":["firstName"]
                }
            }
        ]
        '

Bookテヌブル。

$ aws dynamodb create-table \
    --endpoint-url http://localhost:8000 \
    --table-name Books \
    --attribute-definitions \
        AttributeName=isbn,AttributeType=S \
        AttributeName=title,AttributeType=S \
    --key-schema \
        AttributeName=isbn,KeyType=HASH \
    --provisioned-throughput \
        ReadCapacityUnits=10,WriteCapacityUnits=5 \
    --table-class STANDARD \
    --global-secondary-indexes \
        '
        [
            {
                "IndexName": "BookGlobalSimpleIndex",
                "KeySchema": [{"AttributeName":"title","KeyType":"HASH"}],
                "Projection":{
                    "ProjectionType":"INCLUDE",
                    "NonKeyAttributes":["price"]
                },
                "ProvisionedThroughput": {
                    "ReadCapacityUnits": 10,
                    "WriteCapacityUnits": 5
                }
            }
        ]
        '

こちらを今回の定矩にしたす。これらのテヌブル、セカンダリむンデックスを䜿うアプリケヌションを曞いおいきたしょう。

アプリケヌションの準備

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
$ npmi aws-sdk

䟝存関係は、このようになりたした。

  "devDependencies": {
    "@types/jest": "^27.4.1",
    "@types/node": "^16.11.26",
    "esbuild": "^0.14.25",
    "esbuild-jest": "^0.5.0",
    "jest": "^27.5.1",
    "prettier": "2.5.1",
    "typescript": "^4.6.2"
  },
  "dependencies": {
    "aws-sdk": "^2.1087.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.json

{
  "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

APIはこちら。

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

セカンダリむンデックスを䜿っおみる

では、セカンダリむンデックスを䜿うプログラムを䜜成したす。

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

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;
  }
}

src/book.ts

export class Book {
  isbn: string;
  title: string;
  price: number;
  publicationDate: string;

  constructor(
    isbn: string,
    title: string,
    price: number,
    publicationDate: string
  ) {
    this.isbn = isbn;
    this.title = title;
    this.price = price;
    this.publicationDate = publicationDate;
  }
}

テストコヌドのimport郚分ず、DocumentClientの䜜成。

test/secondaryindex.test.ts

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

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

// ここに、テストを曞く

テストデヌタのロヌドず、削陀を最埌に入れおおきたす。

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

  const books = JSON.parse(
    await fs.promises.readFile(`${__dirname}/books.json`, 'utf8')
  ) as Book[];

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

  await dynamodb.batchWrite(params).promise();
});


// ここに、テストを曞く


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

  const books = JSON.parse(
    await fs.promises.readFile(`${__dirname}/books.json`, 'utf8')
  ) as Book[];

  const params: DocumentClient.BatchWriteItemInput = {
    RequestItems: {
      People: people.map((p) => ({
        DeleteRequest: {
          Key: { familyId: p.familyId, firstName: p.firstName },
        },
      })),
      Books: books.map((b) => ({ DeleteRequest: { Key: { isbn: b.isbn } } })),
    },
  };

  await dynamodb.batchWrite(params).promise();
});

セカンダリむンデックスを䜿うには、ク゚リヌやスキャンを䜿っおアクセスするこずになりたす。ずいうわけで、過去に曞いた゚ントリヌも
芋返しながら曞いおいきたす。

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

Amazon DynamoDBローカル版(DynamoDB Local )とDocumentClientで、スキャンを試す - CLOVER🍀

ク゚リヌでセカンダリむンデックスを䜿う

最初はク゚リヌから詊しおいきたしょう。
グロヌバルセカンダリむンデックスからいきたす。

Peopleテヌブルに䜜成したグロヌバルセカンダリむンデックスを䜿う䟋。こちらは耇合プラむマリヌキヌです。

test("query for people's global secondary index (composite keys)", async () => {
  const params: DocumentClient.QueryInput = {
    TableName: 'People',
    IndexName: 'PeopleGlobalCompositeIndex', // むンデックス名を指定
    KeyConditionExpression: 'lastName = :lastName and age <= :age',
    ExpressionAttributeValues: {
      ':lastName': '磯野',
      ':age': 11,
    },
    ScanIndexForward: false,
    ConsistentRead: false, // グロヌバルセカンダリむンデックスでは true にできないデフォルト false
  };

  const result = await dynamodb.query(params).promise();

  if (result.Items) {
    expect(result.Count).toBe(2);

    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);
  } else {
    throw new Error('test failed');
  }
});

ポむントは、むンデックス名をIndexNameずしお指定するこずですね。合わせおテヌブル名も必芁になりたす。

  const params: DocumentClient.QueryInput = {
    TableName: 'People',
    IndexName: 'PeopleGlobalCompositeIndex', // むンデックス名を指定
    KeyConditionExpression: 'lastName = :lastName and age <= :age',
    ExpressionAttributeValues: {
      ':lastName': '磯野',
      ':age': 11,
    },
    ScanIndexForward: false,
    ConsistentRead: false, // グロヌバルセカンダリむンデックスでは true にできないデフォルト false
  };

たた、グロヌバルセカンダリむンデックスの堎合はConsistentReadをtrueにするず゚ラヌになりたす。
これは、グロヌバルセカンダリむンデックスの特城ずしお挙げられおいた話ですね。

次は、パヌティションキヌのみのプラむマリヌキヌの堎合。

test("query for people's global secondary index (simple key)", async () => {
  const params: DocumentClient.QueryInput = {
    TableName: 'People',
    IndexName: 'PeopleGlobalSimpleIndex', // むンデックス名を指定
    KeyConditionExpression: 'firstName = :firstName',
    ExpressionAttributeValues: {
      ':firstName': 'ノリスケ',
    },
    ScanIndexForward: false,
    // ConsistentRead: false, // グロヌバルセカンダリむンデックスでは true にできないデフォルト false
  };

  const result = await dynamodb.query(params).promise();

  if (result.Items) {
    expect(result.Count).toBe(1);

    const norisuke = result.Items[0] as Person;
    expect(norisuke.familyId).toBe(2);
    expect(norisuke.lastName).toBe('波野');
    expect(norisuke.firstName).toBe('ノリスケ');
    expect(norisuke.age).toBeUndefined();
  } else {
    throw new Error('test failed');
  }
});

よく芋るず、undefinedの項目が混じっおいたす。

    const norisuke = result.Items[0] as Person;
    expect(norisuke.familyId).toBe(2);
    expect(norisuke.lastName).toBe('波野');
    expect(norisuke.firstName).toBe('ノリスケ');
    expect(norisuke.age).toBeUndefined();

これは、むンデックス定矩の時にコピヌする属性に指定しおいなかったからです。

            {
                "IndexName": "PeopleGlobalSimpleIndex",
                "KeySchema": [{"AttributeName":"firstName","KeyType":"HASH"}],
                "Projection":{
                    "ProjectionType":"INCLUDE",
                    "NonKeyAttributes":["lastName"]
                },
                "ProvisionedThroughput": {
                    "ReadCapacityUnits": 10,
                    "WriteCapacityUnits": 5
                }
            }

ここでProtectionに指定しおいる属性はlastNameのみですが、プラむマリヌキヌの属性は垞にコピヌされたす。明瀺的に指定しなかった
ageのみが今回コピヌされおいないわけです。

もっずも、コピヌする察象の属性を増やしたり、テヌブルのすべおの属性をコピヌするようにするこずもできるので、それはやりたいこずず
蚭定次第です。

Bookテヌブルのグロヌバルセカンダリむンデックスにアクセスする䟋。

test("query for books's global secondary index(composite keys)", async () => {
  const params: DocumentClient.QueryInput = {
    TableName: 'Books',
    IndexName: 'BookGlobalSimpleIndex',
    KeyConditionExpression: 'title = :title',
    ExpressionAttributeValues: {
      ':title': 'AWSコンテナ蚭蚈・構築[本栌]入門',
    },
  };

  const result = await dynamodb.query(params).promise();

  if (result.Items) {
    expect(result.Count).toBe(1);

    const containerBook = result.Items[0] as Book;
    expect(containerBook.isbn).toBe('978-4815607654');
    expect(containerBook.title).toBe('AWSコンテナ蚭蚈・構築[本栌]入門');
    expect(containerBook.price).toBe(3300);
    expect(containerBook.publicationDate).toBeUndefined();
  } else {
    throw new Error('test failed');
  }
});

ここたでの内容ず、特に倉わったこずはありたせん。ただ、グロヌバルセカンダリむンデックスの堎合は、ベヌステヌブルずたったく異なる
プラむマリヌキヌ構成を取り、テヌブルのようにク゚リヌを実行できるこずが確認できたしたね。

続いお、ロヌカルセカンダリむンデックス。これはPeopleテヌブルのみです。

test("query for people's local secondary index (composite keys)", async () => {
  const params: DocumentClient.QueryInput = {
    TableName: 'People',
    IndexName: 'PeopleLocalCompositeIndex',
    KeyConditionExpression: 'familyId = :familyId and age < :age',
    ExpressionAttributeValues: {
      ':familyId': 2,
      ':age': 10,
    },
    ProjectionExpression: 'familyId,lastName,firstName,age',
  };

  const result = await dynamodb.query(params).promise();

  if (result.Items) {
    expect(result.Count).toBe(1);

    const tarao = result.Items[0] as Person;
    expect(tarao.familyId).toBe(2);
    expect(tarao.lastName).toBe('波野');
    expect(tarao.firstName).toBe('むクラ');
    expect(tarao.age).toBe(1);
  } else {
    throw new Error('test failed');
  }
});

今回のロヌカルセカンダリむンデックスの堎合、定矩がこうなのでふ぀うにやるずlastNameがundefinedになりたす。

        [
            {
                "IndexName": "PeopleLocalCompositeIndex",
                "KeySchema": [{"AttributeName":"familyId","KeyType":"HASH"},
                                {"AttributeName":"age","KeyType":"RANGE"}],
                "Projection":{
                    "ProjectionType":"INCLUDE",
                    "NonKeyAttributes":["firstName"]
                }
            }
        ]

ですが、ロヌカルセカンダリむンデックスの堎合はProjectionExpressionに属性を指定するこずで、コピヌしおいない属性も参照するこずが
できたす。これを「フェッチ」ず呌ぶそうです。

  const params: DocumentClient.QueryInput = {
    TableName: 'People',
    IndexName: 'PeopleLocalCompositeIndex',
    KeyConditionExpression: 'familyId = :familyId and age < :age',
    ExpressionAttributeValues: {
      ':familyId': 2,
      ':age': 10,
    },
    ProjectionExpression: 'familyId,lastName,firstName,age',
  };

ProjectionExpressionを指定しない堎合は、今回のテヌブル、ロヌカルセカンダリむンデックスの定矩ではlastNameはundefinedになりたす。

ProjectionExpressionを䜿うこずで、コピヌ察象に含たれおいない属性もフェッチするこずができたすが、圓然性胜ずトレヌドオフになるので
頻繁に取埗する項目であればコピヌ察象に加えた方が良いでしょう。

ずころで、グロヌバルセカンダリむンデックスに察しおコピヌ察象に指定しおいない属性をProjectionExpressionに含めるず、゚ラヌに
なりたす。

test("query for people's global secondary index (simple key), invalid projection", async () => {
  const params: DocumentClient.QueryInput = {
    TableName: 'People',
    IndexName: 'PeopleGlobalSimpleIndex', // むンデックス名を指定
    KeyConditionExpression: 'firstName = :firstName',
    ExpressionAttributeValues: {
      ':firstName': 'ノリスケ',
    },
    ScanIndexForward: false,
    ProjectionExpression: 'familyId,lastName,firstName,age',
  };

  try {
    await dynamodb.query(params).promise();
  } catch (e) {
    const error = e as Error;
    expect(error.name).toBe('ValidationException');
    expect(error.message).toBe(
      'One or more parameter values were invalid: Global secondary index PeopleGlobalSimpleIndex does not project [age]'
    );
  }
});
スキャンでセカンダリむンデックスを䜿う

最埌はスキャンです。

グロヌバルセカンダリむンデックスから。

test("scan for people's global secondary index (composite keys)", async () => {
  const params: DocumentClient.ScanInput = {
    TableName: 'People',
    IndexName: 'PeopleGlobalCompositeIndex',
    FilterExpression: 'lastName = :lastName and age >= :age',
    ExpressionAttributeValues: {
      ':lastName': '磯野',
      ':age': 50,
    },
  };

  const result = await dynamodb.scan(params).promise();

  if (result.Items) {
    expect(result.Count).toBe(2);

    const fune = result.Items[0] as Person;
    expect(fune.familyId).toBe(1);
    expect(fune.lastName).toBe('磯野');
    expect(fune.firstName).toBe('フネ');
    expect(fune.age).toBe(50);

    const namihei = result.Items[1] as Person;
    expect(namihei.familyId).toBe(1);
    expect(namihei.lastName).toBe('磯野');
    expect(namihei.firstName).toBe('波平');
    expect(namihei.age).toBe(54);
  } else {
    throw new Error('test failed');
  }
});

ポむントは、ク゚リヌの時ず同様にIndexNameでむンデックス名を指定するずころですね。

  const params: DocumentClient.ScanInput = {
    TableName: 'People',
    IndexName: 'PeopleGlobalCompositeIndex',
    FilterExpression: 'lastName = :lastName and age >= :age',
    ExpressionAttributeValues: {
      ':lastName': '磯野',
      ':age': 50,
    },
  };

グロヌバルセカンダリむンデックスの堎合はConsistentReadをtrueに指定できないこず、ProjectionExpressionにコピヌ察象に
指定しおいない属性を指定できないのもク゚リヌず同じです。

test("scan for people's global secondary index (simple key)", async () => {
  const params: DocumentClient.ScanInput = {
    TableName: 'People',
    IndexName: 'PeopleGlobalSimpleIndex',
    FilterExpression: 'lastName = :lastName and age >= :age',
    ExpressionAttributeValues: {
      ':lastName': '磯野',
      ':age': 50,
    },
  };

  const result = await dynamodb.scan(params).promise();

  if (result.Items) {
    expect(result.Count).toBe(2);

    const fune = result.Items[0] as Person;
    expect(fune.familyId).toBe(1);
    expect(fune.lastName).toBe('磯野');
    expect(fune.firstName).toBe('フネ');
    expect(fune.age).toBeUndefined();

    const namihei = result.Items[1] as Person;
    expect(namihei.familyId).toBe(1);
    expect(namihei.lastName).toBe('磯野');
    expect(namihei.firstName).toBe('波平');
    expect(namihei.age).toBeUndefined();
  } else {
    throw new Error('test failed');
  }
});

ロヌカルセカンダリむンデックスの堎合は、ProjectionExpressionを利甚するこずでコピヌ察象に指定しない属性の倀をフェッチできるのも
ク゚リヌず同じですね。

test("scan for people's local secondary index (composite keys)", async () => {
  const params: DocumentClient.ScanInput = {
    TableName: 'People',
    IndexName: 'PeopleLocalCompositeIndex',
    FilterExpression: 'lastName = :lastName and age >= :age',
    ExpressionAttributeValues: {
      ':lastName': '磯野',
      ':age': 50,
    },
    ProjectionExpression: 'familyId,lastName,firstName,age',
    ConsistentRead: true,
  };

  const result = await dynamodb.scan(params).promise();

  if (result.Items) {
    expect(result.Count).toBe(2);

    const fune = result.Items[0] as Person;
    expect(fune.familyId).toBe(1);
    expect(fune.lastName).toBe('磯野');
    expect(fune.firstName).toBe('フネ');
    expect(fune.age).toBe(50);

    const namihei = result.Items[1] as Person;
    expect(namihei.familyId).toBe(1);
    expect(namihei.lastName).toBe('磯野');
    expect(namihei.firstName).toBe('波平');
    expect(namihei.age).toBe(54);
  } else {
    throw new Error('test failed');
  }
});

ク゚リヌを芋た埌だず、あたり特城的なこずがありたせんでした 。

ずりあえず、参照のみですがセカンダリむンデックスの雰囲気は良しずしたしょう。

たずめ

今回は、2皮類のセカンダリむンデックスを定矩しお、ク゚リヌやスキャンで利甚するずころをやっおみたした。

ざっくりずセカンダリむンデックスの性質はわかったので、今回は目的を果たしたかな、ず思いたす。

ちゃんず䜿おうず思うずただただたくさん知らなくおはいけないこずがあるず思うのですが、今回はこれで区切りで 。