📖

re:Invent 2024: DynamoDBの高度なデータモデリング手法 - AWS Data Hero解説

2024/01/01に公開

はじめに

海外の様々な講演を日本語記事に書き起こすことで、隠れた良質な情報をもっと身近なものに。そんなコンセプトで進める本企画で今回取り上げるプレゼンテーションはこちら!

📖 AWS re:Invent 2024 - Advanced data modeling with Amazon DynamoDB (DAT404)

この動画では、Amazon DynamoDBのデータモデリングについて、AWS Data HeroのAlex DeBrieが詳しく解説しています。DynamoDBの3つの重要な特徴として、フルマネージド、一貫したパフォーマンス、Serverlessフレンドリーを挙げ、パーティショニングの仕組みやPrimary Keyの重要性について説明しています。また、Secondary indexesの活用方法やDynamoDB Streamsを使った二重書き込み問題の解決、外部システムとの連携についても具体例を交えて解説しています。特に、Array indexingのような複雑な要件に対して、DynamoDB Streamsを使った独自のインデックス構築方法や、Split/Sortを活用した効果的なデータモデリング手法など、実践的なテクニックが豊富に紹介されています。
https://www.youtube.com/watch?v=hjqrDqVaiw0
※ 動画から自動生成した記事になります。誤字脱字や誤った内容が記載される可能性がありますので、正確な情報は動画本編をご覧ください。
※ 画像をクリックすると、動画中の該当シーンに遷移します。

re:Invent 2024関連の書き起こし記事については、こちらのSpreadsheet に情報をまとめています。合わせてご確認ください!

本編

DynamoDBデータモデリングの導入:Alex DeBrieの経験から

Thumbnail 0

はい、それでは始めましょう。お集まりいただき、ありがとうございます。私はAlex DeBrieです。本日はAmazon DynamoDBを使用した高度なデータモデリングについてお話しさせていただきます。実は、DynamoDBのデータモデリングについて、AWS re:Inventで話をさせていただくのは今年で6年目になります。

Thumbnail 20

Thumbnail 40

今年は少し趣向を変えてみました。これまでのre:Inventでは、通常1つの大きな例を取り上げ、それを段階的に説明しながら、データモデリングの問題にどうアプローチするかを示すために、興味深い概念を紹介してきました。それを5年間続けてきましたので、YouTubeには多くの事例が公開されています。 また、今年は他にも素晴らしい講演があり、データモデリングの基本概念に関するプレゼンテーションやアーキテクチャに関する講演もありますので、ぜひそちらもご覧ください。

Thumbnail 50

Thumbnail 80

こちらが本日のアジェンダですが、これまでカバーしていなかったトピックを中心に構成しています。DynamoDB Streamsは非常に興味深く、多くのアプリケーションで役立つ機能なので、システムにどのように組み込めるかを見ていきます。また、サービスの設計やアーキテクチャを考える際のNapkin mathについて議論し、人々が複雑に考えすぎたり、間違いを犯しやすい領域についても取り上げます。 私はAWS Data HeroのAlex DeBrieです。AWSの社員ではありませんが、特にDynamoDBやAWS全般に関してAWSコミュニティで活発に活動しています。DynamoDBのデータモデリングに関する包括的なガイドである「The DynamoDB Book」の著者であり、DynamoDB、AWS、および関連技術に関する独立したコンサルタントとして働いています。

DynamoDBの基本概念:Mechanical Sympathyとその特徴

Thumbnail 110

Thumbnail 130

では、背景説明と、今年学んだ概念から始めましょう。 これは、Formula Oneドライバーのジャッキー・スチュワートが提唱した「Mechanical sympathy」という古い概念です。彼は、車から最高のパフォーマンスを引き出すためには、その仕組みを理解する必要がある - タイヤが路面とどのように相互作用するのか、アクセルを踏んだときにエンジンで何が起こるのか、といったことを説明しました。 この考え方はレースカーに限らず適用できます。AWS Well-Architected Frameworkでは、Mechanical sympathyを「ツールやシステムがどのように最適に動作するかを理解した上で使用すること」と定義しています。

Thumbnail 170

この概念はDynamoDBにとって非常に重要です。One-to-manyの関係を実装する方法や、フィルタリングの扱い方、トランザクションをいつ使用するかといった実践的な側面を知る必要があります。しかし、それと同時に、DynamoDBを効果的に使用するためには、DynamoDBの概念的なフレームワークとアーキテクチャを理解する必要があります。 ここで3つの重要な特徴を強調させてください。まず、DynamoDBはフルマネージドでAWS独自のものです。他のクラウド、オンプレミス、さらにはEC2インスタンス上でも実行できません - AWSからフルマネージドサービスとしてのみ利用可能です。

Thumbnail 190

Thumbnail 220

この高レベルアーキテクチャのスライドは、2年前にSenior Principal のAmrith Kumarが発表したものです。このスライドは、DynamoDBのリージョンインフラストラクチャの複雑さを示しており、ロードバランサー、リクエストルーター、メタデータシステム、自動管理コンポーネント、そして障害や復旧に対応する大規模なストレージノードの群れが協調して動作している様子を表しています。 これにより、シングルテナントのソリューションでは実現できない高可用性システムが実現されています。

Thumbnail 230

Thumbnail 280

アプリケーションの観点から見ると、DynamoDBのプロダクトNorth Starは「どんなスケールでも一貫したパフォーマンス」です。これは無制限の操作を意味するわけではありませんが、ユーザーの作成、最近の注文の取得、レコードの更新といった操作が、1メガバイトのデータであっても100テラバイトのデータであっても、同時クエリの量に関係なく、同様のパフォーマンスで実行できることを意味します。 そして最後に、DynamoDBはServerlessフレンドリーです。多くの大規模顧客は当初、一貫したパフォーマンスを理由にDynamoDBを選択しましたが、AWS Lambdaとそのハイパーエフェメラルなコンピューティングモデルの登場により、特にその接続モデルと運用特性に関して、DynamoDBのサービスフレンドリーな性質がさらに価値を増しました。これらがDynamoDBの3つの重要な特徴です。

DynamoDBを使用する場合、これらの特徴の少なくとも1つは魅力的に感じるはずです。データベースの選択肢は多く、昨日発表されたD SQLも素晴らしいものです。これらの特徴のいずれかが魅力的に感じる場合はDynamoDBを選択し、そうでない場合は他のものを検討すべきでしょう。

DynamoDBのアーキテクチャと操作:パーティショニングとAPIの重要性

Thumbnail 340

では、そのプロダクトNorth Starである「どんなスケールでも一貫したパフォーマンス」について、具体的なデータを使って詳しく見ていきましょう。ここに異なるユーザーが格納されているテーブルがあります。 これらのユーザーはすべて「アイテム」と呼ばれ、それぞれ異なる属性を持っています。このテーブルで最も重要な部分は、左側にあるプライマリーキー、つまりusernameです。このプライマリーキーはテーブル内の各レコードを一意に識別し、一貫したパフォーマンスを実現する上で非常に重要な役割を果たします。

Thumbnail 370

Thumbnail 380

Thumbnail 390

DynamoDBは、舞台裏で連携して動作する異なるサービスで構成されています。特別なノードにすべてのデータが格納されているわけではありません。DynamoDBでテーブルを作成すると、バックグラウンドで異なるストレージパーティションが作成されます。これらのパーティションは小さく、10ギガバイト以下です。この例では2つのパーティションがあると想像してみましょう。実際には最低4つあると思いますが、説明を簡単にするために2つにしています。 DynamoDBにリクエストが到着すると、例えば新しいユーザーを作成する場合、そのリクエストはDynamoDBのフロントエンドリクエストルーターに到達します。ルーターは、プライマリーキーやパーティションキーの値など、テーブルのメタデータを参照します。

Thumbnail 410

Thumbnail 420

システムは、Partition Keyとして渡されたUsernameの値を見て、それをハッシュ化し、そのアイテムがどのパーティションに属するかを判断します。この例では、新しいユーザーを作成していて、それがパーティション1に割り当てられるかもしれません。この仕組みの素晴らしい点は、水平方向への優れたスケーラビリティです - 3つ目のパーティション、さらに6つ、あるいは1000個のパーティションを追加することができます。テーブルがどれだけ大きくなっても、100テラバイトのテーブルであっても、最初のステップは常に一定時間の操作となります。100テラバイトのデータが約10ギガバイト程度に絞り込まれ、その範囲内で処理が行われるのです。これこそが、どのようなスケールでも一貫したパフォーマンスを実現できる理由なのです。

Thumbnail 440

Thumbnail 460

Partition Keyによる分割は極めて重要です。DynamoDBはPartition Keyによる分割を行うだけでなく、Primary Keyを通じたアクセスを強く推奨しています。データにアクセスする際は、Primary Key、特にPartition Keyを使用することが求められます。この例では、Partition Keyという1つの要素だけを持つシンプルなPrimary Keyを持つテーブルを示しています。これは本質的に、無限に成長可能なハッシュマップのようなものです。単一アイテムに対する操作を行うことができますが、Composite Primary Keyを持つテーブルを作ることもできます。

Composite Primary Keyの場合、Partition KeyとSort Keyという2つの要素があります。Partition Keyは依然として、レコードを異なるパーティションに割り当てる主要な役割を果たしています。これはGroup By操作のように考えることができます - 特定のPartition Keyに基づいてレコードをグループ化するのです。一緒にアクセスするレコードを同じグループにまとめたい場合に使用し、同じPartition Keyを持つレコードはSort Keyに従って順序付けられます。この例では、Sort KeyとしてULIDを使用しています。ULIDは先頭にタイムスタンプを付加するユニークな識別子で、アプリケーション内の注文レコードにタイムスタンプによる順序付けを作成します。

Thumbnail 520

Thumbnail 540

Thumbnail 550

パーティショニングと、DynamoDB APIがそのパーティショニングとどのように連携するかを理解することが、DynamoDBを理解する上で最も重要です。ここで重要なのは、アイテムが主にPartition Key に従ってパーティション全体に分散されるということです。DynamoDB APIは、その使い方について多くのことを示唆しています。DynamoDBでは、SQLのように解析やプランニングが必要なテキスト文字列を設定するのではなく、特定のメソッドを呼び出すことになります。私はDynamoDB APIを3つのカテゴリーに分けて考えています。最初のカテゴリーは単一アイテムのアクション、つまり基本的なCRUD(Create、Read、Update、Delete)操作です。

Thumbnail 560

Thumbnail 600

この場合、単一の特定アイテムに対して操作を行うため、完全なPrimary Keyを渡す必要があります。そのアイテムを持つパーティションに直接アクセスして操作を行いたいからです。すべての書き込み操作は単一アイテムのアクションとなります。Partition Keyだけを指定して、そのPartition Keyを持つすべてのレコードを更新するような「Update Where」や一括更新のような操作はできません。複数のレコード(ユーザーのすべての注文や、IoTデバイスの最新のセンサー読み取り値など)を読み取りたい場合は、Query操作を使用します。Composite Primary Keyを持つテーブルやインデックスがある場合、これらの複数レコード取得操作を行い、1つのリクエストで複数のレコードを取得できます。ここでのポイントは、Partition Keyが必要だということです - 特定のパーティションにルーティングして、それらのレコードを見つけ、どのようなスケールでも一貫したパフォーマンスを提供する必要があるからです。DynamoDBはPartition Keyを必要とします。

Thumbnail 630

Thumbnail 640

また、Sort keyには条件を設定することができます。Sort keyは順序付けされているため、タイムスタンプやアルファベット順など、アプリケーションにとって意味のあるものを設定する必要があります。これにより、特定の時間の前後や期間中のレコードを取得できます。モデリングを行う際は、このSort keyについてよく考えてください。最後の操作カテゴリーは、テーブル全体 からデータを取得するScan操作です。リレーショナルデータベースでテーブルスキャンを避けるのと同様に、この操作はできるだけ控えめに使用すべきです。

Secondary Indexesとデータモデリングの基本原則

先ほどのテーブルに戻りますが、Partition keyに従ってデータが分割されているため、ほとんどすべてのアクセスはPrimary keyを通じて行われます。この場合、ユーザーにアクセスする際はほとんどの場合、ユーザー名で取得したいので理想的です。しかし、アイテムに対して他のアクセスパターンが必要な場合はどうでしょうか?例えば、特定のチームに属するすべてのユーザーを取得したい場合。ユーザーはこの方法ではパーティション分割されていません。100テラバイトのテーブルの異なるパーティションすべてをスキャンしたくはありません。このような場合、どのように対処すればよいのでしょうか?

Thumbnail 690

そこで登場するのがSecondary indexesです。テーブルにSecondary indexを設定すると、新しいPrimary keyでデータのコピーが作成されます。異なるPartition keyとSort keyを設定できます。この例では、Partition keyをチーム名、Sort keyをユーザー名に設定するかもしれません。ここで行っているのは、異なるPrimary keyに従って整理された同じデータのレプリカ を作成することです。これにより、チーム名に基づいてクエリ操作を実行し、特定のチーム名に属するすべてのレコードを取得できます。

Thumbnail 710

Thumbnail 720

Secondary indexesは、DynamoDBでのモデリングにおいて重要な部分です。ここでのポイントは、これがデータの完全に管理されたコピーであるということです - コピーがキーワードです。 このコピーに対して書き込みを行うことはできません。コピーに書き込むのではなく、メインレコードに書き込み、それをコピーに反映させます。これにより、追加の読み取りベースのアクセスパターンが可能になりますが、すべての書き込みは依然としてテーブルのメインのPrimary key を通じて行う必要があります。

Thumbnail 730

Thumbnail 760

Secondary indexesには2種類あります。 ほとんどの場合、Global secondary indexと呼ばれるものを使用することをお勧めします。これはより柔軟で、Jeff Bezosの言う「両方向のドア」のようなもので、必要に応じて元に戻したり変更したりできます。Global secondary indexesの大きな欠点は、非同期でレプリケーションが行われることで、書き込みを行ってからSecondary indexに反映されるまでに若干の遅延が発生します。もう一つのタイプはLocal secondary indexと呼ばれ、同期的に書き込みが行われるため、強い整合性のある読み取りが可能ですが、柔軟性が低く、 身動きが取れなくなる可能性があります。Local secondary indexの使用は、より「一方向のドア」に近いものと言えます。

Thumbnail 770

Thumbnail 780

Thumbnail 790

Thumbnail 800

このような背景を踏まえて、DynamoDBに関するメカニカルシンパシーについて考えてみましょう。DynamoDBについて知っておくべきことと知る必要のないことについての知見があり、どこでメカニカルシンパシーが必要かということですが、時々ユーザーから実際には知る必要のない部分について質問を受けることがあります。それは単なる豆知識として面白いかもしれませんが、効果的なデータモデリングを行うために知る必要はありません。例えば、コンセンサスにPaxosとRaftのどちらを使用しているのか、トランザクション時に2フェーズコミットと3フェーズコミットのどちらを使用しているのか、あるいはそれらを選択できるのかといったことです。

Thumbnail 810

Thumbnail 820

Thumbnail 830

PostgreSQLやMySQLなどのデータベースの運用に慣れている方は、メモリバッファの設定をどのように行うべきか気になるかもしれません。これらの事項は興味深く、論文や講演でも取り上げられていますが、DynamoDBはこういった面ではほとんどブラックボックスです。しかし、モデリングに影響を与える特定の概念的フレームワークについては、理解しておく必要があります。最も重要なのは、DynamoDBがデータをどのようにパーティショニングしているか、そのパーティションにおけるプライマリーキーの重要性、そしてどのようにしてスケールに関係なく一貫したパフォーマンスを提供できるのかということです。

Thumbnail 860

これに直接関連するのがAPIの構造です。シングルアイテムのアクション、クエリ、スキャンといったAPIの構造をしっかりと理解することが重要です。PartiQLと呼ばれるものを使用してDynamoDBに対してSQLを書くこともできますが、それは構いません。ただし、まずはAPIの構造を理解することが大切です。なぜなら、DynamoDBはテーブル設計やシステムの使い方についてのヒントを与えてくれているからです。APIの構造とDynamoDBの概念的フレームワークを理解せずにSQLクエリを投げるだけでは、非効率なクエリを書くことになり、スケールに関係なく一貫したパフォーマンスを得ることができません。

DynamoDBのデータモデリング:アプローチと考慮事項

Thumbnail 870

Thumbnail 880

Thumbnail 890

もう一つの重要な側面は、追加のアクセスパターンを可能にするセカンダリインデックスです。そして見過ごされがちな考慮事項として、DynamoDBの課金がデータモデリングにどのように影響するかを考えることがあります。これについては後ほど詳しく説明します。これらが知っておくべき重要な事項であり、その他にも制限、ページネーション、一貫性モデルなども重要です。しかし、DynamoDBで適切なモデリングを行うために概念的な理解が必要なのは、これらの点です。では、データモデリングの基本について説明していきましょう。基本的な事項について説明したいのは、多くの人がモデルを複雑にしすぎたり、凝りすぎたりしているのを見かけるからです。DynamoDBに関しては基本に立ち返ることが良いと思います。DynamoDBは非常に基本的な構成要素を提供しており、凝りすぎるのではなく、それらを上手く活用すべきです。

DynamoDBが他のデータベースと異なる点は、クエリプランナーがないことです。PostgreSQLやMySQL、あるいは別のNoSQLデータベースであるMongoDBなどを使用する場合、クエリプランナーがあり、クエリを発行すると、統計に基づいて特定の操作を最も効率的に処理する方法を判断します。しかし、DynamoDBはハッシュマップやBツリーなどのコアデータ構造への直接アクセスを提供し、それを扱うのはユーザー次第です。つまり、あなたがクエリプランナーとなるため、モデリングを行う際にはそのことを考慮する必要があります。

Thumbnail 950

Thumbnail 960

Thumbnail 980

データモデルを設計する前に、まずドメインについてしっかりと理解を深めることが大切です。 そして、制約条件について検討しましょう。例えば、アプリケーションでユーザーを作成する場合、同じユーザー名やメールアドレスを持つユーザーが重複しないようにする必要があります。また、チームベースのSaaSアプリケーションであれば、チームプランで許可されている以上のメンバーを追加できないようにする必要があります。これらの制約条件は、モデルに組み込む必要があるため、しっかりと考慮しておきましょう。 もう一つ考慮すべき点は、データの分散です。1対多の関係がある場合、親アイテムに対して関連アイテムはどのくらいの数になるでしょうか?10個程度に制限されるのか、それとも数百、数千、数百万と無制限になる可能性があるのでしょうか?また、Partition Keyの分散具合や、データの均一な分散、特定のアイテムやItem Collectionsがホットスポットになる可能性についても検討が必要です。

Thumbnail 1010

Thumbnail 1020

Thumbnail 1030

アイテムのサイズについても考慮が必要です。後ほど説明しますが、これは課金に影響するため、データモデリングの段階で計画しておく必要があります。 これらの側面を理解したら、クエリの計画を立てましょう。これにはアクセスパターンを把握することが含まれます。特にDynamoDBのデータモデルを初めて扱う場合は、 アクセスパターンを明示的にリストアップすることをお勧めします。必要なインデックスや条件、その他の要件を確実に把握するために、アクセスパターンを文書化しておくことをお勧めします。読み書き両方のアクセスパターンをリストアップし、それぞれのシナリオの対処方法を把握しておきましょう。

Thumbnail 1050

Thumbnail 1060

Thumbnail 1090

Thumbnail 1100

Primary KeyやAPI、Secondary Indexなど、DynamoDBの基本を理解したら、 具体的なニーズに合わせてテーブルを設計できます。ここでのポイントは、特定の要件に合わせて設計することです。リレーショナルデータベースのように、まず正規化してから構造を作り、その後でクエリについて考えるというアプローチは避けましょう。代わりに、まずアクセスパターンについて考え、それらのニーズに特化した設計を行います。 多くの操作は、ユーザー名でユーザーを取得したり、ユーザーIDとクエストIDでクエストを取得したりするような、シンプルな単一アイテムのアクションを使用する基本的な機能であるべきです。 複数のレコードをリストアップする必要がある場合、例えばGuildに所属する全ユーザーや、ユーザーの全クエストを取得する場合には、クエリ操作を追加します。

Thumbnail 1110

Thumbnail 1120

Thumbnail 1140

Thumbnail 1150

Thumbnail 1160

読み取りベースの追加パターンにはSecondary Indexを活用し、 必要に応じてTransactionを使用します。DynamoDBのTransactionを使用すると、複数のレコードを1つのリクエストでアトミックに操作できるため、特定の状況で有用です。 DynamoDBでは、柔軟性と効率性の間にトレードオフが存在します。 DynamoDBを使用する理由を考える際は、その主要な特徴を念頭に置いてください。最大規模のDynamoDBユーザーの多くは、シンプルなPrimary Keyを使用し、主に個別のKey-Value操作を行い、クエリやSecondary Indexをあまり使用せずに、スケールにおける予測可能なパフォーマンスを重視しています。

Thumbnail 1180

Thumbnail 1190

一方で、従来のデータベースの運用オーバーヘッドに疲れ果てて、DynamoDBに移行するケースもあります。その場合、何が起こるかが正確に分かっているため、広範な柔軟性は必要ありません。 このような場合、DynamoDBのウェブサイトで紹介されているリレーショナルデータのモデリング例のように、アプリケーション専用に調整された複雑なシングルテーブル設計を採用することもあります。この場合、柔軟性はそれほど重要な懸念事項ではありません。

DynamoDBの活用:ユースケースと設計の柔軟性

Thumbnail 1210

Thumbnail 1230

Thumbnail 1240

Amazon DynamoDBを使用する別の理由として、AWS AppSyncとGraphQLとの優れた統合が挙げられます。GraphQLの概念は柔軟性を重視しており、クエリに柔軟性を持たせたい場合、完全に非正規化されたシングルテーブル設計よりも、正規化されたモデルを採用する傾向にあります。あるいはその中間的なアプローチを取ることもあります。また、Serverlessアーキテクチャでの使いやすさを重視する場合もあるでしょう。アクセスパターンに合わせたモデリングの利点を得たいものの、すべてを完全に理論化する必要はない、という考え方です。

Thumbnail 1250

Thumbnail 1260

DynamoDBを使用する理由を理解することは重要で、どのようなモデリングスタイルでも考慮すべきヒントがいくつかあります。最も重要なのは、アクセスパターンを念頭に置いた設計です。テーブルを正規化してからクエリを考えるのではなく、まずアクセスパターンを設計することが大切です。もう一つ重要な考慮点は、意味のあるプライマリーキーを使用することです。リレーショナルデータベースでは、プライマリーキーとして自動増分の整数がよく使用されますが、DynamoDBでは実際にアクセスする意味のある値を使用したほうが良いでしょう。ユーザーの場合、これはユーザー名やメールアドレスなど、一意の識別子として機能するものになります。

Thumbnail 1280

Thumbnail 1300

Thumbnail 1330

不必要にItem Collectionsを過負荷にすることは避けるべきです。よく見かける問題として、同じPartition Keyを共有するレコードの集合であるItem Collectionに、多すぎる異なるアイテムタイプが含まれているケースがあります。例えば、astute_alexというPartition Keyに対して、ユーザーレコード、インベントリ、複数のクエスト、フレンドシップなどが含まれているような場合です。このような状況を見かけると、ユーザー、インベントリ、クエスト、フレンドシップを一緒に取得する必要があるアクセスパターンが本当にあるのか確認します。多くの場合、その答えは「いいえ」です - クエストやフレンドシップは別々に取得したり、ユーザーとインベントリだけを取得したりすることが多いのです。Partition Keyは一緒に取得するアイテムをグループ化するべきなので、これらのアイテムを全て一緒に取得する必要がないのであれば、分けて保存するべきです。

Thumbnail 1360

クエストを1つのItem Collectionにまとめ、フレンドシップを別のItem Collectionにまとめることを検討してください。ユーザーとインベントリは、時々別々に取得し、時には一緒に取得する場合があれば、グループ化することは理にかなっています。実際に一緒に取得する必要がある場合にのみ、アイテムを同じコレクションに組み合わせるようにしましょう。そうしないと、クエリの作成が難しくなり、Partition Keyの分散も悪くなってしまいます。もう一つ重要な点は、早い段階で書き込みについて考えることです。維持すべき条件がある場合、それらはプライマリーキーを通じて実現する必要があります。一意性の要件やその他の条件を維持する必要がある場合は、これらを最初に考慮してください。読み取りパターンは後から追加する方が、書き込みパターンを後から実装するよりも容易です。

Thumbnail 1380

Thumbnail 1410

Conditional Writesを必ず使用してください。これは非常に便利な機能で、理想的には、条件付きの書き込み操作が必要な場合に、その条件をCondition Expressionで表現できるようにシステムを設計すべきです。Secondary Indexから読み取りを行い、計算を実行し、その後で何も変更されていないことを確認する条件付きの書き込みを行うよりも、直接テーブルに書き込む方が望ましいです。目標は、DynamoDBとの複数回のやり取りを避け、直接テーブルに書き込むことです。最後に、階層構造をフラット化することを忘れないでください。正規化されたモデルを持つリレーショナルの世界から来た場合、深い関係性を持つデータ構造は、適切に非正規化する必要があります。

非正規化テクニック:Embeddingとデータの複製

Thumbnail 1450

Thumbnail 1480

非正規化には2つの主要なアプローチがあります。1つ目はEmbeddingで、文字列やブール値、整数といったスカラー値だけでなく、複雑なオブジェクトを属性として持つことができます。Embeddingは、一対一または限定的な一対多の関係で特に効果を発揮します。先ほどのユーザー名の例に戻ると、特定のユーザーのUIに影響を与える可能性のある様々な設定を含むユーザー設定オブジェクトがあります。完全な正規化では別テーブルに分割することが推奨されますが、ユーザーと一緒に設定を取得することが常であり、設定単独で操作する必要がない場合は、Embeddingして問題ありません。このアプローチは、住所や関連アイテムの数が限られているものに適しています。

Thumbnail 1490

避けるべきなのは、無制限の一対多関係のEmbeddingです。ユーザーの例に戻ると、すべての注文をユーザーレコードに直接Embeddingすることを考えるかもしれません。このアプローチは理想的ではありません。時間とともにアイテムサイズが肥大化し、DynamoDBのアイテムサイズ制限に達する可能性があるためです。また、アイテム全体の読み書きのたびにコストが発生し、大きなコレクションを扱う際に個々のアイテムを操作することが難しくなります。

Thumbnail 1520

個々のアイテムの操作が難しくなるため、Queryオペレーションとitem collectionsを使用して注文をグループ化することをお勧めします。これにより、必要なときにすべてを一度に取得できる一方で、小さなアイテムを扱う柔軟性と個々のレコードを操作する能力を維持できます。Embeddingの主要なトレードオフは、アイテムサイズの増大と、より多くの読み取りが必要になる可能性とのバランスです。ユーザー設定を別のテーブルや別のアイテムに分割すると、ユーザーとその設定を取得するために複数の読み取りを実行する必要があります。

Thumbnail 1540

Thumbnail 1550

Thumbnail 1580

非正規化でより一般的に見られる要素は、データの複製です。ここでの主要なトレードオフは、読み取りの高速化と、より困難または高コストな書き込みのバランスです。特に階層的なデータでは、親データを子レコードに複製することで、親の属性で検索したり、直接表示したりすることができ、理にかなっています。ここでの重要な考慮点は、ツリーを下る必要なく高速な読み取りを実現できる一方で、データが変更された場合により多くの書き込みが必要となり、維持コストが高くなることです。理想的には、不変の値を複製することで、更新の心配をする必要がなくなります。

Thumbnail 1620

システムを設計する際、特定の値を不変にするか、ユーザーが変更することを少し難しくすることを検討できます。GitHubのユーザー名やGitHub Organizationの変更を考えてみましょう。これらは変更可能ですが、通常はバッチ操作として時間をかけて行われます。これは、裏側で複製されたデータをすべて更新しているためです。複製には複数のメリットがあります。まず、読み取りの回数を全体的に減らすことができ、より高速で低コストになります。さらに、読み取りを逐次的な操作から並列操作に移行できます。例えば、一対多の関係において、親を最初に取得してから追加のリクエストを行うのではなく、親の識別情報を複製していれば並列にリクエストを行うことができます。

DynamoDBのNapkin Math:パフォーマンスと課金の基本

Thumbnail 1660

Thumbnail 1670

さらに高度な概念について見ていきましょう。私が特にここ数年で重要だと感じているのは、DynamoDBに関する数学的な考え方、特にNapkin mathについてです。 現在Turbo Pufferで働いているSimon Eskildsenによるプレゼンテーションを見つけました。5年前のこの講演では、シニアエンジニアやアーキテクトは、システム全体を構築する前に、レイテンシーやスループット、コストなどのシステムパフォーマンスを理解するために、Napkin mathができるべきだと説明しています。彼のアプローチは特に有用で、DynamoDBはこの種の計算に適しています。

Thumbnail 1750

Thumbnail 1760

Thumbnail 1770

Thumbnail 1780

DynamoDBはどのような規模でも一貫したパフォーマンスを提供するため、特定の基本的な数値を把握しておく必要があります。 クライアントサイドの操作では、適度なサイズのアイテムの読み取り操作には約5ミリ秒かかります。 書き込み操作には最大20ミリ秒かかる可能性があり、調整と2フェーズコミットを必要とするトランザクションには100ミリ秒以上かかる可能性があります。 DynamoDBの制限は明確に定義されています。例えば、レコードのバッチを取得するためにクエリリクエストを行う場合、そのリクエストで返されるデータは最大1メガバイトまでです。これにより、無制限の操作を防ぎ、どのような規模でも一貫したパフォーマンスを維持できます。5メガバイトのデータを処理する必要がある場合、5回の連続したリクエストを行い、その都度ページネーショントークンを取得して操作を継続する必要があります。2回目のリクエストを行い、ページネーショントークンを取得し、すべてのデータを取得するまでこのプロセスを繰り返します。

Thumbnail 1810

データセットにホットアイテムやホットパーティションがある場合、制限が何であるか、そしてそれがユーザーに提供しているSLAに適合するかどうかを理解することが重要です。単一のDynamoDBパーティションは、1秒あたり1000書き込みユニットと3000読み取りユニットを処理できます。この制限内に収まるかどうか、あるいはキャッシング、書き込みバッファリング、書き込みシャーディングを実装する必要があるかどうかを判断する必要があります。

Thumbnail 1840

Thumbnail 1850

これがパフォーマンスの基本的な数値についての説明でした。 Amazon DynamoDBでモデリングを始める際には、課金の基本的な数値も理解しておくと便利です。DynamoDBの課金の仕組みについて少し背景を説明しましょう。従来のデータベースを考えると、 CPUやメモリの組み合わせを持つインスタンスに基づいて課金されます - IOPSが含まれている場合もあれば、別途支払う場合もあります。基本的に、実行する操作の数に関係なく、このリソースの束に対して支払うことになります。これでは、Simonが話すような見積もりやNapkin mathを行うのが難しくなると思います。

Thumbnail 1870

Thumbnail 1880

Thumbnail 1890

Thumbnail 1910

DynamoDBでは、操作に対して直接課金されます。Read Capacity Units(RCU)とWrite Capacity Units(WCU)に基づいて課金されます。DynamoDBの基本的な数値とそれらが何であるかを見てみましょう。 Read Capacity Unitを1単位消費することで、最大4キロバイトのデータを読み取ることができます。 強力な整合性のある読み取りを必要としない場合、これを半分に減らすことができます。ほとんどの場合、それをお勧めします - 最大4キロバイトのデータを読み取る場合、0.5 Read Capacity Unitしか消費しません。また、Write Capacity Unitでは、リクエストで最大1キロバイトのデータを書き込むことができます。

Thumbnail 1920

Thumbnail 1930

Thumbnail 1940

Thumbnail 1950

料金設定にはこれらのベースレートを使用できます。面白いことに、スライドを作成してから現在までの間に、DynamoDBがオンデマンド課金を50%値下げしたので、内容を変更しました。新しい料金では、100万RCUあたり12.5セントです。100万回の書き込みごとに67.5セントが課金され、ストレージについては1ギガバイトあたり月額25セントが課金されます。ただし注意点として、これはリージョン固有の料金で、us-east-1の場合です。別のリージョンを使用する場合は、異なる数値を当てはめる必要があります。また、これはオンデマンド料金なので、支払うべき最高額となります。テーブルの使用率を高く維持できる場合はProvisioned Capacityを使用できますが、オンデマンドの方が運用が簡単なので、これ以上の料金を支払わないようにしましょう。

Thumbnail 1970

Thumbnail 1980

Thumbnail 1990

このようなパフォーマンスと課金のベースレートを頭に入れておくと非常に役立ちます。このような概算計算を考える上で、いくつかの興味深い示唆や注意点があります。まず第一に、パフォーマンスと課金は別の問題として考えます。この概算計算を行う際、アプリケーションのスループットやレイテンシー、そしてコストについて考える必要がありますが、これらがどのように相互に影響するかを心配する必要はありません。

Thumbnail 2010

Thumbnail 2030

データベース全般について考えると、同時クエリが増えたり、システム内のデータが増えたり、より大きな読み取りを行うと、応答時間にどのような影響があるでしょうか。通常、CPUやメモリ、ディスクなどの重要なリソースが飽和状態になるポイントがあり、そこで応答時間が急激に上昇するカーブを描きます。一方、DynamoDBでは、システムの限界に達することはあっても、そのような急激な上昇は見られません。プロビジョニングした容量や、パーティションレベルでDynamoDBが処理できる限界を超えた場合、スロットリングが発生します。良い点は、同じ応答時間を維持したまま、次の秒に十分な容量が利用可能になったら再試行してくださいというエラーが返されることです。

Thumbnail 2060

Thumbnail 2070

テールレイテンシーやスケールにおけるテールについて読んだことがある方なら、このような長いテールの応答時間への対処が、サービスのクライアントとしても、クライアントを持つサービス提供者としても非常に難しいことをご存知でしょう。タイムアウトは様々な問題を引き起こす厄介な要素です。私が特に気に入っているのは、これらの保証を得るために十分なプロビジョニングを行ったかどうかを考えることなく、このパフォーマンスを見積もれることです。

Thumbnail 2080

Thumbnail 2100

二つ目のポイントとして、DynamoDBの料金体系はアプリケーションの構築方法に影響を与えるべきだということです。AWSは、そのサービスを適切に使用する方法を教え、Mechanical Sympathyを構築するのを支援する時に最高の働きをします。DynamoDBはこれを2つの領域で実現していると思います:良いパフォーマンスを引き出すためのAPIを通じて、そして課金方法を通じて、何が高価で、何を考慮し最適化すべきかを示すことです。このように、特にRCUやWCUといったCapacity Unitsなど、異なるベースレートがあります。

DynamoDBの課金モデル:最適化のヒント

Capacity Unitには様々な乗数が存在します。RCUやWCUなどのCapacity Unitについて説明する際、1つのリクエストで複数のRCUやWCUを消費する可能性があります。例えば、10キロバイトのアイテムを書き込む場合、1キロバイトごとに1つのWrite Capacity Unit、つまり合計10 WCUが必要になります。クエリ結果の1ページ全体を読み取る場合、1回のリクエストで最大1メガバイトのデータを読み取ることができますが、それには250 RCUのコストがかかります。

Thumbnail 2150

Thumbnail 2160

Thumbnail 2170

Secondary Indexesの場合、メインテーブルと3つのSecondary Indexesがあれば、メインテーブルと各Secondary Indexesへの書き込みが必要となり、それぞれの書き込みに対してコストが発生します。 1つのリクエストで複数のレコードを扱うトランザクションの場合、各レコードのコストに加えて、Two-phase commitと調整通信のための追加コストが発生します。同様に、Global Tablesを使用して複数のリージョンに書き込む場合、すべての整合性のある読み取り と関連する操作のコストを支払う必要があります。DynamoDBの良いところは、どの操作がコストがかかるのか、そしてそれらをどのように最適化すべきかを明確に示してくれることです。 大きなアイテムはより多くのリソースを必要とし、それに応じて課金され、Secondary Indexesは追加の書き込みを必要とし、トランザクションは調整のためのコストがかかります。

Thumbnail 2200

Thumbnail 2210

Thumbnail 2220

これらの考慮事項を踏まえて、私はデータモデリングの際に常に乗数を意識し、考慮するようにアドバイスしています。 重要な側面の1つは、アイテムサイズを慎重に見直すことです。時間とともに、その重要性を実感するようになりました。 使用していない属性は削除し、可能性のある属性をすべて含めるのではなく、必要なものだけを残すことが重要です。レコード内の大きな値については、圧縮を検討するか、特に大きなBlobタイプのデータの場合は、そのレコードを操作するたびに読み書きのコストが発生するのを避けるためにAmazon S3に保存することを検討してください。

Thumbnail 2240

Thumbnail 2270

もう1つの重要な考慮事項は、 Secondary Indexesを制限し、その必要性を慎重に評価することです。Write Capacity Unitは、Read Capacity Unitの5倍のコストがかかり、サイズは4分の1しかないため、書き込みは読み取りの約20倍のコストがかかります。追加の読み取りパターンのためにSecondary Indexを使用する代わりに、over-readやover-fetchを行う方が、コスト効率が良い場合もあります。また、Projectionを使用してIndex内の アイテムのサイズを制限することもできます。Secondary Indexesはデータのコピーなので、アイテム全体をコピーするのではなく、Projectする属性を指定できます。これにより、読み書きのためのアイテムサイズが小さくなるだけでなく、Projectされていない属性を変更する際にメインテーブルへの書き込み操作をスキップできます。

Thumbnail 2300

もう1つの重要な考慮事項は、トランザクションを制限することです。DynamoDBのトランザクションは5年前から利用可能で、特定のユースケースでは価値がありますが、主に低ボリュームで高価値な操作に使用すべきです。この方針が推奨される理由は、トランザクションがバックグラウンドでTwo-phase commitを使用しており、トランザクションの競合エラーによる拒否を避けたいためです。さらに、トランザクションでは通常の2倍の操作コストがかかるため、課金と遅延の両面で、その価値がコストに見合うことを確認してください。

Thumbnail 2340

Thumbnail 2360

Thumbnail 2370

Global tablesは非常に強力な機能で、最近では強整合性のGlobal tablesも導入されました。しかし、フェイルオーバーシナリオなのか、アクティブ-アクティブ構成なのか、具体的なユースケースを理解することが重要です。Global tablesは書き込みコストを実質的に2倍にするため、コストに大きな影響を与えます。そのため、 この機能が本当に必要かどうかをよく検討する必要があります。ほとんどの場合、結果整合性のある読み取りで十分であり、強整合性のある読み取りの代わりにこちらを使用することで、読み取りコストを50%削減できます。

DynamoDB Streams:アーキテクチャとデータモデリングのユースケース

Thumbnail 2390

これが数学的な側面であり、これらの考慮事項はデータのモデリング方法やアプリケーションの構築方法に影響を与えるべきです。では、DynamoDB Streamsの実践的な側面に移りましょう。 これは私が特に気に入っているDynamoDBの機能の1つで、他のデータベースを使用する際には、DynamoDB Streamsやchange data captureの扱いやすさが恋しくなります。DynamoDB Streamsについて説明すると、テーブルを作成した後で有効にすることができます。

Thumbnail 2410

Thumbnail 2420

デフォルトでは無効になっていますが、 有効にすると、テーブルでの書き込み操作(挿入、更新、削除のいずれか)が行われるたびに、その変更の記録がDynamoDB Streamに生成されます。 そして、Lambda関数やKinesis Client Library、あるいはお好みであれば独自のAPIを使用して、そのストリームを処理し、レコードのバッチを扱って、これらの変更に対して操作を行うことができます。

Thumbnail 2440

Thumbnail 2450

Thumbnail 2460

Thumbnail 2470

Change data captureの人気は高まっており、私はDynamoDBの実装方法を高く評価しています。ここでDynamoDB Streamsの3つのユースケースについてお話ししたいと思います。 そのうち2つはアーキテクチャのユースケースで、1つはデータモデリングのユースケースです。最初のアーキテクチャのユースケースは、 イベント駆動型アプリケーションでよく発生する二重書き込みの問題を解決することです。 二重書き込みの問題とは、アプリケーションがDynamoDBに書き込みを行う際、例えば注文が入ってきた時に、 追加のアクションが必要になる場合です。例えば、SQSにメッセージを配置したり、EventBridgeに何かを発行したりする必要があるかもしれません。

Thumbnail 2480

Thumbnail 2510

Thumbnail 2520

課題は、トランザクションではない方法で2つの異なるシステムに書き込みを行うことです。DynamoDBへの書き込みは成功したものの、EventBridgeへの発行に失敗した場合、特に両方の操作が成功する必要がある場合、孤立した注文や不整合な状態が発生する可能性があります。リトライやロールバックなど、さまざまな解決策を試みる人もいますが、これらを正しく実装するのは複雑になりがちです。二重書き込みを処理する主な方法は、トランザクショナルアウトボックスパターンを使用するか、change data captureを使用することです。 DynamoDB Streamsを使用すると、テーブルに注文レコードを挿入した際、それがストリームに記録され、 Lambdaを使ってEventBridgeにプッシュすることができます。利点は、EventBridgeがダウンしていたり、コンピュート処理にエラーが発生したりしても、データは永続的で復旧可能な状態を保ち、システム間の不整合な状態を防ぐことができることです。

Thumbnail 2540

Thumbnail 2550

Thumbnail 2570

Thumbnail 2580

Thumbnail 2590

DynamoDB Streamsの処理に関する重要なポイントをいくつかご紹介させていただきます。この件については、私のブログで詳しく解説しています。EventBridgeなど他のサービスにイベントを送信する際は、転送前にイベントを適切に整理することが重要です。DynamoDB Streamから複数のコンシューマーが直接読み取りを行うのは避けるべきです。その理由の1つは、DynamoDB Streamsのコンシューマー数が2つまでに制限されているからです。さらに、DynamoDB Streamsのイベントは、アイテムや古いバージョン、新しいバージョン、DynamoDBのデータ型を含む生のフォーマットで提供されます。これらを、「ユーザー作成」や「ユーザー更新」といった、関連フィールドと完全なレコードを持つ、アプリケーションにとってより意味のあるドメインイベントに変換する必要があります。

Thumbnail 2610

Thumbnail 2620

もう1つ重要な考慮点は、ストリーム処理のメカニズムと失敗時の動作を理解することです。ストリーム処理は、失敗したバッチの再試行やデッドレターキューの使用が容易な従来のキュー処理とは異なります。ストリームでは、レコードを順番に処理する必要があり、レコードを処理できない場合は、特別な処理メカニズムを実装しない限りブロックされてしまいます。幸いなことに、KafkaやKinesisの普及により、これらの概念は広く理解されるようになってきています。

Thumbnail 2660

Thumbnail 2670

Thumbnail 2700

DynamoDB Streamsを有効にする際は、実装方法を慎重に検討する必要があります。通常は、完全マネージド型であるネイティブのDynamoDB Streamsの実装が推奨されます。読み取りに対してのみ課金される点も魅力です。Lambdaを使用する場合は、読み取りの料金さえかかりません。また、順序付けや重複処理に関して強力な保証を提供します。主な制限は、コンシューマーが最大2つまでという点で、Global TablesやZero-ETLなどのストリームポジションを消費する機能を使用する場合はさらに制約が厳しくなります。より多くのコンシューマーが必要な場合は、ネイティブのAmazon Kinesis Streamsを使用できます。これなら5から20のコンシューマーをサポートできますが、追加の管理オーバーヘッドとストリーム自体への課金が発生します。ただし、Kinesis Streamsは順序付けと重複に関する保証が若干弱くなります。

Thumbnail 2720

このようなトレードオフを考慮する必要があります。複数のコンシューマーが必要だと判断しない限り、ほとんどの場合はDynamoDB Streamsをお勧めします。

Thumbnail 2750

これが二重書き込みの問題を解決する最初のユースケースでした。次は、DynamoDB Streamsのもう1つのアーキテクチャ的なユースケースである、セカンダリシステムへのエクスポートについて見ていきましょう。ここでもMechanical Sympathyの考え方に立ち返り、DynamoDBが得意なことと不得意なことについて考えてみましょう。DynamoDBには明確な長所と短所があり、無理に適さないものを押し込むべきではありません。

Thumbnail 2760

Thumbnail 2770

Thumbnail 2780

DynamoDBは、小規模な読み書き操作、トランザクショナルな条件付き書き込み、低レイテンシー、高可用性が必要な、典型的なOLTPワークロードを得意としています。これは、基幹アプリケーションに求められる機能そのものです。 しかし、DynamoDBは柔軟性が必要な処理や、パターンが予測できないような処理は得意としていません。 私がいつも挙げる3つの例として、集計や分析、複雑なフィルタリング、全文検索があります。これらの一部はクエリプランで対応できる場合もありますが、多くの場合、DynamoDBに無理に当てはめようとするのは非常に困難です。

Thumbnail 2800

このような場合、システムの規模に応じて、DynamoDBを他のシステムで補完することがよくあります。基本的な分析であればAmazon S3とAthenaを、より本格的な分析にはAmazon Redshiftを、リアルタイムの複雑なフィルタリングが必要な場合はAmazon OpenSearchが適しているでしょう。 DynamoDB Streamsは、これらのシステムを連携させる接着剤のような役割を果たし、アプリケーションで更新が発生するたびに、そのデータを二次システムにコピーすることができます。

Thumbnail 2840

Thumbnail 2850

この外部システムとの同期には、注意すべき点がいくつかあります。まず最初に考えるべきは、初期データのエクスポートです。多くの場合、最初はDynamoDBで始めて、後から検索機能や複雑なフィルタリング、分析機能が必要だと気付くことがあります。Streamには全履歴データがあるわけではなく、過去24時間分のデータしか保持されません。また、有効化していない場合は、有効化するまでデータは記録されません。では、どうやって初期データをDynamoDBから外部システムに取り込めばよいのでしょうか?DynamoDBはこの領域で改善を重ねており、全データをS3にエクスポートするオプションを提供しています。このバッチエクスポートとDynamoDB Streamsによる継続的なデータ連携の両方で、データの取り出しが容易になってきています。

Thumbnail 2870

Thumbnail 2880

Thumbnail 2890

アーキテクチャ的な観点から見ると、OpenSearchのような二次システムがある場合、まずDynamoDBテーブルでStreamsを有効化します。これにより、その時点以降のすべての操作が記録されます。Streamsを有効化したら、初期データをS3にエクスポートします。エクスポートが完了したら、そのデータを外部システムにロードし、Streamからのデータ処理と取り込みを開始します。これにより、スナップショットとその後のすべての操作が同期された状態を維持できます。

Thumbnail 2910

Thumbnail 2940

Thumbnail 2960

とはいえ、このStreamの維持には依然として複雑な課題があります。例えば、OLTP/OLAP間のインピーダンスミスマッチがあります。OLTPは多数の小さな書き込み、更新、削除を行いたがるのに対し、SnowflakeやRedshiftのようなOLAPシステムは、大規模なバッチ取り込みや、できれば追加のみの操作を好みます。このデータパイプラインを維持するためのツールは揃っていますが、可能であればマネージドオプションを検討することをお勧めします。AWSはOpenSearchやRedshiftへのZero-ETL統合のようなマネージドソリューションを開発しています。私の最初の仕事はデータパイプラインの構築でしたが、これには何の栄光もありません。常に何らかの理由で問題が発生し、うまく動いているときは誰も気にしませんが、問題が起きると皆が怒り出します。そのため、マネージドソリューションを検討することをお勧めします。

Thumbnail 2980

素晴らしいことに、DynamoDBはエクスポートとストリームという2つの基本機能を提供しており、AWSのZero-ETLであれ、サードパーティシステムであれ、効果的に管理できるようになっています。最後のユースケースは、トリガーやストアドプロシージャに関するものです。これはより多くデータモデリングのユースケースと言えます。DynamoDB Streamsをトリガーやストアドプロシージャと比較すると、少し変な顔をされることがあります。というのも、最近ではそれらをあまり使用しなくなってきているからです。ストアドプロシージャは通常、PL/SQLやPL/pgSQLなどの異なる言語で書く必要があり、個別にデプロイする必要があります。また、その種の処理の可視性やテストがあまりありません。

しかし、DynamoDB Streamsではこの問題はそれほど顕著ではありません。なぜなら、AWS LambdaやKinesisクライアントライブラリで処理できるからです。同じアプリケーションコード、同じビジネスロジックを使用し、通常のテスト手順で処理することができます。これは非常に優れたパターンとして活用できます。

Thumbnail 3030

ここで、トリガーを使用したデータモデリングの例を1つ見ていきたいと思います。その際に、配列インデックスに関して学んだことを活用します。時には、データを非正規化して項目に配列を埋め込んでいる場合があり、その配列内の項目で検索したいことがあります。しかし、DynamoDBはネイティブで配列インデックスを提供していません - アプリケーションの配列に対して二次インデックスを作成することはできません。

高度なデータモデリング技術:配列インデックスと効率的なクエリ設計

Thumbnail 3050

Thumbnail 3060

Thumbnail 3070

では、これはどのように見えるでしょうか?GitHubやJiraのような課題管理システムを例に考えてみましょう。チームがプロジェクトを作成し、そのプロジェクトには課題があり、その課題にはさまざまなタグがあります。リリースバージョンや課題の種類、影響を受けるコンポーネントなど、自由な形式のものです。ここで実現したいのは、タグベースの検索です。つまり、「feature」プラス「auth」プラス「sprint-23」のような条件で、該当するすべての課題を取得したいのです。

Thumbnail 3080

Thumbnail 3100

これをどのように実現できるでしょうか?アプリケーションにこのようなデータがあるとします - 右側に、特定の項目に対するタグの配列を持つtagsという属性があることがわかります。インデックスを作成できない場合、どのように実現すればよいでしょうか?まず最初に考えるべきは、力技で解決できないかということです。追加のインデックスが必要なのか、それとも現状のままで対応できるのか?常に特定のプロジェクト内で検索を行うのであれば、プロジェクト内のすべてのレコードを取得して力技で処理することも可能かもしれません。

Thumbnail 3110

Thumbnail 3120

Thumbnail 3130

Thumbnail 3140

ざっくりと計算してみましょう。Issueのアイテムサイズは1から5キロバイト程度とします。そして、プロジェクトごとのIssue数について考えると、中央値のプロジェクトでは約1,000個のタグがあり、P95(95パーセンタイル)では、Linuxのような大規模プロジェクトで10,000個以上になることもあります。単純計算をしてみると、中央値のケースでは、2メガバイトのデータを取得する必要があります。一度に1メガバイトしか取得できないため、2回のリクエストが必要になります。これには40から100ミリ秒かかり、500 RCUを消費することになります。

Thumbnail 3150

Thumbnail 3160

しかし、P95レベルになると、50メガバイトものデータを取得する必要が出てきます。これは50回のリクエストと12,500 RCUを意味します - これは現実的ではありませんね。つまり、力任せのアプローチは望ましくありません。ここで、DynamoDB Streamsの出番です。このデータを再インデックス化し、提供されているビルディングブロックを使って構築することができます。レコードが流れてきたとき、つまりIssueが作成、更新、削除されたときに、これらを別のテーブルや別のアイテムとしてインデックス化します。

Thumbnail 3180

Thumbnail 3210

ここで実現しているのは、基本的に転置インデックスです。Issueをパーティションキーでグループ化しています。プロジェクトとタグを組み合わせたProject Tag、作成日時のタイムスタンプ、そして参照先のIssueがあります。特定のプロジェクトの特定のタグを持つIssueをすべて見つけたい場合、これら3つのアイテムコレクションをすべて取得する必要があります。まずFeatureのものを取得し、次にAuthとSprintのものを取得します。これらは並列で実行できるのが利点です - 3つの異なるリクエストですが、すべて並行して処理されます。これらの結果セットを取得したら、3つすべての条件を満たすIssueを見つけるために積集合を取ります。

Thumbnail 3220

Thumbnail 3230

Thumbnail 3240

ざっくりと計算してみましょう。通常、クエリあたり3つのタグがあるとします。Issueアイテムのサイズは必要最小限のデータしか含まないので、おそらく200バイト程度です。そして、ユニークなタグごとのIssue数については、中央値で10個程度でしょう - sprint-23のようなタグが付いているIssueはそれほど多くないはずです。ただし、大きな機能領域に関連するものは、200個や場合によっては500個のIssueにそのタグが付いているかもしれません。

Thumbnail 3250

Thumbnail 3260

これを踏まえて、もう一度計算してみましょう。中央値では、タグごとに2キロバイトを読み取る必要があり、3回のリクエストで、3 RCUのコストがかかります。これはずっと安上がりです。P95レベルでも同様で、約30 RCU程度です。このように、インデックスを工夫することで、はるかに効率的な操作が可能になりましたが、さらに改善の余地があるかもしれません。

Thumbnail 3270

Thumbnail 3280

Thumbnail 3290

データの非正規化 について、そして私たちがどのようにそれを適用したかについてお話ししましょう。データにEmbeddingを行っていますが、 Tagを見てみると、別のテーブルとして分離していません。なぜなら、アイテムを取得する際に、そのすべてのTagも表示したいからです。そのため、このTag情報をそのままEmbeddingしています。また、 作成しているこれらのIssue Tag itemsは、純粋なデータの複製となっています。ここで疑問が生じるかもしれません。もっとデータを複製できないでしょうか?現在複製しているのはかなり基本的なアイテムですが、インデックスに完全なアイテムを複製することも可能です。Issues by Tagテーブルに完全なアイテムを複製することができます。

Thumbnail 3310

Thumbnail 3340

クエリを実行する際、 一つのTag item collectionだけを取得すれば済みます。なぜなら、このプロジェクトのFeatureを検索できるからです。それらのTagの中から、マッチさせたいすべてのTagを持っていないものをフィルタリングできます。これにより、異なるItem collectionにアクセスする必要がなくなり、完全なIssue itemを直接返すことができます。完全なレコードを取得するための追加の読み取りも必要ありません 。

Thumbnail 3350

Thumbnail 3360

Thumbnail 3370

Thumbnail 3380

このトレードオフは、より多くの複製が発生することです。利点としては、必要な読み取り回数が減少し、レイテンシーが低くなることです。しかし、書き込みコストが高くなり、メンテナンスも増加します。なぜなら、Issueや Tagが変更されるたびに、より多くのアイテムに影響を与える必要があるからです。これは、TriggerやStored Procedureをどのように使用できるかの一例です。これらの その他のユースケースには、集計の維持があります。さらなる応用例として、 バージョン履歴の追跡や階層的なロールアップがあります。DynamoDB Streamsは、私がDynamoDBで最も気に入っている機能の一つで、多くの素晴らしいユースケースを可能にします。

Thumbnail 3400

Thumbnail 3420

時間が迫っていますが、DynamoDBの活用方法についてまとめさせていただきます。DynamoDBには、できることに関して独自の特徴があります。このアプリケーションにアプローチする際、どのように考えればよいでしょうか?繰り返しになりますが、まずは基本を使うことが重要です。 シンプルなアイテム操作やQuery操作などを使用してください。これらのヒントをすべてのデータモデリングスタイルに適用し、まずはこれらについて考えてください。多くのデータモデルでは、これで90%は解決できるはずです。Array indexingなど、DynamoDBがサポートしていないような複雑な要件に直面した場合、 DynamoDBは自分でクエリプランナーを構築・維持するための優れたビルディングブロックを提供しています。

Thumbnail 3430

Thumbnail 3460

必要に応じて、Streamsを使用したArray indexingや、その他のタイプのロールアップを実行できます。 素晴らしい点は、実現可能性を判断するためのNapkin mathを行い、トレードオフを理解できることです。多くの場合、基本的な機能で十分であり、ビルディングブロックを使用して他の機能を構築できますが、すべてを実現できるわけではありません。外部システムが本当に必要な場合、Amazon OpenSearchやRedshift、Snowflakeなどの二次的なシステムを使用します。そこでは、DynamoDBからの初期エクスポート とDynamoDB Streamsの両方を使用してデータを取得できます。

Thumbnail 3470

Thumbnail 3480

Thumbnail 3500

早急にそこまで進まないでください。Split(分割)とSort(並び替え)は驚くほど強力で、様々な用途に活用できます。先ほど見たComposite Primary Keyでは、Partition Keyによってデータをグループ化し、その中で並び替えを行うことができ、多くのケースに対応できます。ここで重要なのは、Tenant ID、ユーザーメール、Device IDなど、高いカーディナリティを持つものでグループ化し、同じItem Collectionに一緒にアクセスする必要があるレコードのみを配置することです。同じItem Collection内に複数のアイテムがある場合は、意味のある方法で並び替えを行い、単一の結果セットで取得できない場合や、フィルタリングしたい場合のことを考慮してください。タイムスタンプ、バージョンID、あるいはタイムスタンプを含むユニークな識別子などを使用するとよいでしょう。

Thumbnail 3520

Thumbnail 3530

Thumbnail 3540

このSplitとSortは、実に興味深い方法で活用できます。例えば、GEO検索やGeohashingでは、短いGeohashをPartition Keyとし、より長いGeohashをSort Keyとして使用できます。これにより、アイテムを地理的に分割し、より近いものを見つけるために並び替えることができます。Jason Hunterさんは、DynamoDBのデザインパターンに関する素晴らしい講演で、IP検索の例を紹介しています。対象となるドメインを理解していれば、多くのことに対応できるようにモデリングすることができます。SplitとSortは予想以上に強力な機能なのです。

Thumbnail 3550

私はDynamoDBが大好きです。DynamoDBは多くのことができると思います。データベースの選択肢は豊富で、優れたものが多くありますが、DynamoDBは非常に強力で、独自の特性を活用できると考えています。以上で終わりになります。ご清聴ありがとうございました。この後も会場におりますので、何か質問がありましたらお気軽にお声がけください。


※ こちらの記事は Amazon Bedrock を利用することで全て自動で作成しています。
※ 生成AI記事によるインターネット汚染の懸念を踏まえ、本記事ではセッション動画を情報量をほぼ変化させずに文字と画像に変換することで、できるだけオリジナルコンテンツそのものの価値を維持しつつ、多言語でのAccessibilityやGooglabilityを高められればと考えています。

Discussion