Amazon Web Services ブログ

Amazon Aurora DSQL でスケーラブルなアプリケーションを構築する


「スキーマ変更は利用の少ない深夜に、フェイルオーバー試験は休日に。」データベースを止めないための調整に、エンジニアの時間が少しずつ削られていく。そんな場面に直面することはありませんか。9/14(月) 14:00 から麻布台ヒルズで開催する「Amazon Aurora DSQL Day Tokyo」では、こうした運用調整からエンジニアを解放する分散 SQL データベース Amazon Aurora DSQL について、来日するプロダクトマネージャーと日本のスペシャリスト SA が、技術詳細から適用判断のポイントまでを解説します。IVRy 様、MIXI 様、東京海上日動システムズ様など、実際に本番システムで採用されたお客様の生の知見を聞ける現地開催イベントです。深夜や休日の調整にあてていた時間をサービス開発に使えるようになるための第一歩としてご活用いただけます。参加には事前登録が必要ですので、ぜひ上記のリンクからお申し込みください。


本記事は 2026 年 7 月 28 日 に公開された「Building scalable applications on Amazon Aurora DSQL」を翻訳したものです。

本記事では、Amazon Aurora DSQL の分散アーキテクチャで効果的にスケールするアプリケーションを設計するための実践的なガイダンスを紹介します。スケーラビリティを制限しがちなパターンの見分け方、ワークロードを効率よく分散する実証済みの設計パターンの適用方法、そして Aurora DSQL に最適化したトランザクション戦略の実装方法を学べます。主キーの選定、スキーマ設計の原則、インデックス戦略、マルチリージョン最適化を取り上げつつ、AWS リージョン全体で ACID(原子性、一貫性、独立性、耐久性)を完全に維持する方法を解説します。

分散データベースは水平スケールを実現しますが、その真価はデータモデルが読み書きのノード分散を考慮したときに発揮されます。Amazon Aurora DSQL は追加設定なしで幅広いワークロードを処理できるようになっていますが、ホットキーの競合を最小化する設計は、いまだに最も効果の大きい最適化の 1 つです。

こうした概念を具体例で理解するため、本記事全体を通してマルチリージョンの Web アプリケーションをリファレンスアーキテクチャとして使います。多数のユーザーが同時に読み書きを行い、低レイテンシーを期待し、強い一貫性を必要とするシステムです。まさに分散データベースをフルに使い込むタイプのアプリケーションであり、Aurora DSQL 上で開発する際に直面する設計判断を順を追って見ていくのに適しています。

アーキテクチャを理解する

次のセクションでは、このアーキテクチャの仕組みを説明します。

アクティブ-アクティブなマルチリージョン書き込み

Aurora DSQL はリージョンをまたいだ複数のアクティブライターをサポートします。今回のソーシャルアプリケーションでいえば、米国のユーザーは us-east-1us-west-2 に接続し、ヨーロッパのユーザーは us-east-1 に接続するといった構成が可能で、どちらのエンドポイントもリージョン間レイテンシーなしで読み取りを受け付け、書き込みは 2 つのリージョン間で同期的にレプリケートされます。

Diagram of active-active multi-Region writes: users connect to the nearest Aurora DSQL endpoint, and both endpoints accept reads and writes that are synchronously replicated across Regions

すべての書き込みを最寄りのリージョンで処理できます。単一のプライマリがボトルネックになることはありません。

ただし、複数のアクティブライターになると、新たな疑問が生まれます。2 つのライターが同じ行を同時に変更しようとしたら、何が起こるのでしょうか。

楽観的同時実行制御(OCC)

Aurora DSQL は楽観的同時実行制御(OCC)を使います。トランザクションはロックを取らずに実行され、コミット時に検証されます。2 つのトランザクションが同じ行を変更した場合、片方は成功し、もう片方は拒否されます。競合が多いほど拒否も増えるため、スケールの鍵はスキーマ設計によって競合を減らすことにあります。

OCC では、トランザクションは他のトランザクションがロックを解放するのを待ってブロックされることはありません。コミット時に競合が検出されると、中断されたトランザクションには即座にエラーが返り、アプリケーションは最新のデータで再試行できます。通常は次の試行でミリ秒以内に成功します。

OCC がアプリケーションに与える意味

OCC は、いくつかの重要な点で責任をアプリケーション層に移します。DSQL 上でスケーラブルなアプリケーションを構築するには、最初からこうした挙動を前提に設計する必要があります。

1. すべての書き込みパスで再試行を扱う必要がある

OCC 競合でトランザクションが中断されると、アプリケーションは SerializationError を受け取ります。これはバグでも例外的な状況でもなく、同時実行下における通常の動作の一部です。アプリケーションはこのエラーを捕捉し、最新のデータでトランザクションを再試行する必要があります。

2. トランザクションの範囲が競合確率に直結する。

トランザクションの保持時間が長くなるほど、また操作する行が多くなるほど、コミット前に別のトランザクションが重複するデータを変更する可能性が高くなります。DSQL 上のスケーラブルなアプリケーションでは、トランザクションを短く対象を絞ったものにします。

3. スキーマ設計が競合の発生範囲を決める

複数の独立した操作がすべて同じ行を更新する場合、論理的には無関係であっても競合が発生します。ステータスの更新、カウンターのインクリメント、タイムスタンプの更新はいずれも独立した操作です。しかし、これらが同じ行を共有していると、互いに直列化されてしまいます。OCC の内部的な仕組みを深く知りたい場合は、Concurrency Control in Amazon Aurora DSQL を参照してください。

4. すべてのワークロードが同じように競合するわけではない

読み取り専用の操作は競合しません。異なる行への挿入も競合しません。競合を引き起こすのは同じ行への同時変更だけです。スケーラブルなアプリケーションでは、最初の 3 つのカテゴリに該当する操作の割合を最大化します。

操作の種類 同時書き込みと競合するか?
読み取り専用トランザクション しない
異なる行への挿入 しない
異なる行の更新 しない
同じ行の更新 する(片方のトランザクションが中断)

強いスナップショット分離

Aurora DSQL は強いスナップショット分離で動作します。各トランザクションは、トランザクション開始時に取得されたデータベースの一貫したスナップショットから読み取ります。つまり、トランザクション内での読み取りは安定しており、コミットされていないデータや、処理中に他のトランザクションがコミットした変更を参照することはありません。コミット時の OCC による競合検出と組み合わさることで、ダーティリードやノンリピータブルリードへの対策コードを書くことなく、一貫した読み取りが得られます。

これはアプリケーションにとって直接的な影響をもらします。レコードを読み取るとき、子レコードを挿入する前に親行の存在を確認するとき、あるいはトランザクション内で関連する行のセットを読むとき、全て一貫した時点のデータを読み取っています。これらの操作に対して、防御的な再読み取りやアプリケーションレベルの一貫性チェックを追加する必要はありません。スナップショットはトランザクションの実行中ずっと安定しています。

主キー戦略

適切な識別子の型を選ぶ

Aurora DSQL は主キーとして UUID、IDENTITY 列、シーケンスをサポートします。推奨されるデフォルトは UUID です。UUID は調整が不要で、書き込みを均一に分散させます。IDENTITY 列やシーケンスなど、人間が読みやすい整数 ID (請求書番号やチケット ID など)が必要な場合にのみ使用してください。ハイブリッドパターンで両方を組み合わせることもできます。UUID の主キーに加えて、シーケンスで生成した表示用番号を持たせる方法です。

UUID: 主キーのデフォルト

CREATE TABLE users (
    user_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    ...
);

トレードオフとして、UUID は作成時刻順にソートできません(時系列クエリには別途 created_at のインデックスが必要)。また、口頭での伝達が難しくなります(「user 47291」と「user abc-123…」を比べてみてください)。

整数 ID が必要な場合

DSQL は、分散環境向けに設計されたキャッシュモデルを持つ IDENTITY 列をサポートしています。キャッシュサイズは、各セッションがローカルで事前に確保する連番 ID の数を制御し、厳密なグローバル順序を犠牲にしてリージョン間の調整を削減します。任意のキャッシュ値が有効な標準の PostgreSQL とは異なり、DSQL には目的の異なる 2 つのモードがあります。厳密な順序付き割り当てのための CACHE 1 と、高スループットな分散割り当てのための CACHE >= 65536 です。どちらのモードにするかは最初に選択してください。

ID 列

ユースケース 推奨アプローチ
高スケールなワークロードの主キー UUID(gen_random_uuid())
順序が重要な、人が読める ID(請求書、アカウント ID) CACHE 1 の ID 列
高い挿入レートでの、人が読める ID CACHE >= 65536 の ID 列
内部用の主キー + ユーザー向けの番号 ハイブリッド: UUID 主キー + シーケンスの表示 ID

DSQL のような分散システムで連番 ID を生成するには、リージョン間の調整が必要です。IDENTITY 列はこれを管理するためにキャッシュモデルをしようしています。各セッションがローカルで ID のブロックを事前に確保するため、行を挿入するたびにリージョン間のラウンドトリップを行う必要がありません。選択するキャッシュサイズによって、厳密なグローバル順序と挿入スループットのトレードオフが決まります。高頻度の挿入には CACHE >= 65536 が適切です。各セッションが ID のブロックをローカルで事前確保し、挿入ごとにリージョン間の調整なしで払い出します。

-- High-frequency inserts: distributed allocation, no per-insert coordination
CREATE TABLE posts (
    post_id BIGINT GENERATED ALWAYS AS IDENTITY (CACHE 65536),
    ...
);

連番順序を保つことが重要で、挿入レートが中程度(毎秒数百件以下)の場合は CACHE 1 が適切です。

-- Low-frequency inserts where sequential ordering is important
CREATE TABLE invoices (
    invoice_id BIGINT GENERATED ALWAYS AS IDENTITY (CACHE 1),
    ...
);

どちらのモードを使用する場合でも、連続しない ID 値を扱えるようにアプリケーションを設計してください。ロールバック、セッションの終了、および同時割り当てにより、欠番が生じるのは正常な動作です。

ハイブリッドパターン: UUID 主キー + シーケンスの表示 ID

UUID が分散書き込みを効率よく処理し、シーケンスがユーザー向けの番号を生成します。DSQL は各 DDL ステートメントを独立したトランザクションで実行するため、シーケンスとテーブルは別々に作成します。

-- Step 1: Create the sequence (its own transaction)
CREATE SEQUENCE post_display_seq CACHE 65536;

-- Step 2: Create the table referencing the sequence (separate transaction)
CREATE TABLE posts (
    post_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),      -- Internal: distributed
    post_number BIGINT DEFAULT nextval('post_display_seq'),  -- External: human-readable
    ...
);

post_id はすべての結合や関連テーブル間の参照列として使います。post_number は URL に表示したりユーザーに共有したりする番号です。

クイックリファレンス: 識別子の型の選択

ユースケース 推奨アプローチ
高スケールなワークロードの主キー UUID(gen_random_uuid())
順序が重要な、人が読める ID(請求書、アカウント ID) CACHE 1 の ID 列
高い挿入レートでの、人が読める ID CACHE >= 65536 の ID 列
内部用の主キーとユーザー向けの番号 ハイブリッド: UUID 主キーとシーケンスの表示 ID

OCC のためのスキーマ設計

テーブルの構造が OCC の競合率を直接左右し、競合率が実効スループットを決めます。これは Aurora DSQL 上で構築する際に最も影響の大きい設計判断の 1 つです。

ホット行の問題

頻繁に更新される複数のフィールドが、エンティティごとに 1 つの行を共有する素朴なスキーマを考えてみましょう。

-- PROBLEMATIC: Everything in one row per entity
CREATE TABLE accounts (
    account_id UUID PRIMARY KEY,
    ...
    request_count INT DEFAULT 0,        -- Updated on every request
    active_connections INT DEFAULT 0,   -- Updated constantly
    last_active TIMESTAMP,              -- Updated on every action
    total_spend DECIMAL DEFAULT 0,      -- Updated on every transaction
    ...
);

トラフィックが急増すると、すべてのリクエストが request_count を更新します。同時に、接続イベントが active_connections を更新します。課金イベントが total_spend を更新します。さらに、あらゆるアクションが last_active を更新します。これらがすべて同じ行を対象とするため、OCC によってほとんどが中断されます。

中程度の同時実行数であっても、このスキーマはピーク時に 5 パーセントを超える競合率を生みます。

解決策: 競合パターンで分離する

論理的に独立した操作が行を共有しないよう、テーブルを分割します。

-- Cold data: updated rarely
CREATE TABLE accounts (
    account_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name TEXT, email TEXT, plan TEXT,
    ...
);

-- Hot counters: isolated from cold data
CREATE TABLE account_stats (
    account_id UUID PRIMARY KEY,
    request_count INT DEFAULT 0,
    active_connections INT DEFAULT 0,
    ...
);

-- Append-only: ZERO conflicts
CREATE TABLE billing_events (
    account_id UUID NOT NULL,
    event_id UUID NOT NULL,
    PRIMARY KEY (account_id, event_id)
);

これにより、設定の更新が使用量カウンターのインクリメントと競合しなくなります。課金イベントは専用テーブルへの挿入となり、新しい行の挿入は他の新しい行の挿入とは競合しません。競合率は 5 パーセント超から 1 パーセントを大きく下回る水準へと下がります。

追記のみのパターンで競合を緩和する

行が挿入されるだけで更新されないテーブルは、OCC 環境下では本質的に競合が発生しません。この気づきは、状態変化のモデリング方法を変えます。

-- Instead of updating a "status" column in place, track state as events:
CREATE TABLE resource_events (
    event_id UUID DEFAULT gen_random_uuid(),
    resource_id UUID NOT NULL,
    event_type TEXT NOT NULL,   -- 'created', 'updated', 'suspended', 'deleted'
    ...
    PRIMARY KEY (resource_id, created_at, event_id)
);

DSQL の設計上の制約を理解する

Aurora DSQL は、分散スケーラビリティを実現するために意図的なアーキテクチャ上のトレードオフを行っています。PostgreSQL の一部の機能はサポートされていないか、異なる挙動をします。最新のリストについては Aurora DSQL の PostgreSQL 互換性ドキュメントを参照してください。以降のセクションでは、アプリケーション設計に最も影響を与える可能性が高い制約について説明します。

トリガーを使わないカウンター更新

トリガーがサポートされないため、以前は自動的に処理されていたカウンター更新は、アプリケーションコード内に明示的に記述することになります。これはむしろ利点です。ロジックが可視化され、テスト可能になり、トランザクション内でいつ実行するかを正確に制御できます。

async def publish_post(conn, user_id, content, media_urls):
    async with conn.transaction():
        post_id = await conn.fetchval('''
            INSERT INTO posts (user_id, content, media_urls, status)
            VALUES ($1, $2, $3, 'published')
            RETURNING post_id
        ''', user_id, content, media_urls)

        # What a trigger used to do --- now explicit and within the same transaction
        await conn.execute('''
            UPDATE user_stats SET post_count = post_count + 1
            WHERE user_id = $1
        ''', user_id)

        return post_id

参照整合性

Aurora DSQL は外部キー制約をサポートしており、データベースレベルで参照整合性を強制できます。一方、分散データベースにおいて外部キーが追加する操作ごとに追加読み取りやトランザクション制限のオーバーヘッドを避けるため、参照整合性をアプリケーション層で扱うこともできます。ただしその場合、存在しない親レコードを参照する子レコードの挿入を防ぐものは何もありません。

現実的に対処しましょう。

async def create_order_item(conn, user_id, order_id, product_id, quantity):
    async with conn.transaction():
        # Application-enforced FK: verify parent exists
        order = await conn.fetchrow(
            'SELECT order_id, status FROM orders WHERE order_id = $1', order_id)
        if not order:
            raise NotFoundError("Order does not exist")
        ...
        return await conn.fetchval('''
            INSERT INTO order_items (order_id, user_id, product_id, quantity)
            VALUES ($1, $2, $3, $4) RETURNING item_id
        ''', order_id, user_id, product_id, quantity)

リトライロジックとエラー分類

すべてのエラーがリトライに値するわけではありません。DSQL 上の堅牢なアプリケーションは、エラーを分類して適切に対応します。

リトライされる操作を安全に再実行できるようにする

OCC のリトライは、同じ操作が複数回実行されることを意味します。カウンターのインクリメントのように状態を積み上げる操作は、無条件にリトライすると誤った結果を生みます。ナチュラルキーとともに ON CONFLICT DO NOTHING を使って挿入を安全にし、副作用は挿入が新規であったかどうかに基づいて制御してください。

ナチュラルなユニーク制約がない操作には、クライアント側で生成した冪等性キーを使います。

CREATE TABLE payments (
    payment_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    idempotency_key TEXT NOT NULL,
    account_id UUID NOT NULL,
    UNIQUE(idempotency_key, account_id)
    ...
);
async def process_payment(conn, idempotency_key, account_id, amount):
    async with conn.transaction():
        result = await conn.fetchrow('''
            INSERT INTO payments (idempotency_key, account_id, amount)
            VALUES ($1, $2, $3)
            ON CONFLICT (idempotency_key, account_id) DO NOTHING
            RETURNING payment_id
        ''', idempotency_key, account_id, amount)

        if result:
            # First execution --- apply side effects
            await conn.execute('''
                UPDATE account_stats SET total_spend = total_spend + $1
                WHERE account_id = $2
            ''', amount, account_id)
            return {"status": "created"}
        return {"status": "already_processed"}

エラー分類

リトライするかどうかを判断する前に、エラーを 3 つのカテゴリに分類します。

  • バックオフしてリトライ: OCC 競合(asyncpg では SerializationError)。同時実行下で想定される事象であり、リトライしても安全です。
  • 再接続してリトライ: 接続の失敗(ConnectionDoesNotExistErrorInterfaceError)。リトライの前に新しい接続を取得します。
  • 決してリトライしない: 制約違反(UniqueViolationErrorCheckViolationError)。これらは一時的な状態ではなくロジックエラーを示します。
  • 操作が冪等な場合のみリトライ: タイムアウト(QueryCanceledError)。トランザクションの状態が不明なため、再実行しても安全な操作の場合にのみリトライします。

正確な例外クラスはドライバーのバージョンによって異なる場合があるため、使用するドライバーのドキュメントを参照してください。

指数バックオフとジッターを用いたリトライ

より詳しい議論については、指数バックオフとジッターに関する Marc Brooker のブログ記事を参照してください。

async def execute_with_retry(pool, operation, max_retries=3, base_delay_ms=10):
    for attempt in range(max_retries + 1):
        try:
            async with pool.acquire() as conn:
                return await operation(conn)
        except SerializationError:
            if attempt == max_retries: raise
            delay = (base_delay_ms * (2 ** attempt)) / 1000.0
            jitter = random.uniform(0, delay * 0.5)
            await asyncio.sleep(delay + jitter)
        except (ConnectionDoesNotExistError, InterfaceError, OSError):
            if attempt == max_retries: raise
            await asyncio.sleep(0.1)
        except (UniqueViolationError, CheckViolationError):
            raise  # Never retry business logic errors

リージョンフェイルオーバーの処理

リージョンが正常でなくなった場合、DNS(たとえば Amazon Route 53 のヘルスチェックとフェイルオーバールーティング)を使って、アプリケーショントラフィックを正常なリージョンへ切り替えることを推奨します。これにより、クエリがリージョン間で暗黙的にルーティングされる際に発生する、予期しないレイテンシーの急上昇を回避できます。DNS フェイルオーバーが利用できない場合は、アプリケーションレベルでセカンダリエンドポイントにフォールバックする方法が暫定的な対処として使えます。詳しい実装ガイドについては、Implement multi-region endpoint routing for Amazon Aurora DSQLGitHub 上のサンプルコードを参照してください。

DDL はトランザクション非対応

DSQL では、DDL と DML を 1 つのトランザクション内で混在させることはできません。

-- THIS FAILS:
BEGIN;
CREATE TABLE new_feature (...);
INSERT INTO new_feature VALUES (...);
COMMIT;

-- DO THIS INSTEAD (two separate operations):
CREATE TABLE new_feature (...);
-- Then:
BEGIN;
INSERT INTO new_feature VALUES (...);
COMMIT;

expand-contract パターン

ブール値の is_active フラグを置き換えるために status 列を追加するケースを考えます。これには 3 つのフェーズが必要です。

フェーズ 1 — Expand(拡張): DEFAULT 値を指定せずに新しい列を追加します(DSQL では列の追加とデフォルト値の設定を別々の操作として行う必要があります)。

-- Step 1: Add the column
ALTER TABLE accounts ADD COLUMN status TEXT;

-- Step 2: Populate existing rows in batches (separate transaction)
UPDATE accounts SET status = CASE WHEN is_active THEN 'active' ELSE 'inactive' END WHERE status IS NULL LIMIT 500;

フェーズ 2 — Migrate(移行): すべての挿入と更新で is_activestatus の両方に書き込むアプリケーションコードをデプロイします。二重書き込みのデプロイがすべてのインスタンスで安定したら、読み取りパスを is_active から status に切り替えます。

async def create_account(pool, account_id, active: bool):
    async with pool.acquire() as conn:
        await conn.execute('''
            INSERT INTO accounts (account_id, is_active, status)
            VALUES ($1, $2, $3)
        ''', account_id, active, 'active' if active else 'inactive')


async def set_account_active(pool, account_id, active: bool):
    async with pool.acquire() as conn:
        await conn.execute('''
            UPDATE accounts
            SET is_active = $2,
                status = $3
            WHERE account_id = $1
        ''', account_id, active, 'active' if active else 'inactive')

フェーズ 3 — Contract(縮小): すべての読み取りが新しい列を使うようになったら、古い列を削除します。

ALTER TABLE accounts DROP COLUMN is_active;

移行のベストプラクティス

  • スキーマの変更はコードの変更とは分けてデプロイします。
  • データのバックフィルは小さなトランザクション(100~500 行)にバッチ化し、OCC の急増を防ぎます。
  • 移行中は競合率を監視します。急増したら、バッチのレートを落とします。
  • 新しいスキーマをすべてのリージョンで利用する前に、DDL がリージョン全体に伝播するまで時間を置きます。
  • 追加を先行させます。まず列を追加し、次にコードをデプロイし、それから古い列を削除します。

分散環境でのクエリパフォーマンス

トランザクションを短く保つ

長いトランザクション(読み書きする行が多い)ほど検証フェーズが長くなり、競合確率も高まります。

# BAD: External API call inside the transaction
async with conn.transaction():
    order = await conn.fetchrow('SELECT * FROM orders WHERE id = $1', order_id)
    shipping_cost = await call_shipping_api(order)  # 200ms external call!
    await conn.execute('UPDATE orders SET shipping = $1', shipping_cost)

# GOOD: External work outside, database work inside
order = await conn.fetchrow('SELECT * FROM orders WHERE id = $1', order_id)
shipping_cost = await call_shipping_api(order)
async with conn.transaction():
    await conn.execute('UPDATE orders SET shipping = $1 WHERE id = $2',
                       shipping_cost, order_id)

N+1 クエリを避ける

各クエリは DSQL へのネットワークオーバーヘッドを伴います。N+1 パターンは、ローカルデータベースに比べて相対的にコストが高くなります。

# BAD: 101 round trips
posts = await conn.fetch('SELECT * FROM posts WHERE user_id = $1 LIMIT 100', uid)
for post in posts:
    comments = await conn.fetch(
        'SELECT * FROM comments WHERE post_id = $1', post['post_id'])

# GOOD: 1 round trip
results = await conn.fetch('''
    SELECT p.*, c.comment_id, c.content as comment_content
    FROM posts p
    LEFT JOIN comments c ON p.post_id = c.post_id
    WHERE p.user_id = $1
    ORDER BY p.created_at DESC LIMIT 100
''', uid)

大きな操作をバッチ化する

Aurora DSQL はトランザクションあたり 3,000 行の変更上限が設けられているため、大きな操作は分割する必要があります。変更をバッチ化しましょう。

# BAD: One transaction touching 100,000 rows
async with conn.transaction():
    await conn.execute('UPDATE users SET tier = $1 WHERE created_at < $2',
                       'legacy', cutoff_date)

# GOOD: Chunked into small transactions
while True:
    async with conn.transaction():
        updated = await conn.fetchval('''
            WITH batch AS (
                SELECT user_id FROM users
                WHERE created_at < $1 AND tier != 'legacy'
                LIMIT 200
            )
            UPDATE users SET tier = 'legacy'
            WHERE user_id IN (SELECT user_id FROM batch)
            RETURNING count(*)
        ''', cutoff_date)

        if updated == 0:
            break

モニタリングと可観測性

DSQL 上のソーシャルアプリケーションでは、次のメトリクスが設計の健全性を教えてくれます。

メトリクス 健全 要調査 要再設計
OCC 競合率 低い 中程度 高い
コミットの p99 レイテンシー 低い 中程度 高い
接続プールの使用率 低い 中程度 高い
リトライ設定回数内でトランザクションが成功しなかった割合 ほぼゼロ 無視できない程度 頻発

ベストプラクティスのまとめ

スキーマ設計

  • ホット(頻繁に更新される)データとコールド(まれにしか更新されない)データを別々のテーブルに分けます。
  • ログ、イベント、インタラクションには追記のみのテーブルを使います(OCC 競合はゼロ)。
  • 主キーには UUID v4 を選びます。均一に分散するため調整が不要です。
  • 人が読める整数 ID を高スループットで必要とする場合は、CACHE >= 65536 の IDENTITY 列またはシーケンスを使います。
  • スケーラビリティとユーザー向けの読みやすい番号の両方が必要な場合は、ハイブリッドパターン(UUID 主キーとシーケンスの表示 ID)を使います。
  • アクセスパターンとパーティションパターンに合った複合キーを設計します。

トランザクション

  • トランザクションは短く保ちます。
  • 外部の作業(API 呼び出し、計算)はトランザクションの境界の外に移します。
  • OCC 競合は指数バックオフ + ジッターでリトライします。
  • データを変更しないクエリには BEGIN READ ONLY を使います。
  • 関連する書き込みは 1 つのトランザクションにまとめ、競合の発生面を減らします。

冪等性

  • 挿入にはクライアント側で生成した UUID またはナチュラル複合キーを使います。
  • 自然な重複排除の仕組みがない変更エンドポイントには、冪等性キーを追加します。
  • ON CONFLICT DO NOTHING の戻り値を確認し、初回実行かリトライかを判別します。
  • 重複排除なしにカウンターのインクリメントを無条件にリトライしないでください。

エラー処理

  • エラーを、リトライ可能(OCC 競合、接続断)かリトライ不可(制約違反)かで分類します。
  • リトライされる操作が再実行しても安全であることを確認します。
  • 接続の失敗に備えてリージョンのフェイルオーバーを実装します。
  • スキーマのデバッグのために競合の詳細をログに残します。

パフォーマンス

  • 高頻度の読み取りクエリにはカバリングインデックスを使います。
  • 書き込みの多いテーブルではインデックスを絞ります(それぞれが OCC の検証コストを増やします)。
  • OFFSET よりもキーセットページネーションを優先します。
  • N+1 クエリのパターンを避けます。代わりに JOIN やバッチ取得を使います。
  • 大きなバッチ操作は小さなトランザクションに分割します。

まとめ

本記事では、Amazon Aurora DSQL でスケールするアプリケーションの設計方法を学びました。アクティブ-アクティブなマルチリージョン書き込みと OCC がアプリケーションにどのような影響を与えるかを確認しました。また、主キーの選び方やホットデータとコールドデータの分離による競合の抑制方法、トランザクションを短く保ちシリアライゼーションエラーを安全に再試行する方法、そして 3,000 行の変更上限を守りながら expand-contract パターンでスキーマを進化させる方法を学びました。Aurora DSQL がコンセンサス、レプリケーション、フェイルオーバーを担うため、皆さんはデータモデリングと堅牢なトランザクション処理に集中できます。

Aurora DSQL を使い始めるには、AWS マネジメントコンソールを使うか、Aurora DSQL のドキュメントにある入門ガイドに従ってください。まずは高スループットなテーブル 1 つにこれらのパターンを適用し、OCC の競合率を観察しながら、そこから拡大していきましょう。

著者について

Tejas Dubey

Tejas Dubey

Tejas は、エンタープライズのお客様が回復力とセキュリティを備え、AI にも対応したクラウドアーキテクチャを構築できるよう支援する AWS のテクニカルアカウントマネージャーです。新しいテクノロジーを実際のビジネス価値に変えることに情熱を注いでいます。仕事を離れると、息子を追いかけたり、ピックルボールをしたり、最新のガジェットをいじったりしています。

Edigar Chikwama

Edigar Chikwama

Amazon Web Services のテクニカルアカウントマネージャーです。エンタープライズのお客様と協力し、AWS 環境の最適化、運用面の回復力の向上、ベストプラクティスの採用を支援しています。組織が AWS 上でスケーラブルで Well-Architected なソリューションを構築できるよう支援することに情熱を注いでいます。

Rajesh Kantamani

Rajesh Kantamani

シニアデータベーススペシャリスト SA です。Amazon Web Services 上でのデータベースソリューションの設計、移行、最適化を支援し、スケーラビリティ、セキュリティ、パフォーマンスの実現を専門としています。余暇には、家族や友人と屋外で過ごすことを楽しんでいます。


この記事は Kiro が翻訳を担当し、Solutions Architect の Kenta Nagasue がレビューしました。