🎯 最終演習

SQLでECサイトの売上集計レポートを作ってみよう(総合演習)

ここまでの SQL レッスンで学んだことを全部つないで、実務で通用する売上レポートを1本仕上げます。新しい構文は出てきません。JOIN・GROUP BY・HAVING・CASE式・WITH句——すでに練習したものだけで組み立てます。

題材は架空のECサイト「プロ道ストア」です。顧客・商品・注文・注文明細という実際のECサイトとほぼ同じ4つのテーブルを用意しました。「先月の売上を商品別に出して」と頼まれたときに書くのが、まさにこの種のクエリです。

1ステップずつ進むので、途中で止めてもそこまでの分は動きます。各ステップの「実行する」ボタンはブラウザの中でSQLiteを動かしているので、条件を書き換えて試すこともできます。

完成イメージ

最後まで進むと、こう表示されるところまで作ります。下の実行結果は、このページを作るときに実際にコードを走らせて得たものです。

┌─────────┬──────────┬──────────┬───────┬──────┬──────┐
│  年月   │ 売上合計 │ 周辺機器 │ 家具  │ 書籍 │ 評価 │
├─────────┼──────────┼──────────┼───────┼──────┼──────┤
│ 2026-06 │ 21160    │ 18760    │ 0     │ 2400 │ 通常 │
│ 2026-07 │ 28880    │ 16480    │ 12400 │ 0    │ 通常 │
│ 2026-08 │ 46480    │ 33080    │ 3800  │ 9600 │ 好調 │
└─────────┴──────────┴──────────┴───────┴──────┴──────┘

STEP 1ECサイトのデータベースを作る

🎯 このステップのゴール: 顧客・商品・注文・注文明細の4テーブルを用意する

まずはデータの置き場所を作ります。ECサイトのデータベースは、たいてい次の4つに分かれています。

customers(顧客)・products(商品)・orders(注文)・order_items(注文明細)——なぜ注文が2つに分かれているのかが要点です。1回の注文で複数の商品を買えるからです。「誰がいつ注文したか」はordersに1行、「何をいくつ買ったか」はorder_itemsに商品の数だけ行が入ります。

この分け方を正規化と呼びます。同じ情報を2か所に持たないので、商品の値段を変えるときもproductsの1行を直すだけで済みます。実務のデータベースはほぼ必ずこの形をしています。

まずは中身を確かめましょう。SELECT * FROM productsで商品一覧が出れば準備完了です。

実行結果
┌────┬──────────────────────┬──────────┬───────┐
│ id │         name         │ category │ price │
├────┼──────────────────────┼──────────┼───────┤
│ 1  │ ワイヤレスマウス     │ 周辺機器 │ 2980  │
│ 2  │ メカニカルキーボード │ 周辺機器 │ 12800 │
│ 3  │ USB-Cハブ            │ 周辺機器 │ 4500  │
│ 4  │ ノートPCスタンド     │ 家具     │ 3800  │
│ 5  │ デスクライト         │ 家具     │ 6200  │
│ 6  │ 入門プログラミング本 │ 書籍     │ 2400  │
└────┴──────────────────────┴──────────┴───────┘

STEP 23つの表をつないで明細を作る

🎯 このステップのゴール: JOINで注文・商品・顧客を1枚の表にまとめる

データが4つに分かれているということは、使うときは繋ぎ直す必要があるということです。ここが正規化の代償で、JOINの出番です。

やることは単純で、「どの列とどの列が同じものを指しているか」をONで教えるだけです。oi.order_id = o.idは「明細の注文番号は、注文表のidのこと」という意味になります。これを3回繰り返せば、4つの表が1枚に繋がります。

ここでASによる別名が効いてきます。order_items AS oiと短くしておくと、oi.quantityのように書けて読みやすくなります。表が3つ以上になると、別名なしでは書いていられません。

金額はその場で計算しますp.price * oi.quantity AS 金額のように、保存されていない値もSELECTの中で作れます。単価と数量さえあれば金額は導けるので、わざわざ保存しないのが定石です。

実行結果
┌────────────┬──────┬──────────────────────┬──────┬───────┐
│   注文日   │ 顧客 │         商品         │ 数量 │ 金額  │
├────────────┼──────┼──────────────────────┼──────┼───────┤
│ 2026-06-03 │ 佐藤 │ ワイヤレスマウス     │ 2    │ 5960  │
│ 2026-06-03 │ 佐藤 │ 入門プログラミング本 │ 1    │ 2400  │
│ 2026-06-15 │ 鈴木 │ メカニカルキーボード │ 1    │ 12800 │
│ 2026-07-02 │ 佐藤 │ USB-Cハブ            │ 3    │ 13500 │
│ 2026-07-02 │ 佐藤 │ ワイヤレスマウス     │ 1    │ 2980  │
│ 2026-07-18 │ 高橋 │ デスクライト         │ 2    │ 12400 │
│ 2026-08-05 │ 田中 │ ノートPCスタンド     │ 1    │ 3800  │
│ 2026-08-05 │ 田中 │ ワイヤレスマウス     │ 1    │ 2980  │
│ 2026-08-21 │ 鈴木 │ USB-Cハブ            │ 1    │ 4500  │
│ 2026-08-21 │ 鈴木 │ メカニカルキーボード │ 2    │ 25600 │
│ 2026-08-28 │ 高橋 │ 入門プログラミング本 │ 4    │ 9600  │
└────────────┴──────┴──────────────────────┴──────┴───────┘

STEP 3商品別に売上を集計する

🎯 このステップのゴール: GROUP BYとSUMで、明細を商品ごとの合計にまとめる

明細のままでは「何が売れているか」が分かりません。商品ごとにまとめます

GROUP BY p.nameと書くと、同じ商品名の行がひとかたまりに畳まれます。そのかたまりごとにSUM()で合計、COUNT()で件数を出せます。「11行の明細」が「6行の商品別サマリ」に変わります。

ここにSQLで最も多い間違いがあります。GROUP BYに書いていない列は、そのままではSELECTに書けません。かたまりの中で値がひとつに決まらないからです。SUM()のような集計関数で包むか、GROUP BYに足すか、どちらかが必要です。

並べ替えにORDER BY 売上 DESC別名をそのまま使えますORDER BYSELECTより後に処理されるので、付けた別名を知っているのです。

実行結果
┌──────────────────────┬────────┬───────┐
│         商品         │ 販売数 │ 売上  │
├──────────────────────┼────────┼───────┤
│ メカニカルキーボード │ 3      │ 38400 │
│ USB-Cハブ            │ 4      │ 18000 │
│ デスクライト         │ 2      │ 12400 │
│ 入門プログラミング本 │ 5      │ 12000 │
│ ワイヤレスマウス     │ 4      │ 11920 │
│ ノートPCスタンド     │ 1      │ 3800  │
└──────────────────────┴────────┴───────┘

STEP 4売れ筋だけを絞り込む

🎯 このステップのゴール: HAVINGで集計後に絞り、LIMITで上位だけを取り出す

「売上1万円以上の商品トップ3を出して」——実務でよく来る依頼です。ここでWHEREでは書けないことに気づきます。

理由は処理される順番です。WHERE集計する前の1行1行に効きます。でも「売上1万円以上」は集計した後でないと分かりません。そこで使うのがHAVINGです。GROUP BYで畳んだ結果に対して条件をかけられます。

WHEREは集計前、HAVINGは集計後——この一言を覚えておけば迷いません。WHERE p.category = '周辺機器'HAVING SUM(...) >= 10000は同時に使えて、それぞれ違う段階で効きます。

最後にLIMIT 3で上位3件だけにします。ORDER BYで並べてからLIMITで切る——この組み合わせが「トップN」の定番です。並べ替えないままLIMITを付けると、どの3件が来るか決まりません

実行結果
┌──────────┬──────────────────────┬───────┐
│   分類   │         商品         │ 売上  │
├──────────┼──────────────────────┼───────┤
│ 周辺機器 │ メカニカルキーボード │ 38400 │
│ 周辺機器 │ USB-Cハブ            │ 18000 │
│ 家具     │ デスクライト         │ 12400 │
└──────────┴──────────────────────┴───────┘

STEP 5月別の推移を出す

🎯 このステップのゴール: 日付から年月を取り出し、月ごとにまとめる

売上は推移で見たいものです。2026-06-03のような日付から年月だけを取り出してまとめましょう。

使うのはstrftime('%Y-%m', o.ordered_on)です。日付を好きな形の文字列にする関数で、%Yが4桁の年、%mが2桁の月を表します。これで2026-06という文字列ができ、これをGROUP BYに使えば月ごとに畳めます

注文数の数え方に注意が要ります。COUNT(DISTINCT o.id)DISTINCTを付けているのが要点です。明細をJOINしているので1回の注文が商品の数だけ行に増えています。そのままCOUNT(*)すると明細の数を数えてしまい、注文数が実際より多く出ます。

これはJOINした表を集計するときに最も踏みやすい罠です。数字が妙に大きいときは、たいていこれを疑います。

実行結果
┌─────────┬────────┬───────┐
│  年月   │ 注文数 │ 売上  │
├─────────┼────────┼───────┤
│ 2026-06 │ 2      │ 21160 │
│ 2026-07 │ 2      │ 28880 │
│ 2026-08 │ 3      │ 46480 │
└─────────┴────────┴───────┘

STEP 6クロス集計にして仕上げる

🎯 このステップのゴール: WITH句で下ごしらえし、CASE式で横に並べる

最後の仕上げです。行に月、列にカテゴリを並べたクロス集計表にします。報告資料でそのまま使える形です。

まずWITH sales AS (...)で下ごしらえをします。CTE(共通テーブル式)と呼ばれるもので、「JOINして必要な列だけにした中間結果」に名前を付けられます。後半の集計はその名前を使うだけになるので、1本の長いクエリが「準備」と「集計」の2段に分かれて読みやすくなります

横に並べるのがSUM(CASE WHEN ... THEN ... ELSE 0 END)です。条件付き集計と呼ばれる定番の書き方で、「周辺機器のときだけ金額を、それ以外は0を足す」という意味になります。カテゴリの数だけ列を並べれば、クロス集計の完成です。

最後の評価列もCASEです。集計した結果に対してもCASEが使えます。しきい値の30000を書き換えれば、判定の基準をすぐ変えられます。

完成です。WHEREを足して期間を絞る、カテゴリの列を増やす——書き換えて試してみてください。

実行結果
┌─────────┬──────────┬──────────┬───────┬──────┬──────┐
│  年月   │ 売上合計 │ 周辺機器 │ 家具  │ 書籍 │ 評価 │
├─────────┼──────────┼──────────┼───────┼──────┼──────┤
│ 2026-06 │ 21160    │ 18760    │ 0     │ 2400 │ 通常 │
│ 2026-07 │ 28880    │ 16480    │ 12400 │ 0    │ 通常 │
│ 2026-08 │ 46480    │ 33080    │ 3800  │ 9600 │ 好調 │
└─────────┴──────────┴──────────┴───────┴──────┴──────┘

よくある質問

この売上レポートを実務でそのまま使えますか?

考え方はそのまま使えます。実務では返品・キャンセル・税・送料を差し引く必要があるので、ordersに状態の列を足してWHERE status = '完了'で絞るのが一般的です。テーブルの分け方とJOINの組み立て方は、ここで作ったものと同じです。

実行するとエラーになります

多いのはGROUP BYに無い列をSELECTに書いてしまう間違いです。集計関数で包むか、GROUP BYに足してください。

次に多いのがJOINのONの書き間違いです。oi.product_id = p.idのところをoi.id = p.idなどとすると、エラーにならないまま結果の行数だけがおかしくなります。件数が想定と違うときは、まずONを疑ってください。

売れていない商品も0円として表示したい

JOINLEFT JOINに変え、productsを主役(FROMの側)にします。一度も売れていない商品は集計結果がNULLになるので、COALESCEで0に置き換えます。INNER JOINとLEFT JOINの違いが効いてくる場面です。

🚩 次はこのカテゴリへ

SQLで集計できるようになったら、次はその結果を加工して見せる番です。Pythonカテゴリの最終演習では、同じように集計する家計簿アプリを作ります。SQLが「データベースに集計させる」のに対し、Pythonは「手元で集計する」——この2つの違いが見えてくると、道具の使い分けができるようになります。

Pythonのレッスンへ進む
広告スペース(バナー)
広告スペース(記事内)