BI探検ノート

SQL で、売上のない店舗が一覧から消える(JOIN と LEFT JOIN の違い)

約5分

濵口 翔長崎で、Power BI のつまずきを記録しています
目次
  1. 使ったデータ
  2. JOIN だと、品川が消える
  3. LEFT JOIN にすると、行が戻る
  4. NULL を 0 にする
  5. 左右を間違えやすい点
  6. 関連: ウィンドウ関数でも確かめた
  7. Power BI との関係
  8. この記事で確認できたこと・できていないこと

確認日2026年10月21日。PostgreSQL 16.13(Docker のコンテナ)で、疑似データを使って、実際に動かして確かめました。疑似データです。実在の店舗や売上ではありません。 確認できていないことは、末尾にまとめました。

店舗ごとの売上を SQL で集計したら、売上が1件もない店舗が、一覧から消えていた。原因は、JOIN の種類でした。LEFT JOIN にすると、その店舗の行が戻ります。

結論(30秒版)

  • 店舗の表と売上の表を JOIN(内部結合)すると、売上が1件もない店舗は、結果に出ませんでした。
  • LEFT JOIN にすると、その店舗の行が出て、売上は NULL(空白)でした。
  • COALESCE(SUM(売上), 0) で、NULL を 0 にできました。
  • Power BI で、データのない店舗が表に出る・出ないのと、同じ種類の話です。

使ったデータ

店舗が4つ、売上が2,193行(2024年1月1日〜2025年12月31日、渋谷・新宿・梅田の3店舗の毎日分)です。品川には、売上を1件も入れていません。

CREATE TABLE stores(store_id int PRIMARY KEY, store_name text, area text);
CREATE TABLE sales(sale_id serial PRIMARY KEY, sale_date date,
                   store_id int REFERENCES stores, amount int);

INSERT INTO stores VALUES
  (1,'渋谷','東京'), (2,'新宿','東京'), (3,'梅田','大阪'), (4,'品川','東京');

売上の行は、generate_series で、日付と店舗の組み合わせから作りました。金額は、規則的に作った疑似の値です(作成の式は、この記事では省略します)。

JOIN だと、品川が消える

SELECT st.store_name, SUM(s.amount) AS total
FROM stores st
JOIN sales s USING(store_id)
GROUP BY st.store_name
ORDER BY total DESC;

結果は、3行でした。

store_nametotal
梅田1131816
新宿1131744
渋谷1129872

品川は、出ません。JOIN(内部結合)は、両方の表で対応する行がある組み合わせだけを返すためです。品川には、sales に対応する行がないので、結果から落ちます。

LEFT JOIN にすると、行が戻る

SELECT st.store_name, SUM(s.amount) AS total
FROM stores st
LEFT JOIN sales s USING(store_id)
GROUP BY st.store_name
ORDER BY total DESC NULLS LAST;

結果は、4行でした。

store_nametotal
梅田1131816
新宿1131744
渋谷1129872
品川(NULL)

stores を左側(基準)にして、sales に対応する行がなくても、左側の行を残します。品川の total は、NULL(空白)です。

ORDER BY total DESC だけだと、PostgreSQL では NULL が先頭に来ました。末尾にしたいので、NULLS LAST を付けています。

NULL を 0 にする

SELECT st.store_name, COALESCE(SUM(s.amount), 0) AS total
FROM stores st
LEFT JOIN sales s USING(store_id)
GROUP BY st.store_name
ORDER BY total DESC;

品川の total が、0 になりました。COALESCE は、最初の NULL でない値を返す関数です。

左右を間違えやすい点

LEFT JOIN は、左側の表の行を残します。FROM の次に書いた表が、左側です。FROM sales s LEFT JOIN stores st と書くと、残るのは、売上の行です。売上のない店舗は、やはり出ません。「一覧に出したい側」を、FROM の側に置きます。

関連: ウィンドウ関数でも確かめた

同じデータで、次のクエリも動かして、結果を確かめました。それぞれ、別の記事にまとめる予定です。

  • 月別の累計: SUM(amt) OVER (ORDER BY month)。2024年は、1月 132,912、2月までで 258,660、3月までで 392,292 でした。
  • 前年同月比: LAG(amt, 12) OVER (ORDER BY month)。2025年1月は、前年の 132,912 に対して 151,512(114.0%)でした。
  • 構成比: SUM(SUM(s.amount)) OVER () で割る。3店舗とも、約 33% でした。
  • エリア内の順位: RANK() OVER (PARTITION BY area ORDER BY total DESC)。

Power BI の DAX で同じことをする方法は、累計、前年比、割合の記事に書いています。

Power BI との関係

Power BI のカードや表で、データのない店舗が「—」になったり、行ごと消えたりするのと、起きていることは同じ種類です。詳しくは、カードの値が「—」になるときに「0」を表示する方法に、実機で確かめた内容を書いています。

この記事では、SQL の動きだけを確かめました。Power BI が内部でどんな SQL を発行するかは、確かめていません。

この記事で確認できたこと・できていないこと

確認日: 2026年10月21日。PostgreSQL 16.13(Docker、Alpine ベース)。疑似データ(店舗4、売上2,193行)。

確認できたこと

  • JOIN で、売上のない店舗が結果から消えること
  • LEFT JOIN で、その店舗の行が NULL の売上で出ること
  • COALESCE(SUM(...), 0) で、0 になること
  • ORDER BY ... DESC で NULL が先頭に来ること、NULLS LAST で末尾にできること
  • 累計・前年同月比・構成比・エリア内の順位のクエリが、PostgreSQL 16 で動くこと

確認できていないこと

  • PostgreSQL 以外(MySQL、SQL Server、BigQuery など)での動き。NULLS LAST は、PostgreSQL の書き方です。ほかのデータベースでは、書き方が違うことがあります。
  • 大きなデータでの速さ
  • Power BI が発行する SQL との対応
  • 公式ドキュメントとの照合