Redshift Serverlessで「なんか高い」を60%削減した8ヶ月の記録

「思ったより高い」「なんでこのクエリが遅い」——Redshift Serverless移行後の洗礼、うちのチームも経験しました。WPU設定・クエリ最適化・Auto Suspendの試行錯誤で月コスト60%削減に至った実践記録です。

先日、社内のデータ分析基盤をRedshift ProvisionedからRedshift Serverlessに移行して約8ヶ月が経った。最初の2ヶ月は「思ったより高い」「なんでこのクエリが遅い」という連続で、チームの雰囲気もちょっと暗かった。でも地道に設定を見直していくうちに、月のコストが移行当初のピーク比で約60%削減できた。正直まだ最適化の余地はあると思っているけど、今時点の知見をまとめておきたい。

同じくRedshift Serverlessで「なんか高い」「なんか遅い」と感じてるチームの参考になれば。ちなみに以前書いたAurora→Redshift Zero-ETL統合の記事Aurora→Redshift Zero-ETL、4時間のバッチが15分になった話と合わせて読むと文脈が掴みやすいかもしれない。

Redshift Serverlessの構成と運用アーキテクチャ

まず今うちが使っている構成を図で示す。マルチAZ構成でワークグループを複数に分けているのがポイントで、これがコスト削減の一番の鍵になった。

graph TB
    subgraph VPC["VPC (10.0.0.0/16)"]
        subgraph AZ1["AZ-1a"]
            WG1["Workgroup: analytics\n(WPU: 32-256)"]
            WG2["Workgroup: etl-batch\n(WPU: 64-512)"]
        end
        subgraph AZ2["AZ-1c"]
            WG1
            WG2
        end
        subgraph PRIV["Private Subnet"]
            EP1["VPC Endpoint\n(Redshift)"]
        end
    end

    subgraph NS["Namespace: prod-analytics"]
        DB1[("analytics_db")]
        DB2[("staging_db")]
    end

    subgraph DataSources["データソース"]
        S3["S3\n(Data Lake)"]
        AURORA["Aurora PostgreSQL\n(Zero-ETL)"]
        KINESIS["Kinesis\n(ストリーム)"]  
    end

    subgraph Consumers["コンシューマー"]
        QS["QuickSight"]
        JUPYTER["SageMaker\nJupyterLab"]
        APP["社内BIアプリ"]
        DBT["dbt Core 2.0"]
    end

    subgraph Monitoring["監視"]
        CW["CloudWatch"]
        BUDGETS["AWS Budgets"]
        CA["Cost Anomaly\nDetection"]
    end

    S3 --> WG2
    AURORA --> NS
    KINESIS --> WG2
    WG1 --> DB1
    WG2 --> DB1
    NS --> WG1
    NS --> WG2
    EP1 --> WG1
    EP1 --> WG2
    WG1 --> QS
    WG1 --> JUPYTER
    WG1 --> APP
    WG2 --> DBT
    WG1 --> CW
    WG2 --> CW
    CW --> BUDGETS
    CW --> CA

移行当初はワークグループを1つにまとめていた。BIのアドホッククエリもdbtのバッチ処理も同じワークグループで動かしていたせいで、夜間バッチの重いクエリでWPUが上限まで張り付き、そのまま翌朝まで高コスト状態が続くという地獄だった。

「ワークグループを用途別に分ける」という一見シンプルな解が、実は最大のコスト改善になった。バッチ処理用のワークグループは夜間だけ高WPUで動かし、昼間は低WPUに落とすスケジューリングを入れた。個人的には「そんな単純な話か」と思いつつも、これだけで雰囲気がガラっと変わったのは正直驚きだった。

WPU設定の罠とAuto Suspendの正しい使い方

Redshift Serverlessで最初にハマったのがWPU(Redshift Processing Units)の設定だ。「Serverlessだから勝手にスケールするんでしょ?」という認識でいたら、WPUには最小値と最大値の設定があって、最小値を高くしすぎるとアイドル中もコストが発生するという罠にはまった。これ、最初に誰かに教えてほしかった。

-- ワークグループのWPU設定確認
SELECT
    workgroup_name,
    base_capacity,
    max_capacity,
    status
FROM
    sys_serverless_workgroups
ORDER BY
    workgroup_name;

実行結果はこんな感じ:

 workgroup_name | base_capacity | max_capacity | status
----------------+---------------+--------------+--------
 analytics      |            32 |          256 | ACTIVE
 etl-batch      |            64 |          512 | ACTIVE

移行当初、analyticsワークグループのbase_capacityを128に設定していた。「BIが遅いと文句言われたくない」という心理から過剰スペックにしていたわけだが、これが痛かった。アイドル時も128 WPU分の課金が発生し続けていた。コスト表を見たときは軽く絶望した。

2026年時点でのWPU料金は以下の通り(東京リージョン):

設定項目変更内容月額概算インパクト
base_capacity128 WPU → 32 WPU約▲42万円/月
max_capacity1024 WPU → 256 WPUピーク制御で▲8万円/月
Auto Suspendなし → 300秒約▲15万円/月

Auto Suspendは「アイドル状態が続いたらクラスターを停止する」設定だが、デフォルトでは無効になっている。これを知らずに放置していたのも痛かった。夜中の2時から6時はほぼ誰もクエリを投げていないのに、WPUが動き続けていた。地味に最悪な設定ミスだったと思う。

import boto3

client = boto3.client('redshift-serverless', region_name='ap-northeast-1')

# Auto Suspendを300秒に設定(分析用ワークグループ)
response = client.update_workgroup(
    workgroupName='analytics',
    configParameters=[
        {
            'parameterKey': 'auto_mv',
            'parameterValue': 'true'
        },
        {
            'parameterKey': 'enable_user_activity_logging',
            'parameterValue': 'true'
        }
    ],
    # 2026年からサポートされたsuspend設定
    idleTimeout=300  # 秒単位
)
print(f"Updated: {response['workgroup']['workgroupName']}")

300秒(5分)に設定してから、夜間のコストがグっと落ちた。ただし注意点として、Auto Suspend後の最初のクエリはウォームアップ時間(30〜60秒)が発生する。BIダッシュボードの初回ロードが遅いと苦情が来る場合は、クエリスケジューラーで定期的なウォームアップクエリを入れるか、朝9時にAuto Resumeする仕組みを入れると吉。

うちのチームは EventBridge Scheduler でシンプルに解決した:

# Lambda関数でウォームアップ(EventBridge Schedulerから起動)
import boto3
import psycopg2

def handler(event, context):
    """毎朝8:55にワークグループを起こしておく"""
    conn = psycopg2.connect(
        host='your-workgroup.account.ap-northeast-1.redshift-serverless.amazonaws.com',
        port=5439,
        database='analytics_db',
        user='admin',
        password='...',
        connect_timeout=60
    )
    cur = conn.cursor()
    cur.execute("SELECT 1")  # ウォームアップクエリ
    cur.close()
    conn.close()
    print("Warm-up complete")

SELECT 1 1行でウォームアップになるのはちょっと笑えるけど、ちゃんと機能している。

クエリ最適化で見えた本当のボトルネック

正直、WPU設定の見直しよりもクエリ最適化の方が効果が大きかった。SYS_QUERY_HISTORYビューを掘り返したら、全コストの約40%が3つのクエリから発生していた。「まさかこの3本が…」というやつで、犯人はいつも意外なところにいる。

-- コストの高いクエリTOP10を特定
SELECT
    query_id,
    query_text,
    elapsed_time / 1000000.0 AS elapsed_seconds,
    spilled_bytes / 1024 / 1024 / 1024.0 AS spilled_gb,
    returned_rows,
    status
FROM
    SYS_QUERY_HISTORY
WHERE
    start_time >= DATEADD(day, -7, GETDATE())
    AND status = 'success'
    AND query_type = 'SELECT'
ORDER BY
    elapsed_time DESC
LIMIT 10;

spilled_bytesが巨大なクエリが3つあって、これがWPUを長時間占有していた。Redshiftではディスクスピルが発生するとパフォーマンスが劇的に悪化し、それだけWPU使用時間が長くなってコストに直結する

原因を調べると、こういう状況だった:

  • ソートキーが設定されていないテーブルにFULL TABLEスキャンをかけていた
  • JOINするテーブルの統計情報が古く、最適でない実行計画になっていた
  • DISTSTYLEがAUTOのまま放置されていて、大きいテーブルがEVENになっていた

ANALYZEを手動で実行したら実行計画が変わって、一部のクエリは実行時間が1/3以下になった:

-- 統計情報の更新
ANALYZE sales_fact;
ANALYZE customer_dim;
ANALYZE date_dim;

-- ソートキーの確認と最適化
SELECT
    tablename,
    sortkey1,
    sortkey1_enc,
    diststyle,
    distkey
FROM
    SVV_TABLE_INFO
WHERE
    schema = 'public'
    AND tablename IN ('sales_fact', 'customer_dim')
ORDER BY
    size DESC;

売上ファクトテーブルにCOMPOUND SORTKEYを設定し直した結果がこちら:

xychart-beta
    title "クエリ実行時間の改善(秒)"
    x-axis ["売上集計", "顧客分析", "在庫レポート", "売上予測", "KPIダッシュボード"]
    y-axis "実行時間(秒)" 0 --> 300
    bar [245, 189, 312, 167, 98]
    line [67, 45, 89, 52, 28]

棒グラフが最適化前、折れ線が最適化後。平均して73%の実行時間削減。これはマジでうれしかった。クエリが速くなるとチームの士気まで上がるから不思議だ。

dbt連携と具体的なコスト管理の実装

dbt Core 2.0移行後の知見記事でも触れたけど、dbtとRedshift Serverlessの組み合わせは相性がいい反面、モデルの実体化戦略を間違えるとコストが跳ね上がる。ここは本当にハマりポイントなので丁寧に書いておく。

うちのチームで効いた設定はこれ:

# dbt_project.yml
models:
  analytics:
    staging:
      +materialized: view  # ステージングはビューで十分
      +schema: staging
    intermediate:
      +materialized: ephemeral  # 中間モデルはCTE化
    marts:
      +materialized: table
      +sort: ["event_date", "customer_id"]
      +dist: "customer_id"
      +schema: marts
    reports:
      +materialized: incremental  # レポートは増分処理
      +incremental_strategy: merge
      +unique_key: report_id
      +sort: ["report_date"]
      +dist: "region_code"

特にincrementalモデルへの切り替えが効いた。フルリフレッシュを毎回回していたモデルを増分処理に変えたら、dbtジョブ全体の実行時間が2時間から35分に短縮された。WPU使用時間がそのまま減るので、コストへの影響が大きい。これは「やってみたら予想以上だった」施策の筆頭だった。

コストモニタリングもCloudWatchメトリクスを使って自動化している:

import boto3
from datetime import datetime, timedelta

cw = boto3.client('cloudwatch', region_name='ap-northeast-1')

def get_wpu_usage(workgroup_name: str, hours: int = 24) -> dict:
    """過去N時間のWPU使用量を取得"""
    end_time = datetime.utcnow()
    start_time = end_time - timedelta(hours=hours)
    
    response = cw.get_metric_statistics(
        Namespace='AWS/Redshift-Serverless',
        MetricName='ComputeSeconds',
        Dimensions=[
            {'Name': 'WorkgroupName', 'Value': workgroup_name}
        ],
        StartTime=start_time,
        EndTime=end_time,
        Period=3600,  # 1時間単位
        Statistics=['Sum']
    )
    
    total_compute_seconds = sum(
        dp['Sum'] for dp in response['Datapoints']
    )
    
    # RPU-秒をコストに変換($0.375 per RPU-hour、東京リージョン)
    # 1 WPU = 1 RPU
    estimated_cost_usd = (total_compute_seconds / 3600) * 0.375
    
    return {
        'workgroup': workgroup_name,
        'total_compute_seconds': total_compute_seconds,
        'estimated_cost_usd': estimated_cost_usd,
        'period_hours': hours
    }

# 実行例
for wg in ['analytics', 'etl-batch']:
    usage = get_wpu_usage(wg)
    print(f"{usage['workgroup']}: "
          f"{usage['total_compute_seconds']:.0f}秒 "
          f"(${usage['estimated_cost_usd']:.2f})")

実行結果:

analytics: 28800秒 ($3.00)
etl-batch: 115200秒 ($12.00)

これをLambdaで毎日実行してSlackに通知している。月初に「今月のペースだと▲▲万円」という予測も一緒に流すようにしたら、チームのコスト意識が上がった。数字が見えると人間の行動って変わるんだなと改めて実感した。AWS Budgets・Cost Anomaly Detectionの実運用記事も参考になるかもしれない。

8ヶ月運用で見えてきたコスト推移

実際の月次コスト推移を示す。移行直後から徐々に最適化が進んでいった様子がわかる:

xychart-beta
    title "Redshift Serverless月次コスト推移(万円)"
    x-axis ["2025-11", "2025-12", "2026-01", "2026-02", "2026-03", "2026-04", "2026-05", "2026-06"]
    y-axis "コスト(万円)" 0 --> 60
    line [52, 55, 48, 41, 35, 28, 22, 21]

移行直後の2025年11〜12月がピークで55万円。そこから地道に最適化して、直近は21万円まで落ちてきた。ちょうど60%削減という数字になった。振り返ると「なぜあのまま放置していたのか」という施策ばかりで、少し複雑な気持ちでもある。

各施策のタイムラインと効果をまとめるとこうなった:

時期施策効果
2026年1月ワークグループ分離(analytics/etl-batch)-13%/月
2026年2月Auto Suspend有効化(300秒)-21%/月
2026年3月ソートキー・統計情報最適化-15%/月
2026年4月dbt増分モデル化-20%/月
2026年5月クエリキュー設定・優先度調整-8%/月
2026年6月ベースWPU削減(128→32)-5%/月

一番効いたのはAuto Suspendと統計情報の組み合わせ。地味だけど、これが大きかった。派手な施策より地味なチューニングの方が効くというのは、インフラあるあるかもしれない。

まとめ

Redshift Serverless最適化で学んだポイントを整理する。どれも「やってみれば簡単」なのに、知らないと何ヶ月も無駄にする類の話だと思っている。

  1. ワークグループは用途別に分ける — BIクエリとバッチ処理を同じワークグループで動かすのはコスト観点でアンチパターン。分離するだけで運用の柔軟性が大幅に上がる

  2. Auto SuspendとベースWPUが最大のコスト要因 — デフォルト設定のままだとアイドル時間の無駄が多い。300秒のAuto Suspend設定と、必要最小限のbaseWPU設定が基本

  3. スピルするクエリを潰せ — SYS_QUERY_HISTORYのspilled_bytesを定期的に確認して、ディスクスピルが発生しているクエリを特定・改善する。実行時間もコストも劇的に改善する

  4. dbtのモデル実体化戦略を見直す — view・ephemeral・incremental・tableを適切に使い分けるだけで、バッチ全体のWPU使用時間が大幅に短縮できる

  5. CloudWatchでコストの日次監視を自動化する — 月末に請求を見て驚くのではなく、日次で「このペースだと月▲▲万円」と把握できる仕組みを作る

正直、まだROWGROUPSやMaterialized ViewのRefreshタイミングの最適化など、試せていないことも残っている。次のフェーズで取り組もうと思っているところ。皆さんの環境ではどんな最適化が効きましたか?Xで話しかけてもらえると喜びます。

U

Untanbaby

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

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

関連記事