コンセプトから学ぶAmazon DynamoDB【LSI篇】

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

よく訓練されたアップル信者、都元です。今回はローカル・セカンダリ・インデックス(LSI)について深堀りしてみましょう。

ここまでの復習

その前に、色々復習しておきましょう。

DynamoDBのインデックス3種類

まず、DynamoDBが持てるインデックス3種類をあらためてまとめておきます。

  • 暗黙的な、キー(ハッシュキーのみ、または、ハッシュキー+レンジキー)による検索のためのインデックス
  • 明示的に定義した、ローカル・セカンダリ・インデックス(LSI)
  • 明示的に定義した、グローバル・セカンダリ・インデックス(GSI)

このうち、暗黙的なインデックスは1テーブルにつき1つだけ、LSI及びGSIは1テーブルにつき各5個まで定義できます。つまり、1つのテーブルには最小で1個、最大で11個のインデックスがあります。検索を行う場合は、まず「どのインデックスを使って検索をするか」を指定し、その上で検索条件を指定します。具体的には後ほど。

ちなみに上記「暗黙的なインデックス」のことを、シンプルに「テーブル」と呼ぶこともある、と覚えておいてください。具体的には「テーブルに対するクエリ」と言えば、この暗黙的インデックスを使った検索です。一方、各LSI及びGSIには固有の名前が付いており、「(その名前)に対するクエリ」という表現で、GSIまたはLSIを使ったクエリであることを表します。

複合キーテーブルとパーティション

復習になりますが、複合キーテーブルでは、キーとして「ハッシュキー」「レンジキー」の2つを指定します。DynamoDBではパーティションと呼ばれる複数のノードにデータを分散配置することによって、性能を確保しています。このパーティション分けの拠り所となるのがハッシュキーの値です。

つまり、同じハッシュキーの値を持つアイテムは、同じパーティションに保存されます。そして、これらのアイテムは、レンジキーによってソートされた状態で保存されています、多分。(実装は非公開のため、予想です)

とある1つのパーティションの中で、レンジキーによるインデックスがあると考えても良いと思います。詳しくはこの辺りを御覧ください。

ローカル・セカンダリ・インデックス(LSI)とは

さて、今回はLSIにフォーカスします。前提として、LSIは複合キーテーブルにしか定義できません。つまり、ハッシュキーテーブルでは利用できないインデックスです。

以前コンセプトから学ぶAmazon DynamoDB【複合キーテーブル篇】において、Replayというサンプルのテーブルを示しました。これは、Idがハッシュキー、ReplyDateTimeがレンジキーとなる複合キーテーブルです。

// Reply
{
  "Id": "DynamoDB#DynamoDB Thread 1",
  "ReplyDateTime": "2011-12-11T00:40:57.165Z",
  "Message": "DynamoDB Thread 1 Reply 1 text",
  "PostedBy": "User A"
}
{
  "Id": "DynamoDB#DynamoDB Thread 1",
  "ReplyDateTime": "2011-12-18T00:40:57.165Z",
  "Message": "DynamoDB Thread 1 Reply 1 text",
  "PostedBy": "User A"
}
(略)

このテーブルはキーによる暗黙的なインデックスによって「DynamoDB#DynamoDB Thread 1というスレッドの、2011-12-25の返信を全部取り出したい」といったクエリを高速に実行できます。(下記サンプルは、本稿末尾のサンプルデータテーブルの作成及びアイテムのインポートを行った状態で実行できます。)

$ aws dynamodb query \
  --table-name Reply \
  --key-condition-expression "Id = :id AND begins_with(ReplyDateTime, :date)" \
  --expression-attribute-values '{":id":{"S":"DynamoDB#DynamoDB Thread 1"},":date":{"S":"2011-12-25"}}'

{
    "Count": 1,
    "Items": [
        {
            "Message": {
                "S": "DynamoDB Thread 1 Reply 3 text"
            },
            "PostedBy": {
                "S": "User B"
            },
            "ReplyDateTime": {
                "S": "2011-12-25T00:40:57.165Z"
            },
            "Id": {
                "S": "DynamoDB#DynamoDB Thread 1"
            }
        }
    ],
    "ScannedCount": 1,
    "ConsumedCapacity": null
}

しかしこれと同時に「DynamoDB#DynamoDB Thread 1というスレッドの、User Bによる返信を全部取り出したい」というアプリケーション要件も存在する場合を考えてみましょう。このようなときに活躍するのがLSIです。

このテーブルでは、暗黙的なインデックスとは別に、Idがハッシュキー、PostedByがレンジキーとなるLSI「PostedBy-index」を定義し構築してあります。このインデックスを使えば「DynamoDB#DynamoDB Thread 1というスレッドの、User Bによる返信を全部取り出したい」いったクエリも高速に実行できるようになります。

この「PostedBy-index」に対する(≠暗黙的なインデックスに対する)問い合わせ例は下記の通りです。(このサンプルは、本稿末尾のサンプルデータテーブルの作成及びアイテムのインポートを行った状態で実行できます。)

$ aws dynamodb query \
  --table-name Reply \
  --index-name PostedBy-index \
  --key-condition-expression "Id = :id AND PostedBy = :by" \
  --expression-attribute-values '{":id":{"S":"DynamoDB#DynamoDB Thread 1"},":by":{"S":"User B"}}'

{
    "Count": 1,
    "Items": [
        {
            "PostedBy": {
                "S": "User B"
            },
            "Id": {
                "S": "DynamoDB#DynamoDB Thread 1"
            },
            "ReplyDateTime": {
                "S": "2011-12-25T00:40:57.165Z"
            }
        }
    ],
    "ScannedCount": 1,
    "ConsumedCapacity": null
}

何が「ローカル」なのか?

前述の通り、LSIは複合キーテーブルにしか定義できません。そしてLSIは、そのテーブルのハッシュキーと同じハッシュキーでしか定義できません。つまり、PostedByをハッシュキー、IdをレンジキーとなるLSIを定義はできません。ハッシュキーは必ずIdである必要があります。

これは何を意味するのかというと。同じパーティション内の複数のアイテムは、基本的に暗黙的なインデックスに従って整理されています。これに加えて、別の規則(ここではPostedBy)で整理インデックスを構築する、これがLSIです。

つまり「ローカル」というのは「同じパーティション内」ということを表した言葉です。

インデックスへの射影(プロジェクション)

上記、テーブル(暗黙インデックス)に対する問い合わせではMessageattributeが含まれていたのに、PostedBy-indexに対する問い合わせではMessageが含まれていません。なぜでしょうか。

テーブルには「全てのattribute」が含まれています。これは当たり前で、そして直感的だと思います。

一方、インデックスには「そのインデックスのキーとなるattribute (index key attributes)」及び「テーブルのキーattribute(primary key attributes)」 が最低限全て含まれますが、それ以外のattributeについては含まない可能性があります。言い換えると、インデックスにはテーブルのattributeの一部しか射影されていません。

本稿で挙げた例は、最低限のattributeのみが射影されたインデックスです。つまり、PostedBy-indexにはId, PostedBy, ReplayDateTimeという3つのattributeしか含まれていません。仮にMessageが欲しければ、PostedBy-indexに対する問い合わせをした後、得られたテーブルのキー(Id及びReplayDateTime)を使って、テーブルに対する問い合わせを行います。

インデックスに対する問い合わせの結果、一発でMessageが得られれば楽なのに、なぜこのような設定ができるのか。これにはインデックス容量の削減という効果があります。

実はLSIには「同じハッシュキーを持つitem」の合計サイズに、10GBという制限があります。便利だからといって、全てのattributeをインデックスに射影していると、この上限にぶつかる可能性が出てきます。10GBの壁への到達を防止するために、必要最低限のattributeだけを射影するように、LSIを設計する必要があるのです。

付録:サンプルデータの投入

本稿で利用するサンプルデータは、AWS CLIのコマンドでは下記のように投入してください。

参考:Amazon DynamoDB Developer Guide - サンプルテーブルとデータ

aws dynamodb create-table \
    --table-name Forum \
    --attribute-definitions AttributeName=Name,AttributeType=S \
    --key-schema AttributeName=Name,KeyType=HASH \
    --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1

aws dynamodb put-item \
    --table-name Forum \
    --item '{"Name":{"S":"DynamoDB"},"Category":{"S":"Amazon Web Services"},"Threads":{"N":"3"},"Messages":{"N":"4"},"Views":{"N":"1000"},"LastPostBy":{"S":"User A"},"LastPostDateTime":{"S":"2012-01-03T00:40:57.165Z"}}'

aws dynamodb put-item \
    --table-name Forum \
    --item '{"Name":{"S":"Amazon S3"},"Category":{"S":"AWS"},"Threads":{"N":"1"}}'
aws dynamodb create-table \
    --table-name Thread \
    --attribute-definitions AttributeName=ForumName,AttributeType=S AttributeName=Subject,AttributeType=S \
    --key-schema AttributeName=ForumName,KeyType=HASH AttributeName=Subject,KeyType=RANGE \
    --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1

aws dynamodb put-item \
    --table-name Thread \
    --item '{"ForumName":{"S":"DynamoDB"},"Subject":{"S":"DynamoDB Thread 1"},"Message":{"S":"DynamoDB thread 1 message text"},"LastPostedBy":{"S":"User A"},"Views":{"N":"0"},"Replies":{"N":"0"},"Answered":{"N":"0"},"Tags":{"SS":["index","primarykey","table"]},"LastPostDateTime":{"S":"2012-01-03T00:40:57.165Z"}}'

aws dynamodb put-item \
    --table-name Thread \
    --item '{"ForumName":{"S":"DynamoDB"},"Subject":{"S":"DynamoDB Thread 2"},"Message":{"S":"DynamoDB thread 2 message text"},"LastPostedBy":{"S":"User A"},"Views":{"N":"0"},"Replies":{"N":"0"},"Answered":{"N":"0"},"Tags":{"SS":["index","primarykey","rangekey"]},"LastPostDateTime":{"S":"2012-01-03T00:40:57.165Z"}}'

aws dynamodb put-item \
    --table-name Thread \
    --item '{"ForumName":{"S":"Amazon S3"},"Subject":{"S":"Amazon S3 Thread 1"},"Message":{"S":"Amazon S3 Thread 1 message text"},"LastPostedBy":{"S":"User A"},"Views":{"N":"0"},"Replies":{"N":"0"},"Answered":{"N":"0"},"Tags":{"SS":["largeobject","multipart upload"]},"LastPostDateTime":{"S":"2012-01-03T00:40:57.165Z"}}'
aws dynamodb create-table \
    --table-name Reply \
    --attribute-definitions AttributeName=Id,AttributeType=S AttributeName=ReplyDateTime,AttributeType=S AttributeName=PostedBy,AttributeType=S \
    --key-schema AttributeName=Id,KeyType=HASH AttributeName=ReplyDateTime,KeyType=RANGE \
    --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 \
    --local-secondary-indexes "IndexName=PostedBy-index,KeySchema=[{AttributeName=Id,KeyType=HASH},{AttributeName=PostedBy,KeyType=RANGE}],Projection={ProjectionType=KEYS_ONLY}"

aws dynamodb put-item \
    --table-name Reply3 \
    --item '{"Id":{"S":"DynamoDB#DynamoDB Thread 1"},"ReplyDateTime":{"S":"2011-12-11T00:40:57.165Z"},"Message":{"S":"DynamoDB Thread 1 Reply 1 text"},"PostedBy":{"S":"User A"}}'

aws dynamodb put-item \
    --table-name Reply \
    --item '{"Id":{"S":"DynamoDB#DynamoDB Thread 1"},"ReplyDateTime":{"S":"2011-12-18T00:40:57.165Z"},"Message":{"S":"DynamoDB Thread 1 Reply 1 text"},"PostedBy":{"S":"User A"}}'

aws dynamodb put-item \
    --table-name Reply \
    --item '{"Id":{"S":"DynamoDB#DynamoDB Thread 1"},"ReplyDateTime":{"S":"2011-12-25T00:40:57.165Z"},"Message":{"S":"DynamoDB Thread 1 Reply 3 text"},"PostedBy":{"S":"User B"}}'

aws dynamodb put-item \
    --table-name Reply \
    --item '{"Id":{"S":"DynamoDB#DynamoDB Thread 2"},"ReplyDateTime":{"S":"2011-12-25T00:40:57.165Z"},"Message":{"S":"DynamoDB Thread 2 Reply 1 text"},"PostedBy":{"S":"User A"}}'

aws dynamodb put-item \
    --table-name Reply \
    --item '{"Id":{"S":"DynamoDB#DynamoDB Thread 2"},"ReplyDateTime":{"S":"2012-01-03T00:40:57.165Z"},"Message":{"S":"DynamoDB Thread 2 Reply 2"},"PostedBy":{"S":"User A"}}'

また、今回は使いませんが、ProductCatalogのサンプルも置いておきます。

aws dynamodb create-table \
    --table-name ProductCatalog \
    --attribute-definitions AttributeName=Id,AttributeType=N \
    --key-schema AttributeName=Id,KeyType=HASH \
    --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1

aws dynamodb put-item \
        --table-name ProductCatalog \
        --item '{"Id":{"N":"101"},"Title":{"S":"Book101Title"},"ISBN":{"S":"111-1111111111"},"Authors":{"SS":["Author1"]},"Price":{"N":"2"},"Dimensions":{"S":"8.5x11.0x0.5"},"PageCount":{"N":"500"},"InPublication":{"B":"true"},"ProductCategory":{"S":"Book"}}'

aws dynamodb put-item \
        --table-name ProductCatalog \
        --item '{"Id":{"N":"102"},"Title":{"S":"Book102Title"},"ISBN":{"S":"222-2222222222"},"Authors":{"SS":["Author1","Author2"]},"Price":{"N":"20"},"Dimensions":{"S":"8.5x11.0x0.8"},"PageCount":{"N":"600"},"InPublication":{"B":"true"},"ProductCategory":{"S":"Book"}}'

aws dynamodb put-item \
        --table-name ProductCatalog \
        --item '{"Id":{"N":"103"},"Title":{"S":"Book103Title"},"ISBN":{"S":"333-3333333333"},"Authors":{"SS":["Author1","Author2"]},"Price":{"N":"2000"},"Dimensions":{"S":"8.5x11.0x1.5"},"PageCount":{"N":"600"},"InPublication":{"B":"false"},"ProductCategory":{"S":"Book"}}'

aws dynamodb put-item \
        --table-name ProductCatalog \
        --item '{"Id":{"N":"201"},"Title":{"S":"18-Bike-201"},"Description":{"S":"201Description"},"BicycleType":{"S":"Road"},"Brand":{"S":"MountainA"},"Price":{"N":"100"},"Gender":{"S":"M"},"Color":{"SS":["Red","Black"]},"ProductCategory":{"S":"Bicycle"}}'

aws dynamodb put-item \
        --table-name ProductCatalog \
        --item '{"Id":{"N":"202"},"Title":{"S":"21-Bike-202"},"Description":{"S":"202Description"},"BicycleType":{"S":"Road"},"Brand":{"S":"Brand-CompanyA"},"Price":{"N":"200"},"Gender":{"S":"M"},"Color":{"SS":["Green","Black"]},"ProductCategory":{"S":"Bicycle"}}'

aws dynamodb put-item \
        --table-name ProductCatalog \
        --item '{"Id":{"N":"203"},"Title":{"S":"19-Bike-203"},"Description":{"S":"203Description"},"BicycleType":{"S":"Road"},"Brand":{"S":"Brand-CompanyB"},"Price":{"N":"300"},"Gender":{"S":"W"},"Color":{"SS":["Red","Green","Black"]},"ProductCategory":{"S":"Bicycle"}}'

aws dynamodb put-item \
        --table-name ProductCatalog \
        --item '{"Id":{"N":"204"},"Title":{"S":"18-Bike-204"},"Description":{"S":"204Description"},"BicycleType":{"S":"Mountain"},"Brand":{"S":"Brand-CompanyB"},"Price":{"N":"400"},"Gender":{"S":"W"},"Color":{"SS":["Red"]},"ProductCategory":{"S":"Bicycle"}}'

aws dynamodb put-item \
        --table-name ProductCatalog \
        --item '{"Id":{"N":"205"},"Title":{"S":"20-Bike-205"},"Description":{"S":"205Description"},"BicycleType":{"S":"Hybrid"},"Brand":{"S":"Brand-CompanyC"},"Price":{"N":"500"},"Gender":{"S":"B"},"Color":{"SS":["Red","Black"]},"ProductCategory":{"S":"Bicycle"}}'

色々試した後は、下記のコマンドでテーブルを削除することにより、不要な課金を抑えることができます。

aws dynamodb delete-table --table-name Forum
aws dynamodb delete-table --table-name Thread
aws dynamodb delete-table --table-name Reply
aws dynamodb delete-table --table-name ProductCatalog