SQL:テーブルの結合(INNER JOIN)
1学習の目的
- INNER JOIN で複数のテーブルをつなぎ、別々に保存されたデータを1つの表として取り出せるようになる。
- 「どの顧客が、どの商品を、いくら買ったか」を1本のSQLで出せるようになる。ここからSQLは本格的なデータ分析の道具になる。
2基礎解説
実務のデータベースは複数のテーブルに分かれています。注文テーブルには商品名も顧客名も入っておらず、IDだけが記録されています。それをつなぐのが JOIN です。
下のSQLを実行環境で流してから始めてください。
CREATE TABLE customers ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, area TEXT ); INSERT INTO customers VALUES (1, '田中商事', '東京'), (2, '鈴木工業', '大阪'), (3, '佐藤物産', '東京'), (4, '高橋建設', '福岡'); CREATE TABLE orders ( id INTEGER PRIMARY KEY, customer_id INTEGER, product_id INTEGER, qty INTEGER, order_date TEXT ); INSERT INTO orders VALUES (1, 1, 1, 2, '2026-01-05'), (2, 1, 2, 5, '2026-01-08'), (3, 2, 4, 1, '2026-01-12'), (4, 3, 2, 10, '2026-01-15'), (5, 1, 3, 3, '2026-02-03'), (6, 2, 5, 20, '2026-02-10'), (7, 3, 1, 1, '2026-02-14');
| テーブル | 内容 | 他とのつながり |
|---|---|---|
| products | 商品マスタ(7件) | id |
| customers | 顧客マスタ(4件) | id |
| orders | 注文履歴(7件) | customer_id / product_id |
- JOIN は「つなぐ条件」を ON で指定する。ON orders.product_id = products.id のように、どの列とどの列が対応するかを書く。
- テーブルに別名を付けると短く書ける。FROM orders o とすれば、以降 o.qty と書ける。
- 同じ列名が複数テーブルにあるときは必ず「テーブル名.列名」で指定する。name だけでは商品名か顧客名か分からずエラーになる。
- INNER JOIN は「両方に存在する行」だけを返す。注文が1件もない顧客は結果から消える。
- JOIN はいくつでもつなげられる。注文 → 顧客 → 商品と3つ結合すれば、「誰が何を買ったか」が1行になる。
現場使用例:注文明細に商品名を表示、会員情報と購入履歴の紐付け、部署名つきの社員一覧。実務のSQLでJOINが出てこないことはまずない。
-- 注文に商品名を付ける SELECT o.id, p.name, o.qty FROM orders o INNER JOIN products p ON o.product_id = p.id; -- 金額まで計算する SELECT o.id, p.name, o.qty, p.price * o.qty AS 金額 FROM orders o INNER JOIN products p ON o.product_id = p.id; -- 3つのテーブルをつなぐ SELECT c.name AS 顧客, p.name AS 商品, o.qty FROM orders o INNER JOIN customers c ON o.customer_id = c.id INNER JOIN products p ON o.product_id = p.id; -- 結合してから集計する SELECT c.name AS 顧客, SUM(p.price * o.qty) AS 売上 FROM orders o INNER JOIN customers c ON o.customer_id = c.id INNER JOIN products p ON o.product_id = p.id GROUP BY c.name ORDER BY 売上 DESC;
3基本ドリル(10問)
3列7行。1 ノートPC 2 が先頭
FROM orders o INNER JOIN products p ON o.product_id = p.id と書く。
SELECT o.id, p.name, o.qty FROM orders o INNER JOIN products p ON o.product_id = p.id;
注文テーブルには商品名が入っていない。product_id だけが記録されており、JOIN で商品マスタとつないで初めて名前が分かる。
A. WITH B. ON C. AS D. BY 選択★☆☆無料
解答 == B
「〜において」を意味する2文字。
B
ON で「どの列とどの列が対応するか」を指定する。これを書き忘れると、全行と全行が掛け合わされた巨大な結果になる。
2列7行。1 田中商事 が先頭
ON o.customer_id = c.id でつなぐ。
SELECT o.id, c.name FROM orders o INNER JOIN customers c ON o.customer_id = c.id;
つなぐ列の名前は違ってよい。orders 側は customer_id、customers 側は id——名前が違っても、対応関係さえ正しければつながる。
SELECT o.id, p.name FROM orders o INNER JOIN products p ____ o.product_id = p.id;
2列7行
結合条件を導く2文字のキーワード。
ON
ON の後ろに書くのが結合条件。WHERE の条件とは役割が違い、「行を絞る」のではなく「行をつなぐ」ためのもの。
A. 結果に含まれる B. 結果から消える C. NULLとして表示される D. エラーになる 選択★☆☆無料
解答 == B
INNER は「内側」。両方にあるものだけ。
B
両方のテーブルに対応がある行だけが残る。高橋建設は注文が0件なので、結果に現れない。次章の LEFT JOIN を使うと残せる。
4列7行。1 ノートPC 2 256000
p.price * o.qty で計算する。テーブル別名を付けると読みやすい。
SELECT o.id, p.name, o.qty, p.price * o.qty AS 金額 FROM orders o INNER JOIN products p ON o.product_id = p.id;
金額はどこにも保存されていない。単価は商品テーブル、数量は注文テーブルにあり、結合して初めて計算できる。
SELECT o.id, p.name FROM orders ____ INNER JOIN products p ON o.product_id = p.id;
2列7行
orders の別名。SELECT の中で使っている1文字。
o
テーブル名の直後に書くだけで別名になる(AS は省略可)。長いテーブル名を何度も書かずに済み、SQLが読みやすくなる。
A. 商品名が返る B. 顧客名が返る C. どちらか判別できずエラー D. 両方返る 選択★★☆無料
解答 == C
同じ名前の列が2つあるとき、SQLはどちらを選べるか。
C
「ambiguous column name」というエラーになる。p.name のようにテーブルを明示すれば解決する。
3列7行。田中商事 ノートPC 2 が先頭
INNER JOIN を2回書く。customers と products の両方につなぐ。
SELECT c.name AS 顧客, p.name AS 商品, o.qty FROM orders o INNER JOIN customers c ON o.customer_id = c.id INNER JOIN products p ON o.product_id = p.id;
JOIN はいくつでもつなげられる。IDしか持たない注文テーブルが、人間に読める形に変わった。
2列5行。USBメモリ 20 が先頭
結合してから GROUP BY p.name で集計する。
SELECT p.name AS 商品, SUM(o.qty) AS 数量 FROM orders o INNER JOIN products p ON o.product_id = p.id GROUP BY p.name ORDER BY 数量 DESC;
結合してから集計できる。JOIN で1つの表にまとめてしまえば、これまで学んだ GROUP BY がそのまま使える。
4実践シナリオ(5問)
5列7行。2026-01-05 田中商事 ノートPC 2 256000 が先頭
3テーブルを結合し、ORDER BY o.id で並べる。
SELECT o.order_date AS 日付, c.name AS 顧客, p.name AS 商品, o.qty AS 数量, p.price * o.qty AS 金額 FROM orders o INNER JOIN customers c ON o.customer_id = c.id INNER JOIN products p ON o.product_id = p.id ORDER BY o.id;
これが「注文明細」そのもの。3つのテーブルに分かれて保存されたデータが、1枚の伝票として組み上がった。
3列3行。田中商事 3 297500 が先頭
3テーブルを結合してから GROUP BY c.name。売上は SUM(p.price * o.qty)。
SELECT c.name AS 顧客, COUNT(*) AS 件数, SUM(p.price * o.qty) AS 売上 FROM orders o INNER JOIN customers c ON o.customer_id = c.id INNER JOIN products p ON o.product_id = p.id GROUP BY c.name ORDER BY 売上 DESC;
顧客は4社あるのに3行しか出ない。高橋建設は注文が0件なので、INNER JOIN では消えてしまう。「売上0円の顧客」を出したいなら次章のLEFT JOINが要る。
2列2行。東京 457500 / 大阪 75000
GROUP BY c.area で集計する。福岡の高橋建設は注文がないため現れない。
SELECT c.area AS エリア, SUM(p.price * o.qty) AS 売上 FROM orders o INNER JOIN customers c ON o.customer_id = c.id INNER JOIN products p ON o.product_id = p.id GROUP BY c.area ORDER BY 売上 DESC;
顧客マスタの属性で集計できるのがJOINの威力。注文テーブルにはエリア情報が一切ないのに、エリア別の売上が出せている。
3列2行。田中商事 ノートPC 256000 / 佐藤物産 ノートPC 128000
WHERE p.price * o.qty >= 50000 で絞る。WHERE は結合の後に効く。
SELECT c.name AS 顧客, p.name AS 商品, p.price * o.qty AS 金額 FROM orders o INNER JOIN customers c ON o.customer_id = c.id INNER JOIN products p ON o.product_id = p.id WHERE p.price * o.qty >= 50000 ORDER BY 金額 DESC;
結合した結果に対して WHERE が効く。単価も数量も別テーブルにあるが、つないだ後なら1つの表として条件を書ける。
4列3行。ノートPC 2 3 384000 が先頭
結合 → GROUP BY p.name → ORDER BY 売上 DESC → LIMIT 3。
SELECT p.name AS 商品, COUNT(*) AS 注文数, SUM(o.qty) AS 数量, SUM(p.price * o.qty) AS 売上 FROM orders o INNER JOIN products p ON o.product_id = p.id GROUP BY p.name ORDER BY 売上 DESC LIMIT 3;
数量とお金は別の順位になる。数量ではUSBメモリが20個で最多だが、売上では3万円で下位。何を「売れている」と呼ぶかで、答えが変わる。
5仕上げ課題
表示する列
① 顧客名 → 別名「顧客」
② エリア → 別名「エリア」
③ 注文件数 → 別名「注文件数」
④ 購入した商品の種類数(重複を除く)→ 別名「商品種類」
⑤ 売上合計(単価 × 数量)→ 別名「売上」
⑥ 平均注文額(売上 ÷ 注文件数、小数第1位まで)→ 別名「平均注文額」
条件:売上が 10万円 以上の顧客のみ
並び順:売上の大きい順
期待される結果(6列 × 2行)
顧客 | エリア | 注文件数 | 商品種類 | 売上 | 平均注文額
田中商事 | 東京 | 3 | 3 | 297500 | 99166.7
佐藤物産 | 東京 | 2 | 2 | 160000 | 80000.0
6列 × 2行が完全一致
3テーブルを結合し、GROUP BY で顧客ごとにまとめる。商品種類は COUNT(DISTINCT p.id)、平均注文額は ROUND(SUM(p.price*o.qty) * 1.0 / COUNT(*), 1)。売上10万円以上は集計後の条件なので HAVING。
SELECT c.name AS 顧客, c.area AS エリア, COUNT(*) AS 注文件数, COUNT(DISTINCT p.id) AS 商品種類, SUM(p.price * o.qty) AS 売上, ROUND(SUM(p.price * o.qty) * 1.0 / COUNT(*), 1) AS 平均注文額 FROM orders o INNER JOIN customers c ON o.customer_id = c.id INNER JOIN products p ON o.product_id = p.id GROUP BY c.name, c.area HAVING SUM(p.price * o.qty) >= 100000 ORDER BY 売上 DESC;
3つのテーブル、8つの句が1本のSQLに集約された。JOIN・GROUP BY・HAVING・ORDER BY・集計関数・DISTINCT——ここまで学んだすべてが1つのクエリに入っている。これが実務のレポートSQLの標準的な姿だ。
* 1.0 という書き方に注目してほしい。SQLiteでは整数どうしの割り算が整数になってしまうため、小数で計算させるために 1.0 を掛けている。297500 ÷ 3 は本来 99166.67 だが、これを書かないと 99166 になる。数字がわずかに違うだけのバグは、気づきにくく影響が大きい。
そして最も重要なのは、この結果に高橋建設が現れていないことだ。売上10万円未満だからではない。注文が1件もないため、INNER JOIN の段階で消えている。もし「取引のない顧客も含めた全社リスト」を求められていたら、このSQLは誤った報告になる。
次章では LEFT JOIN を学ぶ。「対応がない行も残す」結合で、高橋建設を売上0円として表示できるようになる。INNER と LEFT のどちらを選ぶかは、業務の要求次第——その判断ができて初めて、JOINを理解したと言える。