この実習でできるようになること
- 結合後の一行が何を表すかを説明し、1対多と1対多の掛け合わせを発見できる。
- 集計する粒度をそろえてから結合する方法を使える。
- NULL、同額の異なる取引、明細のない受注を含めて検算できる。
前提と課題
架空の文具店で、顧客別の売上と入金を同じレポートへ出します。受注には複数の商品明細があり、入金も分割されることがあります。最初のSQLでは、受注、商品明細、入金明細を一度に結合してからSUMを取りました。
ところが、受注101の売上が実際の2,500円ではなく5,000円になりました。売上も入金も同じように倍になっているため、差額が0円という見た目だけでは異常に気付けません。この演習は税、送料、返金を扱わず、単価は受注明細に記録した取引時点の値とします。
- 顧客
- C1、C2、C3の3件。顧客IDは一意。
- 受注
- 101→C1、102→C1、103→C2、104→C3。受注IDは一意。
- 商品明細(単価×数量)
- 101:1,000×2と500×1、102:1,200×1、103:800×3、104:なし
- 入金明細(金額)
- 101:1,500と1,000、102:1,200、103:1,000、104:なし
自分で求めてみよう
顧客を全て表示し、顧客別の売上、入金、未収額(売上−入金)を求めてください。受注104のように明細がない場合は金額を0円とし、集計の前後で総額が保存されることも確かめましょう。
途中の式や表も紙に書いてから、次の手順と照合してください。
途中の手順と検算
1. まずSQLを使わず、期待する答えを手で作る
受注101の売上は1,000×2+500×1=2,500円、入金は1,500+1,000=2,500円です。102の売上と入金はともに1,200円、103の売上は2,400円、入金は1,000円です。104はどちらも明細がないので、この教材の規則では0円とします。
C1は101と102を持つので売上3,700円、入金3,700円、未収0円です。C2は2,400円、1,000円、1,400円、C3は全て0円になります。この小さな正解表を持っておくと、複雑なSQLの結果がもっともらしい数字でも誤りを発見できます。実際の業務では、明細のない受注を異常として別途通知する設計も考えられます。
| 顧客 | 売上 | 入金 | 未収 |
|---|---|---|---|
| C1 | 3,700円 | 3,700円 | 0円 |
| C2 | 2,400円 | 1,000円 | 1,400円 |
| C3 | 0円 | 0円 | 0円 |
2. 二つの「多」を同時に結合すると何行になるかを見る
受注101に商品明細が2件、入金明細が2件あるため、両方を受注IDで結合すると2×2=4行になります。一行は商品明細そのものではなく、「ある商品明細とある入金明細の組合せ」です。単価×数量の2,000円は入金2件に対応して2回、500円も2回現れます。
結果として売上は2,000+2,000+500+500=5,000円、入金も1,500+1,000+1,500+1,000=5,000円になります。GROUP BYを顧客IDにしても、重複した入力行の合計を計算する点は変わりません。行が増えた後で集計するだけでは、取引の単位へ戻れないのが原因です。
| 受注101の組合せ | 商品明細金額 | 入金明細金額 |
|---|---|---|
| 商品明細1×入金1 | 2,000 | 1,500 |
| 商品明細1×入金2 | 2,000 | 1,000 |
| 商品明細2×入金1 | 500 | 1,500 |
| 商品明細2×入金2 | 500 | 1,000 |
3. 商品明細と入金明細を、それぞれ受注単位へ集約する
二つの明細表を直接つなぐ前に、商品明細を受注IDごとにSUM(unit_price * quantity)、入金明細を受注IDごとにSUM(amount)で集約します。こうして各表を「受注IDごとに最大一行」にすると、一つの受注について金額同士が掛け合わされなくなります。
SQLの共通表式を使うなら、集約部分は次のように表せます。列名・表名はこの演習用です。売上は金額の列を集計するのであり、商品明細の件数や入金の件数を掛ける処理はありません。
WITH line_totals AS ( SELECT order_id, SUM(unit_price * quantity) AS sales FROM order_lines GROUP BY order_id ), payment_totals AS ( SELECT order_id, SUM(amount) AS paid FROM payments GROUP BY order_id )
4. 顧客を起点にLEFT JOINし、0円の表示条件を明示する
全顧客を残すため、customersを起点にorders、line_totals、payment_totalsをLEFT JOINします。受注104には対応する集約行がないので、結合結果の金額はNULLになります。SUMの結果がNULLになる場合をCOALESCEで0へ置き換えると、明細のない顧客も指定どおり表示できます。
SELECT c.customer_id, COALESCE(SUM(l.sales), 0) AS sales, COALESCE(SUM(p.paid), 0) AS paid, COALESCE(SUM(l.sales), 0) - COALESCE(SUM(p.paid), 0) AS unpaid FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id LEFT JOIN line_totals l ON l.order_id = o.order_id LEFT JOIN payment_totals p ON p.order_id = o.order_id GROUP BY c.customer_id ORDER BY c.customer_id;
このSELECTは、前の手順のWITH句に続けて実行します。結合キーの一意性も前提です。例えば顧客表に同じIDを重複登録できるなら、集約を直しても再び行が増えます。主キー・外部キーの設計とデータの状態を確認することも、SQLの正しさを支えます。
5. 全体の総額と、誤った修正が通らないデータで検算する
元の商品明細から求めた総売上は2,500+1,200+2,400=6,100円、元の入金総額は2,500+1,200+1,000=4,700円です。レポートの列合計も同じで、未収合計は1,400円になります。顧客が3行表示されることと合わせて確認すると、金額と行の欠落を別々に検査できます。
SUM(DISTINCT 金額)で重複を消す修正は不適切です。1,000円の正当な入金が別々に2件あれば、同額でも2,000円と数える必要があります。重複を判断する単位は金額の値ではなく取引です。元表の合計との照合、同額の別取引、明細0件、明細が2件以上というテストを組み合わせましょう。
解答と、ここまでの要点
商品明細と入金明細を別々に受注IDで集約してから結合します。顧客別の結果はC1=売上3,700円・入金3,700円、C2=2,400円・1,000円、C3=0円・0円です。
売上合計6,100円、入金合計4,700円、未収合計1,400円が元表からの計算と一致します。結合後の行が表す単位を確認することが、重複の根本的な対処になります。
つまずいたときに確認すること
- 売上と入金の差が合っていることだけで、双方の総額まで正しいと判断する。
- 1対多の表を二つつないだ後、GROUP BYさえ書けば重複を防げると思う。
- SUM(DISTINCT 金額)で、同額の正当な取引まで一つにまとめてしまう。
- LEFT JOINした表の条件を不用意にWHEREへ置き、対応行のない顧客を落とす。
条件が変わっても説明できますか?
選択後に結果と理由を確かめられます。解答は折りたたみからいつでも読めます。
選んでから確認できます。確認問題は通常の演習記録には加算しません。
解答と理由を見る
解答:6行
商品明細の各1件が入金2件と組になるため3×2=6行です。集計前の行数と一行の意味を考えると、金額が何回現れるかを予測できます。
選んでから確認できます。確認問題は通常の演習記録には加算しません。
解答と理由を見る
解答:1,000円になり、正当な別取引を失う。
DISTINCTは取引IDではなく金額の値を重複排除します。二つとも1,000なので一つ分しか残りません。受注単位で先に集約してから結合する方法なら、別取引を保ったまま重複結合を避けられます。
次の学習
手順を見ずにもう一度解き、以下の関連問題でも同じ考え方を使えるか確かめましょう。
- 問27:次のsales表に対してSQLを実行する。結果に含まれるshopはどれか。amountにNULLはない。 shop | amount A | 400 A | 700 B | 900 B | 50 C | 1200 SE…
- 問28:departments表とemployees表は次のとおりである。SQLの結果で、dept_idが1、2、3の順に並んだ人数はどれか。employee_idは主キーである。 departments dept_id | …
- 問63:受注明細表に「受注番号、明細番号、顧客番号、顧客住所、商品番号、数量」を保存している。主キーは受注番号と明細番号の組合せで、同じ顧客番号の現在住所は常に一つとする。顧客が転居したとき一部の行だけを直したため、同じ顧客番…
- 問64:在庫数stockが1の商品の注文を、二つの処理が同時に受け付ける。各処理は最初にstockを読み、1以上なら後でstockを0に更新するため、両方が注文成功になることがある。データベースは、次の条件付きUPDATEを一…
参照資料とケースの範囲
計算条件、表、シナリオ、確認問題は本サイト独自のものです。用語と考え方の確認には以下の一次資料を使っています。実務では組織の手順や実際の製品仕様も確認してください。