Redshift Serverlessを本番投入して月コストが2倍になった話と、その後8ヶ月で60%削減した記録
「Serverlessだから放置でいい」と思ったら予算アラートが炸裂。WPU設定・クエリ最適化・ストレージ戦略を見直して月コスト60%削減を達成するまでの実録です。
Redshift Serverlessを本番投入して最初に気づいた「設定しっぱなし地獄」
半年ほど前、うちのチームで分析基盤をプロビジョニング型からRedshift Serverlessに完全移行した。きっかけはシンプルで、クラスター管理に疲れたのと、利用時間が一日のうち4〜6時間程度しかない分析ワークロードに対してずっとインスタンスを起動しておくのが無駄だったから。
移行してみると最初はすごく快適だった。スケーリングを気にしなくていい、ノードの管理がない。でも3週間ほど経ったある日、Budgetアラートが飛んできた。月のRedshiftコストが予想の2.3倍になっていた。
当時の反省を一言で言うと、「Serverlessだから放置でいい」という思い込みが命取りだった。Serverlessというネーミングの罠にまんまとハマった形だ。その後8ヶ月かけてWPU設定・クエリ最適化・ストレージ戦略を見直し、最終的に月コストを60%以上削減できた。その過程で得た知見をまとめておきたい。
同じチームで運用しているAurora→Redshift Zero-ETL統合の実装記も合わせて読んでもらえると、データフロー全体像が掴みやすいと思う。
うちのチームのアーキテクチャ全体像
構成を整理するとこんな感じだ。BIツールのQuickSight・DataGrip・社内ダッシュボードから複数のワークグループにアクセスしている。
graph TB
subgraph VPC["VPC (10.0.0.0/16)"]
subgraph AZ_A["AZ: ap-northeast-1a"]
subgraph Private_A["Private Subnet"]
WG1["Redshift Serverless\nWorkgroup: analytics-prod"]
WG2["Redshift Serverless\nWorkgroup: analytics-dev"]
end
end
subgraph AZ_B["AZ: ap-northeast-1c"]
subgraph Private_B["Private Subnet"]
WG1B["(Multi-AZ Standby)"]
end
end
subgraph Ingestion["データ取り込みレイヤー"]
ZeroETL["Aurora→Redshift\nZero-ETL Integration"]
Firehose["Kinesis Data Firehose"]
Glue["AWS Glue\n(dbt実行環境)"]
end
end
subgraph S3_Layer["S3 Data Lake"]
S3_Raw["s3://raw-data/"]
S3_Processed["s3://processed-data/"]
S3_External["External Schema\n(Redshift Spectrum)"]
end
subgraph BI["BIツール・分析クライアント"]
QS["Amazon QuickSight\n(SPICE + Direct Query)"]
DataGrip["DataGrip\n(開発者用)"]
Dashboard["社内ダッシュボード\n(FastAPI + psycopg3)"]
end
subgraph Monitoring["モニタリング"]
CW["CloudWatch\nMetrics/Alarms"]
CA["Cost Anomaly\nDetection"]
end
subgraph NS["Redshift Serverless Namespace"]
NS1["Namespace: analytics"]
NS1 --- WG1
NS1 --- WG2
end
ZeroETL --> WG1
Firehose --> S3_Raw
S3_Raw --> Glue
Glue --> WG1
S3_Processed --> S3_External
S3_External --> WG1
WG1 --> QS
WG1 --> DataGrip
WG1 --> Dashboard
WG2 --> DataGrip
WG1 --> CW
WG1 --> CA
ポイントは、本番と開発でワークグループを分離してWPU上限を別々に設定していること。最初はNamespace一つにWorkgroup一つという構成だったけど、開発者が重いクエリを流すと本番のQuickSightが詰まるという地獄を経験してから分けた。これ、やらかしてから気づくやつ。
WPU設計が9割を決める話
WPUの基本と最初にやった失敗
Redshift ServerlessのWPU(Redshift Processing Unit)は8〜512の範囲で設定できる。2026年7月時点では最大512WPUまで使えるようになっている。
で、最初の失敗はデフォルト設定のまま放置していたことだ。当時の設定を確認したら、最大WPUが512になっていた。実際のワークロードは平均32〜64WPU程度しか使っていないにもかかわらず、スパイクに備えて上限を高くしたままにしていた。ここが罠で、Redshift Serverlessは使用したWPU × 時間で課金されるため、ベースコストはそれほど変わらないが、想定外のクエリが走ると一気に上振れする。正直、「念のため高めに設定しておこう」という発想が一番危ない。
実際のWPU使用量と請求額の推移がこちら。2026年1月がまさに「事件」の月だ。
xychart-beta
title "月別WPU使用量(RPU-hours)と請求額(万円)の推移"
x-axis ["2025/11", "2025/12", "2026/1", "2026/2", "2026/3", "2026/4", "2026/5", "2026/6"]
y-axis "RPU-hours" 0 --> 12000
bar [9800, 10200, 11400, 8200, 6100, 4800, 4200, 3900]
line [9800, 10200, 11400, 4800, 4200, 3900]
2026年1月がピークで、そこから本格的に最適化を始めた。2026年6月時点で約60%削減できている。グラフで見ると一目瞭然で、「なんでもっと早くやらなかったんだ」という気持ちになる。
ワークグループごとのWPU上限設定
見直した設定がこちら。
| ワークグループ | 変更前 最大WPU | 変更後 最大WPU | 用途 | 備考 |
|---|---|---|---|---|
| analytics-prod | 512 | 128 | QuickSight・ダッシュボード | ピーク時間帯に追加調整 |
| analytics-dev | 512 | 32 | 開発者クエリ | コスト上限として機能 |
| analytics-batch | 新設 256 | 256 | dbt・夜間バッチ | 時間帯制限と組み合わせ |
バッチ専用のワークグループを分離したのが地味に一番効いた。夜間のdbtバッチが昼間の分析クエリと競合して、お互いのWPUを食い合っていた状態が解消された。「分けるだけ」でここまで変わるのか、という感じ。
WPU使用量のモニタリング設定
現状把握なしに最適化はできないので、まずこのクエリを毎日CloudWatch Logs Insightsに流して異常なコンピューティング消費がないかチェックしている。
-- WPU使用状況のモニタリングクエリ
SELECT
DATE_TRUNC('hour', start_time) AS hour,
workgroup_name,
SUM(compute_seconds) / 3600.0 AS compute_hours,
COUNT(*) AS query_count,
AVG(execution_time) / 1000.0 AS avg_exec_sec,
MAX(execution_time) / 1000.0 AS max_exec_sec
FROM sys_query_history
WHERE start_time >= DATEADD(day, -7, GETDATE())
AND status = 'success'
GROUP BY 1, 2
ORDER BY 1 DESC, compute_hours DESC;
CloudWatchのカスタムダッシュボードと組み合わせた監視設計についてはCloudWatch vs Datadog 2026の比較記事も参考になるかもしれない。
クエリ最適化で体感した「劇的すぎる」改善
実際に発見した犯人クエリ
WPU設定の次にやったのがクエリの棚卸し。sys_query_historyとsys_query_detailを使って、過去30日間でコンピューティングを一番食っているクエリを抽出した。
-- 高コストクエリの特定
SELECT
query_id,
LEFT(query_text, 100) AS query_preview,
user_name,
compute_seconds,
execution_time / 1000.0 AS exec_sec,
returned_rows,
scanned_rows,
ROUND(100.0 * scanned_rows / NULLIF(returned_rows, 0), 1) AS scan_to_return_ratio
FROM sys_query_history
WHERE start_time >= DATEADD(day, -30, GETDATE())
AND status = 'success'
AND compute_seconds > 100
ORDER BY compute_seconds DESC
LIMIT 20;
上位に入ってきたクエリのひとつがこれだった(実際のテーブル名は変えてある)。
-- 最適化前: 全テーブルスキャンが走っていた問題クエリ
SELECT
u.user_id,
u.name,
COUNT(o.order_id) AS order_count,
SUM(o.amount) AS total_amount
FROM users u
JOIN orders o ON u.user_id = o.user_id
WHERE o.created_at >= '2026-01-01'
GROUP BY u.user_id, u.name;
これで毎回スキャン行数が1.2億行を超えていた。EXPLAINで実行計画を確認したら、ordersテーブルへのフルスキャンが走っていた。一見なんでもなさそうなクエリがこんなに暴れていたのか、と正直引いた。
ソートキーと分散キーの見直し
-- 最適化後のテーブル定義
CREATE TABLE orders (
order_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
amount DECIMAL(12,2),
status VARCHAR(20),
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP
)
DISTSTYLE KEY
DISTKEY (user_id) -- JOINキーに合わせて分散
SORTKEY (created_at, user_id); -- WHERE句で頻出するカラムをソートキーに
テーブルを再構築してクエリを再実行したところ、スキャン行数が1.2億行 → 380万行に激減した。実行時間は18秒 → 1.4秒。体感でも明らかに違う。テーブル定義って本当に大事で、後から直すのは骨が折れるので最初から設計しておきたかった、というのが正直なところだ。
AQUA・自動最適化との付き合い方
2026年時点のRedshift ServerlessにはAQUA(Advanced Query Accelerator)と自動テーブル最適化(ATO)が組み込まれている。ATOについては、VACUUMとANALYZEを自動でやってくれるので基本的にはお任せでいいんだけど、問題があって。
ATOが深夜に動くタイミングと夜間バッチのdbt実行が被ると、WPU消費が跳ね上がる。これを避けるために、メンテナンストラックのスケジュールをdbtと被らない時間帯に明示的に設定した。
-- 統計情報の手動確認(ATOが追いついていない場合)
SELECT
schema_name,
table_name,
stats_off,
tbl_rows,
estimated_visible_rows
FROM svv_table_info
WHERE stats_off > 10 -- 統計情報が10%以上ずれているテーブル
ORDER BY stats_off DESC
LIMIT 20;
stats_offが高いテーブルは手動でANALYZEをかけておく運用にした。地味だけどクエリプランの精度が上がって、想定外の遅いクエリが減った。自動化に頼りきらず、たまは自分の目で確認するのが大事だと思う。
Redshift Spectrum + Intelligent-TieringでS3コストも最適化
ホットデータとコールドデータの分離戦略
ストレージコストの観点では、Redshift内部に保持するデータとS3に外出しするデータの境界設計が重要だ。うちのチームでは以下の方針で整理した。
flowchart LR
subgraph Redshift["Redshift Serverless (マネージドストレージ)"]
Hot["直近90日間のデータ\n高頻度アクセス\n集計済みテーブル"]
end
subgraph S3_Spectrum["S3 + Redshift Spectrum"]
Warm["90日〜1年\nアクセス頻度: 週1回程度\nParquet形式"]
Cold["1年以上\nアクセス頻度: 月1回以下\nGlacier Instant Retrieval"]
end
subgraph Query["クエリエンジン"]
Spectrum_Engine["Redshift Spectrum Engine"]
end
Hot -->|"UNLOAD (日次)"| Warm
Warm -->|"S3 Lifecycle Policy"| Cold
Warm --> Spectrum_Engine
Cold --> Spectrum_Engine
Hot --> Spectrum_Engine
-- External Schemaの作成(Redshift Spectrum用)
CREATE EXTERNAL SCHEMA spectrum_analytics
FROM DATA CATALOG
DATABASE 'analytics_glue_db'
IAM_ROLE 'arn:aws:iam::123456789012:role/RedshiftSpectrumRole'
CREATE EXTERNAL DATABASE IF NOT EXISTS;
-- PartitionedなExternal Tableの例
CREATE EXTERNAL TABLE spectrum_analytics.orders_historical (
order_id BIGINT,
user_id BIGINT,
amount DECIMAL(12,2),
status VARCHAR(20),
created_at TIMESTAMP
)
PARTITIONED BY (year INT, month INT)
STORED AS PARQUET
LOCATION 's3://analytics-data-lake/orders/historical/';
この構成にしてから、Redshiftの内部ストレージ使用量が68%削減された。S3の費用はかかるけど、Redshiftのマネージドストレージ単価との差が大きいので全体コストは下がる。個人的にはこのホット/コールド分離が、コスト削減施策の中でいちばん投資対効果が高かった。
S3 Intelligent-Tieringとの組み合わせ
S3側はIntelligent-Tieringを有効化して自動的にコールドデータをGlacier Instant Retrievalに移動させている。設定はS3 Intelligent-TieringとGlacierで年間コスト40%削減した話で詳しく書いたのでそちらも参照してほしい。
Redshift Spectrumからのクエリ時間はマネージドストレージより遅いけど、月1回程度しかアクセスしない歴史データに対してそのコストは十分許容範囲だ。
2026年版の新機能活用と今後の課題
Zero-ETL統合のさらなる活用
2026年前半にRedshift ServerlessのZero-ETL統合が安定してきた。うちのチームでもAurora PostgreSQLからのZero-ETL統合を使っており、これ以前はGlueジョブ経由で4〜6時間かかっていたデータ反映が15分以内になった。
ただし正直まだ検証中の部分もあって、Aurora側でDDL変更(カラム追加など)が走ると統合が一時停止するケースがある。本番で2回ほど気づかずにデータが古いまま朝の会議に臨んでしまったことがあって、CloudWatchアラームで「zero-etl-status: ERROR」を検知する監視を追加した。あの朝の会議は思い出したくない。
# Zero-ETL統合ステータスの監視Lambda(簡略版)
import boto3
import json
def lambda_handler(event, context):
client = boto3.client('redshift-serverless', region_name='ap-northeast-1')
# Zero-ETL統合のステータス確認
integrations = client.list_integrations()
sns_client = boto3.client('sns')
alerts = []
for integration in integrations.get('Integrations', []):
status = integration.get('Status', '')
name = integration.get('IntegrationName', '')
if status not in ['ACTIVE']:
alerts.append({
'integration': name,
'status': status,
'errors': integration.get('Errors', [])
})
if alerts:
sns_client.publish(
TopicArn='arn:aws:sns:ap-northeast-1:123456789012:data-pipeline-alerts',
Subject='[ALERT] Redshift Zero-ETL Integration Error',
Message=json.dumps(alerts, ensure_ascii=False, indent=2)
)
return {'statusCode': 200, 'alerts': len(alerts)}
AI駆動型クエリ最適化(Amazon Q in Redshift)
2026年のアップデートでAmazon QがRedshiftコンソールに統合されて、クエリの最適化提案をAIが出してくれるようになった。実際に試してみたところ、JOIN順序の提案やソートキーの推奨が思ったより精度高くて驚いた。
ただし全部鵜呑みにすると危ない場面もある。データ分布が偏ったテーブルに対して「分散キーをこれにしろ」という提案が来たんだけど、そのカラムはNULLが30%含まれていて実際には性能が悪化した。提案はあくまで参考にしつつ、EXPLAINで自分でプランを確認する習慣は崩さない方がいい。AIが悪いというより、テーブルの実態を知っているのは自分たちだ、という話だと思う。
皆さんのチームではAmazon Qのクエリ最適化提案、どれくらい信頼して使ってますか?ここは結構好みが分かれそう。
dbt × Redshift Serverlessの相性
dbt Core 2.0との組み合わせは基本的に好調だ。dbt-redshiftアダプターが2026年版でauto_refreshオプションに対応したので、マテリアライズドビューの自動更新管理がしやすくなった。
でも一点だけ困っていること。dbtのモデル数が400を超えてきたあたりから、full refresh時のWPU消費が予想外に大きくなっている。モデルの依存グラフを見直してインクリメンタル化を進めているところで、正直まだ完全には解決できていない。dbt Core 2.0移行後の本番知見も参考にしてほしい。
実際の最適化成果まとめ
8ヶ月間の取り組みで得られた数値をまとめると:
| 最適化項目 | Before | After | 削減率 |
|---|---|---|---|
| 月間WPU消費 (RPU-hours) | 11,400 | 3,900 | -66% |
| 月間コスト (概算) | 約50万円 | 約18万円 | -64% |
| 平均クエリ実行時間 | 8.2秒 | 2.1秒 | -74% |
| Redshiftマネージドストレージ | 4.2TB | 1.3TB | -69% |
| dbt full refresh時間 | 42分 | 18分 | -57% |
月32万円の削減は、正直思ってた以上だった。
まとめ
Redshift Serverless最適化で本当に効いたことを整理すると:
- WPU上限設定をワークロード別に細かく設定する — デフォルト512WPUのままにしない。本番・開発・バッチでワークグループを分離してそれぞれ上限を設ける
- ソートキー・分散キーの設計が9割 — 自動最適化に任せきりにせず、実際のクエリパターンを
sys_query_historyで分析してテーブル設計に反映する - ホット/コールドデータの分離でストレージコストを大幅削減 — Redshift Spectrum + S3 Intelligent-Tieringの組み合わせは投資対効果が高い
- Zero-ETL統合は便利だがステータス監視を怠らない — DDL変更で統合が停止するケースがあるため、CloudWatchアラームとLambdaで自動検知する仕組みが必要
- dbtのインクリメンタルモデル化はコスト削減と直結する — full refreshの頻度と対象を絞り込むだけでWPU消費が劇的に変わる
次のアクションとしては、Amazon Q in Redshiftの推奨をCI/CDパイプラインに組み込んで、プルリクエスト時にクエリ最適化の提案が自動で出るようにする実験を始めたいと思っている。ここはまだ試行錯誤中だけど、うまくいったら別記事で書く予定。
Redshift Serverlessを「放置」から「管理」に変えると、コストと性能の両方が劇的に改善する。特にWPU設計とソートキー見直しは今すぐやれる施策なので、まだ手をつけていない方はぜひ試してみてほしい。