SQL:総合演習:売上分析レポート
1学習の目的
- SQL01〜SQL16で学んだすべてを組み合わせ、実務水準の分析レポートを自力で組み立てられるようになる。
- 「どの場面でどの道具を使うか」を判断できるようになる。SELECT一本だけだった最初の章から、ここまで積み上げてきたものを1つのクエリに収める。
2基礎解説
| 層 | 役割 | 使う道具(学んだ章) |
|---|---|---|
| ① 抽出 | 必要な行を選ぶ | WHERE・論理演算・NULL処理(SQL02〜05) |
| ② 結合 | 複数テーブルをつなぐ | INNER/LEFT JOIN(SQL09・10) |
| ③ 集計 | まとめて数値化する | 集計関数・GROUP BY・HAVING(SQL06〜08) |
| ④ 加工 | 見やすい形にする | CASE・文字列・日付(SQL12・13) |
- まず「何を求めたいか」を日本語で書き出す。「カテゴリ別・月別の売上」「平均以上の顧客」など、SQLを書く前に目的を明確にする。
- JOIN → GROUP BY → 集計関数の順で骨格を作る。動く最小限のクエリをまず完成させ、それから列や条件を足していく。
- NULLと0を最初に意識する。LEFT JOINで対応が無い行、COALESCEで置き換えるべき値——SQL05・SQL10・SQL13で学んだ罠は、分析クエリでこそ牙を剥く。
- CASEやサブクエリは最後に加える。基本の集計が正しく動いてから、ランク付けや平均比較といった仕上げの加工を重ねる。
- 常に手計算で検算する。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問)
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で最初に習ったこの形が、すべての土台になる。
A. いきなり複雑なSQLを書く B. 何を求めたいかを日本語で明確にする C. すべての列を先にSELECTする D. インデックスを先に作る 選択★☆☆無料
解答 == B
目的が曖昧なまま書き始めると、途中で迷子になる。
B
「カテゴリ別の売上が知りたい」のか「顧客ごとの傾向が知りたい」のかで、書く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で学んだ最重要の罠が、ここでも生きている。
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で学んだ最重要のテクニック。
A. INNER JOIN B. LEFT JOIN C. 結合しない D. サブクエリのみ 選択★☆☆無料
解答 == B
片方のテーブルを全件残す結合。
B
INNER JOINだと取引0件の顧客が消える(SQL09・10)。全体像を見たい分析では、まずLEFT JOINを検討する。
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で学んだ基本作法。
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がここでも必須になる。
A. 手作業で1件ずつ確認する B. CASE式で条件分岐する C. 別のテーブルを作る D. Excelに貼り付けて計算する 選択★★☆無料
解答 == B
「条件によって値を変える」処理(SQL13)。
B
CASE式は分析クエリの仕上げに欠かせない。集計した数値を、そのままレポート上の「意味のある区分」に変換できる。
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句のサブクエリ」がここで再登場する。
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問)
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つの軸を指定する——月別だけ、カテゴリ別だけでは見えない「月ごとにどのカテゴリが強いか」が分かる、実務のクロス集計そのもの。
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で作ったのと同じ形。総合演習といっても、真新しい構文が必要なわけではない——これまでの道具を正しく組み合わせられるかがすべて。
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)。売れ筋だけでなく死に筋を把握することも、在庫管理では同じくらい重要になる。
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で学んだとおり。
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仕上げ課題
表示する列
① 顧客名 → 別名「顧客」
② エリア → 別名「エリア」
③ 注文件数(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本のレポートにたどり着いた。次にあなたが分析するデータは、あなた自身の仕事の中にあるはずです。
お疲れさまでした。