<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>On-Clause on Tarragon</title><link>https://tarrragon.github.io/blog/tags/on-clause/</link><description>Recent content in On-Clause on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 01 Sep 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/on-clause/index.xml" rel="self" type="application/rss+xml"/><item><title>SQL.3 連接的起手式：配對條件的位置與連接種類的選擇</title><link>https://tarrragon.github.io/blog/backend/01-database/sql/join-starts-from-the-relationship/</link><pubDate>Tue, 01 Sep 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/sql/join-starts-from-the-relationship/</guid><description>&lt;p>寫一個連接要先決定的是&lt;strong>哪兩列該配在一起&lt;/strong>，而 SQL 給了這個決定一個專屬的位置：&lt;code>ON&lt;/code>。本篇把這件事叫&lt;strong>配對&lt;/strong>——關係還在組的那個過程，哪一列跟哪一列湊成一列；這是為了指認它而取的稱呼，SQL 的標準術語裡沒有這個詞。條件寫進那裡，配對關係就成為查詢結構的一部分；條件留在 &lt;code>WHERE&lt;/code>，配對關係就退化成一道篩子。&lt;/p>
&lt;p>本篇的查詢跑在&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/sample-bookstore-database/" data-link-title="SQL 各篇共用的範例資料庫：一家小書店的顧客、訂單與評價表" data-link-desc="SQL 各篇共用的顧客、訂單與評價表的建表語句與初始資料，以及各篇輸出所用的引擎版本、錯誤訊息與列序的閱讀約定">共用的範例資料庫&lt;/a>上：三位顧客佳穎、宗翰、雅文，而只有佳穎下過兩張訂單（101 與 102，金額 300 與 500）。&lt;/p>
&lt;p>這一篇處理的是&lt;strong>起手式&lt;/strong>——寫連接時第一個下筆的地方，而它決定了條件之後能放哪裡。&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/on-describes-where-filters/" data-link-title="SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責" data-link-desc="條件寫在連接的配對階段與寫在關係組完之後的篩選階段的差別、外連接的保護在 WHERE 被抵銷的機制，以及判斷一個條件該放哪一邊的測試">SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責&lt;/a> 處理的是條件寫下之後放哪一邊——那一篇會把配對與&lt;strong>成品&lt;/strong>（關係組完之後得到的那一批列）並排；本篇兩個詞都會用到，而判斷一個條件屬於配對還是成品的完整判定表在那一篇。順序上這一篇在前：先有配對關係，才有條件該放哪裡的問題。&lt;/p>
&lt;h2 id="從全部配對縮小的起手式與關聯代數的定義">從全部配對縮小的起手式與關聯代數的定義&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/relational-algebra/" data-link-title="Relational Algebra（關聯代數）" data-link-desc="看到「子句其實是某個運算」或想知道 SQL 的規則從哪裡來時，查這一層在底下的是什麼">關聯代數&lt;/a>把連接定義成兩個運算的組合：先取&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/cartesian-product/" data-link-title="Cartesian Product（笛卡兒積）" data-link-desc="看到 CROSS JOIN、或結果的列數遠多於預期時，查這個運算什麼時候會被展開">笛卡兒積&lt;/a>，再用條件選取。所以下面這個寫法是把定義翻成 SQL：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">姓名&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">訂單編號&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">CROSS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>它讀起來自然是有來由的——每一種配對先擺出來，再挑走不要的。而它是&lt;strong>語意層的定義&lt;/strong>：代數用它說明連接的結果集是什麼，沒有規定寫法要照這個順序展開。引擎也沒有照著做，這一段的計畫與 &lt;code>JOIN ... ON&lt;/code> 寫同一個條件逐字相同（&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/declared-intent-vs-behaviour/" data-link-title="SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證" data-link-desc="LEFT JOIN、CROSS JOIN、DISTINCT、COALESCE 與表的別名各自對讀的人宣告了什麼、宣告落空時下一個修改的人付出什麼代價，以及把關鍵字換成最弱的版本再跑一次的查證動作">SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證&lt;/a>）。&lt;/p>
&lt;p>這個起手式在內連接底下一直行得通，因為條件放 &lt;code>ON&lt;/code> 與放 &lt;code>WHERE&lt;/code> 在內連接底下回同一批列。行得通的經驗會累積，直到問題需要保留配不到的列。&lt;/p>
&lt;h2 id="從全部配對縮小與寫進-on-在外連接處的差別">從全部配對縮小與寫進 ON 在外連接處的差別&lt;/h2>
&lt;p>書店要一份名單：每位顧客與他 300 元以上的訂單，而沒有這種訂單的顧客也要列出來。&lt;/p>
&lt;p>從全部配對縮小：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">CROSS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">金額&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">300&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 佳穎 101 / 佳穎 102&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>宗翰與雅文不在名單上。&lt;code>CROSS JOIN&lt;/code> 這個關鍵字沒有外連接的形式，所以要讓宗翰與雅文這種沒有訂單的顧客也留在結果裡（這是外連接做的事，下文叫&lt;strong>保護&lt;/strong>：&lt;strong>被保護的表&lt;/strong>裡配不到對象的列照樣留下，另一張表——下文叫&lt;strong>補 &lt;code>NULL&lt;/code> 的表&lt;/strong>——的欄位在那一列補上 &lt;code>NULL&lt;/code>），這個起手式本身就得換掉。&lt;/p>
&lt;p>把 &lt;code>CROSS JOIN&lt;/code> 換成 &lt;code>LEFT JOIN&lt;/code> 而條件留在原處：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">LEFT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">金額&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">300&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 佳穎 101 / 佳穎 102&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>兩人仍然不在。&lt;code>LEFT&lt;/code> 保護下來的那些列右邊全是 &lt;code>NULL&lt;/code>，而 &lt;code>WHERE&lt;/code> 那一步把它們篩掉了（&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/on-describes-where-filters/" data-link-title="SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責" data-link-desc="條件寫在連接的配對階段與寫在關係組完之後的篩選階段的差別、外連接的保護在 WHERE 被抵銷的機制，以及判斷一個條件該放哪一邊的測試">SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責&lt;/a>）。&lt;strong>換掉關鍵字補不回那些列，因為要動的是條件的位置。&lt;/strong>&lt;/p>
&lt;p>把配對關係整個寫進 &lt;code>ON&lt;/code>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">LEFT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">金額&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">300&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 佳穎 101 / 佳穎 102 / 宗翰 （空）/ 雅文 （空）&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>三位顧客都在。&lt;strong>從全部配對縮小與把配對關係寫進 &lt;code>ON&lt;/code> 這兩個起手式，差別在這裡從「寫法偏好」變成「答得出來與答不出來」&lt;/strong>：從全部配對縮小的起手式把所有條件都放進 &lt;code>WHERE&lt;/code>，在成品上篩，而外連接的保護正是在成品那一步被抵銷的。&lt;/p>
&lt;h2 id="連接種類的選擇配不到的列在答案裡有沒有位置">連接種類的選擇：配不到的列在答案裡有沒有位置&lt;/h2>
&lt;p>哪兩列該配在一起說完之後，接著要決定的是配不到對象的列怎麼處理。判斷標準是一句話：&lt;strong>這些列在答案裡有沒有位置。&lt;/strong>&lt;/p>
&lt;p>有位置就用外連接，而哪一側有位置決定寫 &lt;code>LEFT&lt;/code>、&lt;code>RIGHT&lt;/code> 還是 &lt;code>FULL&lt;/code>；沒有位置就用內連接（&lt;code>JOIN&lt;/code>）（&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/outer-join/" data-link-title="Outer Join（外連接）" data-link-desc="想確認 LEFT / RIGHT / FULL 各自保護哪一側、或連接之後有些人從名單上消失時">外連接&lt;/a>那張卡寫三者各保護誰）。「哪些顧客一張單都沒下」這個問題的答案整個在補 &lt;code>NULL&lt;/code> 的那幾列上，內連接會讓它無從回答。&lt;/p>
&lt;p>還有第三種情形：只問配不配得到，右表的欄位一個都不輸出。那時要的運算是半連接——配起來之後只帶走左邊那一份，右邊只當條件（&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/semi-join-and-anti-join/" data-link-title="Semi-join 與 Anti-join（半連接與反連接）" data-link-desc="看到 EXISTS 或 NOT EXISTS 時，查它在代數裡對應哪個運算、以及列數上限為什麼是左邊那張表的列數">Semi-join 與 Anti-join&lt;/a>）。寫成 &lt;code>JOIN&lt;/code> 會讓配得到多列的那些左列重複出現，&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/in-exists-join/" data-link-title="SQL.8 IN、EXISTS 與 JOIN：列數、可取用的欄位與 NULL 的處理" data-link-desc="同一個「找出有對應資料的那些」的問題用成員判斷、存在性判斷與連接各寫一次的結果，選用的判斷標準，以及子查詢含 NULL 時 NOT IN 與 NOT EXISTS 的分岔">SQL.8 IN、EXISTS 與 JOIN&lt;/a> 寫這三種寫法在列數上的差別，以及選哪一種的判斷標準。&lt;/p></description><content:encoded><![CDATA[<p>寫一個連接要先決定的是<strong>哪兩列該配在一起</strong>，而 SQL 給了這個決定一個專屬的位置：<code>ON</code>。本篇把這件事叫<strong>配對</strong>——關係還在組的那個過程，哪一列跟哪一列湊成一列；這是為了指認它而取的稱呼，SQL 的標準術語裡沒有這個詞。條件寫進那裡，配對關係就成為查詢結構的一部分；條件留在 <code>WHERE</code>，配對關係就退化成一道篩子。</p>
<p>本篇的查詢跑在<a href="/blog/backend/01-database/sql/sample-bookstore-database/" data-link-title="SQL 各篇共用的範例資料庫：一家小書店的顧客、訂單與評價表" data-link-desc="SQL 各篇共用的顧客、訂單與評價表的建表語句與初始資料，以及各篇輸出所用的引擎版本、錯誤訊息與列序的閱讀約定">共用的範例資料庫</a>上：三位顧客佳穎、宗翰、雅文，而只有佳穎下過兩張訂單（101 與 102，金額 300 與 500）。</p>
<p>這一篇處理的是<strong>起手式</strong>——寫連接時第一個下筆的地方，而它決定了條件之後能放哪裡。<a href="/blog/backend/01-database/sql/on-describes-where-filters/" data-link-title="SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責" data-link-desc="條件寫在連接的配對階段與寫在關係組完之後的篩選階段的差別、外連接的保護在 WHERE 被抵銷的機制，以及判斷一個條件該放哪一邊的測試">SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責</a> 處理的是條件寫下之後放哪一邊——那一篇會把配對與<strong>成品</strong>（關係組完之後得到的那一批列）並排；本篇兩個詞都會用到，而判斷一個條件屬於配對還是成品的完整判定表在那一篇。順序上這一篇在前：先有配對關係，才有條件該放哪裡的問題。</p>
<h2 id="從全部配對縮小的起手式與關聯代數的定義">從全部配對縮小的起手式與關聯代數的定義</h2>
<p><a href="/blog/backend/01-database/sql/knowledge-cards/relational-algebra/" data-link-title="Relational Algebra（關聯代數）" data-link-desc="看到「子句其實是某個運算」或想知道 SQL 的規則從哪裡來時，查這一層在底下的是什麼">關聯代數</a>把連接定義成兩個運算的組合：先取<a href="/blog/backend/01-database/sql/knowledge-cards/cartesian-product/" data-link-title="Cartesian Product（笛卡兒積）" data-link-desc="看到 CROSS JOIN、或結果的列數遠多於預期時，查這個運算什麼時候會被展開">笛卡兒積</a>，再用條件選取。所以下面這個寫法是把定義翻成 SQL：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">姓名</span><span class="p">,</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">訂單編號</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">CROSS</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">顧客編號</span><span class="p">;</span></span></span></code></pre></div><p>它讀起來自然是有來由的——每一種配對先擺出來，再挑走不要的。而它是<strong>語意層的定義</strong>：代數用它說明連接的結果集是什麼，沒有規定寫法要照這個順序展開。引擎也沒有照著做，這一段的計畫與 <code>JOIN ... ON</code> 寫同一個條件逐字相同（<a href="/blog/backend/01-database/sql/declared-intent-vs-behaviour/" data-link-title="SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證" data-link-desc="LEFT JOIN、CROSS JOIN、DISTINCT、COALESCE 與表的別名各自對讀的人宣告了什麼、宣告落空時下一個修改的人付出什麼代價，以及把關鍵字換成最弱的版本再跑一次的查證動作">SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證</a>）。</p>
<p>這個起手式在內連接底下一直行得通，因為條件放 <code>ON</code> 與放 <code>WHERE</code> 在內連接底下回同一批列。行得通的經驗會累積，直到問題需要保留配不到的列。</p>
<h2 id="從全部配對縮小與寫進-on-在外連接處的差別">從全部配對縮小與寫進 ON 在外連接處的差別</h2>
<p>書店要一份名單：每位顧客與他 300 元以上的訂單，而沒有這種訂單的顧客也要列出來。</p>
<p>從全部配對縮小：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">CROSS</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">金額</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="mi">300</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- 佳穎 101 / 佳穎 102</span></span></span></code></pre></div><p>宗翰與雅文不在名單上。<code>CROSS JOIN</code> 這個關鍵字沒有外連接的形式，所以要讓宗翰與雅文這種沒有訂單的顧客也留在結果裡（這是外連接做的事，下文叫<strong>保護</strong>：<strong>被保護的表</strong>裡配不到對象的列照樣留下，另一張表——下文叫<strong>補 <code>NULL</code> 的表</strong>——的欄位在那一列補上 <code>NULL</code>），這個起手式本身就得換掉。</p>
<p>把 <code>CROSS JOIN</code> 換成 <code>LEFT JOIN</code> 而條件留在原處：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">顧客編號</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">金額</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="mi">300</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- 佳穎 101 / 佳穎 102</span></span></span></code></pre></div><p>兩人仍然不在。<code>LEFT</code> 保護下來的那些列右邊全是 <code>NULL</code>，而 <code>WHERE</code> 那一步把它們篩掉了（<a href="/blog/backend/01-database/sql/on-describes-where-filters/" data-link-title="SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責" data-link-desc="條件寫在連接的配對階段與寫在關係組完之後的篩選階段的差別、外連接的保護在 WHERE 被抵銷的機制，以及判斷一個條件該放哪一邊的測試">SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責</a>）。<strong>換掉關鍵字補不回那些列，因為要動的是條件的位置。</strong></p>
<p>把配對關係整個寫進 <code>ON</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="k">ON</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">金額</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="mi">300</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- 佳穎 101 / 佳穎 102 / 宗翰 （空）/ 雅文 （空）</span></span></span></code></pre></div><p>三位顧客都在。<strong>從全部配對縮小與把配對關係寫進 <code>ON</code> 這兩個起手式，差別在這裡從「寫法偏好」變成「答得出來與答不出來」</strong>：從全部配對縮小的起手式把所有條件都放進 <code>WHERE</code>，在成品上篩，而外連接的保護正是在成品那一步被抵銷的。</p>
<h2 id="連接種類的選擇配不到的列在答案裡有沒有位置">連接種類的選擇：配不到的列在答案裡有沒有位置</h2>
<p>哪兩列該配在一起說完之後，接著要決定的是配不到對象的列怎麼處理。判斷標準是一句話：<strong>這些列在答案裡有沒有位置。</strong></p>
<p>有位置就用外連接，而哪一側有位置決定寫 <code>LEFT</code>、<code>RIGHT</code> 還是 <code>FULL</code>；沒有位置就用內連接（<code>JOIN</code>）（<a href="/blog/backend/01-database/sql/knowledge-cards/outer-join/" data-link-title="Outer Join（外連接）" data-link-desc="想確認 LEFT / RIGHT / FULL 各自保護哪一側、或連接之後有些人從名單上消失時">外連接</a>那張卡寫三者各保護誰）。「哪些顧客一張單都沒下」這個問題的答案整個在補 <code>NULL</code> 的那幾列上，內連接會讓它無從回答。</p>
<p>還有第三種情形：只問配不配得到，右表的欄位一個都不輸出。那時要的運算是半連接——配起來之後只帶走左邊那一份，右邊只當條件（<a href="/blog/backend/01-database/sql/knowledge-cards/semi-join-and-anti-join/" data-link-title="Semi-join 與 Anti-join（半連接與反連接）" data-link-desc="看到 EXISTS 或 NOT EXISTS 時，查它在代數裡對應哪個運算、以及列數上限為什麼是左邊那張表的列數">Semi-join 與 Anti-join</a>）。寫成 <code>JOIN</code> 會讓配得到多列的那些左列重複出現，<a href="/blog/backend/01-database/sql/in-exists-join/" data-link-title="SQL.8 IN、EXISTS 與 JOIN：列數、可取用的欄位與 NULL 的處理" data-link-desc="同一個「找出有對應資料的那些」的問題用成員判斷、存在性判斷與連接各寫一次的結果，選用的判斷標準，以及子查詢含 NULL 時 NOT IN 與 NOT EXISTS 的分岔">SQL.8 IN、EXISTS 與 JOIN</a> 寫這三種寫法在列數上的差別，以及選哪一種的判斷標準。</p>
<p><strong>配不到的列要不要保護，這個決定由問題決定，不由現有資料決定。</strong> 「翻遍現在的訂單表，每一列都配得到顧客」只證明此刻如此，而查詢要活得比這一批資料久。反過來說，資料保證配得到的時候寫 <code>LEFT</code> 也有代價——它宣告了一個從未發生的情況，下一個讀的人會以為那些列存在（<a href="/blog/backend/01-database/sql/declared-intent-vs-behaviour/" data-link-title="SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證" data-link-desc="LEFT JOIN、CROSS JOIN、DISTINCT、COALESCE 與表的別名各自對讀的人宣告了什麼、宣告落空時下一個修改的人付出什麼代價，以及把關鍵字換成最弱的版本再跑一次的查證動作">SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證</a>）。要判斷配對有沒有保證，去看<a href="/blog/backend/01-database/sql/knowledge-cards/constraint/" data-link-title="Constraint（約束）" data-link-desc="看到「這個保證要來自約束而不是來自習慣」，或想確認某一欄會不會出現 NULL 時，查這一層由誰擋">約束</a>，而那條保證要真的生效才算數（<a href="/blog/backend/01-database/sql/foreign-key-and-referential-integrity/" data-link-title="SQL.18 外鍵與參照完整性：宣告、生效與查詢得到的保證" data-link-desc="外鍵擋下哪些寫入與 NULL 的放行、SQLite、PostgreSQL 與 MySQL 讓宣告與生效分開的位置與查法、約束生效後查詢省掉的判斷、父列刪除時子列的去向，以及外鍵涵蓋不到的寫入與規則">SQL.18 外鍵與參照完整性：宣告、生效與查詢得到的保證</a>）。</p>
<h2 id="業務描述對應到查詢的位置on連接種類與-where">業務描述對應到查詢的位置：ON、連接種類與 WHERE</h2>
<p>「每位顧客與他 300 元以上的訂單，沒有這種訂單的顧客也要列出來」這句話裡有三個決定，各自對應查詢的一個位置。</p>
<p><strong>哪兩列該配在一起。</strong> 顧客的那一列與訂單的那一列，靠顧客編號對上，而且只跟 300 元以上的訂單配。這一整句進 <code>ON</code>——「只跟 300 元以上的訂單配」描述的是配對，所以它屬於那裡。</p>
<p><strong>配不到的列要不要。</strong> 「沒有這種訂單的顧客也要列出來」說了要，而要保護的是顧客那一側，所以是 <code>LEFT JOIN</code>，左邊放顧客。<strong>兩張表的時候這樣判就夠</strong>；三張以上時「左邊」是前面幾個連接累積出來的結果，鏈中間放一個 <code>RIGHT</code> 會把前段保護下來的列整批丟掉。累積的判讀，以及同一組資料上四列變一列的實跑，在 <a href="/blog/backend/01-database/sql/join-left-operand-accumulates/" data-link-title="SQL.4 鏈式連接的左運算元：累積方式與外連接的保護範圍" data-link-desc="每個 JOIN 的左運算元由哪些表合成、ON 因此引用得到哪些欄位、LEFT 與 RIGHT 各自保護哪些列，以及 WHERE 引用鏈尾的表時哪些列會被撤掉保護">SQL.4 鏈式連接的左運算元：累積方式與外連接的保護範圍</a>。</p>
<p><strong>成品裡再篩什麼。</strong> 這句話沒有再提條件，所以 <code>WHERE</code> 是空的。業務描述裡沒有針對顧客那一側的限制時，空的 <code>WHERE</code> 是一個合理的結果。</p>
<p>順序反過來走也能檢查已經寫好的查詢：<code>WHERE</code> 裡引用右表欄位的條件，回頭問它描述的是配對，還是成品——多數形態上它描述的是配對，而它待在那裡正在抵銷某個保護。<strong>有一個形態剛好相反</strong>：<code>WHERE 右表.某欄 IS NULL</code> 挑的正是外連接補上 <code>NULL</code> 的那些列，它描述的是成品，待在 <code>WHERE</code> 是對的位置（<a href="/blog/backend/01-database/sql/on-describes-where-filters/" data-link-title="SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責" data-link-desc="條件寫在連接的配對階段與寫在關係組完之後的篩選階段的差別、外連接的保護在 WHERE 被抵銷的機制，以及判斷一個條件該放哪一邊的測試">SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責</a> 有完整的判定表，<a href="/blog/backend/01-database/sql/declared-intent-vs-behaviour/" data-link-title="SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證" data-link-desc="LEFT JOIN、CROSS JOIN、DISTINCT、COALESCE 與表的別名各自對讀的人宣告了什麼、宣告落空時下一個修改的人付出什麼代價，以及把關鍵字換成最弱的版本再跑一次的查證動作">SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證</a> 用它示範查孤兒訂單）。</p>
<h2 id="只提到被保護的表的條件該放的位置">只提到被保護的表的條件該放的位置</h2>
<p>上一節那句業務描述裡，「哪兩列該配在一起」與「配不到的列要不要」都在問條件與連接種類該怎麼配。而條件提到哪一側，會讓「哪兩列該配在一起」的答案整個不同。換一對表看得清楚。「每張訂單與它的評價，只要三月五日以後下的單」——被保護的是訂單，而「三月五日以後」只提到訂單。條件寫進 <code>ON</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">訂單編號</span><span class="p">,</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">下單日</span><span class="p">,</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">星等</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">評價</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="k">ON</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">訂單編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">訂單編號</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">下單日</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="s1">&#39;2026-03-05&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 101  2026-03-02  （空）
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">-- 102  2026-03-09  （空）</span></span></span></code></pre></div><p>三月二日那張照樣回來，篩選沒有發生——<code>LEFT</code> 的定義是左邊每一列都留著，而 <code>ON</code> 只決定右邊配不配得上，配不上就補 <code>NULL</code>。<strong>這個條件改變的是配對</strong>：101 本來配得上那筆五星評價，多了這個條件之後整個 <code>ON</code> 對它判假，於是連評價一起丟了。同一個條件改放 <code>WHERE</code> 才回到要的那批列：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">訂單編號</span><span class="p">,</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">下單日</span><span class="p">,</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">星等</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">評價</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">訂單編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">訂單編號</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">下單日</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="s1">&#39;2026-03-05&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 102  2026-03-09  （空）</span></span></span></code></pre></div><p>只提到被保護的表的條件描述的是成品，屬於 <code>WHERE</code>。判斷標準因此要分兩步：先問這個條件提到哪幾張表，只提被保護的表的直接進 <code>WHERE</code>；兩張表都提到、或只提補 <code>NULL</code> 的表的，才問它描述配對還是成品。<strong>自連接寫成外連接時也是這樣</strong>。自連接指同一張表在一段查詢裡出現兩次、各自扮演一個角色，例如同一張訂單表一次代表較早的訂單、一次代表較晚的訂單；兩次出現裡被保護的那一次，它自己的過濾條件寫進 <code>ON</code> 同樣篩不掉列。配對的兩側是同一張表時，兩次出現要各自有名字才分得開，<a href="/blog/backend/01-database/sql/table-occurrence-and-alias/" data-link-title="SQL.7 表的出現與別名：表名的指稱、別名的作用範圍與自連接" data-link-desc="FROM 裡的表名指的是什麼、同名的兩次出現為什麼被引擎擋下、自連接的前後配對、同組比較與層級關係，以及別名取名的依據">SQL.7 表的出現與別名：表名的指稱、別名的作用範圍與自連接</a> 用三個引擎的錯誤訊息示範為什麼非取名不可，並把自連接處理的關係分成前後配對、同組比較與層級關係三種形態。放右表的條件另有一組判定，以及它為什麼對左表條件恆為真，在 <a href="/blog/backend/01-database/sql/on-describes-where-filters/" data-link-title="SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責" data-link-desc="條件寫在連接的配對階段與寫在關係組完之後的篩選階段的差別、外連接的保護在 WHERE 被抵銷的機制，以及判斷一個條件該放哪一邊的測試">SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責</a>。</p>
<p>所以空的 <code>WHERE</code> 不是一個要去追求的形狀。上一節那句業務描述留下空的 <code>WHERE</code>，是因為它確實沒有再提條件；把一個只提被保護的表的條件塞進 <code>ON</code> 去換一個空的 <code>WHERE</code>，換來的是一個篩不掉列、還會丟掉本來成立的配對的條件。</p>
]]></content:encoded></item><item><title>SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責</title><link>https://tarrragon.github.io/blog/backend/01-database/sql/on-describes-where-filters/</link><pubDate>Mon, 31 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/sql/on-describes-where-filters/</guid><description>&lt;p>&lt;code>ON&lt;/code> 說的是 &lt;code>JOIN&lt;/code> 左右兩邊的列怎麼配成一個 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/relation/" data-link-title="Relation（關係）" data-link-desc="看到 SQL 教材說「組成一個關係」而不確定它指的是不是資料表時">relation&lt;/a>，&lt;code>WHERE&lt;/code> 說的是從配好的關係裡篩掉什麼。這兩件事落在求值順序的不同步：&lt;code>FROM&lt;/code> 那一步把關係組出來，&lt;code>WHERE&lt;/code> 那一步才開始篩。&lt;/p>
&lt;p>本篇要反覆用到這兩件事的名字。&lt;strong>配對&lt;/strong>指關係還在組的那個過程——哪一列跟哪一列湊成一列；&lt;strong>成品&lt;/strong>指關係組完之後得到的那一批列——已經湊好、包含補了 NULL 的那些列在內的整批資料。這兩個詞是本篇為了指認這兩個時刻而取的稱呼，SQL 的標準術語裡沒有它們。&lt;/p>
&lt;p>內連接底下，條件寫在 &lt;code>ON&lt;/code> 與寫在 &lt;code>WHERE&lt;/code> 的結果相同，於是它們看起來可以互換。&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/outer-join/" data-link-title="Outer Join（外連接）" data-link-desc="想確認 LEFT / RIGHT / FULL 各自保護哪一側、或連接之後有些人從名單上消失時">外連接&lt;/a>底下不同，而不同的地方正好是外連接存在的理由。條件擺在哪一邊不影響查詢跑不跑得動，只影響外連接剛保護下來的那些列留不留得住。保護指 &lt;code>LEFT JOIN&lt;/code> 把左表配不到對象的列也留在結果裡、右表那幾欄補上 &lt;code>NULL&lt;/code>。&lt;code>LEFT JOIN&lt;/code> 退化成內連接的名單，來源就是把條件放進 &lt;code>WHERE&lt;/code> 這一步。&lt;/p>
&lt;h2 id="同一個條件放-on-與放-where-的結果">同一個條件放 ON 與放 WHERE 的結果&lt;/h2>
&lt;p>書店想要一份名單：全部顧客都要列出來，而評價欄只顯示五星的那些。條件寫進 &lt;code>ON&lt;/code>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">姓名&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">評價&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">星等&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">LEFT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">LEFT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">評價&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">評價&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">訂單編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">訂單編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">評價&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">星等&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>回來四列：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">佳穎 5
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">佳穎 NULL
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">宗翰 NULL
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">雅文 NULL&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>三個顧客都在。佳穎出現兩次，因為她有兩張訂單——第一張有五星評價，第二張沒有評價可配，補 NULL。（一位顧客變成兩列是連接在改變列數，本篇只處理條件決定的留不留，連接怎麼改變列數在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/join-changes-rows-and-nulls/" data-link-title="SQL.6 連接之後的列數與空缺：列數膨脹、外連接補的 NULL 與三值邏輯" data-link-desc="連接讓聚合多算的機制與計數、加總、平均、中位數各自的修法，以及外連接補出來的 NULL 在 count、等號比較、NOT IN 與 NOT EXISTS 上的行為">SQL.6 連接之後的列數與空缺&lt;/a>。）&lt;/p>
&lt;p>同一個星等條件改放 &lt;code>WHERE&lt;/code>，其餘不動：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">姓名&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">評價&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">星等&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">LEFT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">LEFT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">評價&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">評價&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">訂單編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">訂單編號&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">評價&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">星等&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 佳穎 5&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>宗翰、雅文與佳穎的第二張訂單全部消失，結果與內連接相同。書店要的是全部顧客，所以這一版的答案錯了，而查詢照樣跑、不報錯——這一類錯的共同性質在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/well-formed-is-not-correct/" data-link-title="SQL.13 查詢的合法性與答案的正確性：引擎檢查的範圍、答案錯掉的成因與查證方法" data-link-desc="合法與問對了分別由誰負責、答案錯掉時引擎為什麼不報錯，以及靜態分析、約束與反例資料各自判得出哪些錯">SQL.13 查詢的合法性與答案的正確性：引擎檢查的範圍、答案錯掉的成因與查證方法&lt;/a>。需求若恰好只要有五星評價的那些，答案就對了，剩下的是文字的問題：&lt;code>LEFT&lt;/code> 仍然寫在那裡，照字面讀的人會以為沒配到的顧客都在，那是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/declared-intent-vs-behaviour/" data-link-title="SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證" data-link-desc="LEFT JOIN、CROSS JOIN、DISTINCT、COALESCE 與表的別名各自對讀的人宣告了什麼、宣告落空時下一個修改的人付出什麼代價，以及把關鍵字換成最弱的版本再跑一次的查證動作">SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證&lt;/a> 的主題。&lt;/p>
&lt;h2 id="條件放-on-與放-where-在外連接底下分岔的原因">條件放 ON 與放 WHERE 在外連接底下分岔的原因&lt;/h2>
&lt;p>&lt;code>LEFT JOIN&lt;/code> 的定義是左邊每一列都留著，右邊配不上的補 NULL。條件寫在 &lt;code>ON&lt;/code> 裡的時候，它參與的是「配不配得上」這個判斷——宗翰沒有訂單所以配不上任何評價，佳穎的第二張訂單配不上五星評價，照定義都補 NULL 留下來。&lt;/p>
&lt;p>條件寫在 &lt;code>WHERE&lt;/code> 裡的時候，關係已經組完了，補上的那些 NULL 也已經在裡面。這時候問「星等等於 5」，那些補 NULL 的列答不出 5，於是被篩掉。&lt;strong>外連接剛剛保護下來的列，&lt;code>WHERE&lt;/code> 這一步又把它們拿掉了。&lt;/strong>&lt;/p>
&lt;p>內連接底下看不出差別，是因為它本來就不保留配不上的列。沒有被保護的列可以被抵銷，所以兩種寫法自然同值。&lt;/p>
&lt;p>開頭那段顧客接訂單、再接評價的查詢有三張表，第二個 &lt;code>LEFT JOIN&lt;/code> 保護的「左邊」因此是顧客與訂單累積出來的結果；鏈越長，保護的範圍怎麼跟著累積走，是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/join-left-operand-accumulates/" data-link-title="SQL.4 鏈式連接的左運算元：累積方式與外連接的保護範圍" data-link-desc="每個 JOIN 的左運算元由哪些表合成、ON 因此引用得到哪些欄位、LEFT 與 RIGHT 各自保護哪些列，以及 WHERE 引用鏈尾的表時哪些列會被撤掉保護">SQL.4 鏈式連接的左運算元&lt;/a> 的題目。&lt;/p>
&lt;h2 id="保護存不存活的測試把補了-null-的那一列代進條件">保護存不存活的測試：把補了 NULL 的那一列代進條件&lt;/h2>
&lt;p>&lt;code>WHERE&lt;/code> 只留下判成真的列。而條件的答案有三種——真、假、以及&lt;strong>未知&lt;/strong>：拿 &lt;code>NULL&lt;/code> 去跟任何值比較，答案既不是真也不是假（三值邏輯的推導，以及它讓 &lt;code>NOT IN&lt;/code> 整段失效的形態，在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/join-changes-rows-and-nulls/" data-link-title="SQL.6 連接之後的列數與空缺：列數膨脹、外連接補的 NULL 與三值邏輯" data-link-desc="連接讓聚合多算的機制與計數、加總、平均、中位數各自的修法，以及外連接補出來的 NULL 在 count、等號比較、NOT IN 與 NOT EXISTS 上的行為">SQL.6 連接之後的列數與空缺：列數膨脹、外連接補的 NULL 與三值邏輯&lt;/a>）。&lt;/p>
&lt;p>外連接補上的那些列，右表欄位全是 &lt;code>NULL&lt;/code>，所以問題變成一句話：&lt;strong>這個條件對「什麼都沒有」這一列給出什麼答案。&lt;/strong> 真的留下，假與未知都被丟掉。&lt;/p></description><content:encoded><![CDATA[<p><code>ON</code> 說的是 <code>JOIN</code> 左右兩邊的列怎麼配成一個 <a href="/blog/backend/01-database/sql/knowledge-cards/relation/" data-link-title="Relation（關係）" data-link-desc="看到 SQL 教材說「組成一個關係」而不確定它指的是不是資料表時">relation</a>，<code>WHERE</code> 說的是從配好的關係裡篩掉什麼。這兩件事落在求值順序的不同步：<code>FROM</code> 那一步把關係組出來，<code>WHERE</code> 那一步才開始篩。</p>
<p>本篇要反覆用到這兩件事的名字。<strong>配對</strong>指關係還在組的那個過程——哪一列跟哪一列湊成一列；<strong>成品</strong>指關係組完之後得到的那一批列——已經湊好、包含補了 NULL 的那些列在內的整批資料。這兩個詞是本篇為了指認這兩個時刻而取的稱呼，SQL 的標準術語裡沒有它們。</p>
<p>內連接底下，條件寫在 <code>ON</code> 與寫在 <code>WHERE</code> 的結果相同，於是它們看起來可以互換。<a href="/blog/backend/01-database/sql/knowledge-cards/outer-join/" data-link-title="Outer Join（外連接）" data-link-desc="想確認 LEFT / RIGHT / FULL 各自保護哪一側、或連接之後有些人從名單上消失時">外連接</a>底下不同，而不同的地方正好是外連接存在的理由。條件擺在哪一邊不影響查詢跑不跑得動，只影響外連接剛保護下來的那些列留不留得住。保護指 <code>LEFT JOIN</code> 把左表配不到對象的列也留在結果裡、右表那幾欄補上 <code>NULL</code>。<code>LEFT JOIN</code> 退化成內連接的名單，來源就是把條件放進 <code>WHERE</code> 這一步。</p>
<h2 id="同一個條件放-on-與放-where-的結果">同一個條件放 ON 與放 WHERE 的結果</h2>
<p>書店想要一份名單：全部顧客都要列出來，而評價欄只顯示五星的那些。條件寫進 <code>ON</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">姓名</span><span class="p">,</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">星等</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">顧客編號</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">評價</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">訂單編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">訂單編號</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">星等</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5</span><span class="p">;</span></span></span></code></pre></div><p>回來四列：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">佳穎  5
</span></span><span class="line"><span class="ln">2</span><span class="cl">佳穎  NULL
</span></span><span class="line"><span class="ln">3</span><span class="cl">宗翰  NULL
</span></span><span class="line"><span class="ln">4</span><span class="cl">雅文  NULL</span></span></code></pre></div><p>三個顧客都在。佳穎出現兩次，因為她有兩張訂單——第一張有五星評價，第二張沒有評價可配，補 NULL。（一位顧客變成兩列是連接在改變列數，本篇只處理條件決定的留不留，連接怎麼改變列數在 <a href="/blog/backend/01-database/sql/join-changes-rows-and-nulls/" data-link-title="SQL.6 連接之後的列數與空缺：列數膨脹、外連接補的 NULL 與三值邏輯" data-link-desc="連接讓聚合多算的機制與計數、加總、平均、中位數各自的修法，以及外連接補出來的 NULL 在 count、等號比較、NOT IN 與 NOT EXISTS 上的行為">SQL.6 連接之後的列數與空缺</a>。）</p>
<p>同一個星等條件改放 <code>WHERE</code>，其餘不動：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">姓名</span><span class="p">,</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">星等</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">顧客編號</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">評價</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">訂單編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">訂單編號</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">星等</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="c1">-- 佳穎  5</span></span></span></code></pre></div><p>宗翰、雅文與佳穎的第二張訂單全部消失，結果與內連接相同。書店要的是全部顧客，所以這一版的答案錯了，而查詢照樣跑、不報錯——這一類錯的共同性質在 <a href="/blog/backend/01-database/sql/well-formed-is-not-correct/" data-link-title="SQL.13 查詢的合法性與答案的正確性：引擎檢查的範圍、答案錯掉的成因與查證方法" data-link-desc="合法與問對了分別由誰負責、答案錯掉時引擎為什麼不報錯，以及靜態分析、約束與反例資料各自判得出哪些錯">SQL.13 查詢的合法性與答案的正確性：引擎檢查的範圍、答案錯掉的成因與查證方法</a>。需求若恰好只要有五星評價的那些，答案就對了，剩下的是文字的問題：<code>LEFT</code> 仍然寫在那裡，照字面讀的人會以為沒配到的顧客都在，那是 <a href="/blog/backend/01-database/sql/declared-intent-vs-behaviour/" data-link-title="SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證" data-link-desc="LEFT JOIN、CROSS JOIN、DISTINCT、COALESCE 與表的別名各自對讀的人宣告了什麼、宣告落空時下一個修改的人付出什麼代價，以及把關鍵字換成最弱的版本再跑一次的查證動作">SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證</a> 的主題。</p>
<h2 id="條件放-on-與放-where-在外連接底下分岔的原因">條件放 ON 與放 WHERE 在外連接底下分岔的原因</h2>
<p><code>LEFT JOIN</code> 的定義是左邊每一列都留著，右邊配不上的補 NULL。條件寫在 <code>ON</code> 裡的時候，它參與的是「配不配得上」這個判斷——宗翰沒有訂單所以配不上任何評價，佳穎的第二張訂單配不上五星評價，照定義都補 NULL 留下來。</p>
<p>條件寫在 <code>WHERE</code> 裡的時候，關係已經組完了，補上的那些 NULL 也已經在裡面。這時候問「星等等於 5」，那些補 NULL 的列答不出 5，於是被篩掉。<strong>外連接剛剛保護下來的列，<code>WHERE</code> 這一步又把它們拿掉了。</strong></p>
<p>內連接底下看不出差別，是因為它本來就不保留配不上的列。沒有被保護的列可以被抵銷，所以兩種寫法自然同值。</p>
<p>開頭那段顧客接訂單、再接評價的查詢有三張表，第二個 <code>LEFT JOIN</code> 保護的「左邊」因此是顧客與訂單累積出來的結果；鏈越長，保護的範圍怎麼跟著累積走，是 <a href="/blog/backend/01-database/sql/join-left-operand-accumulates/" data-link-title="SQL.4 鏈式連接的左運算元：累積方式與外連接的保護範圍" data-link-desc="每個 JOIN 的左運算元由哪些表合成、ON 因此引用得到哪些欄位、LEFT 與 RIGHT 各自保護哪些列，以及 WHERE 引用鏈尾的表時哪些列會被撤掉保護">SQL.4 鏈式連接的左運算元</a> 的題目。</p>
<h2 id="保護存不存活的測試把補了-null-的那一列代進條件">保護存不存活的測試：把補了 NULL 的那一列代進條件</h2>
<p><code>WHERE</code> 只留下判成真的列。而條件的答案有三種——真、假、以及<strong>未知</strong>：拿 <code>NULL</code> 去跟任何值比較，答案既不是真也不是假（三值邏輯的推導，以及它讓 <code>NOT IN</code> 整段失效的形態，在 <a href="/blog/backend/01-database/sql/join-changes-rows-and-nulls/" data-link-title="SQL.6 連接之後的列數與空缺：列數膨脹、外連接補的 NULL 與三值邏輯" data-link-desc="連接讓聚合多算的機制與計數、加總、平均、中位數各自的修法，以及外連接補出來的 NULL 在 count、等號比較、NOT IN 與 NOT EXISTS 上的行為">SQL.6 連接之後的列數與空缺：列數膨脹、外連接補的 NULL 與三值邏輯</a>）。</p>
<p>外連接補上的那些列，右表欄位全是 <code>NULL</code>，所以問題變成一句話：<strong>這個條件對「什麼都沒有」這一列給出什麼答案。</strong> 真的留下，假與未知都被丟掉。</p>
<table>
  <thead>
      <tr>
          <th>條件</th>
          <th>對補 NULL 的那一列</th>
          <th>保護</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>評價.星等 = 5</code></td>
          <td>NULL 比不出來，判成未知</td>
          <td>取消</td>
      </tr>
      <tr>
          <td><code>評價.星等 IS NOT NULL</code></td>
          <td>判成假</td>
          <td>取消</td>
      </tr>
      <tr>
          <td><code>評價.星等 IS NULL</code></td>
          <td>判成真</td>
          <td>保住</td>
      </tr>
      <tr>
          <td><code>評價.星等 IS NULL OR 評價.星等 = 5</code></td>
          <td>前半判成真，整串就是真</td>
          <td>保住</td>
      </tr>
      <tr>
          <td><code>COALESCE(評價.星等, 0) &gt;= 0</code></td>
          <td>NULL 先被換成 0，判成真</td>
          <td>保住</td>
      </tr>
  </tbody>
</table>
<p><strong>能不能保住不看用了哪個運算子。</strong> <code>IS NOT NULL</code> 看起來與 <code>IS NULL</code> 成對，而在表上的「保護」欄一個是取消、一個是保住，正是因為它對空缺判成假；而 <code>OR</code> 與 <code>COALESCE</code> 這兩種寫法把未知轉成真，保護就留住了。</p>
<p><strong>這張表上的條件全部提到右表，而左表的條件進不了這張表。</strong> 補 <code>NULL</code> 的那一列上左表欄位仍然是原本的值，拿它去問什麼都判得出真假，所以保護一定保得住——這個測試對左表條件恆為真，什麼都判別不出來。左表條件實際的問題出在寫進 <code>ON</code> 的時候：它篩不掉左表的任何一列，還會讓本來配得上的那些配對判假。同一個條件 <code>顧客.姓名 &lt;&gt; '佳穎'</code> 放在三個位置：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 放 WHERE：佳穎被篩掉，宗翰與雅文的保護完好
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">姓名</span><span class="p">,</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">訂單編號</span><span class="p">,</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">星等</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">顧客編號</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">評價</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">訂單編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">訂單編號</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">星等</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">姓名</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="s1">&#39;佳穎&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="c1">-- 宗翰  NULL  NULL
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">-- 雅文  NULL  NULL
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="c1">-- 放進顧客接訂單的 ON：佳穎與她的兩張訂單判假、配不上，照 LEFT 的定義補 NULL 留下
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">姓名</span><span class="p">,</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">訂單編號</span><span class="p">,</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">星等</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w"></span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">姓名</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="s1">&#39;佳穎&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w"></span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">評價</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">訂單編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">訂單編號</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">星等</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w"></span><span class="c1">-- 佳穎  NULL  NULL     三位顧客全回來，佳穎的兩張訂單一起消失
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1">-- 宗翰  NULL  NULL
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1">-- 雅文  NULL  NULL
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w"></span><span class="c1">-- 放進接評價的 ON：顧客與訂單在上一步已經配好，佳穎的兩張訂單留著，只是配不到評價
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">姓名</span><span class="p">,</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">訂單編號</span><span class="p">,</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">星等</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w"></span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">顧客編號</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w"></span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">評價</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">訂單編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">訂單編號</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">星等</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">姓名</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="s1">&#39;佳穎&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w"></span><span class="c1">-- 佳穎  101   NULL
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="c1">-- 佳穎  102   NULL
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="c1">-- 宗翰  NULL  NULL
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="c1">-- 雅文  NULL  NULL</span></span></span></code></pre></div><p>所以左表條件根本不需要走這個測試，它一開始就屬於 <code>WHERE</code>。</p>
<p><strong>這個測試回答的是保護存不存活，不回答條件該放哪一邊。</strong> 歸屬要另外問一句：這個條件描述的是配對，還是成品。「只跟五星的評價配」描述的是配對，屬於 <code>ON</code>；「最後的名單裡只要五星那幾列」描述的是成品，屬於 <code>WHERE</code>。兩句話說的是不同的事，而內連接讓這兩種寫法碰巧回同一批列。</p>
<p>多數形態上「保護存不存活」與「條件該放哪一邊」這兩個問題同向——取消了保護的條件，通常正是因為它在描述成品。<code>IS NULL</code> 是這兩個問題答案不同向的那一個形態：它保住了保護，而它要的正好是那些被保護下來的列，所以它同時是「保護存活」與「屬於 <code>WHERE</code>」。下一節寫的就是這個形態。</p>
<h2 id="找沒配到的那些列is-null-條件的歸屬">找沒配到的那些列：IS NULL 條件的歸屬</h2>
<p>有時候要的正是「沒配到的那些」。書店想知道哪些顧客一張單都沒下過：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">姓名</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">顧客編號</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">訂單編號</span><span class="w"> </span><span class="k">IS</span><span class="w"> </span><span class="k">NULL</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 宗翰、雅文</span></span></span></code></pre></div><p>這個條件描述的是成品，所以它屬於 <code>WHERE</code>。它與上面那個「退化成內連接」的形態看起來很像——同樣是在 <code>WHERE</code> 裡引用右表欄位——而要的結果剛好相反：這一個挑的正是外連接補上 NULL 的那些列。</p>
<p><strong>分辨這兩種形態的仍然是那個問題：這個條件描述的是配對，還是成品。</strong> 「一張單都沒下過」是關於成品的敘述，所以它在 <code>WHERE</code> 是對的位置。</p>
<p>同一題「找沒消費過的顧客」換一個切面，走得到同一個結論。<a href="/blog/backend/01-database/sql/grouping-key-decides-the-unit/" data-link-title="SQL.9 分組鍵的選擇：鍵與一組所代表的對象" data-link-desc="外連接之後拿右表欄位當鍵的後果、輸出非分組欄時 MySQL、DuckDB 與 SQLite 的反應、補進鍵的欄位要滿足的函數相依，以及改用 NOT EXISTS 回答的那一類問題">SQL.9 分組鍵的選擇：鍵與一組所代表的對象</a> 從「沒消費的人不會出現在訂單表」這個業務事實走到 <code>NOT EXISTS</code>，並說明在外連接之後分組、再數出零的那種寫法，那個零是查詢自己製造再讀回去的產物。</p>
]]></content:encoded></item></channel></rss>