AWS | DynamoDB | クエリを使ったデータ取得の備忘録

AWS

こんにちは、サーバーサイドエンジニアとしてお仕事をさせて頂いて、毎日充実した日々を送っている小幡です。わからないことだらけすぎて、辛い事も多いですが、1つ1つが必要な事で、やりがいを感じています。

そんななか、最近は個人で開発しているAndroidアプリで、DynamoDBからデータを取得することもあり、様々な場面で必要となるDynamoDBからのデータ取得について、知見がたまってきたかなぁと感じたので、備忘録として残しておこうと思います。

全然話は違うのですが、1年前に作ったTwitterAPIを使用したトレンド保存サイトを、調整しようと試みたところ、すっかり忘れており、READMEだったり、備忘録を残しておけば、もう少し早く復旧できたのかなぁという後悔もあり、備忘録作成の意欲が高まった次第です。

MySQLとDynamoDBについて

さて、まずは前提として・・・

MySWLを使っていたときは、PHP(Laravel)を使用しており、DynamoDBを使っている今は、Pythonを使用していることで、何から記して行けば良いものか・・・。

そもそもMySQLはRDBMS(リレーショナルデータベースマネジメントシステム)と呼ばれているのもの1つであり、データの格納方法のイメージはExcelの様な列と行が丁寧に区切られているという感じです。

未経験エンジニアとしても、とても理解し易く、イメージもExcelなので、なんとなく理解した気にすぐなれます。ローカル開発環境でもXAMPだったり、手軽に遊び感覚で始める事ができるので習得もしやすいと思います。

しかし、簡単に表面は知ることができますが、奥を知ると先が見えなくなります。設計や運用は非常に難しく、あまり下手なことは言えないなぁという印象です。

筑波大学のYouTubeの講義動画でもRDBMSについてのものがあり、学習機会がたくさんあります。文系出身の私からすると、なかなか難しい内容で、研究者の名前や論文などが紹介されており、さすが大学の講義だなぁと思いますので、興味がある方は是非一度ご覧ください。

一方、DynamoDBはAWS(アマゾンウェブサービス)の1つのサービスとして利用可能なデータベースです。NoSQLという概念の一部として紹介されるようなデータベースで、RDBMSで使用するSQLを使用せずに、扱うことができるデータベースというイメージです。

そのなかでも、DynamoDBはキー・バリュー型のデータベースと呼ばれており、任意のキー(値)に紐づくバリュー(データ)が格納されているデータベースのイメージでExcelの様な列と行のイメージではありません。このため、未経験エンジニアの私としても、わからないことがたくさん出てきてしまったということです。

キー・バリュー型のイメージ

そんな入門が難しそうなイメージですが、最初の一歩は極めて単純で、理解し易いものになっています。

以下のイメージを見てみましょう。

{'東京': 100, '名古屋': 200, '大阪': 300}

DynamoDBから取得したデータは、なんやかんやあって最終的には、以上のように取得できたりします。こうなると、「東京は100、名古屋は200、大阪は300なんだなぁ」というイメージが簡単に想像できるかと思います。

つまり、キーが東京、バリューが100、となるわけです。簡単ですね。

メリットとデメリット

DynamoDBの最大のメリットは、MySQLに比べると、とても高速であるということ。そしてデータベース設計に少しミスがあったとしても、修正が簡単ということだと思います。

高速であるというのは、正確に計測したわけではないので、体感レベルなのですが、とある知り合いが作成したというSNSのアプリを触らせてもらった時に、その返答の速さに感動したことを覚えています。RDBMSの遅さと言えば、よくあるWebサイトで使われている遅さはデータベースとの通信で時間を使ってしまっていると思うので、「リンクボタン(リクエスト)、ポチ、ちょっと白い画面が1秒くらい、からの画面表示(レスポンス)」という体感時間なので約1秒くらいです。こちらWordPress使用のサイトも、そのくらいの速度だと思います。レンタルサーバのレスポンスは普通レベルなので。

余談ですが、先日お世話になったクライアントでは、大手レンタルサーバを使用していましたが、そちらはサーバのレスポンス自体が意図的に遅くなる使用だったので、体感で2秒くらいの遅延がありました。WordPress更新する作業だけでも、遅すぎて辛かったです。いくら5Gの時代でネットの通信速度が速くなったとしても、まだまだ見えない壁があるものです。

LINEの公式ユーザーと自動で返信されるトークなども見ていると、一瞬で返信してくれる優秀な公式ユーザもあれば、1秒くらいの遅延があったりするのは、このような様々な要因があるものかと考えられます。もちろん様々なレスポンスが考えられ、内部で何か複雑な処理をしていたり外部のAPIを使用している場合はデータベースやネットワークとは関係のないことかもしれませんが。

またメリット2つ目の、設計ミスが多少あったとして修正し易いというのは、GSI(グローバルセカンダリーインデックス)の話が主になります。RDBでは主キーという一意のIDを使用しますが、DynamoDBの場合は主キー(プライマリーキー)がパーティションキーとソートキーの二つを設定することが可能です。もちろんRDBMSと同様に1つの主キーを設定することが可能なので、RDBの様に使いたいけど高速なDynamoDBを使うという方法も無いわけではないらしいです。

特に個人開発で、それほどデータを厳密に管理する必要がなく、ユーザエクスペリエンス向上のためにDynamoDBを選択するというのはありなのではないかなぁと考えていますが、もちろん実務ではもっと慎重な検討が必要です。

厄介な、PKとSK

そして便利であるパーティションキー(PK)とソートキー(SK)ですが、未経験エンジニアの鬼門となっているのではないでしょうか?

そもそも、なぜ主キーだけでなくソートキーが必要なのか?と聞かれると、DynamoDBは従来のRDBMSに比べて検索機能が曖昧なものになっているからだと考えて居ます。とある記事で読んだのですが(引用元は忘れてしまいましたが)、皆さんもよく使われているであろうGmailもNoSQLが使用されているらしく、その結果、ユーザが保有している受信メールの数を正確に集計することを止めたらしいというのです。RDBでは容易で厳密である集計機能は、簡単に説明すると、データの数は正確に素早く集計することができます。

しかし、この素晴らしい機能がNoSQLではあまり機能していないようです。つまり単一のデータを返却するのは早いですが、複数のデータを正確に集計するのが苦手です。一度Gmailの受信フォルダ右上を見てもらえると、わかりやすいかとおもいます。

話はそれましたが、DynamoDBの場合、SKでソートしてデータを取得したりする訳です。

さらに同じテーブル内で、PKとSKを別のデータに設定することもできます。これがセカンダリーインデックスと呼ばれており、テーブル作成時にはLSI(ローカルセカンダリーインデックス)が作成可能です。テーブル作成後でも設定可能なのがGSIというイメージです。他にも設定できる詳細が違いますので、使用する際は、マニュアルをご覧ください。

Boto3の実際のコード

ここからが本番ですが、Pythonを使用する場合Boto3を使用し、データの取得などを行います。

注意したいのは、各項目の内容です。ここではput_item と query について説明します。

まずは、put_item を使用してみます。以下の例では、まずdynamodbにリソースの設定(実際に使っている環境を適宜設定する)、tableにはテーブルの指定先(使用するテーブルを設定)を格納しているイメージです。

その後put_itemを使用しますが、引数には Item={} という指定をします。この時、先ほど説明したPKやSKが設定されている場合は、最低限それらを設定しないとエラーが返却されます。他に保存したいデータがある場合は、入れたい文だけ、辞書型の中身を拡張していくだけで、追加可能だと思います。

import boto3

def put_data(name, year):
    dynamodb = boto3.resource('dynamodb', endpoint_url="http://localhost:8000")

    table = dynamodb.Table('User')
    response = table.put_item(
       Item={
            'name': name,
            'year': year,
        }
    )
    return response

ちなみにget_item もほとんど同じです。注意したいのはPKとSKが設定されている場合とPKのみの違いと、ItemではなくKeyに引数を設定することだと思います。

table.get_item(
   Key={
        'name': name,
        'year': year,
    }
)

そしていよいよ、私が最も難しいと考えて居るクエリを使ったデータの取得です。

最初は非常に簡単で、「指定した名前と同じキーを持つデータを全て取得する」、をコードにすると以下になります。

table.query(
    KeyConditionExpression=Key('name').eq(name)
)

Key()の中に指定したものが、eq()の中に指定したものが一緒の場合データが取得できます。

最初、まったく知らなかったのですが、Shellなどで、条件分岐(条件比較、if文)する場合は”==”や”<”は使わず、”eq”や”lt”を使います。eqはイコール、ltはless thanなのでより小さい(未満)という意味になります。

ただし、最初のPK指定の場合はeq固定の様ですので、なかなか複雑なデータベース設計をする場合は注意が必要です。

ちなみにテーブル内全てのデータ取得も簡単で、以下のようになります。

table.scan()

ここからが大変なのですが、まずは以下をご覧ください。


table.query(
    IndexName='SecondIndexName',
    KeyConditionExpression=Key('name').eq(name)
    FilterExpression=Attr('year').contains('2021'),
    ScanIndexForward=False,
)

これはキーがnameで引数のnameと等しいもののデータを取り出し、キーがyearでバリューが’2021’を含むもの(contains)を取得するというイメージです。その他、様々なデータの取得方法があります。

IndexName -> セカンダリーインデっクスの名前を指定することができる
KeyConditionExpression -> PKを指定する。PKとSKを指定することもできる。
FilterExpression -> 取得するデータのフィルターを指定することができる。
ScanIndexForward -> データの取得順を昇順、降順を指定することができる。

その他詳細なマニュアルはこちらをご覧ください

可能性は無限大という感じですね。私の場合は以上の4つ(ScanIndexForwardを使用せず3つ)の項目を使用するだけで多彩なデータを取得できるので、まずはこちらをしっかりと使いこなせるようにしていきたいと考えています。最も重要なKeyConditionExpressionは、これだけでデータ取得の速度や、量が決まり、全体の中でも必須項目となっているため、しっかりとマスターしておきたいところです。

まだまだ eq, gte, lt など比較条件をローマ字で表すのには慣れませんが、Shellと合わせて身に着けたいところですね。他にもFilterExpressionでは、様々なフィルターが用意されているので、データ取得後の処理で、データを整えるのも重要ですが、ここのフィルターで整理できるとよりスマートかもしれません。

本来はデータベース設計から慎重に行い、必要最低限の処理で正しいデータを取得できることが良いかもしれませんが、これらの方法も正しく理解できると尚良いなぁと思いました。

まとめ

DynamoDBを使用する際はPKとSKが肝心で、データ取得方法がたくさんあります。

高速ではあるものの、集計などが苦手なので、慎重な検討が必要だと考えます。

クエリでは、目的や設定する項目の意味を正しく理解することが重要だなぁと改めて認識しました。

さいごに

最近ではAppSync、GraphQLを念頭に置いたAWS Amplifyなるサービスを使用して個人Androidアプリ開発してみたりと、技術の発展が早く感じていたりします。

しかし基礎であるSQLやRDBMSは、まだまだ現役だと思います。まだまだこちらも奥が深い世界なので、引き続き学習していけたら良いなぁと考えて居ます。

あぁ、Twitterトレンドランキングを更新しようと作業中なのですが、ローカル開発環境が消失してしまいMySQLを再インストールしていたりしますが、また純粋なSQLでテーブル作ったりしていて、しっかり備忘録を残しておけば良かったなぁと思っています。どうやってDB構築したんですかねぇ昔の自分・・・(笑

この記事を書いた人

小幡 知弘

1990年茨城県神栖市生まれ
2013年大阪芸術大学卒業
Python×Webエンジニア