SQL / LESSON 17 / SQL17

SQL:総合演習:売上分析レポート

1学習の目的

2基礎解説

役割使う道具(学んだ章)
① 抽出必要な行を選ぶWHERE・論理演算・NULL処理(SQL02〜05)
② 結合複数テーブルをつなぐINNER/LEFT JOIN(SQL09・10)
③ 集計まとめて数値化する集計関数・GROUP BY・HAVING(SQL06〜08)
④ 加工見やすい形にするCASE・文字列・日付(SQL12・13)
✅ 分析クエリを組み立てる手順
  1. まず「何を求めたいか」を日本語で書き出す。「カテゴリ別・月別の売上」「平均以上の顧客」など、SQLを書く前に目的を明確にする。
  2. JOIN → GROUP BY → 集計関数の順で骨格を作る。動く最小限のクエリをまず完成させ、それから列や条件を足していく。
  3. NULLと0を最初に意識する。LEFT JOINで対応が無い行、COALESCEで置き換えるべき値——SQL05・SQL10・SQL13で学んだ罠は、分析クエリでこそ牙を剥く。
  4. CASEやサブクエリは最後に加える。基本の集計が正しく動いてから、ランク付けや平均比較といった仕上げの加工を重ねる。
  5. 常に手計算で検算する。SQL14で学んだとおり、「自分の予測」と「実行結果」を照らし合わせる習慣が、分析の正しさを担保する。

この章の題材:これまで使ってきた products・customers・orders の3テーブルから、経営会議に出せる水準の分析レポートを作ります。

-- 骨格:月別×カテゴリ別の売上(JOIN→GROUP BY→集計)
SELECT
  SUBSTR(o.order_date, 1, 7) AS 月,
  p.category AS 分類,
  SUM(p.price * o.qty) AS 売上
FROM orders o
JOIN products p ON o.product_id = p.id
WHERE p.category IS NOT NULL
GROUP BY 月, 分類
ORDER BY 月, 売上 DESC;

-- 仕上げ:平均との比較をCASEで加える
SELECT
  c.name AS 顧客,
  COALESCE(SUM(p.price * o.qty), 0) AS 売上,
  CASE
    WHEN COALESCE(SUM(p.price * o.qty), 0) >= (
      SELECT AVG(t.売上) FROM (
        SELECT COALESCE(SUM(p2.price * o2.qty), 0) AS 売上
        FROM customers c2
        LEFT JOIN orders   o2 ON c2.id = o2.customer_id
        LEFT JOIN products p2 ON o2.product_id = p2.id
        GROUP BY c2.id
      ) t
    ) THEN '平均以上'
    ELSE '平均未満'
  END AS 判定
FROM customers c
LEFT JOIN orders   o ON c.id = o.customer_id
LEFT JOIN products p ON o.product_id = p.id
GROUP BY c.name
ORDER BY 売上 DESC;

3基本ドリル(10問)

SQL17-D01 商品ごとの総売上(price*qtyの合計)を、商品名(別名「商品」)と売上(別名「売上」)で取り出し、売上の多い順に並べるSQLを書け。 コーディング★☆☆無料
期待される結果

5行。ノートPCが先頭

ヒント

JOINしてから GROUP BY p.name、ORDER BY 売上 DESC。

模範解答
SELECT p.name AS 商品, SUM(p.price * o.qty) AS 売上
FROM orders o
JOIN products p ON o.product_id = p.id
GROUP BY p.name
ORDER BY 売上 DESC;
解説

JOIN→GROUP BY→ORDER BYという分析の基本骨格。SQL09で最初に習ったこの形が、すべての土台になる。

SQL17-D02 分析クエリを組み立てるとき、最初にやるべきことはどれか。
A. いきなり複雑なSQLを書く B. 何を求めたいかを日本語で明確にする C. すべての列を先にSELECTする D. インデックスを先に作る
選択★☆☆無料
期待される結果

解答 == B

ヒント

目的が曖昧なまま書き始めると、途中で迷子になる。

模範解答
B
解説

「カテゴリ別の売上が知りたい」のか「顧客ごとの傾向が知りたい」のかで、書くSQLはまったく変わる。目的を先に決めることが、遠回りを防ぐ。

SQL17-D03 顧客ごとの注文件数を、顧客名(別名「顧客」)と件数(別名「件数」、取引0件も表示)で取り出すSQLを書け。 コーディング★☆☆無料
期待される結果

4行。高橋建設が0

ヒント

LEFT JOINしてCOUNT(o.id)を使う(SQL10のCOUNT(*)の罠を思い出す)。

模範解答
SELECT c.name AS 顧客, COUNT(o.id) AS 件数
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.name;
解説

COUNT(*)ではなくCOUNT(o.id)を使う判断——SQL10で学んだ最重要の罠が、ここでも生きている。

SQL17-D04 月別の売上を集計するSQLを完成させよ。 穴埋め★☆☆無料
コード
SELECT ____(o.order_date, 1, 7) AS 月, SUM(p.price*o.qty) AS 売上
FROM orders o JOIN products p ON o.product_id=p.id
GROUP BY 月;
期待される結果

2行。2026-01と2026-02

ヒント

日付から一部を切り出す関数(SQL12)。

模範解答
SUBSTR
解説

SUBSTRで日付から年月を作る——月次レポートの土台になる、SQL12で学んだ最重要のテクニック。

SQL17-D05 「取引のない顧客も含めて」分析したいとき、使うべき結合はどれか。
A. INNER JOIN B. LEFT JOIN C. 結合しない D. サブクエリのみ
選択★☆☆無料
期待される結果

解答 == B

ヒント

片方のテーブルを全件残す結合。

模範解答
B
解説

INNER JOINだと取引0件の顧客が消える(SQL09・10)。全体像を見たい分析では、まずLEFT JOINを検討する。

SQL17-D06 category が NULL でない商品について、カテゴリごとの平均単価(別名「平均」、小数第1位まで)を取り出すSQLを書け。 コーディング★★☆無料
期待される結果

2行。PC 86500.0

ヒント

WHERE category IS NOT NULL、GROUP BY category、ROUND(AVG(price),1)。

模範解答
SELECT category, ROUND(AVG(price), 1) AS 平均
FROM products
WHERE category IS NOT NULL
GROUP BY category;
解説

NULLを先に除いてから集計するのがSQL07・08で学んだ基本作法。

SQL17-D07 注文が3件以上ある顧客を取り出すSQLを完成させよ。 穴埋め★★☆無料
コード
SELECT c.name, COUNT(o.id) AS 件数
FROM customers c LEFT JOIN orders o ON c.id=o.customer_id
GROUP BY c.name
____ COUNT(o.id) >= 3;
期待される結果

1行。田中商事 3

ヒント

集計後の条件を指定するキーワード(SQL08)。

模範解答
HAVING
解説

WHEREでは書けない「集計後の条件」——SQL08で学んだHAVINGがここでも必須になる。

SQL17-D08 顧客の売上合計に応じてランク分けしたい。最も適した書き方はどれか。
A. 手作業で1件ずつ確認する B. CASE式で条件分岐する C. 別のテーブルを作る D. Excelに貼り付けて計算する
選択★★☆無料
期待される結果

解答 == B

ヒント

「条件によって値を変える」処理(SQL13)。

模範解答
B
解説

CASE式は分析クエリの仕上げに欠かせない。集計した数値を、そのままレポート上の「意味のある区分」に変換できる。

SQL17-D09 全顧客の平均売上(取引0円を含む)を1つの値として取り出すSQLを書け(サブクエリを使うこと)。 コーディング★★☆無料
期待される結果

1行。133125.0

ヒント

FROM句のサブクエリで顧客ごとの売上を作り、その結果をAVGする(SQL11)。

模範解答
SELECT AVG(t.売上) AS 平均売上
FROM (
  SELECT COALESCE(SUM(p.price * o.qty), 0) AS 売上
  FROM customers c
  LEFT JOIN orders   o ON c.id = o.customer_id
  LEFT JOIN products p ON o.product_id = p.id
  GROUP BY c.id
) t;
解説

集計結果をさらに集計する——SQL11で学んだ「FROM句のサブクエリ」がここで再登場する。

SQL17-D10 売上上位3商品を、商品名(別名「商品」)・数量(別名「数量」)・売上(別名「売上」)で取り出すSQLを書け。 コーディング★★☆無料
期待される結果

3行。ノートPCが先頭

ヒント

GROUP BY → ORDER BY 売上 DESC → LIMIT 3。

模範解答
SELECT p.name AS 商品, SUM(o.qty) AS 数量, SUM(p.price * o.qty) AS 売上
FROM orders o
JOIN products p ON o.product_id = p.id
GROUP BY p.name
ORDER BY 売上 DESC
LIMIT 3;
解説

ランキングの基本形(SQL04・09)。ここまでの全章がこの1問に集約されている。

4実践シナリオ(5問)

SQL17-S01 【月別×カテゴリ別売上マトリクス】月(別名「月」)とカテゴリ(別名「分類」)ごとの売上(別名「売上」)を取り出し、月の順・売上の多い順に並べるSQLを書け(categoryがNULLの商品は除く)。 コーディング★★☆無料
期待される結果

4行。2026-01のPCが先頭

ヒント

3テーブルは不要。ordersとproductsのJOINで足りる。WHEREでNULLを除く。

模範解答
SELECT
  SUBSTR(o.order_date, 1, 7) AS 月,
  p.category AS 分類,
  SUM(p.price * o.qty) AS 売上
FROM orders o
JOIN products p ON o.product_id = p.id
WHERE p.category IS NOT NULL
GROUP BY 月, 分類
ORDER BY 月, 売上 DESC;
解説

GROUP BYに2つの軸を指定する——月別だけ、カテゴリ別だけでは見えない「月ごとにどのカテゴリが強いか」が分かる、実務のクロス集計そのもの。

SQL17-S02 【顧客ランク一覧】顧客ごとの売上(0円含む)に応じて「ゴールド」(20万円以上)「シルバー」(10万円以上)「ブロンズ」(それ未満)にランク分けし、顧客名(別名「顧客」)・売上(別名「売上」)・ランク(別名「ランク」)を売上の多い順に取り出すSQLを書け。 コーディング★★☆無料
期待される結果

4行。田中商事 ゴールド が先頭

ヒント

LEFT JOIN + COALESCE + CASE(SQL10・13の組み合わせ)。

模範解答
SELECT
  c.name AS 顧客,
  COALESCE(SUM(p.price * o.qty), 0) AS 売上,
  CASE
    WHEN COALESCE(SUM(p.price * o.qty), 0) >= 200000 THEN 'ゴールド'
    WHEN COALESCE(SUM(p.price * o.qty), 0) >= 100000 THEN 'シルバー'
    ELSE 'ブロンズ'
  END AS ランク
FROM customers c
LEFT JOIN orders   o ON c.id = o.customer_id
LEFT JOIN products p ON o.product_id = p.id
GROUP BY c.name
ORDER BY 売上 DESC;
解説

SQL13で作ったのと同じ形。総合演習といっても、真新しい構文が必要なわけではない——これまでの道具を正しく組み合わせられるかがすべて

SQL17-S03 【死に筋商品レポート】一度も注文されていない商品を、商品名(別名「商品」)・分類(NULLは「未分類」、別名「分類」)・在庫(別名「在庫」)で取り出すSQLを書け。 コーディング★★☆無料
期待される結果

2行。新商品A / サンプル品

ヒント

LEFT JOINしてWHERE o.id IS NULL(SQL10・SQL11のNOT EXISTSでも可)。

模範解答
SELECT
  p.name AS 商品,
  COALESCE(p.category, '未分類') AS 分類,
  p.stock AS 在庫
FROM products p
LEFT JOIN orders o ON p.id = o.product_id
WHERE o.id IS NULL;
解説

「無いこと」を見つける分析(SQL10・11)。売れ筋だけでなく死に筋を把握することも、在庫管理では同じくらい重要になる。

SQL17-S04 【平均超えの優良顧客】全顧客の平均売上(0円含む)を基準に、平均以上の顧客だけを、顧客名(別名「顧客」)・売上(別名「売上」)で取り出すSQLを書け(サブクエリを使うこと)。 コーディング★★★無料
期待される結果

2行。田中商事 / 佐藤物産

ヒント

D09で作った平均をHAVINGの中でサブクエリとして使う(SQL11)。

模範解答
SELECT c.name AS 顧客, COALESCE(SUM(p.price * o.qty), 0) AS 売上
FROM customers c
LEFT JOIN orders   o ON c.id = o.customer_id
LEFT JOIN products p ON o.product_id = p.id
GROUP BY c.name
HAVING COALESCE(SUM(p.price * o.qty), 0) >= (
  SELECT AVG(t.売上) FROM (
    SELECT COALESCE(SUM(p2.price * o2.qty), 0) AS 売上
    FROM customers c2
    LEFT JOIN orders   o2 ON c2.id = o2.customer_id
    LEFT JOIN products p2 ON o2.product_id = p2.id
    GROUP BY c2.id
  ) t
)
ORDER BY 売上 DESC;
解説

SQL11の仕上げ課題と同じ構造がここでも使われている。「取引のない顧客も含めた正しい平均」を基準にすることの重要性は、SQL11で学んだとおり。

SQL17-S05 【エリア別サマリー】エリアごとに、顧客数(別名「顧客数」)・総売上(別名「売上」、0円含む)・平均注文額(別名「平均注文額」、小数第1位まで、注文が無ければNULLのままでよい)を取り出し、売上の多い順に並べるSQLを書け。 コーディング★★★無料
期待される結果

3行。東京が先頭

ヒント

顧客数はCOUNT(DISTINCT c.id)、平均注文額は売上合計÷注文件数(COUNT(o.id)で0除算に注意——0件なら自動でNULLになる)。

模範解答
SELECT
  c.area AS エリア,
  COUNT(DISTINCT c.id) AS 顧客数,
  COALESCE(SUM(p.price * o.qty), 0) AS 売上,
  ROUND(SUM(p.price * o.qty) * 1.0 / NULLIF(COUNT(o.id), 0), 1) AS 平均注文額
FROM customers c
LEFT JOIN orders   o ON c.id = o.customer_id
LEFT JOIN products p ON o.product_id = p.id
GROUP BY c.area
ORDER BY 売上 DESC;
解説

NULLIFという新顔の関数:第1引数と第2引数が等しければNULLを返す。0件のとき0で割ってエラーになるのを防ぐ、実務でよく使われる安全策。ここまで学んだ知識を組み合わせれば、初見の関数も名前から意味を推測して使いこなせる。

5仕上げ課題

SQL17-FINAL 【経営会議用 総合売上分析レポート】これがSQL編の卒業課題。経営陣から「次回の会議で使う分析資料を1本のSQLで出してほしい」と依頼された。

表示する列
① 顧客名 → 別名「顧客」
② エリア → 別名「エリア」
③ 注文件数(0件も表示)→ 別名「注文件数」
④ 売上合計(0円も表示)→ 別名「売上」
⑤ 最もよく買っている商品名(金額ベースで最大、取引が無ければ「なし」)→ 別名「主力商品」
⑥ ランク(20万円以上「ゴールド」/10万円以上「シルバー」/1円以上「ブロンズ」/0円「未取引」)→ 別名「ランク」
⑦ 全顧客平均との比較(平均以上「◎」、平均未満「△」、未取引「-」)→ 別名「評価」

並び順:売上の大きい順

期待される結果(7列 × 4行)
顧客 | エリア | 注文件数 | 売上 | 主力商品 | ランク | 評価
田中商事 | 東京 | 3 | 297500 | ノートPC | ゴールド | ◎
佐藤物産 | 東京 | 2 | 160000 | ノートPC | シルバー | ◎
鈴木工業 | 大阪 | 2 | 75000 | モニター | ブロンズ | △
高橋建設 | 福岡 | 0 | 0 | なし | 未取引 | -
期待される結果

7列 × 4行が完全一致

ヒント

⑤の主力商品は相関サブクエリで「その顧客の注文の中で金額が最大の1件」を取得する(SQL11のS04と同じ考え方)。COALESCEで取引が無い場合に'なし'を返す。平均は全顧客平均のサブクエリ(D09と同じ)。⑦はランクが'未取引'かどうかで判定するのが簡潔。

模範解答
SELECT
  c.name AS 顧客,
  c.area AS エリア,
  COUNT(o.id) AS 注文件数,
  COALESCE(SUM(p.price * o.qty), 0) AS 売上,
  COALESCE(
    (
      SELECT p3.name
      FROM orders o3
      JOIN products p3 ON o3.product_id = p3.id
      WHERE o3.customer_id = c.id
      ORDER BY p3.price * o3.qty DESC
      LIMIT 1
    ), 'なし'
  ) AS 主力商品,
  CASE
    WHEN COALESCE(SUM(p.price * o.qty), 0) = 0 THEN '未取引'
    WHEN COALESCE(SUM(p.price * o.qty), 0) >= 200000 THEN 'ゴールド'
    WHEN COALESCE(SUM(p.price * o.qty), 0) >= 100000 THEN 'シルバー'
    ELSE 'ブロンズ'
  END AS ランク,
  CASE
    WHEN COALESCE(SUM(p.price * o.qty), 0) = 0 THEN '-'
    WHEN COALESCE(SUM(p.price * o.qty), 0) >= (
      SELECT AVG(t.売上) FROM (
        SELECT COALESCE(SUM(p2.price * o2.qty), 0) AS 売上
        FROM customers c2
        LEFT JOIN orders   o2 ON c2.id = o2.customer_id
        LEFT JOIN products p2 ON o2.product_id = p2.id
        GROUP BY c2.id
      ) t
    ) THEN '◎'
    ELSE '△'
  END AS 評価
FROM customers c
LEFT JOIN orders   o ON c.id = o.customer_id
LEFT JOIN products p ON o.product_id = p.id
GROUP BY c.name, c.area
ORDER BY 売上 DESC;
解説

これがSQL編の到達点だ。この1本のクエリには、SQL01からSQL16までのほぼすべてが詰まっている。LEFT JOIN(消えるはずの高橋建設を残す)、COALESCE(0円・NULLの安全な処理)、CASE(ランクと評価の分類)、相関サブクエリ(顧客ごとの主力商品)、FROM句のサブクエリ(全体平均との比較)——これらが1つのSELECT文の中で連携している。

最も難しいのは⑤の主力商品だ。顧客ごとに「その人の注文の中で最も高額な1件」を求めるという条件は、外側のクエリの c.id を内側のサブクエリが参照する相関サブクエリでしか書けない。SQL11で学んだ「顧客ごとの最大」という考え方が、ここで実務のレポートとして結実している。

そして高橋建設の行を見てほしい。注文件数0、売上0、主力商品「なし」、ランク「未取引」、評価「-」——すべての列が矛盾なく「取引が無い」という1つの事実を表している。もしCOALESCEやCASEのNULL処理を1箇所でも間違えていたら、この行のどこかに空欄やNULLが紛れ込み、レポート全体の信頼性を損ねていた。

ここまで来たあなたは、もう「SQLを勉強している人」ではありません。SQLでデータを分析できる人です。SELECT一行から始まったこの17章が、経営会議に出せる1本のレポートにたどり着いた。次にあなたが分析するデータは、あなた自身の仕事の中にあるはずです。

お疲れさまでした。