Step 20:総合演習プロジェクト

🎓 Step 20: 総合演習プロジェクト

学んだ全てを使って実践しよう!

📋 このステップで学ぶこと
  • 大規模ECサイトの売上分析ダッシュボード作成
  • 月次・週次・日次の売上推移(ウィンドウ関数)
  • カテゴリ別ランキング
  • 前年比・前月比の計算
  • RFM分析による顧客セグメント分析
  • 遅いクエリの最適化
  • インデックス設計の実践

🎯 1. プロジェクト概要

1-1. あなたの役割

あなたは大手ECサイトのデータアナリストです。経営陣から、売上分析ダッシュボードの作成を依頼されました!

💼 シナリオ

「来週の経営会議で使う売上レポートを作成してほしい。月次の売上推移、カテゴリ別ランキング、顧客セグメント分析が必要だ。あと、最近レポートの表示が遅いから、なんとかしてくれ!」

1-2. データ規模

💾 データ規模
テーブル 件数 説明
customers 10万件 顧客情報
products 5万件 商品情報
orders 500万件 注文情報
order_details 1000万件 注文明細

1-3. 要件

📌 プロジェクト要件
  1. 売上推移:月次・週次・日次の売上と前期比を表示
  2. ランキング:カテゴリ別の売れ筋商品TOP10を表示
  3. 顧客分析:RFM分析で顧客をセグメント化
  4. パフォーマンス:全てのクエリが1秒以内で実行

1-4. 使用するテーブル構造

テーブル構造:
customers テーブル
├── customer_id (主キー)
├── customer_name
├── email
├── prefecture
└── created_at

products テーブル
├── product_id (主キー)
├── product_name
├── category
├── price
└── stock_quantity

orders テーブル
├── order_id (主キー)
├── customer_id (外部キー)
├── order_date
├── amount
└── status

order_details テーブル
├── detail_id (主キー)
├── order_id (外部キー)
├── product_id (外部キー)
├── quantity
└── unit_price
    

📊 2. 課題1:売上推移の分析

2-1. 課題1-1:月次売上推移(過去12ヶ月)

過去12ヶ月の月次売上を集計し、前月比前年同月比を計算してください。

求めるデータ:
年月 売上 注文数 前月比 前年同月比
2024-01 5,000,000 2,500 +15%
2024-02 5,500,000 2,800 +10% +18%
2024-03 6,000,000 3,000 +9% +20%

ステップ1:月次売上を集計する

まず、月ごとの売上と注文数を集計します。

※横にスクロールできます

— WITH句で月次売上を集計 WITH monthly_sales AS ( SELECT strftime(‘%Y-%m’, order_date) as year_month, — 年月形式に変換 SUM(amount) as total_sales, — 売上合計 COUNT(*) as order_count — 注文件数 FROM orders WHERE order_date >= date(‘now’, ‘-13 months’) — 過去13ヶ月(前年同月比計算用) GROUP BY strftime(‘%Y-%m’, order_date) — 年月でグループ化 )
コードの解説:
  • strftime('%Y-%m', order_date) → 日付を「2024-03」形式に変換
  • date('now', '-13 months') → 今日から13ヶ月前(前年同月比を計算するため)
  • WITH句 → 一時的な結果を作成して再利用

ステップ2:前月比を計算する

LAG関数を使って、1ヶ月前の売上を取得し、成長率を計算します。

※横にスクロールできます

— 前月比を計算 ROUND( (total_sales – LAG(total_sales, 1) OVER (ORDER BY year_month)) * 100.0 / LAG(total_sales, 1) OVER (ORDER BY year_month), 1 ) as mom_growth — MoM = Month over Month(前月比)
コードの解説:
  • LAG(total_sales, 1) → 1行前(1ヶ月前)の売上を取得
  • 成長率 = (今月 - 前月) / 前月 × 100
  • ROUND(..., 1) → 小数点第1位で四捨五入

ステップ3:前年同月比を計算する

LAG関数で12ヶ月前の売上を取得します。

※横にスクロールできます

— 前年同月比を計算 ROUND( (total_sales – LAG(total_sales, 12) OVER (ORDER BY year_month)) * 100.0 / LAG(total_sales, 12) OVER (ORDER BY year_month), 1 ) as yoy_growth — YoY = Year over Year(前年同月比)
コードの解説:
  • LAG(total_sales, 12) → 12行前(12ヶ月前=前年同月)の売上を取得
  • 前年同月比があると、季節変動を除いた成長を確認できる

完成コード:

※横にスクロールできます

— 月次売上推移(前月比・前年同月比付き) WITH monthly_sales AS ( SELECT strftime(‘%Y-%m’, order_date) as year_month, SUM(amount) as total_sales, COUNT(*) as order_count FROM orders WHERE order_date >= date(‘now’, ‘-13 months’) GROUP BY strftime(‘%Y-%m’, order_date) ) SELECT year_month, total_sales, order_count, — 前月比 ROUND( (total_sales – LAG(total_sales, 1) OVER (ORDER BY year_month)) * 100.0 / LAG(total_sales, 1) OVER (ORDER BY year_month), 1 ) as mom_growth, — 前年同月比 ROUND( (total_sales – LAG(total_sales, 12) OVER (ORDER BY year_month)) * 100.0 / LAG(total_sales, 12) OVER (ORDER BY year_month), 1 ) as yoy_growth FROM monthly_sales ORDER BY year_month DESC LIMIT 12;
💡 推奨インデックス

※横にスクロールできます

CREATE INDEX idx_orders_date ON orders(order_date);

2-2. 課題1-2:週次売上推移(直近13週)

直近13週間の週次売上を集計し、4週間移動平均を計算してください。

💡 移動平均とは

直近N期間の平均値です。週ごとの変動を平滑化して、トレンドを把握しやすくします。例えば、4週間移動平均は「直近4週間の平均」です。

ステップ1:週次売上を集計する

※横にスクロールできます

— 週ごとの売上を集計 WITH weekly_sales AS ( SELECT strftime(‘%Y-W%W’, order_date) as year_week, — 年-週番号形式 SUM(amount) as total_sales, COUNT(*) as order_count FROM orders WHERE order_date >= date(‘now’, ‘-91 days’) — 13週間 = 91日 GROUP BY strftime(‘%Y-W%W’, order_date) )
コードの解説:
  • strftime('%Y-W%W', order_date) → 「2024-W15」のような週番号形式に変換
  • %W → ISO 8601の週番号(01〜53)

ステップ2:4週間移動平均を計算する

※横にスクロールできます

— 4週間移動平均 ROUND( AVG(total_sales) OVER ( ORDER BY year_week ROWS BETWEEN 3 PRECEDING AND CURRENT ROW — 直近4週間 ), 0 ) as moving_avg_4weeks
コードの解説:
  • ROWS BETWEEN 3 PRECEDING AND CURRENT ROW → 3行前から現在の行まで(計4行)
  • AVG(...) OVER (...) → ウィンドウ関数で移動平均を計算

完成コード:

※横にスクロールできます

— 週次売上と4週間移動平均 WITH weekly_sales AS ( SELECT strftime(‘%Y-W%W’, order_date) as year_week, SUM(amount) as total_sales, COUNT(*) as order_count FROM orders WHERE order_date >= date(‘now’, ‘-91 days’) GROUP BY strftime(‘%Y-W%W’, order_date) ) SELECT year_week, total_sales, order_count, — 4週間移動平均 ROUND( AVG(total_sales) OVER ( ORDER BY year_week ROWS BETWEEN 3 PRECEDING AND CURRENT ROW ), 0 ) as moving_avg_4weeks FROM weekly_sales ORDER BY year_week DESC;

2-3. 課題1-3:日次売上推移(直近30日)

直近30日の日次売上と、月初からの累積売上を計算してください。

💡 累積売上(MTD)とは

MTD = Month To Date(月初来)の略で、その月の初日から現在までの累計です。「今月これまでの売上合計」を把握できます。

ステップ1:日次売上を集計する

※横にスクロールできます

— 日ごとの売上を集計 WITH daily_sales AS ( SELECT order_date, SUM(amount) as total_sales, COUNT(*) as order_count FROM orders WHERE order_date >= date(‘now’, ‘-30 days’) GROUP BY order_date )

ステップ2:月初からの累積売上を計算する

※横にスクロールできます

— 月初からの累積売上(MTD) SUM(total_sales) OVER ( PARTITION BY strftime(‘%Y-%m’, order_date) — 月ごとにリセット ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW — 最初から現在まで ) as cumulative_sales_mtd
コードの解説:
  • PARTITION BY strftime('%Y-%m', order_date) → 月が変わるとリセット
  • UNBOUNDED PRECEDING → パーティションの最初の行から
  • SUM(...) OVER (...) → 累積合計を計算

完成コード:

※横にスクロールできます

— 日次売上と累積売上 WITH daily_sales AS ( SELECT order_date, SUM(amount) as total_sales, COUNT(*) as order_count FROM orders WHERE order_date >= date(‘now’, ‘-30 days’) GROUP BY order_date ) SELECT order_date, total_sales, order_count, — 月初からの累積売上(MTD) SUM(total_sales) OVER ( PARTITION BY strftime(‘%Y-%m’, order_date) ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) as cumulative_sales_mtd, — 全期間の累積売上 SUM(total_sales) OVER ( ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) as cumulative_sales_total FROM daily_sales ORDER BY order_date DESC;

🏆 3. 課題2:カテゴリ別ランキング

3-1. 課題2-1:カテゴリ別売れ筋TOP10

各カテゴリで、今月の売上TOP10の商品を抽出してください。

💡 カテゴリ内ランキングとは

例えば「家電」カテゴリ内での1位〜10位、「食品」カテゴリ内での1位〜10位…というように、カテゴリごとに別々のランキングを作成します。

ステップ1:商品ごとの売上を集計する

まず、今月の商品別売上を計算します。

※横にスクロールできます

— 商品別売上を集計 SELECT p.category, — カテゴリ p.product_id, — 商品ID p.product_name, — 商品名 SUM(od.quantity * od.unit_price) as total_sales, — 売上金額 SUM(od.quantity) as total_quantity — 販売数量 FROM products p JOIN order_details od ON p.product_id = od.product_id JOIN orders o ON od.order_id = o.order_id WHERE o.order_date >= date(‘now’, ‘start of month’) — 今月の注文のみ GROUP BY p.category, p.product_id, p.product_name
コードの解説:
  • date('now', 'start of month') → 今月の1日を取得
  • quantity * unit_price → 売上金額を計算
  • 3つのテーブルをJOINして、商品・注文明細・注文を結合

ステップ2:カテゴリ内での順位を付ける

ROW_NUMBER関数を使って、カテゴリ内での順位を付けます。

※横にスクロールできます

— カテゴリ内での順位を付ける ROW_NUMBER() OVER ( PARTITION BY p.category — カテゴリごとに区切る ORDER BY SUM(od.quantity * od.unit_price) DESC — 売上順 ) as rank_in_category
コードの解説:
  • PARTITION BY p.category → カテゴリごとに1位から順位を付け直す
  • ORDER BY ... DESC → 売上が大きい順
  • ROW_NUMBER() → 重複なしで連番を振る

ステップ3:TOP10だけを抽出する

※横にスクロールできます

— TOP10だけを抽出 WHERE rank_in_category <= 10

完成コード:

※横にスクロールできます

— カテゴリ別売上TOP10 WITH product_sales AS ( SELECT p.category, p.product_id, p.product_name, SUM(od.quantity * od.unit_price) as total_sales, SUM(od.quantity) as total_quantity, ROW_NUMBER() OVER ( PARTITION BY p.category ORDER BY SUM(od.quantity * od.unit_price) DESC ) as rank_in_category FROM products p JOIN order_details od ON p.product_id = od.product_id JOIN orders o ON od.order_id = o.order_id WHERE o.order_date >= date(‘now’, ‘start of month’) GROUP BY p.category, p.product_id, p.product_name ) SELECT category, rank_in_category as rank, product_name, total_sales, total_quantity FROM product_sales WHERE rank_in_category <= 10 ORDER BY category, rank_in_category;
💡 推奨インデックス

※横にスクロールできます

CREATE INDEX idx_products_category ON products(category); CREATE INDEX idx_order_details_order ON order_details(order_id); CREATE INDEX idx_orders_date ON orders(order_date);

3-2. 課題2-2:売上ランクの変動

今月と先月の商品ランキングを比較し、順位の変動を表示してください。

求めるデータ:
今月順位 商品名 売上 先月順位 変動
1 商品A 500,000 3 ↑2
2 商品B 450,000 1 ↓1
3 商品C 400,000 NEW

ステップ1:今月のランキングを作成する

※横にスクロールできます

— 今月のランキング WITH this_month AS ( SELECT p.product_id, p.product_name, SUM(od.quantity * od.unit_price) as total_sales, ROW_NUMBER() OVER ( ORDER BY SUM(od.quantity * od.unit_price) DESC ) as rank FROM products p JOIN order_details od ON p.product_id = od.product_id JOIN orders o ON od.order_id = o.order_id WHERE o.order_date >= date(‘now’, ‘start of month’) GROUP BY p.product_id, p.product_name )

ステップ2:先月のランキングを作成する

※横にスクロールできます

— 先月のランキング last_month AS ( SELECT p.product_id, ROW_NUMBER() OVER ( ORDER BY SUM(od.quantity * od.unit_price) DESC ) as rank FROM products p JOIN order_details od ON p.product_id = od.product_id JOIN orders o ON od.order_id = o.order_id WHERE o.order_date >= date(‘now’, ‘start of month’, ‘-1 month’) AND o.order_date < date('now', 'start of month') GROUP BY p.product_id )
コードの解説:
  • date('now', 'start of month', '-1 month') → 先月の1日
  • date('now', 'start of month') → 今月の1日(先月の終わり)
  • 日付の範囲指定で先月だけを対象にする

ステップ3:順位変動を計算する

※横にスクロールできます

— 順位変動を表示 CASE WHEN lm.rank IS NULL THEN ‘NEW’ — 先月なし = 新商品 WHEN lm.rank > tm.rank THEN ‘↑’ || (lm.rank – tm.rank) — 順位上昇 WHEN lm.rank < tm.rank THEN '↓' || (tm.rank - lm.rank) -- 順位下降 ELSE '→' -- 変化なし END as rank_change

完成コード:

※横にスクロールできます

— 売上ランクの変動 WITH this_month AS ( SELECT p.product_id, p.product_name, SUM(od.quantity * od.unit_price) as total_sales, ROW_NUMBER() OVER (ORDER BY SUM(od.quantity * od.unit_price) DESC) as rank FROM products p JOIN order_details od ON p.product_id = od.product_id JOIN orders o ON od.order_id = o.order_id WHERE o.order_date >= date(‘now’, ‘start of month’) GROUP BY p.product_id, p.product_name ), last_month AS ( SELECT p.product_id, ROW_NUMBER() OVER (ORDER BY SUM(od.quantity * od.unit_price) DESC) as rank FROM products p JOIN order_details od ON p.product_id = od.product_id JOIN orders o ON od.order_id = o.order_id WHERE o.order_date >= date(‘now’, ‘start of month’, ‘-1 month’) AND o.order_date < date('now', 'start of month') GROUP BY p.product_id ) SELECT tm.rank as current_rank, tm.product_name, tm.total_sales, lm.rank as last_month_rank, CASE WHEN lm.rank IS NULL THEN 'NEW' WHEN lm.rank > tm.rank THEN ‘↑’ || (lm.rank – tm.rank) WHEN lm.rank < tm.rank THEN '↓' || (tm.rank - lm.rank) ELSE '→' END as rank_change FROM this_month tm LEFT JOIN last_month lm ON tm.product_id = lm.product_id WHERE tm.rank <= 20 ORDER BY tm.rank;
コードの解説:
  • LEFT JOIN → 先月に販売がない新商品も含める
  • || → 文字列の連結(SQLiteの場合)
  • CASE式 → 条件分岐で矢印と数字を組み合わせる

👥 4. 課題3:顧客セグメント分析(RFM分析)

4-1. RFM分析とは

RFM分析は、顧客を3つの指標でスコアリングし、セグメント化するマーケティング手法です。

RFMの3つの指標:
指標 意味 高いと
Recency(最近性) 最後に購入した日からの日数 最近購入した優良顧客
Frequency(頻度) 購入回数 リピート購入が多い常連
Monetary(金額) 購入金額の合計 高額購入するVIP顧客

4-2. 課題3-1:RFM分析の実装

顧客をRFM分析でセグメント化してください。各指標を5段階でスコアリングし、VIP、ロイヤル顧客、離反リスク、休眠顧客などに分類します。

ステップ1:顧客ごとのRFM値を計算する

※横にスクロールできます

— 顧客ごとのRFM値を計算 WITH customer_rfm AS ( SELECT c.customer_id, c.customer_name, — Recency: 最終購入からの日数 julianday(‘now’) – julianday(MAX(o.order_date)) as recency_days, — Frequency: 購入回数 COUNT(o.order_id) as frequency, — Monetary: 総購入金額 SUM(o.amount) as monetary FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id WHERE o.order_date >= date(‘now’, ‘-1 year’) — 過去1年間 GROUP BY c.customer_id, c.customer_name )
コードの解説:
  • julianday('now') - julianday(MAX(o.order_date)) → 最終購入からの日数
  • julianday() → ユリウス日(日数として計算可能な形式)
  • COUNT(o.order_id) → 注文回数
  • SUM(o.amount) → 購入金額の合計

ステップ2:RFMスコアを計算する(5段階)

※横にスクロールできます

— Rスコア(最近購入ほど高い) CASE WHEN recency_days <= 30 THEN 5 -- 30日以内 WHEN recency_days <= 60 THEN 4 -- 60日以内 WHEN recency_days <= 90 THEN 3 -- 90日以内 WHEN recency_days <= 180 THEN 2 -- 半年以内 ELSE 1 -- それ以上前 END as r_score, -- Fスコア(購入回数が多いほど高い) CASE WHEN frequency >= 20 THEN 5 — 20回以上 WHEN frequency >= 10 THEN 4 — 10回以上 WHEN frequency >= 5 THEN 3 — 5回以上 WHEN frequency >= 2 THEN 2 — 2回以上 ELSE 1 — 1回 END as f_score, — Mスコア(購入金額が大きいほど高い) CASE WHEN monetary >= 100000 THEN 5 — 10万円以上 WHEN monetary >= 50000 THEN 4 — 5万円以上 WHEN monetary >= 20000 THEN 3 — 2万円以上 WHEN monetary >= 10000 THEN 2 — 1万円以上 ELSE 1 — それ未満 END as m_score

ステップ3:セグメントを分類する

※横にスクロールできます

— セグメント分類 CASE WHEN r_score >= 4 AND f_score >= 4 AND m_score >= 4 THEN ‘VIP’ WHEN r_score >= 4 AND f_score >= 3 THEN ‘ロイヤル顧客’ WHEN r_score >= 3 AND m_score >= 4 THEN ‘大口顧客’ WHEN r_score <= 2 AND f_score >= 3 THEN ‘離反リスク’ WHEN r_score >= 4 AND f_score <= 2 THEN '新規顧客' WHEN r_score <= 2 AND f_score <= 2 THEN '休眠顧客' ELSE '一般顧客' END as segment
セグメントの意味:
セグメント 特徴 施策
VIP 最近購入、頻繁、高額 特別優待、限定商品
ロイヤル顧客 最近購入、頻繁 ポイント還元強化
離反リスク 以前は頻繁だが最近なし カムバックキャンペーン
新規顧客 最近購入したが回数少ない 2回目購入促進
休眠顧客 長期間購入なし 再活性化メール

完成コード:

※横にスクロールできます

— RFM分析の完全版 WITH customer_rfm AS ( SELECT c.customer_id, c.customer_name, julianday(‘now’) – julianday(MAX(o.order_date)) as recency_days, COUNT(o.order_id) as frequency, SUM(o.amount) as monetary FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id WHERE o.order_date >= date(‘now’, ‘-1 year’) GROUP BY c.customer_id, c.customer_name ), rfm_scores AS ( SELECT customer_id, customer_name, recency_days, frequency, monetary, — Rスコア CASE WHEN recency_days <= 30 THEN 5 WHEN recency_days <= 60 THEN 4 WHEN recency_days <= 90 THEN 3 WHEN recency_days <= 180 THEN 2 ELSE 1 END as r_score, -- Fスコア CASE WHEN frequency >= 20 THEN 5 WHEN frequency >= 10 THEN 4 WHEN frequency >= 5 THEN 3 WHEN frequency >= 2 THEN 2 ELSE 1 END as f_score, — Mスコア CASE WHEN monetary >= 100000 THEN 5 WHEN monetary >= 50000 THEN 4 WHEN monetary >= 20000 THEN 3 WHEN monetary >= 10000 THEN 2 ELSE 1 END as m_score FROM customer_rfm ) SELECT customer_id, customer_name, recency_days, frequency, monetary, r_score, f_score, m_score, — セグメント分類 CASE WHEN r_score >= 4 AND f_score >= 4 AND m_score >= 4 THEN ‘VIP’ WHEN r_score >= 4 AND f_score >= 3 THEN ‘ロイヤル顧客’ WHEN r_score >= 3 AND m_score >= 4 THEN ‘大口顧客’ WHEN r_score <= 2 AND f_score >= 3 THEN ‘離反リスク’ WHEN r_score >= 4 AND f_score <= 2 THEN '新規顧客' WHEN r_score <= 2 AND f_score <= 2 THEN '休眠顧客' ELSE '一般顧客' END as segment FROM rfm_scores ORDER BY r_score DESC, f_score DESC, m_score DESC;

4-3. 課題3-2:セグメント別集計

各セグメントの顧客数と売上を集計してください。

※横にスクロールできます

— セグメント別サマリー WITH customer_rfm AS ( — 上記のRFM分析クエリと同様 … ), rfm_with_segment AS ( — セグメント分類まで含めたクエリ … ) SELECT segment, COUNT(*) as customer_count, — 顧客数 SUM(monetary) as total_sales, — 総売上 ROUND(AVG(monetary), 0) as avg_sales_per_customer, — 平均売上 ROUND(AVG(frequency), 1) as avg_purchase_frequency, — 平均購入回数 ROUND(AVG(recency_days), 0) as avg_recency_days, — 平均最終購入日数 — 構成比 ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 1) as customer_ratio, ROUND(SUM(monetary) * 100.0 / SUM(SUM(monetary)) OVER (), 1) as sales_ratio FROM rfm_with_segment GROUP BY segment ORDER BY total_sales DESC;
結果例:
セグメント 顧客数 売上 顧客比率 売上比率
VIP 500 50,000,000 0.5% 50%
ロイヤル顧客 2,000 30,000,000 2.0% 30%
一般顧客 80,000 4,000,000 80% 4%
💡 パレートの法則(80:20の法則)

上記の結果から、上位2.5%の顧客(VIP + ロイヤル顧客)が売上の80%を占めていることが分かります。これは「パレートの法則」と呼ばれ、ビジネスでよく見られる現象です。

⚡ 5. 課題4:パフォーマンス最適化

5-1. 課題4-1:遅いクエリの診断と改善

以下のクエリが5秒かかっています。問題を診断し、最適化してください。

⚠️ 問題のあるクエリ(実行時間: 5秒)

※横にスクロールできます

— 遅いクエリ SELECT c.customer_name, COUNT(o.order_id) as order_count, SUM(o.amount) as total_amount, MAX(o.order_date) as last_order_date FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id WHERE YEAR(o.order_date) = 2024 GROUP BY c.customer_id, c.customer_name ORDER BY total_amount DESC;

ステップ1:実行計画を確認する

※横にスクロールできます

EXPLAIN QUERY PLAN SELECT c.customer_name, COUNT(o.order_id) as order_count, SUM(o.amount) as total_amount, MAX(o.order_date) as last_order_date FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id WHERE YEAR(o.order_date) = 2024 GROUP BY c.customer_id, c.customer_name ORDER BY total_amount DESC;
実行計画の結果:
QUERY PLAN
|--SCAN customers
|--SCAN orders
`--USE TEMP B-TREE FOR ORDER BY
        

ステップ2:問題点を特定する

🔍 発見した問題点
  1. YEAR(o.order_date)関数をかけているのでインデックスが効かない
  2. SCAN orders全件スキャンが発生
  3. LEFT JOIN → 全顧客を読んでから絞り込み(非効率)
  4. USE TEMP B-TREEソート処理が発生

ステップ3:インデックスを作成する

※横にスクロールできます

— 1. order_dateにインデックス(日付での絞り込み用) CREATE INDEX idx_orders_date ON orders(order_date); — 2. customer_idにインデックス(JOIN用) CREATE INDEX idx_orders_customer ON orders(customer_id);

ステップ4:クエリを改善する

改善ポイント:
  • YEAR(order_date) = 2024order_date >= '2024-01-01' AND order_date < '2025-01-01' に変更
  • LEFT JOININNER JOIN に変更(注文がある顧客だけでOK)

※横にスクロールできます

-- ✅ 最適化後のクエリ SELECT c.customer_name, COUNT(o.order_id) as order_count, SUM(o.amount) as total_amount, MAX(o.order_date) as last_order_date FROM customers c INNER JOIN orders o ON c.customer_id = o.customer_id -- LEFT → INNER WHERE o.order_date >= '2024-01-01' -- 関数を使わない(範囲検索) AND o.order_date < '2025-01-01' GROUP BY c.customer_id, c.customer_name ORDER BY total_amount DESC;
✅ 改善結果
項目 改善前 改善後
実行時間 5秒 0.3秒
改善率 - 約17倍!

5-2. 課題4-2:N+1問題の解決

以下の処理をN+1問題なしで実装してください。
「各顧客の最新3件の注文を取得する」

⚠️ N+1問題が発生するパターン
1. 全顧客を取得(1回のクエリ)
2. 各顧客の注文を取得(N回のクエリ)
   → 顧客が10万人なら、10万回のクエリ!
    

ステップ1:注文に順位を付ける

ROW_NUMBER関数を使って、顧客ごとに注文を新しい順に番号付けします。

※横にスクロールできます

-- 顧客ごとに注文を番号付け WITH ranked_orders AS ( SELECT o.order_id, o.customer_id, o.order_date, o.amount, ROW_NUMBER() OVER ( PARTITION BY o.customer_id -- 顧客ごとに区切る ORDER BY o.order_date DESC -- 新しい順 ) as row_num FROM orders o )
コードの解説:
  • PARTITION BY o.customer_id → 顧客ごとに1から番号を振り直す
  • ORDER BY o.order_date DESC → 最新の注文が1番
  • row_num = 1 が最新、2 が2番目に新しい...

ステップ2:最新3件だけを抽出する

※横にスクロールできます

-- 最新3件だけを抽出 WHERE ro.row_num <= 3

完成コード:

※横にスクロールできます

-- 各顧客の最新3件の注文(N+1問題なし) WITH ranked_orders AS ( SELECT o.order_id, o.customer_id, o.order_date, o.amount, ROW_NUMBER() OVER ( PARTITION BY o.customer_id ORDER BY o.order_date DESC ) as row_num FROM orders o ) SELECT c.customer_id, c.customer_name, ro.order_id, ro.order_date, ro.amount FROM customers c JOIN ranked_orders ro ON c.customer_id = ro.customer_id WHERE ro.row_num <= 3 ORDER BY c.customer_id, ro.row_num;
✅ N+1問題の解決
方法 クエリ回数
N+1問題あり 1 + 10万 = 100,001回
ウィンドウ関数使用 1回

5-3. 課題4-3:インデックス設計

以下のクエリを最適化するために、最適なインデックスを設計してください。

※横にスクロールできます

SELECT * FROM orders WHERE customer_id = 101 AND order_date >= '2024-01-01' AND status = 'completed' ORDER BY order_date DESC LIMIT 10;

インデックス設計の考え方:

複合インデックスの列の順序:
  1. =条件の列を先に(最も絞り込める)
  2. 範囲条件の列を後に
  3. ORDER BYの列を最後に

このクエリの場合:

条件 順序
customer_id = 101(等価条件) 1番目
status = 'completed'(等価条件) 2番目
order_date >= '2024-01-01' + ORDER BY DESC 3番目

最適なインデックス:

※横にスクロールできます

-- 最適な複合インデックス CREATE INDEX idx_orders_optimal ON orders(customer_id, status, order_date DESC);
コードの解説:
  • customer_id → WHERE句の=条件(最も絞り込める)
  • status → WHERE句の=条件
  • order_date DESC → WHERE句の範囲条件 + ORDER BY

実行計画で確認:

※横にスクロールできます

EXPLAIN QUERY PLAN SELECT * FROM orders WHERE customer_id = 101 AND order_date >= '2024-01-01' AND status = 'completed' ORDER BY order_date DESC LIMIT 10;
期待される結果:
QUERY PLAN
`--SEARCH orders USING INDEX idx_orders_optimal
        

USE TEMP B-TREE が出ない = ソート処理が不要!

🎓 6. 課題5:総合ダッシュボード

6-1. 経営ダッシュボード用の統合クエリ

以下の情報を1つのクエリで取得してください。

💡 必要な情報
  • 今月の売上・注文数
  • 前月比・前年同月比
  • 今月の新規顧客数
  • 売上TOP5商品

ステップ1:今月の売上を計算する

※横にスクロールできます

-- 今月の売上 WITH current_month AS ( SELECT SUM(amount) as sales, COUNT(*) as orders FROM orders WHERE order_date >= date('now', 'start of month') )

ステップ2:先月の売上を計算する

※横にスクロールできます

-- 先月の売上 last_month AS ( SELECT SUM(amount) as sales, COUNT(*) as orders FROM orders WHERE order_date >= date('now', 'start of month', '-1 month') AND order_date < date('now', 'start of month') )

ステップ3:前年同月の売上を計算する

※横にスクロールできます

-- 前年同月の売上 last_year_same_month AS ( SELECT SUM(amount) as sales, COUNT(*) as orders FROM orders WHERE order_date >= date('now', 'start of month', '-1 year') AND order_date < date('now', 'start of month', '-1 year', '+1 month') )

ステップ4:新規顧客数を計算する

※横にスクロールできます

-- 今月の新規顧客数 new_customers AS ( SELECT COUNT(*) as count FROM customers WHERE created_at >= date('now', 'start of month') )

ステップ5:売上TOP5商品を取得する

※横にスクロールできます

-- 売上TOP5商品 top_products AS ( SELECT p.product_name, SUM(od.quantity * od.unit_price) as sales FROM products p JOIN order_details od ON p.product_id = od.product_id JOIN orders o ON od.order_id = o.order_id WHERE o.order_date >= date('now', 'start of month') GROUP BY p.product_id, p.product_name ORDER BY sales DESC LIMIT 5 )

ステップ6:全てを結合する

※横にスクロールできます

-- 結果を出力 SELECT -- 基本指標 cm.sales as current_sales, cm.orders as current_orders, -- 前月比 ROUND((cm.sales - lm.sales) * 100.0 / lm.sales, 1) as mom_growth, -- 前年同月比 ROUND((cm.sales - ly.sales) * 100.0 / ly.sales, 1) as yoy_growth, -- 新規顧客 nc.count as new_customers FROM current_month cm, last_month lm, last_year_same_month ly, new_customers nc;

完成コード:

※横にスクロールできます

-- 総合ダッシュボードクエリ WITH current_month AS ( SELECT SUM(amount) as sales, COUNT(*) as orders FROM orders WHERE order_date >= date('now', 'start of month') ), last_month AS ( SELECT SUM(amount) as sales, COUNT(*) as orders FROM orders WHERE order_date >= date('now', 'start of month', '-1 month') AND order_date < date('now', 'start of month') ), last_year_same_month AS ( SELECT SUM(amount) as sales, COUNT(*) as orders FROM orders WHERE order_date >= date('now', 'start of month', '-1 year') AND order_date < date('now', 'start of month', '-1 year', '+1 month') ), new_customers AS ( SELECT COUNT(*) as count FROM customers WHERE created_at >= date('now', 'start of month') ), top_products AS ( SELECT p.product_name, SUM(od.quantity * od.unit_price) as sales FROM products p JOIN order_details od ON p.product_id = od.product_id JOIN orders o ON od.order_id = o.order_id WHERE o.order_date >= date('now', 'start of month') GROUP BY p.product_id, p.product_name ORDER BY sales DESC LIMIT 5 ) SELECT cm.sales as current_sales, cm.orders as current_orders, ROUND((cm.sales - lm.sales) * 100.0 / lm.sales, 1) as mom_growth, ROUND((cm.sales - ly.sales) * 100.0 / ly.sales, 1) as yoy_growth, nc.count as new_customers, (SELECT group_concat(product_name || ':' || sales, ', ') FROM top_products) as top_5_products FROM current_month cm, last_month lm, last_year_same_month ly, new_customers nc;
結果例:
項目
今月売上 6,500,000
今月注文数 3,200
前月比 +8.3%
前年同月比 +15.2%
新規顧客数 1,250
💡 実用化のヒント
  • ビュー化:毎回書かなくて済む
  • キャッシュ:5〜15分キャッシュ
  • 非同期:バックグラウンドで更新
  • サマリーテーブル:事前集計で高速化

📝 7. プロジェクトのまとめ

7-1. 使用した技術の一覧

✅ このプロジェクトで使用した技術
技術 使用場面
ウィンドウ関数(LAG) 前月比・前年同月比の計算
ウィンドウ関数(ROW_NUMBER) ランキング、最新N件の取得
ウィンドウ関数(AVG OVER) 移動平均の計算
ウィンドウ関数(SUM OVER) 累積売上の計算
WITH句(CTE) 複雑なクエリの整理
JOIN(INNER/LEFT) 複数テーブルの結合
CASE式 条件分岐、セグメント分類
日付関数 日付の計算と抽出
集計関数 SUM, COUNT, AVG, MAX
インデックス設計 パフォーマンス最適化

7-2. パフォーマンス最適化のポイント

📌 パフォーマンス最適化のチェックリスト
  1. 適切なインデックス:WHERE、JOIN、ORDER BY の列にインデックス
  2. 関数を避けるYEAR(date) → 範囲検索に書き換え
  3. N+1問題解決:ウィンドウ関数で一度に取得
  4. LEFT JOINの見直し:INNER JOINで十分な場合は変更
  5. 実行計画確認:EXPLAIN QUERY PLANで検証

7-3. 実務での応用

実務での応用シーン:
  • 定期レポート:バッチで夜間に自動実行
  • リアルタイムダッシュボード:キャッシュ + 増分更新
  • 大量データ分析:パーティショニング + サマリーテーブル
  • 可視化:BIツール連携(Tableau、Metabase、Redash など)

🚀 8. 次のステップ

🎓 おめでとうございます!

SQL応用・パフォーマンス最適化コースを完了しました!
あなたは今や、実務レベルのSQL技術を身につけています。

8-1. さらに学びたい方へ

📚 発展的なトピック
トピック 説明
NoSQL MongoDB、DynamoDB(非構造化データ向け)
データウェアハウス BigQuery、Redshift、Snowflake(大規模分析向け)
データパイプライン ETL、Apache Airflow(データの自動処理)
機械学習 SQL + Python(Pandas)で分析
リアルタイム分析 Apache Kafka、ClickHouse(ストリーミング処理)

8-2. キャリアパス

💼 SQLスキルを活かせる職種:
  • データアナリスト:ビジネス課題をSQLで解決
  • データエンジニア:データ基盤を構築・運用
  • バックエンドエンジニア:高速なAPIを開発
  • BIエンジニア:ダッシュボードを作成
  • データサイエンティスト:分析・機械学習を実施

8-3. 継続的な学習

📖 おすすめの学習方法
  • 実務データで練習:仕事で使って定着させる
  • コミュニティ参加:Stack Overflow、Qiita で質問・回答
  • 公式ドキュメント:PostgreSQL、MySQL の公式ドキュメント
  • ブログ執筆:学んだことをアウトプット
  • オープンデータ:Kaggle のデータセットで練習

❓ よくある質問

Q1: 総合演習が難しすぎます...

大丈夫です!1つずつ段階的に進めましょう。まずは簡単な集計から始めて、徐々に複雑なクエリに挑戦してください。解答例を見ながら、自分で書き直すのも良い練習になります。

Q2: 実務でこんな複雑なクエリを書きますか?

はい、データ分析やレポート作成では日常的に使います。ただし、実務ではビューやサマリーテーブルに保存して再利用することが多いです。毎回複雑なクエリを書くわけではありません。

Q3: どのくらい練習すれば実務レベルになれますか?

3〜6ヶ月の実務経験があれば、十分なスキルが身につきます。このコースで学んだことを仕事で使いながら、徐々に慣れていきましょう。最初は時間がかかっても、繰り返すうちに速くなります。

Q4: おすすめのツールはありますか?

おすすめツール:
  • データベースGUI:DBeaver(無料)、TablePlus、DataGrip
  • BIツール:Metabase(無料)、Redash、Tableau
  • ノートブック:Jupyter Notebook + SQL Magic

Q5: SQLだけで食べていけますか?

はい!データアナリストBIエンジニアとして、SQLを中心に仕事をしている人は多くいます。ただし、Excel、Python、BIツールも合わせて使えると、より市場価値が高まります。

Q6: このコースを終えたら何ができるようになりますか?

このコースで身につくスキル:
  • 実務レベルのSQL分析ができる
  • パフォーマンス問題を診断・解決できる
  • ダッシュボード用のクエリが書ける
  • データ基盤の設計ができる
  • データドリブンな意思決定を支援できる

🎉 コース完了おめでとうございます! 🎉

SQL応用・パフォーマンス最適化コースを修了しました!
あなたは今や、実務で活躍できるSQL技術を身につけています。

学んだことを実務で活かして、データドリブンな意思決定に貢献してください!
Keep Learning, Keep Growing! 🚀

📚 SQL応用コース 全体のまとめ

Step 1〜5:サブクエリ編
  • サブクエリの基本(WHERE句、FROM句、SELECT句)
  • 相関サブクエリ
  • EXISTS、NOT EXISTS
  • ALL、ANY、SOME
  • スカラーサブクエリ
Step 6〜9:ウィンドウ関数編
  • ウィンドウ関数入門(OVER句)
  • 集計ウィンドウ関数(SUM、AVG、COUNT)
  • フレーム指定(ROWS BETWEEN、RANGE BETWEEN)
  • LAG、LEAD、FIRST_VALUE、LAST_VALUE
Step 10〜13:高度なテクニック編
  • CROSS JOINと自己結合
  • 複雑な結合パターン
  • CASE式と条件分岐
  • 高度なINSERT/UPDATE/DELETE
Step 14〜19:パフォーマンス最適化編
  • インデックスの基礎理論
  • インデックス設計の実践
  • インデックスの落とし穴
  • 実行計画の読み方
  • クエリ最適化テクニック
  • パフォーマンスチューニング実践
Step 20:総合演習プロジェクト
  • 売上推移分析(月次・週次・日次)
  • カテゴリ別ランキング
  • RFM分析による顧客セグメント
  • パフォーマンス最適化の実践
  • 経営ダッシュボードの作成
📝

学習メモ

SQL応用・パフォーマンス最適化 - Step 20

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