JOINで表をつなぐ:顧客と注文を結びつけて分析する|マーケターのためのSQL入門 第4回

田中 磨生

監修田中 磨生株式会社Abin 代表取締役

株式会社Abin代表取締役。AI SaaSの上場企業やHR系ベンチャーのマーケティング責任者を経て、2023年BtoBマーケティング特化のコンサルティング会社株式会社Abinを設立。創業から30社以上のBtoBマーケティングの戦略立案から実行までを行う。2025年には株式会社プロベルの役員に就任。NYマーケティング株式会社の執行役員も務める。

プロフィールを見る →

執筆Abin編集部

JOINで表をつなぐ:顧客と注文を結びつけて分析する|マーケターのためのSQL入門 第4回

この回で分かること:①JOINで2つの表を共通の列(キー)でつなぎ、「流入元ごとの売上」のように1つの表だけでは出せない数字を出す方法、②INNER JOINとLEFT JOINの違いと、つないだときに行が「消える」「増える」落とし穴、③WITH句を使って、先にまとめてからつなぐ書き方。前回は、GROUP BYと集計関数で数字をまとめる方法を学びました。今回は、複数の表を組み合わせて、分析の幅を一気に広げます。

注文の表と顧客の表を、共通の列customer_idでつなぐと、注文の各行に顧客の流入元channelが付いた1つの表になることを示した図
図1:JOINは、共通の列で2つの表を横につなぐ

この回の例も、架空の通販「ツギノテ雑貨店」のサンプルデータで実際に実行し、結果を確かめたものです。データは説明のために作った架空のものです。

なぜ表をつなぐのか

第1回で学んだとおり、データベースではテーマごとに表を分けて持ちます。ツギノテ雑貨店なら、顧客の情報(都道府県・登録日・流入元)は customers に、注文の情報(注文日・カテゴリー・金額)は orders に入っています。

ところが、マーケティングで知りたいことの多くは、2つの表にまたがっています。

  • 検索広告から来た顧客は、どれくらい買っているか(流入元は customers、金額は orders)
  • 東京都の顧客がよく買うカテゴリーは何か(都道府県は customers、カテゴリーは orders)
  • 一度も買っていない会員は誰か(会員は customers、購入は orders)

これに答えるには、2つの表を customer_id でつないで、1つの表として扱う必要があります。そのための書き方がJOINです。表計算ソフトでいえば、VLOOKUP関数で別のシートから値を引いてくる操作に近いものです。

FREE MEMBER

無料会員なら、講座が受け放題・AI比較が1日5回

40以上の講座を最後まで読めて、学びの進み具合も記録できます。登録は30秒・無料です。

無料で会員登録する →
🔒 ここから先は無料会員限定です
この講座は、会員でない方は第2回まで読めます。第4回の続きと、残りの回はすべて、無料会員登録で読めます。
MEMBERS ONLY登録30秒・無料
続きは、無料会員登録ですぐ読めます。
会社のメールアドレスに届くリンクを開くだけ。このページに戻って、続きから読めます。
01
講座が全部、最後まで受け放題40以上の講座・300回超の連載を全回読めます。会員でない方は各講座の第1〜3回まで。
02
AI比較が1日5回あなたの会社向けの順位表と稟議書のたたき台を、何度でも作り直せます(会員でない方は1回)。
03
学びの進み具合をまとめて管理読んだ回・確認テストの成績・修了証をマイページに保存。PCでもスマホでも続きから。
04
AIへの質問が1日60回ツギノテの記事から答えを探すAI検索も、たっぷり使えます(会員でない方は20回)。
無料会員登録項目はすべて必須です
Gmail・Yahoo!メール・Outlook・携帯キャリアなどのフリーアドレスでは登録できません。お勤め先のメールアドレスを入れてください。
登録するとプライバシーポリシーに同意したものとみなします。

INNER JOIN:両方の表にあるものだけをつなぐ

まずは、6月の注文に、その注文をした顧客の流入元を付けてみましょう。

SELECT o.order_id, o.customer_id, c.channel, o.amount
FROM orders AS o
INNER JOIN customers AS c
  ON o.customer_id = c.customer_id
WHERE o.order_date >= '2026-06-01'
ORDER BY o.order_date;
order_id customer_id channel amount
O109 C006 検索広告 3600
O110 C004 自然検索 2200
O111 C007 SNS 9800
O112 C002 自然検索 3100
O113 C006 検索広告 7400
O114 C001 検索広告 4500

書き方のポイントは3つです。

  1. FROM 表A JOIN 表B ON 条件:ONのあとに「どの列とどの列が同じなら、行をつなぐか」を書きます。ここでは o.customer_id = c.customer_id です。
  2. 表の別名:FROM orders AS o のように、表にも短い別名を付けられます。2つの表に同じ名前の列(customer_id)があるので、「o.customer_id」「c.channel」のように、どちらの表の列かを書き分けます。
  3. JOINのあとにWHEREやGROUP BY:つないだ結果は1つの表として扱えるので、これまで学んだWHERE・GROUP BY・ORDER BYがそのまま使えます。

「消えた行」に気づく

ここで、6月30日の注文 O115 が結果に入っていないことに気づいたでしょうか。O115 は顧客 C009 の注文ですが、サンプルデータの customers には C009 がいません(退会して顧客の情報が消えた、データの取り込み漏れなど、実務でもよく起こる状況を想定しています)。

INNER JOINは、ONの条件に合う相手が両方の表にある行だけを残します。相手が見つからない行は、黙って結果から消えます。実際、つなぐ前の注文は15件ですが、INNER JOINでつないだ後に数えると14件になります。

SELECT COUNT(*)
FROM orders AS o
INNER JOIN customers AS c
  ON o.customer_id = c.customer_id;
COUNT(*)
14

エラーも警告も出ないので、気づかないまま「売上の合計」を出すと、O115 の1,900円が抜けた数字を報告してしまいます。JOINの前後で件数を数えて、減っていないかを確かめる習慣をつけましょう。

注文の表(15件)と顧客の表を INNER JOIN でつなぐと、14件になりました。考えられる理由として最も適切なものはどれでしょう?

  1. INNER JOIN は、必ず1件減らす決まりがあるから
  2. 顧客の表に存在しない customer_id の注文があり、相手が見つからず結果から消えたから
  3. 注文の表に重複した行があったから
答えと解説

正解は2です。INNER JOINは、両方の表に相手がいる行だけを残します。顧客の表にない C009 の注文 O115 は、つなぐ相手がいないため結果から消えました。エラーにならないので、件数を数えて気づくことが大切です。

LEFT JOIN:片方の表の行をすべて残す

「一度も買っていない会員も含めて、顧客ごとの注文数を出したい」ときは、INNER JOINでは困ります。注文がない顧客は、注文の表に相手がいないため消えてしまうからです。

このようなときはLEFT JOINを使います。LEFT JOINは、FROMに書いた左側の表の行をすべて残し、右側の表に相手がいれば値を付け、いなければNULL(値なし)を入れます。

SELECT c.customer_id, c.channel, o.order_id, o.amount
FROM customers AS c
LEFT JOIN orders AS o
  ON c.customer_id = o.customer_id
WHERE c.customer_id IN ('C007', 'C008');
customer_id channel order_id amount
C007 SNS O111 9800
C008 自然検索 NULL NULL

C008 は注文がありませんが、LEFT JOINなので行は残り、注文の列はNULLになりました。

顧客の表と注文の表の重なりを2つの円で表し、INNER JOINは両方にある部分だけ、LEFT JOINは左側の顧客の表をすべて残し注文がない顧客にはNULLが入ることを、C008とC009の例とともに示した図
図2:INNER JOINは「両方にあるもの」、LEFT JOINは「左側をすべて」残す

LEFT JOINのあとの数え方に注意

LEFT JOINでつないだあとに顧客ごとの注文数を数えるときは、COUNT(*) と COUNT(o.order_id) の違いに注意します。

SELECT c.customer_id,
       COUNT(*)          AS wrong_count,
       COUNT(o.order_id) AS orders
FROM customers AS c
LEFT JOIN orders AS o
  ON c.customer_id = o.customer_id
GROUP BY c.customer_id
ORDER BY c.customer_id;
customer_id wrong_count orders
C001 4 4
C002 3 3
C003 1 1
C004 2 2
C005 1 1
C006 2 2
C007 1 1
C008 1 0

注文のない C008 は、COUNT(*) だと「1」になってしまいます。LEFT JOINで残った行(注文の列がNULLの行)を1行として数えるからです。第3回で学んだとおり、COUNT(列) はNULLを数えないので、COUNT(o.order_id) なら正しく「0」になります。

流入元ごとの成績を出す

LEFT JOINとGROUP BYを組み合わせると、流入元ごとの顧客数・注文数・売上を、注文がない顧客も含めて出せます。

SELECT c.channel,
       COUNT(DISTINCT c.customer_id) AS customers,
       COUNT(o.order_id)             AS orders,
       COALESCE(SUM(o.amount), 0)    AS sales
FROM customers AS c
LEFT JOIN orders AS o
  ON c.customer_id = o.customer_id
GROUP BY c.channel
ORDER BY sales DESC;
channel customers orders sales
検索広告 2 6 28800
自然検索 3 5 14100
メール 1 1 12800
SNS 2 2 12200

COALESCEは「最初のNULLでない値を返す」関数で、売上がNULLになる場合に0を入れるために使っています。検索広告から来た顧客は2人ですが、6件・28,800円と、1人あたりの購入が多いことが分かります。

会員登録はしたが、一度も注文していない顧客を含めて、顧客ごとの注文数を出したいとき、最も適した書き方はどれでしょう?

  1. FROM orders INNER JOIN customers で、COUNT(*) を数える
  2. FROM customers INNER JOIN orders で、COUNT(*) を数える
  3. FROM customers LEFT JOIN orders で、COUNT(o.order_id) を数える
答えと解説

正解は3です。注文のない顧客を残すには、顧客の表を左にしたLEFT JOINを使います。さらに、注文のない行はNULLになるので、COUNT(*) ではなく COUNT(o.order_id) で数えると、正しく0になります。

「相手がいない行」を探す

LEFT JOINのあとに「右側の列がNULLの行」だけを取り出すと、相手がいない行を探せます。これは、データの抜けを見つけたり、施策の対象者を探したりするのに便利です。

一度も注文していない会員(購入を促すメールの対象)を探すなら、次のとおりです。

SELECT c.customer_id, c.prefecture
FROM customers AS c
LEFT JOIN orders AS o
  ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;
customer_id prefecture
C008 北海道

逆に、注文の表を左にすると、顧客の表に存在しない顧客の注文(データの不整合)が見つかります。

SELECT o.order_id, o.customer_id, o.amount
FROM orders AS o
LEFT JOIN customers AS c
  ON o.customer_id = c.customer_id
WHERE c.customer_id IS NULL;
order_id customer_id amount
O115 C009 1900

先ほどINNER JOINで消えた O115 が見つかりました。分析を始める前に、こうした「相手のいない行」がどれくらいあるかを確かめておくと、あとで数字が合わない原因をすぐに突き止められます。

この記事の監修者に相談できます

監修者の田中磨生が代表を務める株式会社Abinで、ご相談をお受けしています。

相談無料ツギノテ編集部に相談する

よくある落とし穴:つなぐと行が「増える」

JOINのもう1つの落とし穴は、行が増えることです。つなぐ相手が複数あると、元の1行が相手の数だけ複製されるからです。

例として、流入元ごとの6月の広告費の表 ad_costs(架空のデータ)を用意しました。

channel month cost
検索広告 2026-06 20000
SNS 2026-06 15000

6月の売上と広告費を流入元ごとに並べようとして、注文・顧客・広告費の3つの表をつないでから合計すると、次のようになります。

SELECT c.channel,
       SUM(o.amount) AS sales,
       SUM(a.cost)   AS cost
FROM orders AS o
INNER JOIN customers AS c
  ON o.customer_id = c.customer_id
INNER JOIN ad_costs AS a
  ON c.channel = a.channel AND a.month = '2026-06'
WHERE o.order_date BETWEEN '2026-06-01' AND '2026-06-30'
GROUP BY c.channel;
channel sales cost
SNS 9800 15000
検索広告 15500 60000

検索広告の広告費が60,000円になっています。本当は20,000円です。6月に検索広告の顧客の注文が3件あったため、広告費の1行が注文の3行それぞれにつながり、3回足されてしまったのです。このように、「1対多」でつないだあとに「1」の側の数字を合計すると、多い側の行数だけ水増しされます。

検索広告の広告費20,000円の1行が、6月の検索広告経由の注文3行それぞれにつながって3行に複製され、合計すると60,000円に水増しされる様子を示した図
図3:1対多でつなぐと、「1」の側の数字が複製される

解決策:先にまとめてから、つなぐ

この問題を避けるには、粒度(1行が何を表すか)をそろえてからつなぐのが基本です。売上を先に流入元ごとにまとめて「1流入元=1行」にしてから、同じく「1流入元=1行」の広告費の表とつなぎます。

ここで便利なのがWITH句です。WITH 名前 AS (SQL) と書くと、その結果に名前を付けて、後ろのSQLで表のように使えます。

WITH june_sales AS (
  SELECT c.channel, SUM(o.amount) AS sales
  FROM orders AS o
  INNER JOIN customers AS c
    ON o.customer_id = c.customer_id
  WHERE o.order_date BETWEEN '2026-06-01' AND '2026-06-30'
  GROUP BY c.channel
)
SELECT a.channel,
       s.sales,
       a.cost,
       ROUND(s.sales * 100.0 / a.cost) AS roas_pct
FROM ad_costs AS a
LEFT JOIN june_sales AS s
  ON a.channel = s.channel
WHERE a.month = '2026-06';
channel sales cost roas_pct
検索広告 15500 20000 78.0
SNS 9800 15000 65.0

広告費は正しく20,000円になり、売上÷広告費(ROAS)も計算できました。なお、この例は説明のための単純化で、「その流入元から最初に来た顧客の、6月の売上すべて」を広告の成果とみなしています。実際の広告の効果測定では、どの接点に成果を割り当てるか(アトリビューション)を別に考える必要があります。

注文と顧客をつないで流入元ごとの売上を先に1流入元1行にまとめ、同じく1流入元1行の広告費の表とつなぐことで、広告費が水増しされずにROASが計算できる流れを示した図
図4:粒度をそろえてからつなぐと、数字が水増しされない

WITH句は、長いSQLを「部品」に分けて、上から順に読めるようにする道具でもあります。第5回のファネル分析やRFM分析でも、WITH句で段階的に組み立てていきます。

注文の表(1行=1件の注文)と、流入元ごとの月の広告費の表(1行=1流入元・1か月)をそのままつないで SUM(cost) を出したところ、広告費が実際より大きくなりました。最も適切な対処はどれでしょう?

  1. 注文を先に流入元ごとにまとめてから、広告費の表とつなぐ
  2. SUM(cost) を AVG(cost) に変えれば、どんな場合も必ず正しくなる
  3. INNER JOIN を LEFT JOIN に変える
答えと解説

正解は1です。原因は、広告費の1行が注文の行数だけ複製されたことです。粒度(1行が何を表すか)をそろえてからつなぐのが基本です。AVGに変えると偶然合う場合もありますが、仕組みを理解しないまま使うと別の場面で間違えます。

実務ではこう使う

JOINが使えると、別々のシステムに入っているデータを組み合わせた分析ができるようになります。たとえば次のようなものです。

  • 広告・SEO・SNSなど流入元ごとの、顧客数・購入数・売上・1人あたり売上
  • 会員の属性(業種・地域など)ごとの、よく買われる商品
  • 資料請求したが商談になっていないリード、会員登録したが購入していない顧客の一覧

実務でJOINを書くときは、次の4つを確かめると、数字の間違いを大きく減らせます。

  1. つなぐ前に、両方の表の粒度とキーを確かめる:1行は何か、キーに重複はないか
  2. JOINの前後で件数を数える:減っていれば「相手のいない行」、増えていれば「1対多の複製」を疑う
  3. INNERかLEFTかを意識して選ぶ:残したいのはどちらの表の行か
  4. 合計は元の表と突き合わせる:つないだ後の売上の合計が、元の注文の表の合計と一致するか
JOINの数字を間違えないための4つの確認、粒度とキーを確かめる・前後で件数を数える・INNERかLEFTかを選ぶ・合計を元の表と突き合わせる、をカードで示した図
図5:JOINで間違えないための4つの確認

顧客のデータを広告・メール・MAなど複数のツールにまたがって統合したい場合は、顧客データ活用とCDP(全6回)や、データをまとめて流し込むETLツールおすすめ比較も参考になります。

FOR BUSINESS

マーケティング・DXの発注先選びに迷ったら

ツギノテを運営する株式会社Abinは、BtoBマーケティングに特化したコンサルティング会社です。創業から30社以上のBtoBマーケティングを支援してきました。

相談無料ツギノテ編集部に相談する

無料会員登録(講座受け放題・AI比較)/セミナーと資料を見る

この回のまとめ

  • JOINは、共通の列(キー)で2つの表を横につなぐ書き方です。ONにつなぐ条件を書き、表に別名を付けて列を書き分けます。
  • INNER JOINは両方の表に相手がいる行だけを残し、相手がいない行は黙って消えます。JOINの前後で件数を確かめます。
  • LEFT JOINは左側の表の行をすべて残し、相手がいなければNULLを入れます。数えるときは COUNT(*) ではなく COUNT(右側の列) を使います。
  • LEFT JOINのあとに「右側がNULL」の行を取り出すと、未購入の会員やデータの不整合を探せます。
  • 1対多でつなぐと「1」の側の数字が複製されて水増しされます。WITH句で先にまとめ、粒度をそろえてからつなぎます。

確認テスト

INNER JOINとLEFT JOINの違いとして正しいものはどれでしょう?

  1. INNER JOINは左側の表をすべて残し、LEFT JOINは両方にある行だけを残す
  2. INNER JOINは両方の表に相手がいる行だけを残し、LEFT JOINは左側の表の行をすべて残す
  3. 2つの違いは、処理の速さだけである
答えと解説

正解は2です。INNER JOINは相手がいない行を消し、LEFT JOINは左側(FROMに書いた表)の行を残して、相手がいない部分をNULLにします。どちらの表の行を残したいかで選びます。

一度も注文していない会員を探したいとき、正しいSQLの組み立て方はどれでしょう?

  1. FROM customers LEFT JOIN orders でつなぎ、WHERE o.order_id IS NULL で絞る
  2. FROM customers INNER JOIN orders でつなぎ、WHERE o.order_id IS NULL で絞る
  3. FROM orders だけで、WHERE customer_id IS NULL で絞る
答えと解説

正解は1です。LEFT JOINで顧客の行をすべて残すと、注文がない顧客は注文の列がNULLになります。それを IS NULL で取り出します。INNER JOINでは、注文のない顧客は最初から消えてしまいます。

LEFT JOINでつないだ結果をGROUP BYして、顧客ごとの注文数を数えるとき、注文のない顧客を正しく0にできる書き方はどれでしょう?

  1. COUNT(*)
  2. COUNT(c.customer_id)
  3. COUNT(o.order_id)
答えと解説

正解は3です。注文のない顧客の行は、注文の列(o.order_id)がNULLになります。COUNT(列) はNULLを数えないので0になります。COUNT(*) や顧客の表の列を数えると1になってしまいます。

WITH句の説明として正しいものはどれでしょう?

  1. 表を永久に新しく作り、データベースに保存する命令
  2. SQLの結果に名前を付けて、後ろのSQLで表のように使えるようにする書き方
  3. JOINの代わりに使う、表をつなぐための命令
答えと解説

正解は2です。WITH句で作った結果は、その1つのSQLの中だけで使える一時的な表のようなものです。先にまとめてからつなぐときや、長いSQLを部品に分けて読みやすくするときに使います。

次回予告

次回は、ここまでの書き方を組み合わせて、マーケティングの代表的な分析をSQLで書きます。商品ページから購入までのファネル、初回購入の月ごとに顧客の継続を追うコホート分析、最新購入日・購入回数・購入金額で顧客を分けるRFM分析を、WITH句で段階的に組み立てます。

参考資料

  • Google Cloud「GoogleSQL:Query syntax(JOIN operation)」(確認日:2026年11月12日)https://docs.cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax

この記事は役に立ちましたか?

いま読まれている記事ランキング

    ランキングをもっと見る →

    この記事の内容について

    掲載している情報の修正や削除をご希望の場合は、こちらからご連絡ください。

    修正・削除の依頼

    NEXTあわせて読みたい

    ツギノテの記事一覧へ

    編集方針

    • 各社の公式サイトを、確認日を書いて調べています
    • 掲載企業から報酬は受け取っていません
    • 有償の掲載がある場合は「PR」と明記します

    運営:株式会社Abin

    © Abin Co., Ltd.