STEP 14:Redshiftクエリ最適化

⚡ STEP 14: Redshiftクエリ最適化

ディストリビューションキー、ソートキー、圧縮でクエリを高速化しよう

📋 このステップで学ぶこと

  • ディストリビューションキー(DISTKEY)の選択と設定
  • ソートキー(SORTKEY)の選択と設定
  • 圧縮(Compression)の仕組みと効果
  • VACUUM、ANALYZEコマンドの使い方
  • 実行計画(EXPLAIN)の読み方
  • 実践演習:パフォーマンスチューニング

🎯 このステップのゴール

このステップを終えると、Redshiftのクエリを10倍以上高速化できるようになります。DISTKEY、SORTKEY、圧縮の3つの最適化テクニックをマスターしましょう。

🔑 1. ディストリビューションキー(DISTKEY)

DISTKEYとは

ディストリビューションキー(DISTKEY)は、データをどのようにノード間で分散させるかを決定するカラムです。

💡 例え話:物流倉庫の配置戦略

【シナリオ】 全国に4つの倉庫(= 4ノード)があります。 どこに何を置くかで、配送効率が大きく変わります。 【戦略1:ランダムに配置(DISTSTYLE EVEN)】 商品をランダムに4倉庫に分散 → 注文が来たら、4倉庫を探し回る必要がある → 遅い! 【戦略2:地域別に配置(DISTSTYLE KEY)】 東北の商品は東北倉庫、関西の商品は関西倉庫… → 「東北向け注文」は東北倉庫だけで完結 → 他の倉庫に問い合わせ不要 = 高速! 【戦略3:全倉庫に在庫(DISTSTYLE ALL)】 人気商品は全倉庫に在庫を持つ → どの倉庫でもすぐ出荷可能 → ただし在庫コスト増 【Redshiftでの対応】 DISTKEY = 地域(どの倉庫に置くか決めるキー) → JOINで使うカラムを指定すると、 関連データが同じノードに集まり、JOIN高速化!

DISTSTYLEの種類

DISTSTYLE 説明 使用シーン
KEY 指定したDISTKEYの値でデータを分散 JOINでよく使うカラムがある sales, orders
EVEN 全ノードに均等に分散(ラウンドロビン) JOINしない、単独で集計 logs, events
ALL 全ノードに全データをコピー 小さいディメンションテーブル products, customers
AUTO Redshiftが自動選択 どれを選ぶか迷う場合

DISTKEYの効果(具体例)

📊 JOINの処理比較

【クエリ】 SELECT s.*, c.customer_name FROM sales s JOIN customers c ON s.customer_id = c.customer_id; 【DISTKEYなし(EVEN分散)の場合】 ┌─────────────────────────────────────────────────────────┐ │ ノード1: sales(customer_id=1,3,5,7…) │ │ ノード2: sales(customer_id=2,4,6,8…) │ │ ノード3: customers(customer_id=1,3,5,7…) │ │ ノード4: customers(customer_id=2,4,6,8…) │ └─────────────────────────────────────────────────────────┘ JOINするには… → ノード間でデータを転送(Redistribute) → ネットワーク通信が発生 → 遅い!(10秒) 【DISTKEY(customer_id)設定の場合】 ┌─────────────────────────────────────────────────────────┐ │ ノード1: sales(customer_id=1,2) + customers(customer_id=1,2) │ │ ノード2: sales(customer_id=3,4) + customers(customer_id=3,4) │ │ ノード3: sales(customer_id=5,6) + customers(customer_id=5,6) │ │ ノード4: sales(customer_id=7,8) + customers(customer_id=7,8) │ └─────────────────────────────────────────────────────────┘ JOINするには… → 各ノード内で完結(データ転送なし) → 高速!(1秒) → 10倍高速化!

DISTSTYLE別のテーブル作成例

📝 DISTSTYLE KEY(大きいファクトテーブル)

— 売上ファクトテーブル(100万件以上) CREATE TABLE sales ( sale_id INT, sale_date DATE, customer_id INT, product_id INT, amount DECIMAL(10,2) ) DISTSTYLE KEY DISTKEY(customer_id); — JOINで使うカラム — customer_idでデータを分散 — 同じcustomer_idのデータは同じノードに配置

📝 DISTSTYLE ALL(小さいディメンションテーブル)

— 顧客マスタ(1万件程度) CREATE TABLE customers ( customer_id INT, customer_name VARCHAR(200), email VARCHAR(200), city VARCHAR(100) ) DISTSTYLE ALL; — 全ノードにコピー — 全ノードに全データがあるので — JOINでデータ転送が不要 → 超高速

📝 DISTSTYLE EVEN(JOINしないテーブル)

— ログテーブル(JOINしない) CREATE TABLE access_logs ( log_id BIGINT, timestamp TIMESTAMP, user_agent VARCHAR(500), ip_address VARCHAR(50), path VARCHAR(200) ) DISTSTYLE EVEN; — 均等に分散 — JOINしないので、DISTKEYは不要 — 均等に分散して並列処理を最大化

DISTKEYの選び方

✅ 良いDISTKEYの条件

  • JOINでよく使うカラム
  • カーディナリティが高い(値の種類が多い)
  • 均等に分散する(偏りが少ない)

例:customer_id, order_id, user_id

❌ 悪いDISTKEYの例

  • カーディナリティが低い:性別(2値)
  • 偏りがある:都道府県(東京に集中)
  • JOINで使わない:タイムスタンプ

問題:データが偏り、一部ノードに負荷集中

データ分散状況の確認

📊 スライス別のデータ分布を確認

— データ分散状況を確認 SELECT TRIM(name) as table_name, slice, COUNT(*) as row_count FROM stv_blocklist WHERE name = ‘sales’ GROUP BY name, slice ORDER BY slice; — 理想的な結果(均等に分散) table_name | slice | row_count ————|——-|———- sales | 0 | 250000 sales | 1 | 250000 sales | 2 | 250000 sales | 3 | 250000 — 各スライスに均等に分散されている ✅ — 悪い結果(偏りがある) table_name | slice | row_count ————|——-|———- sales | 0 | 800000 ← 偏っている! sales | 1 | 50000 sales | 2 | 100000 sales | 3 | 50000 — slice 0に集中 → DISTKEYの選択が悪い ❌

🔢 2. ソートキー(SORTKEY)

SORTKEYとは

ソートキー(SORTKEY)は、データをどの順番で保存するかを決定するカラムです。

💡 例え話:辞書の索引

【辞書で「Redshift」を探す】 【ソートなし(バラバラ)】 「A」「Z」「M」「R」「B」…とランダムに並んでいる → 全ページをめくって探す → 10分かかる(遅い!) 【50音順にソート】 「A」「B」「C」…「R」…「Z」と並んでいる → 「R」のページに直接ジャンプ → 10秒で見つかる(速い!) 【Redshiftでの対応】 SORTKEY(sale_date) を設定すると… データが日付順に保存される WHERE sale_date = ‘2025-01-19’ → 該当日付のブロックに直接アクセス → 全データをスキャンする必要なし → 100倍以上高速化も可能!

SORTKEYの効果(具体例)

📊 範囲検索の処理比較

【クエリ】 SELECT * FROM sales WHERE sale_date BETWEEN ‘2025-01-01’ AND ‘2025-01-31’; 【SORTKEYなしの場合】 データ: 1000万件(1年分) ランダムな順番で保存されている 処理: 全データをスキャン(1000万件) その中から1月分を抽出 時間: 30秒 【SORTKEY(sale_date)設定の場合】 データ: 1000万件(1年分) 日付順に保存されている 処理: 1月のブロックだけスキャン(約80万件) 全データの8%だけ読み込み 時間: 2.4秒 → 12倍高速化! 【Zone Mapの仕組み】 Redshiftは各ブロックの最小値・最大値を記録 ブロック1: 2024-01-01 〜 2024-03-31 → スキップ ブロック2: 2024-04-01 〜 2024-06-30 → スキップ … ブロック12: 2025-01-01 〜 2025-03-31 → 読み込み!

SORTKEYの種類

種類 説明 メリット デメリット
COMPOUND 複数カラムの組み合わせでソート
(最初のカラムが最優先)
VACUUMが速い
一般的に推奨
2番目以降のカラム単独では効果薄
INTERLEAVED 複数カラムを均等に重視 どのカラムでも効果あり VACUUMが非常に遅い

SORTKEYの設定例

📝 単一カラム(最も一般的)

CREATE TABLE sales ( sale_id INT, sale_date DATE, customer_id INT, amount DECIMAL(10,2) ) SORTKEY(sale_date); — 日付順でソート — 効果が高いクエリ SELECT * FROM sales WHERE sale_date BETWEEN ‘2025-01-01’ AND ‘2025-01-31’; — → 日付範囲に該当するブロックだけスキャン

📝 複数カラム(COMPOUND)

CREATE TABLE sales ( sale_id INT, sale_date DATE, customer_id INT, amount DECIMAL(10,2) ) COMPOUND SORTKEY(sale_date, customer_id); — sale_date → customer_id の順でソート — 効果が高いクエリ WHERE sale_date = ‘2025-01-19’ — ✅ 高速 WHERE sale_date = ‘2025-01-19’ AND customer_id = 100 — ✅ 高速 — 効果がないクエリ WHERE customer_id = 100 — ❌ 効果なし — (2番目のカラム単独では効かない)

⚠️ INTERLEAVED SORTKEY(特殊なケースのみ)

CREATE TABLE sales ( sale_id INT, sale_date DATE, customer_id INT, product_id INT ) INTERLEAVED SORTKEY(sale_date, customer_id, product_id); — 3つのカラムを均等に重視 WHERE sale_date = … — ✅ 高速 WHERE customer_id = … — ✅ 高速 WHERE product_id = … — ✅ 高速 — ⚠️ 注意:VACUUMが非常に遅い — 通常はCOMPOUNDを推奨

SORTKEYの選び方

✅ 良いSORTKEYの条件

  • WHERE句でよく使うカラム
  • 範囲検索するカラム(日付、数値)
  • JOIN条件に使うカラム

例:sale_date, created_at, order_date, timestamp

🗜️ 3. 圧縮(Compression)

圧縮とは

Redshiftはカラムナーストレージなので、各カラムを個別に圧縮できます。適切な圧縮でストレージ70〜90%削減も可能です。

💡 例え話:衣類の圧縮袋

【旅行の荷物】 セーター10枚をスーツケースに入れたい 【圧縮なし】 10枚 → スーツケースに入りきらない 追加のバッグが必要 → 荷物が増える → 移動が遅い 【圧縮袋使用】 10枚 → 圧縮して3枚分のサイズに スーツケース1つで足りる → 移動が速い 【Redshiftでの効果】 元データ: 100GB 圧縮後: 20GB(80%削減) ・ストレージ料金: 80%削減 ・読み込み時間: 80%削減(I/O削減) ・クエリ速度: 大幅向上

圧縮の効果(具体例)

📊 実際の圧縮効果

【1億件のsalesテーブル】 カラム 元サイズ 圧縮後 圧縮率 エンコーディング ————————————————————– sale_id 400MB 80MB 80%削減 DELTA sale_date 400MB 40MB 90%削減 DELTA32K customer_id 400MB 60MB 85%削減 DELTA product_name 2000MB 400MB 80%削減 LZO amount 800MB 200MB 75%削減 ZSTD ————————————————————– 合計 4000MB 780MB 80%削減 → ストレージ料金 80%削減!クエリ速度も向上(読み込むデータ量が減る)

圧縮エンコーディングの種類

エンコーディング 説明 適したデータ 圧縮率
RAW 圧縮なし すでに圧縮されたデータ 0%
LZO 汎用圧縮(高速) 長い文字列 60-80%
ZSTD 汎用圧縮(高圧縮率) 様々なデータ 70-90%
BYTEDICT 辞書圧縮 256種類以下の値 80-95%
DELTA 差分圧縮 連続する数値(ID) 70-90%
DELTA32K 差分圧縮(日付向け) 日付、近い値の数値 80-95%

圧縮の設定方法

📝 方法1:COPY時に自動設定(推奨)

— テーブル作成(圧縮指定なし) CREATE TABLE sales ( sale_id INT, sale_date DATE, amount DECIMAL(10,2) ); — COPYコマンドで自動圧縮 COPY sales FROM ‘s3://bucket/data.csv’ IAM_ROLE ‘arn:…’ CSV COMPUPDATE ON; — Redshiftが最適な圧縮を自動選択 — Redshiftがデータを分析して、 — 各カラムに最適な圧縮を選んでくれる

📝 方法2:手動で指定

CREATE TABLE sales ( sale_id INT ENCODE DELTA, — 連続ID → 差分圧縮 sale_date DATE ENCODE DELTA32K, — 日付 → 日付用差分圧縮 product_name VARCHAR(200) ENCODE LZO, — 文字列 → LZO圧縮 category VARCHAR(50) ENCODE BYTEDICT, — カテゴリ → 辞書圧縮 amount DECIMAL(10,2) ENCODE ZSTD — 数値 → 汎用圧縮 ); — 手動設定は上級者向け — 通常はCOMPUPDATE ONで自動設定を推奨

圧縮状況の確認

📊 テーブルの圧縮状況を確認

— テーブルの圧縮状況を確認 SELECT TRIM(tablename) as table_name, TRIM(“column”) as column_name, TRIM(type) as data_type, TRIM(encoding) as encoding FROM pg_table_def WHERE tablename = ‘sales’ ORDER BY column_name; — 実行結果例 table_name | column_name | data_type | encoding ————|————–|———–|———- sales | sale_id | integer | delta sales | sale_date | date | delta32k sales | amount | numeric | zstd sales | product_name | varchar | lzo

🔧 4. VACUUM、ANALYZEコマンド

VACUUMコマンド

VACUUMは、削除されたデータの領域を回収し、ソート順を整理するコマンドです。

💡 例え話:本棚の整理

【本棚の状態】 50音順に本を並べていたが… ・途中の本を何冊か捨てた(DELETE) ・新しい本を最後に追加した(INSERT) 【現在の状態】 「あ」「い」「_」「え」「お」…「わ」「新刊1」「新刊2」 ↑空きスペース ↑ソートされていない 【VACUUM = 本棚の整理】 ・空きスペースを詰める ・新しい本を正しい位置に移動 ・50音順に再整理 【整理後】 「あ」「い」「え」「お」「新刊1」「新刊2」…「わ」 → きれいにソートされた!

📝 VACUUMの実行

— 全テーブルをVACUUM VACUUM; — 特定テーブルをVACUUM VACUUM sales; — ソートのみ(削除領域の回収はしない) VACUUM SORT ONLY sales; — 削除領域の回収のみ(ソートはしない) VACUUM DELETE ONLY sales; — フル再ソート(100%再ソート) VACUUM FULL sales;

⚠️ VACUUMの注意点

  • 時間がかかる:大きなテーブルは数時間
  • リソース消費:CPU、ディスクI/Oを大量に使用
  • 実行タイミング:夜間など、負荷の低い時間帯に実行
  • 自動VACUUM:Redshiftが自動実行するので、通常は手動不要

ANALYZEコマンド

ANALYZEは、テーブルの統計情報を更新するコマンドです。クエリオプティマイザが正しい実行計画を作成するために必要です。

📝 ANALYZEの実行

— 全テーブルをANALYZE ANALYZE; — 特定テーブルをANALYZE ANALYZE sales; — 特定カラムをANALYZE ANALYZE sales(sale_date, customer_id); — ANALYZEが必要なテーブルを確認 SELECT TRIM(tablename) as table_name, stats_off FROM svv_table_info WHERE stats_off > 10 — 統計情報が10%以上古い ORDER BY stats_off DESC;

💡 VACUUMとANALYZEの使い分け

コマンド 目的 実行タイミング
VACUUM 削除領域の回収、ソート順の整理 DELETE/UPDATE後、大量ロード後
ANALYZE 統計情報の更新 大量ロード後、実行計画が不適切な時

📊 5. 実行計画(EXPLAIN)

EXPLAINとは

EXPLAINは、クエリの実行計画を表示するコマンドです。クエリを実行せずに、どのような処理が行われるか確認できます。

📝 EXPLAINの使い方

— 実行計画のみ表示(クエリは実行されない) EXPLAIN SELECT * FROM sales WHERE sale_date = ‘2025-01-19’; — 実行結果例 QUERY PLAN ——————————————————— XN Seq Scan on sales (cost=0.00..10000.00 rows=1000 width=20) Filter: (sale_date = ‘2025-01-19’::date)

実行計画の読み方

用語 意味 備考
XN Seq Scan 順次スキャン(全行スキャン) 遅い、SORTKEYがない
XN Hash Join ハッシュJOIN 一般的なJOIN方法
XN Merge Join マージJOIN ソート済みデータ向け、高速
DS_DIST_ALL_INNER ALL分散テーブルとのJOIN データ移動なし、高速
DS_BCAST_INNER ブロードキャストJOIN 小テーブルを全ノードに配信
DS_DIST_BOTH 両テーブルを再分散 遅い、DISTKEYを見直す
cost 推定コスト 小さいほど速い
rows 推定行数

最適化の例

❌ 最適化前(遅いクエリ)

EXPLAIN SELECT * FROM sales WHERE sale_date = ‘2025-01-19’; — 実行計画 XN Seq Scan on sales (cost=0.00..10000.00 rows=1000 width=20) Filter: (sale_date = ‘2025-01-19’::date) — cost=10000 → 高コスト! — Seq Scan → 全行スキャン(遅い)

✅ 最適化後(速いクエリ)

— SORTKEYを設定した新テーブルを作成 CREATE TABLE sales_optimized AS SELECT * FROM sales SORTKEY(sale_date); VACUUM sales_optimized; ANALYZE sales_optimized; EXPLAIN SELECT * FROM sales_optimized WHERE sale_date = ‘2025-01-19’; — 実行計画 XN Seq Scan on sales_optimized (cost=0.00..100.00 rows=1000 width=20) Filter: (sale_date = ‘2025-01-19’::date) — cost=100 → 100倍改善! — Zone Mapにより、該当日付のブロックのみスキャン

💪 6. 実践演習:パフォーマンスチューニング

演習 1 実践

遅いクエリを最適化して、実行時間を10分の1に短縮してください

状況:

  • salesテーブル(100万件)
  • customersテーブル(1万件)
  • 以下のクエリが10秒かかる
SELECT c.customer_name, SUM(s.amount) as total_sales FROM sales s JOIN customers c ON s.customer_id = c.customer_id WHERE s.sale_date BETWEEN ‘2025-01-01’ AND ‘2025-01-31’ GROUP BY c.customer_name ORDER BY total_sales DESC LIMIT 10;

【解答例】

1. 最適化されたテーブルを作成

— salesテーブル(最適化版) CREATE TABLE sales_optimized ( sale_id INT ENCODE DELTA, sale_date DATE ENCODE DELTA32K, customer_id INT ENCODE DELTA, product_id INT ENCODE DELTA, amount DECIMAL(10,2) ENCODE ZSTD ) DISTSTYLE KEY DISTKEY(customer_id) — JOINで使用 SORTKEY(sale_date); — WHERE句で使用 — customersテーブル(最適化版) CREATE TABLE customers_optimized ( customer_id INT ENCODE DELTA, customer_name VARCHAR(200) ENCODE LZO, email VARCHAR(200) ENCODE LZO ) DISTSTYLE ALL; — 小さいテーブル → 全ノードにコピー

2. データをコピー&最適化

— データをコピー INSERT INTO sales_optimized SELECT * FROM sales; INSERT INTO customers_optimized SELECT * FROM customers; — 統計情報を更新 ANALYZE sales_optimized; ANALYZE customers_optimized;

3. 最適化後のクエリを実行

— 実行時間を計測 SELECT c.customer_name, SUM(s.amount) as total_sales FROM sales_optimized s JOIN customers_optimized c ON s.customer_id = c.customer_id WHERE s.sale_date BETWEEN ‘2025-01-01’ AND ‘2025-01-31’ GROUP BY c.customer_name ORDER BY total_sales DESC LIMIT 10; — 実行時間: 約1秒(10分の1に短縮!)

最適化のポイント:

  • DISTKEY(customer_id):JOINキーで分散 → データ移動なし
  • SORTKEY(sale_date):日付範囲検索が高速
  • DISTSTYLE ALL:小さいcustomersを全ノードにコピー
  • 圧縮設定:I/O削減でさらに高速化

⏰ 演習終了後は必ずクラスターを一時停止!

Redshiftコンソール → クラスター選択 → アクション → クラスターを一時停止

📝 STEP 14 のまとめ

✅ このステップで学んだこと

  • DISTKEY:JOINで使うカラムを指定 → データ移動なしでJOIN高速化
  • SORTKEY:WHERE句で使うカラムを指定 → 範囲検索が高速化
  • 圧縮:ストレージ70-90%削減 + クエリ高速化
  • VACUUM:削除領域の回収、ソート順の整理
  • ANALYZE:統計情報の更新
  • EXPLAIN:実行計画の確認、ボトルネック発見

💡 最適化のチェックリスト

  • □ DISTKEY = JOINで使うカラム
  • □ SORTKEY = WHERE句で使うカラム
  • □ 小さいディメンションテーブルは DISTSTYLE ALL
  • □ 圧縮は COMPUPDATE ON で自動設定
  • □ 定期的に VACUUM と ANALYZE を実行
  • □ EXPLAIN で実行計画を確認

🎯 次のステップの準備

次のSTEP 15では、「AWS Glue入門」を学びます。
ETLサービスを使って、データパイプラインを自動化しましょう!

📝 理解度チェック

問題 1 基礎

DISTKEYとSORTKEYの役割をそれぞれ説明してください。

【解答】

  • DISTKEY:データをノード間でどう分散するかを決定。JOINで使うカラムを指定すると、同じ値のデータが同じノードに配置され、データ移動なしでJOINが可能になり高速化。
  • SORTKEY:データをどの順番で保存するかを決定。WHERE句で範囲検索するカラムを指定すると、ソート済みなので該当ブロックのみスキャンでき高速化。
問題 2 応用

DISTSTYLE ALLはどのような場合に使用すべきですか?メリットとデメリットを説明してください。

【解答】

使用すべき場合:小さいディメンションテーブル(1万件以下が目安)

メリット:

  • 全ノードにデータがあるので、JOIN時のデータ移動が不要
  • JOINが非常に高速

デメリット:

  • ストレージを多く使用(ノード数倍)
  • 更新時は全ノードに反映が必要
問題 3 応用

VACUUMコマンドとANALYZEコマンドの違いを説明してください。

【解答】

  • VACUUM:削除されたデータの領域を回収し、ソート順を整理する。DELETE/UPDATEが多い場合や、データ追加ロード後に実行。
  • ANALYZE:テーブルの統計情報(行数、データ分布など)を更新する。クエリオプティマイザが正しい実行計画を作成するために必要。大量ロード後に実行。

❓ よくある質問

Q1: DISTKEYを後から変更できますか?
直接変更はできません。新しいテーブルを作成して、データをコピーする必要があります。

1. 新しいテーブルを正しいDISTKEYで作成
2. INSERT INTO new_table SELECT * FROM old_table
3. 古いテーブルを削除
4. 新しいテーブルをリネーム

大きなテーブルの場合、時間がかかるので、最初から適切なDISTKEYを設定することが重要です。
Q2: VACUUMはどのくらいの頻度で実行すべきですか?
通常は自動VACUUMで十分です。Redshiftはバックグラウンドで自動的にVACUUMを実行します。

手動VACUUMが必要なケース:
・大量のDELETE/UPDATEを実行した直後
・クエリが急に遅くなった場合
・svv_table_infoでunsortedが高い場合
Q3: COMPOUND SORTKEYとINTERLEAVED SORTKEYの使い分けは?
ほとんどの場合、COMPOUND SORTKEYを使用してください。

COMPOUND(推奨):最初のカラムが最優先、VACUUMが速い
INTERLEAVED(特殊):全カラムを均等に重視、VACUUMが非常に遅い

INTERLEAVEDはVACUUMのコストが高いため、よほどの理由がない限りCOMPOUNDを使いましょう。
Q4: 圧縮エンコーディングは自動設定と手動設定、どちらが良いですか?
初回ロードでは自動設定(COMPUPDATE ON)を推奨します。Redshiftがデータを分析して、最適な圧縮を選択してくれます。

手動設定は上級者向けで、データの特性を熟知している場合のみ有効です。
Q5: EXPLAINで「cost」が大きいのですが、どうすれば良いですか?
以下の順番で最適化を検討してください:
1. SORTKEYを設定(WHERE句のカラム)
2. DISTKEYを設定(JOINのカラム)
3. ANALYZEを実行(統計情報更新)
4. VACUUMを実行(ソート順整理)

特に、DS_DIST_BOTHが表示されている場合は、DISTKEYの見直しが効果的です。
📝

学習メモ

クラウドデータ基盤(AWS・GCP) - Step 14

📋 過去のメモ一覧
#artnasekai #学習メモ
LINE