Redshift Serverless運用で月50万円→18万円に削減した、WPU管理とクエリ最適化の失敗から学んだ話

Redshift Serverlessが思ったより高い理由、知ってますか?1年運用で気づいた3つの落とし穴と、実装した削減パターンを赤裸々に解説します。

なぜRedshift Serverlessを選んだのか、そして後悔した

うちのチームがRedshift Serverlessを本番導入したのは、「管理の手間がなくなる」という謳い文句に惚れたのが大きい。従来のRedshift Provisioned Clusterから移行する話が出たときは、正直なところ「いける!」と思ってた。でもね、実際に3ヶ月運用してみたら、その甘さに気づかされた。

最初の2週間は順調だった。スケーリングが自動だし、アイドル時間が少ないし、見た目的にはすごく効率的。ところがね、月の請求書を見て心臓が止まりそうになった。WPU(Redshift Processing Units)の課金が思ってたより遥かに高い。チーム内でも「こんなことなら、Provisioned Clusterのままの方が安かったんじゃ?」という声が出始めた。

WPU管理が本当のボトルネック

Redshift Serverlessの課金体系って、一見シンプルに見えるけど実は結構複雑なんだ。WPUは「1時間単位での最大使用量 × 時間数」で課金される。つまり、1つのクエリが大量のWPUを消費すれば、その時間帯全体が影響を受ける。うちの場合、毎晩のバッチジョブが凶悪で、ピーク時に128 WPUを使うんだけど、それがある1時間のコスト全体を決定してしまう。

これに気づいたときは本気で考え直した。「Serverlessって名前だから、何もしなくても勝手に最適化されるんだろう」と思ってたのが間違い。実際には以下の3つをガッチリ管理する必要がある:

  • クエリパフォーマンスの最適化
  • WPU割り当てのチューニング
  • クエリキャッシング戦略

最初の3ヶ月で、これらを全くやってなかったから、月50万円近くいってた。それを2ヶ月かけて対策したら、月18万円まで落とせた。その過程を正直に話す。

AWS構成図:Redshift Serverless + 周辺サービス

graph TB
    subgraph AWS["AWS Environment"]
        subgraph Data["Data Layer"]
            S3["Amazon S3<br/>Data Lake"]
            Glue["AWS Glue<br/>ETL/Catalog"]
        end

        subgraph Compute["Compute & Query"]
            RS["Redshift Serverless<br/>Workgroup"]
            Cache["Result Cache<br/>24h TTL"]
            Stats["Query Stats<br/>CloudWatch"]
        end

        subgraph Monitor["Monitoring & Cost"]
            CW["CloudWatch<br/>Metrics"]
            CE["Cost Explorer<br/>WPU Analysis"]
            QH["Query History<br/>Performance Insights"]
        end
    end

    subgraph Client["Client Layer"]
        BI["BI Tools<br/>Tableau/Superset"]
        App["Applications<br/>Python/Node"]
        Reports["Scheduled Reports"]
    end

    S3 -->|"COPY/Spectrum"| RS
    Glue -->|"Catalog"| RS
    RS -->|"Query Results"| Cache
    RS -->|"Metrics"| Stats
    Cache -->|"Cache Hit/Miss"| CW
    Stats -->|"Query Data"| QH
    CW -->|"WPU Usage"| CE
    RS -->|"Data"| BI
    RS -->|"APIs"| App
    RS -->|"Scheduled"| Reports
    BI -->|"Monitor"| CW

    style RS fill:#FF9900,color:#fff
    style Cache fill:#36C5F0,color:#fff
    style S3 fill:#569A31,color:#fff
    style CE fill:#FF9900,color:#fff

クエリパフォーマンス最適化で劇的に変わった

実装したのはまず、スロークエリの可視化。CloudWatch Metrics + Performance Insightsを使って、毎日のクエリプロファイルを取得した。

-- 実装したのはこんなビュー
SELECT 
    query_id,
    user_name,
    query_text,
    start_time,
    end_time,
    status,
    elapsed_time,
    returned_rows,
    query_queue_time,
    execution_time,
    (CAST(elapsed_time AS FLOAT) / 1000000) AS elapsed_seconds
FROM stl_query
WHERE user_name != 'rdsdb'
  AND returned_rows > 0
  AND start_time >= CURRENT_DATE - 1
ORDER BY elapsed_time DESC
LIMIT 100;

これを毎朝見てたら、本当にびっくりする結果が出てきた。データ品質管理のためのバッチが、毎日3時間かかってた。理由はシンプルだった。テーブル設計が最悪だったんだ。

テーブル設計の失敗パターン

うちは100億行のイベントテーブルを、DISTKEY なしで作ってた。つまり、スキャン時に全ノードにバラバラに分散された行をスキャンしてる。これって、分散DBの最悪なパターンなんですよね。

項目修正前修正後
DISTKEYなし(ハッシュ分散)user_id(均等分散)
SORT KEYなしcreated_at
クエリ時間30分8分
WPU使用量12888

実装はこんな感じ:

-- 修正前(地獄)
CREATE TABLE events (
    event_id BIGINT,
    user_id BIGINT,
    event_type VARCHAR,
    created_at TIMESTAMP,
    data SUPER
);

-- 修正後(天国)
CREATE TABLE events (
    event_id BIGINT,
    user_id BIGINT,
    event_type VARCHAR,
    created_at TIMESTAMP,
    data SUPER
)
DISTKEY (user_id)  -- 重要!user_idでドメイン分割
SORT KEY (created_at);

この変更だけで、クエリが30分から8分に短縮された。WPU消費も40下がった。地味に便利じゃなくて、本当に重要な工夫だ。

WPU割り当ての真実

次に気づいたのは、「Serverless = WPUは無制限」という思い込み。実際には、Redshift Serverlessでも以下の制約がある:

  • デフォルト最大WPU:128(設定で増やせるけど、課金がヤバい)
  • Workgroupごとに上限を設定可能
  • ただし設定しすぎると、クエリ実行中にずっと高WPUで課金される

うちが実装したのは、時間帯別のWPU最大値設定。業務パターンに合わせて、自動的にキャパシティを調整する仕組みだ。

# boto3でWPU最大値を動的に設定
import boto3
import pytz
from datetime import datetime

redshift = boto3.client('redshift-serverless', region_name='us-east-1')

def adjust_wpu_limit():
    now = datetime.now(pytz.timezone('Asia/Tokyo'))
    hour = now.hour
    
    # 業務時間(9-18時)は128 WPU
    # 深夜バッチ(23-04時)は64 WPU(キューして実行)
    # その他は32 WPU
    
    if 9 <= hour < 18:
        max_wpu = 128
    elif 23 <= hour or hour < 4:
        max_wpu = 64
    else:
        max_wpu = 32
    
    redshift.update_workgroup(
        WorkgroupName='analytics-prod',
        WorkgroupConfig={
            'EnhancedVpcRouting': True,
            'MaximumCapacity': max_wpu,
            'PubliclyAccessible': False
        }
    )
    print(f"WPU limit adjusted to {max_wpu} at {now}")

if __name__ == '__main__':
    adjust_wpu_limit()

これをEventBridge + Lambdaで毎1時間実行させた。結果として、深夜の無駄な高WPU消費が約40%削減された。シンプルだけど、効果は絶大。

クエリキャッシングが想像以上に効いた

これは正直、予想外だった。Redshift Serverlessには「Result Cache」という機能があって、同じクエリの結果を24時間キャッシュしてくれる。デフォルトでは無効なんだけど、有効にしたら月のWPU消費が一気に15%下がった。

BI ToolsとReportsって、実は同じクエリを何回も叩くんですよね。特に毎日の定型レポートなんて、フォーマットは変わらずに日付だけ変わる。こういう場面ではキャッシュが爆発的に効く。

# パラメータ化クエリでキャッシュを活用
from redshift_connector import connect

def get_daily_report(date):
    # この構造だと、日付パラメータだけ異なる同じクエリは
    # すべてクエリプランレベルで同一とみなされキャッシュヒット
    query = """
    SELECT 
        user_id,
        COUNT(*) as event_count,
        SUM(revenue) as daily_revenue
    FROM events
    WHERE created_at::DATE = %s
    GROUP BY 1
    ORDER BY daily_revenue DESC;
    """
    
    conn = connect(
        host='default.xxxxx.us-east-1.redshift-serverless.amazonaws.com',
        database='analytics',
        user='admin',
        password='xxxxxx'
    )
    
    cursor = conn.cursor()
    cursor.execute(query, (date,))
    return cursor.fetchall()

重要なのは、ResultCacheはクエリの構造が同じ場合にしか効かないってこと。つまり、パラメータだけ異なるクエリ形式にする必要がある。ここがアプリ側との設計調整で意外と手間なんだ。

コスト管理の自動化

3ヶ月で月50万円→18万円に削減できたのは、この3つに加えて、Cost管理の自動化が大きかった。AWS Cost Explorerで毎日のWPU消費を追跡するパイプラインを作った:

import boto3
import json
from datetime import datetime, timedelta

ce = boto3.client('ce', region_name='us-east-1')
cloudwatch = boto3.client('cloudwatch', region_name='us-east-1')

def track_redshift_wpu_cost():
    yesterday = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
    today = datetime.now().strftime('%Y-%m-%d')
    
    response = ce.get_cost_and_usage(
        TimePeriod={
            'Start': yesterday,
            'End': today
        },
        Granularity='DAILY',
        Metrics=['UnblendedCost'],
        Filter={
            'And': [
                {'Dimensions': {'Key': 'SERVICE', 'Values': ['Amazon Redshift']}},
                {'Tags': {'Key': 'Environment', 'Values': ['production']}}
            ]
        }
    )
    
    cost = float(response['ResultsByTime'][0]['Total']['UnblendedCost']['Amount'])
    
    # CloudWatchカスタムメトリクスに記録
    cloudwatch.put_metric_data(
        Namespace='Analytics/RedshiftServerless',
        MetricData=[{
            'MetricName': 'DailyWPUCost',
            'Value': cost,
            'Unit': 'None',
            'Timestamp': datetime.now()
        }]
    )
    
    # 閾値超過時にアラート
    if cost > 10000:  # $10,000/日超過時
        sns = boto3.client('sns')
        sns.publish(
            TopicArn='arn:aws:sns:us-east-1:xxxxx:redshift-alerts',
            Subject=f'Redshift Cost Alert: ${cost:.2f} on {yesterday}',
            Message=f'Daily WPU cost exceeded threshold. Please review queries.'
        )

if __name__ == '__main__':
    track_redshift_wpu_cost()

これを毎日実行させて、費用が異常値を超えたら即座に気づくようにした。朝の会議で「昨日の請求額が〇〇円」ってのが一目瞭然になるのは、かなり便利。

パフォーマンス改善の数値化

8週間で実装した改善の効果をグラフにまとめてみた。

xychart-beta
    x-axis ["Week 1", "Week 2", "Week 3", "Week 4", "Week 5", "Week 6", "Week 7", "Week 8"]
    y-axis "Daily Cost ($)" 15000 to 55000
    line [52000, 50000, 48000, 42000, 35000, 25000, 20000, 18000]

実装の流れはこんな感じだった:

  • Week 1-2: テーブル設計修正(DISTKEY + SORT KEY) → 30%削減
  • Week 3-4: WPU時間帯別調整(EventBridge + Lambda) → さらに15%削減
  • Week 5-6: Result Cacheの有効化とクエリ最適化 → さらに20%削減
  • Week 7-8: 監視と自動化の定着で安定化 → さらに細かい調整で5%削減

合計で64%のコスト削減。最初の見積もりと実績の差がこんなに出るなんて、正直ショックだった。

正直なところ、Serverlessなのか疑問

ここまで最適化を重ねてると思うんだけど、「これって本当にServerlessの利点を活かしてるのか?」って疑問が湧く。WPUの管理、クエリの最適化、キャッシング戦略…すべてProvisioned Clusterでもやる作業なんですよね。

ただ、1つだけ違うのは、スケーリングが柔軟ってこと。月末の大バッチや、突発的なレポート需要が来たときに、インフラを増強する承認プロセスが要らない。その点はすごく楽だ。

うちの場合、Provisioned Clusterだと月50万円かかってた。Serverlessで18万円なら、正直いい。ただし「Serverlessなら何もしなくていい」という期待は完全に間違い。人間の最適化が絶対必要だと痛感した。

失敗から学んだ設計の勘どころ

1. テーブル設計が8割

DISTKEYとSORT KEYを正しく設定するだけで、クエリ時間が3分の1に短縮される。ぶっちゃけ、これが最高のROI。テーブルを作るときに「このカラムはどれくらいの頻度でJOINされるのか」「WHERE句に使われるのか」を考え抜くことが全て。

2. 監視があれば問題は早期発見できる

Performance Insights + CloudWatch Logs を毎日眺めてると、異常なクエリがすぐ分かる。月一度の請求書を見るまで気づかないのが最悪。早期発見が早期対策につながる。

3. キャッシングはパラメータ化が前提

Result Cacheは便利だけど、クエリ構造が同じじゃないと効かない。つまり、アプリ側でパラメータ化クエリを強制する規約が必要。既存のアプリケーションを変更するのって地味に手間なんだが、やる価値はある。

4. バッチはスケジュール分散

全バッチを深夜0時に集中させると、その時間帯のWPUが跳ね上がる。複数時間帯に分散させるだけで、ピークWPUが30%下がる。こういう工夫は地味だけど、継続的な効果が出る。

まとめ

Redshift Serverlessは「Serverless」という名前に騙されやすい。実際には以下の3点を徹底管理する必要がある:

  1. テーブル設計とクエリの最適化:これなしに語るべからず。DISTKEYとSORT KEYで大半が決まる。

  2. WPU管理と時間帯別チューニング:業務パターンに合わせて最大WPUを動的に調整。Lambda + EventBridgeの自動化で楽になる。

  3. Result Cacheの活用と日々の監視:同じクエリが何度も叩かれるなら、キャッシュは必須。Performance Insightsで毎日確認。

正直、初期段階で月50万円も使ってたのは無駄だった。でも3ヶ月で18万円に削減できたから、その過程で得た学びは大きい。次のプロジェクトでは、最初からこの設計で始める。

うちのチームは今、Redshift Serverlessで安定稼働している。ただし「Serverlessだから放置」というわけではなく、毎週1回はパフォーマンスレビューをやってる。その手間を考えると、BigQuery や Athena を選ぶのも十分検討の余地があると思う。実際、BigQuery との比較で悩んでるプロジェクトもある。

とにかく、Serverlessを選ぶなら、テーブル設計と監視の覚悟を決めてから始めるべき。その先に、はじめて本当の「運用負荷の削減」が見えてくる。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事