📖

Dynamoid で gsi が有効化された状態で where できているか確かめる

2024/02/19に公開

Ruby on Rails で DynamoDB を ActiveRecord のように扱える Dynamoid を使用しています。
今回は、Global Secondary Index を使用した Query が発行されているかどうかを確かめる方法を備忘録として残します。

前提

DynamoDB では、Query オペレーションと Scan オペレーションの2種類の API を使用することができます。

Query は hash key(primary key)range key(sort key) の組み合わせで検索を行うことができます。
一方で、Scan は一度テーブルの中身をスキャンするため速度・コストの面で効率的ではなく、ほとんどすべての場合で、Query で検索が可能になるように設計する必要があります。

既存の hash key(primary key)range key(sort key) の組み合わせでは、対応できない場合は global secondary index(gsi)local secondary index(lsi) といったインデックスを設定することでそのインデックスを使って Query を実行することができるようになります。

Dynamoid では、こうしたオペレーションを直接実行する低レベル API を直接実行する仕組みもあるにはあるのですが、Dynamoid の利点を活かすために、where などの ActiveRecord like なメソッドを活用したいところです。

GSI を設定する

今回はこんな感じのテーブルを使用しています。このテーブルをもとに話をすすめます。

class User
  include Dynamoid::Document

  table name: :user, key: :pk, read_capacity: 5, write_capacity: 5
  range :sk, :string

  field :name, :string
  field :display_id, :string
  field :joined_at, :datetime
end

Dynamoid では、検索を行う際に where メソッドを使用することができます。

users = User.where(pk: "user", sk: "user-id-0001")

また、hash key(primary key)range key(sort key) 以外の組み合わせでも、検索を行うことができます。

users = User.where(name: "Kinoshita")

where メソッドでは内部で自動的に効率的なクエリを組み立ててくれます。
今回の場合、前者は Query, 後者は Scan → filter のようなステップで、検索を実行しています。

さて、前者のクエリではユーザーの情報を一発取得することができますが、一方で、例えば、要件として加入日(joined_at)でユーザーを抽出するためには gsi を活用することが必要になりそうです[1]

この場合、hash key(primary key) は pk のままで、range key(sort key) を joined_at にすれば、Query オペレーションで加入日の抽出が可能になりそうです。

Dynamoid では下記のように設定すれば gsi を有効化することができます。

class User
  # (中略)
  field :joined_at, :datetime

  global_secondary_index hash_key: :pk,
                         range_key: :joined_at,
                         projected_attributes: :all
end
users = User.where(pk: "user", "joined_at.between": [Time.at(0), Time.now])

先述の場合と同様に、where 内部で自動的に効率的なクエリを組み立ててくれます。Scan → filter のようなステップではなく、gsi を使用することを自動で選択して、Query オペレーションを実行してくれます。
ただし、注意点として projected_attributesall にする必要があります。
この属性は、gsi に、すべての項目を射影する(:all)か、キーのみを射影するか(:keys_only)を設定する属性です。

gsi を活用して検索できているか確認する

rails console で簡単に確認します。

rails c

まずは、キー同士の組み合わせで where してみます。

Users.where(pk: "user", "sk.begins_with": "user-id").to_a

こんな感じで、どのようなクエリを発行したかが表示されます。

query(
    consistent_read:false,
    scan_index_forward:true,table_name:"users",
    key_conditions:{
        "pk"=>{
            comparison_operator:"EQ",
            attribute_value_list:[{s:"user"}]
        },
        "sk"=>{
            comparison_operator:"BEGINS_WITH",
            attribute_value_list:[{s:"user_id"}]
        }
    },
    query_filter:{
        "type"=>{
            comparison_operator:"IN",
            attribute_value_list:[{s:"User"}]
        }
    },
    attributes_to_get:nil
)

Scan する例も確認します。

User.where("name": "Kinoshita").to_a

こんな感じで 「Scan しないと検索できないけど Scan は遅いよ!」的な Warning が表示された後に検索までのステップと結果が表示されます。

Queries without an index are forced to use scan and are generally much slower than indexed queries!
You can index this query by adding index declaration to user.rb:
* global_secondary_index hash_key: 'some-name', range_key: 'some-another-name'
* local_secondary_index range_key: 'some-name'
Not indexed attributes: :name

(0.02 ms) SCAN - ["user", {:name=>{:eq=>"kinoshita"}, :type=>{:in=>["User"]}}]
[Aws::DynamoDB::Client 200 0.011326 0 retries] scan(table_name:"users",scan_filter:{"name"=>{comparison_operator:"EQ",attribute_value_list:[{s:"kinoshita"}]},"type"=>{comparison_operator:"IN",attribute_value_list:[{s:"User"}]}},attributes_to_get:nil)

同様に、先ほど設定した gsi をキーとするような検索を実行し、warning が表示されずに query オペレーションを実行していれば成功です。

users = User.where(pk: "user", "joined_at.between": [Time.at(0), Time.now]).to_a

参考

https://github.com/Dynamoid/dynamoid

脚注
  1. 今回の場合、pk を U_#{ユーザーID}, sk を joined_{加入日の unix time} みたいな感じの設計をすれば加入日の抽出もできるようになりますが、user 一覧の取得はできなくなります。今回はそういったユースケースであると仮定しているということにします。どちらにせよユースケースに合わせて設計が重要であると学びました・・・ ↩︎

Discussion