<?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>Semi-Join on Tarragon</title><link>https://tarrragon.github.io/blog/tags/semi-join/</link><description>Recent content in Semi-Join on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 02 Sep 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/semi-join/index.xml" rel="self" type="application/rss+xml"/><item><title>SQL.8 IN、EXISTS 與 JOIN：列數、可取用的欄位與 NULL 的處理</title><link>https://tarrragon.github.io/blog/backend/01-database/sql/in-exists-join/</link><pubDate>Mon, 31 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/sql/in-exists-join/</guid><description>&lt;p>「找出有對應資料的那些」這類問題有 &lt;code>IN&lt;/code>、&lt;code>EXISTS&lt;/code> 與 &lt;code>JOIN&lt;/code> 三種常見寫法，而它們在很多情況下回同樣的答案。這種巧合讓它們看起來可以互換，而三者描述的是不同的事，回的列數也不一定相同。&lt;/p>
&lt;p>分開它們的問題只有一個：&lt;strong>除了判斷有沒有，還要不要用到被比對的那張表的欄位。&lt;/strong>&lt;/p>
&lt;h2 id="同一題用-inexists-與-join-各寫一次">同一題用 IN、EXISTS 與 JOIN 各寫一次&lt;/h2>
&lt;p>書店的三位顧客裡只有佳穎下過單，兩張都是她的。要找出下過單的顧客。&lt;/p>
&lt;p>&lt;code>IN&lt;/code> 問的是成員資格——這個顧客編號在不在括號裡那個&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/subquery/" data-link-title="Subquery（子查詢）" data-link-desc="看到括號裡還有一個 SELECT、想知道它什麼時候算、算幾次時">子查詢&lt;/a>交出的清單裡：&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="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">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">IN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客編號&lt;/span>&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="p">);&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="c1">-- 佳穎&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>EXISTS&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="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">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="k">EXISTS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&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">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 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">-- 佳穎&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>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">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="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">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="p">;&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="c1">-- 佳穎、佳穎&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>同一個「找出下過單的顧客」的問題，&lt;code>IN&lt;/code> 與 &lt;code>EXISTS&lt;/code> 回一列而 &lt;code>JOIN&lt;/code> 回兩列——&lt;strong>佳穎在 &lt;code>JOIN&lt;/code> 的結果裡出現了兩次&lt;/strong>。&lt;/p>
&lt;h2 id="列數條件與連接的差別">列數：條件與連接的差別&lt;/h2>
&lt;p>&lt;code>IN&lt;/code> 與 &lt;code>EXISTS&lt;/code> 出現在 &lt;code>WHERE&lt;/code> 裡，它們是條件。條件對每一列只做一件事——留下或丟掉——所以顧客表有三列，過完條件最多剩三列，一列都不會多出來。&lt;/p>
&lt;p>&lt;code>JOIN&lt;/code> 出現在 &lt;code>FROM&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;code>DISTINCT&lt;/code> 之後結果才與 &lt;code>IN&lt;/code>、&lt;code>EXISTS&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="k">DISTINCT&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">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">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="p">;&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="c1">-- 佳穎&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>需要 &lt;code>DISTINCT&lt;/code> 這件事本身是一個訊號&lt;/strong>：它表示這個查詢只是要判斷有沒有，而 &lt;code>JOIN&lt;/code> 順便做了不需要的配對。這種時候 &lt;code>IN&lt;/code> 或 &lt;code>EXISTS&lt;/code> 更貼合意圖，因為它們一開始就沒有把列複製出來。&lt;/p>
&lt;p>&lt;code>IN&lt;/code> 與 &lt;code>EXISTS&lt;/code> 做的運算在&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;code>NOT EXISTS&lt;/code> 留下顧客表裡配不到任何訂單的列，叫反連接（&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;/p>
&lt;h2 id="可取用的欄位被比對的那張表在不在-from-裡">可取用的欄位：被比對的那張表在不在 FROM 裡&lt;/h2>
&lt;p>要列出每位顧客的訂單金額，&lt;code>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">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 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">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="p">;&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="c1">-- 佳穎 300 / 佳穎 500&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>同一件事用 &lt;code>IN&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="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">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">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">IN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客編號&lt;/span>&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="p">);&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="c1">-- SQLite: no such column: 訂單.金額
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">-- DuckDB: Referenced table &amp;#34;訂單&amp;#34; not found!&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>錯誤訊息說得很直接：&lt;code>訂單&lt;/code> 這張表不在 &lt;code>FROM&lt;/code> 裡面。子查詢是一個獨立的查詢，它算完之後只交出一份值的清單，而訂單表沒有成為外層 &lt;code>FROM&lt;/code> 的一部分——外層因此引用不到它的任何欄位。&lt;/p>
&lt;p>帶欄位出來還有另一條路——把子查詢寫在 &lt;code>SELECT&lt;/code> 裡並讓它引用外層的欄位。引用外層欄位的子查詢叫&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/subquery/" data-link-title="Subquery（子查詢）" data-link-desc="看到括號裡還有一個 SELECT、想知道它什麼時候算、算幾次時">相關子查詢&lt;/a>（correlated subquery）：&lt;/p></description><content:encoded><![CDATA[<p>「找出有對應資料的那些」這類問題有 <code>IN</code>、<code>EXISTS</code> 與 <code>JOIN</code> 三種常見寫法，而它們在很多情況下回同樣的答案。這種巧合讓它們看起來可以互換，而三者描述的是不同的事，回的列數也不一定相同。</p>
<p>分開它們的問題只有一個：<strong>除了判斷有沒有，還要不要用到被比對的那張表的欄位。</strong></p>
<h2 id="同一題用-inexists-與-join-各寫一次">同一題用 IN、EXISTS 與 JOIN 各寫一次</h2>
<p>書店的三位顧客裡只有佳穎下過單，兩張都是她的。要找出下過單的顧客。</p>
<p><code>IN</code> 問的是成員資格——這個顧客編號在不在括號裡那個<a href="/blog/backend/01-database/sql/knowledge-cards/subquery/" data-link-title="Subquery（子查詢）" data-link-desc="看到括號裡還有一個 SELECT、想知道它什麼時候算、算幾次時">子查詢</a>交出的清單裡：</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="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="err">顧客編號</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="k">SELECT</span><span class="w"> </span><span class="err">顧客編號</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="c1">-- 佳穎</span></span></span></code></pre></div><p><code>EXISTS</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="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">2</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">EXISTS</span><span class="w"> </span><span class="p">(</span><span class="k">SELECT</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><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 class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- 佳穎</span></span></span></code></pre></div><p><code>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">SELECT</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">姓名</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</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="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="c1">-- 佳穎、佳穎</span></span></span></code></pre></div><p>同一個「找出下過單的顧客」的問題，<code>IN</code> 與 <code>EXISTS</code> 回一列而 <code>JOIN</code> 回兩列——<strong>佳穎在 <code>JOIN</code> 的結果裡出現了兩次</strong>。</p>
<h2 id="列數條件與連接的差別">列數：條件與連接的差別</h2>
<p><code>IN</code> 與 <code>EXISTS</code> 出現在 <code>WHERE</code> 裡，它們是條件。條件對每一列只做一件事——留下或丟掉——所以顧客表有三列，過完條件最多剩三列，一列都不會多出來。</p>
<p><code>JOIN</code> 出現在 <code>FROM</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>）。加上 <code>DISTINCT</code> 之後結果才與 <code>IN</code>、<code>EXISTS</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="k">DISTINCT</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">姓名</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</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="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="c1">-- 佳穎</span></span></span></code></pre></div><p><strong>需要 <code>DISTINCT</code> 這件事本身是一個訊號</strong>：它表示這個查詢只是要判斷有沒有，而 <code>JOIN</code> 順便做了不需要的配對。這種時候 <code>IN</code> 或 <code>EXISTS</code> 更貼合意圖，因為它們一開始就沒有把列複製出來。</p>
<p><code>IN</code> 與 <code>EXISTS</code> 做的運算在<a href="/blog/backend/01-database/sql/knowledge-cards/relational-algebra/" data-link-title="Relational Algebra（關聯代數）" data-link-desc="看到「子句其實是某個運算」或想知道 SQL 的規則從哪裡來時，查這一層在底下的是什麼">關聯代數</a>裡叫半連接：顧客表與訂單表配對之後只留下顧客表的列，每一列至多出現一次。否定式的 <code>NOT EXISTS</code> 留下顧客表裡配不到任何訂單的列，叫反連接（<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>）。</p>
<h2 id="可取用的欄位被比對的那張表在不在-from-裡">可取用的欄位：被比對的那張表在不在 FROM 裡</h2>
<p>要列出每位顧客的訂單金額，<code>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">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 class="k">FROM</span><span class="w"> </span><span class="err">顧客</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="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="c1">-- 佳穎 300 / 佳穎 500</span></span></span></code></pre></div><p>同一件事用 <code>IN</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="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">金額</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="err">顧客編號</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="k">SELECT</span><span class="w"> </span><span class="err">顧客編號</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="c1">-- SQLite: no such column: 訂單.金額
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">-- DuckDB: Referenced table &#34;訂單&#34; not found!</span></span></span></code></pre></div><p>錯誤訊息說得很直接：<code>訂單</code> 這張表不在 <code>FROM</code> 裡面。子查詢是一個獨立的查詢，它算完之後只交出一份值的清單，而訂單表沒有成為外層 <code>FROM</code> 的一部分——外層因此引用不到它的任何欄位。</p>
<p>帶欄位出來還有另一條路——把子查詢寫在 <code>SELECT</code> 裡並讓它引用外層的欄位。引用外層欄位的子查詢叫<a href="/blog/backend/01-database/sql/knowledge-cards/subquery/" data-link-title="Subquery（子查詢）" data-link-desc="看到括號裡還有一個 SELECT、想知道它什麼時候算、算幾次時">相關子查詢</a>（correlated subquery）：</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="w"> </span><span class="p">(</span><span class="k">SELECT</span><span class="w"> </span><span class="err">金額</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><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></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">              </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="err">訂單編號</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</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">-- 佳穎 300 / 宗翰 NULL / 雅文 NULL</span></span></span></code></pre></div><p>它一次只交得出一個值，所以要多欄或多列時仍然回到 <code>JOIN</code>；而它的執行方式是逐列的，代價怎麼算見 <a href="/blog/backend/01-database/sql/cost-lives-in-the-plan/" data-link-title="SQL.17 查詢的代價：資料分布、索引與寫法各自的影響" data-link-desc="找重複值的分組、自連接與 EXISTS 在有無索引與不同重複次數下的實測時間與計畫，以及比較查詢快慢時要補上的資料量、分布、索引與統計資訊">SQL.17 查詢的代價：資料分布、索引與寫法各自的影響</a>。</p>
<p>這時候佳穎出現兩次是對的，因為問的是每一張訂單的金額，而她確實有兩張。<strong>這一題本來就要每張訂單各佔一列，所以列數變多正是這個寫法要交出來的東西。</strong></p>
<h2 id="選用的判斷標準要不要用到被比對那張表的欄位">選用的判斷標準：要不要用到被比對那張表的欄位</h2>
<p><strong>要不要用到被比對的那張表的欄位。</strong> 要用到就得 <code>JOIN</code>，因為只有它把那張表帶進 <code>FROM</code>。這時候要順帶檢查列數是不是預期內的——後面如果接聚合，<code>sum</code> 與 <code>count</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 連接之後的列數與空缺</a>。</p>
<p>只是判斷有沒有，就用 <code>IN</code> 或 <code>EXISTS</code>。它們是條件，對每一列只做留或丟，寫出來的意圖比 <code>JOIN</code> 加 <code>DISTINCT</code> 清楚——而哪一種比較快是另一個問題，答案取決於資料與索引。<a href="/blog/backend/01-database/sql/cost-lives-in-the-plan/" data-link-title="SQL.17 查詢的代價：資料分布、索引與寫法各自的影響" data-link-desc="找重複值的分組、自連接與 EXISTS 在有無索引與不同重複次數下的實測時間與計畫，以及比較查詢快慢時要補上的資料量、分布、索引與統計資訊">SQL.17 查詢的代價</a> 用一道重複值的題目量分組、自連接與 <code>EXISTS</code> 三種寫法，並示範加一個索引之後排名重排——<code>EXISTS</code> 從最慢掉到與分組同一個量級。</p>
<h2 id="in-與-exists-的分岔子查詢含-null-時的否定式">IN 與 EXISTS 的分岔：子查詢含 NULL 時的否定式</h2>
<p><code>IN</code> 與 <code>EXISTS</code> 在「有沒有」這件事上同值，而在一個地方分岔：<strong>子查詢的結果裡有 NULL 的時候。</strong></p>
<p>清單裡只要混了一個 NULL，<code>NOT IN</code> 就再也不會為真：值在清單裡的列判成明確的假，值不在清單裡的列因為那個 NULL 判成未知，兩種都被 <code>WHERE</code> 丟掉，整段查詢回零列且不報錯；<code>NOT EXISTS</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">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">103</span><span class="p">,</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;2026-03-20&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">100</span><span class="p">);</span><span class="w">   </span><span class="c1">-- 訂單 103 沒有填顧客編號
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></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">SELECT</span><span class="w"> </span><span class="err">姓名</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="err">顧客編號</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="k">SELECT</span><span class="w"> </span><span class="err">顧客編號</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</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><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">-- 多了訂單 103 之後：零列，不報錯（子查詢交出的清單是 1、1、NULL）
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="err">姓名</span><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">8</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">EXISTS</span><span class="w"> </span><span class="p">(</span><span class="k">SELECT</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><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 class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w"></span><span class="c1">-- 共用資料與多了訂單 103 之後都是：宗翰、雅文</span></span></span></code></pre></div><p>完整的推導在 <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>，包括 <code>NOT EXISTS</code> 的安全範圍為什麼限定在等號配對。零列與「確實沒有」在結果上分不開，所以 <code>NOT IN</code> 碰到 NULL 的錯只能靠寫的人預先知道「子查詢有 NULL 時 NOT IN 會失效」那條規則——<a href="/blog/backend/01-database/sql/well-formed-is-not-correct/" data-link-title="SQL.13 查詢的合法性與答案的正確性：引擎檢查的範圍、答案錯掉的成因與查證方法" data-link-desc="合法與問對了分別由誰負責、答案錯掉時引擎為什麼不報錯，以及靜態分析、約束與反例資料各自判得出哪些錯">SQL.13 查詢的合法性與答案的正確性：引擎檢查的範圍、答案錯掉的成因與查證方法</a> 把它歸成<strong>誤解語意模型</strong>：寫的人對某條規則的預期與語意模型的規定不同，而誤解語意模型修得掉，改對之後永遠對。</p>
<p>所以肯定式的 <code>IN</code> 與 <code>EXISTS</code> 可以按可讀性挑，而<strong>否定式一律用 <code>NOT EXISTS</code></strong>——除非能保證子查詢那一欄不會有 NULL，而那個保證要來自<a href="/blog/backend/01-database/sql/knowledge-cards/constraint/" data-link-title="Constraint（約束）" data-link-desc="看到「這個保證要來自約束而不是來自習慣」，或想確認某一欄會不會出現 NULL 時，查這一層由誰擋">約束</a>而不是來自習慣：翻遍現在的資料都沒有 NULL 只證明此刻沒有，而查詢要活得比這一批資料久。</p>
<p>還有一個形態上的差別：本篇 <code>IN</code> 的子查詢與外層無關，可以單獨拿出來執行，叫獨立子查詢；<code>EXISTS</code> 的子查詢引用了外層的欄位，離開外層就跑不動，是相關子查詢。讀陌生的查詢時，看子查詢裡有沒有出現外層的表名，就分得出它是算一次的獨立子查詢，還是對外層每一列各問一次的相關子查詢。</p>
<p>「語意相同就按可讀性挑」這個處置的適用範圍不只 <code>IN</code>、<code>EXISTS</code> 與 <code>JOIN</code> 這三種寫法：哪幾類寫法差異是免費的、哪一類會與效能分岔，在 <a href="/blog/backend/01-database/sql/readable-and-fast-mostly-align/" data-link-title="SQL.21 可讀性與執行代價：兩者一致與分岔的情形" data-link-desc="寫法差異在什麼情形下免費、什麼情形下分岔，以及分岔時該動查詢還是動 schema">SQL.21 可讀性與執行代價：兩者一致與分岔的情形</a>。</p>
]]></content:encoded></item><item><title>Semi-join 與 Anti-join（半連接與反連接）</title><link>https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/semi-join-and-anti-join/</link><pubDate>Wed, 02 Sep 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/semi-join-and-anti-join/</guid><description>&lt;p>半連接把兩個 &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;/p>
&lt;p>SQL 用 &lt;code>EXISTS&lt;/code> 與 &lt;code>NOT EXISTS&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="c1">-- 半連接：下過單的顧客
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">姓名&lt;/span>&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">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">EXISTS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&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">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 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>&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="c1">-- 反連接：從未下單的顧客
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">姓名&lt;/span>&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">7&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="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">EXISTS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&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">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;h2 id="名字裡的半指結果只帶走一邊">名字裡的「半」指結果只帶走一邊&lt;/h2>
&lt;p>三個運算作用在同一對關係上，差別在結果帶走什麼。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>運算&lt;/th>
 &lt;th>結果的欄位&lt;/th>
 &lt;th>結果的列數&lt;/th>
 &lt;th>SQL 寫法&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>連接&lt;/td>
 &lt;td>左右兩邊都有&lt;/td>
 &lt;td>可能超過左邊的列數&lt;/td>
 &lt;td>&lt;code>JOIN ... ON&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>半連接&lt;/td>
 &lt;td>只有左邊的&lt;/td>
 &lt;td>至多左邊的列數&lt;/td>
 &lt;td>&lt;code>EXISTS&lt;/code>、&lt;code>IN&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>反連接&lt;/td>
 &lt;td>只有左邊的&lt;/td>
 &lt;td>至多左邊的列數&lt;/td>
 &lt;td>&lt;code>NOT EXISTS&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>右邊那份資料在這兩個運算裡的角色是條件，它的欄位到不了結果。要把右邊的欄位列出來就得用連接，那條判斷標準在 &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：列數、可取用的欄位與 NULL 的處理&lt;/a>。&lt;/p>
&lt;h2 id="列數上限省掉去重這一步">列數上限省掉去重這一步&lt;/h2>
&lt;p>半連接問的是配不配得到，所以配到幾筆不影響結果——一位顧客手上有兩張訂單或二十張訂單，&lt;code>EXISTS&lt;/code> 都只讓她出現一次。同一個問題用連接寫，她會照訂單張數各出現一次，要補 &lt;code>DISTINCT&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>DISTINCT&lt;/code> 因此是一個訊號：這個查詢問的是有沒有，而連接順便做了它不需要的配對。&lt;/p>
&lt;h2 id="反連接有兩條描述路徑">反連接有兩條描述路徑&lt;/h2>
&lt;p>&lt;code>NOT EXISTS&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;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="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">2&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">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="k">IS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>兩段回同一批列，路徑不同——一條由條件直接表達，另一條靠外連接補出來的 &lt;code>NULL&lt;/code> 當標記。&lt;/p>
&lt;p>&lt;code>NOT IN&lt;/code> 的行為與這兩條分岔：子查詢的結果裡出現一個 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/null/" data-link-title="NULL（空值）" data-link-desc="查 NULL 在比較、計數與分組裡各自怎麼表現時">NULL&lt;/a>，整段就回零列且不報錯。反連接的安全寫法因此落在 &lt;code>NOT EXISTS&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;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;/p>
&lt;p>計畫上看不看得到這兩個運算的名字，各家不同。DuckDB 1.5.5 把它印成 &lt;code>Join Type: SEMI&lt;/code>；SQLite 3.51.0 沒有對應的運算子，它把 &lt;code>EXISTS&lt;/code> 的子查詢編成帶一個 &lt;code>LIMIT 1&lt;/code> 計數器的迴圈，找到第一筆就跳出去——語意相同，而&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/query-plan/" data-link-title="Query Plan（執行計畫）" data-link-desc="想知道一段查詢實際會怎麼被執行、或被要求「先看計畫」時">執行計畫&lt;/a>上只看得到一個子查詢節點。&lt;/p>
&lt;p>它們與&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>分別是連接結果大小的兩個極端。積是條件缺席時的上限，列數是兩邊相乘；半連接把上限壓到左邊的列數，因為右邊配到幾列都只算一次。中間那一段是連接，結果的大小由配對的重複程度決定。&lt;/p></description><content:encoded><![CDATA[<p>半連接把兩個 <a href="/blog/backend/01-database/sql/knowledge-cards/relation/" data-link-title="Relation（關係）" data-link-desc="看到 SQL 教材說「組成一個關係」而不確定它指的是不是資料表時">relation</a> 配起來之後只留下左邊那一份：結果的欄位全部來自左邊，而左邊的每一列至多出現一次。留下的是在右邊找得到對象的那些列。反連接留下的是另一半——左邊那些在右邊配不到任何對象的列，結果的欄位同樣只有左邊的。</p>
<p>SQL 用 <code>EXISTS</code> 與 <code>NOT EXISTS</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">-- 半連接：下過單的顧客
</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="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">WHERE</span><span class="w"> </span><span class="k">EXISTS</span><span class="w"> </span><span class="p">(</span><span class="k">SELECT</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><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 class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- 反連接：從未下單的顧客
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="err">姓名</span><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">7</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">EXISTS</span><span class="w"> </span><span class="p">(</span><span class="k">SELECT</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><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><h2 id="名字裡的半指結果只帶走一邊">名字裡的「半」指結果只帶走一邊</h2>
<p>三個運算作用在同一對關係上，差別在結果帶走什麼。</p>
<table>
  <thead>
      <tr>
          <th>運算</th>
          <th>結果的欄位</th>
          <th>結果的列數</th>
          <th>SQL 寫法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>連接</td>
          <td>左右兩邊都有</td>
          <td>可能超過左邊的列數</td>
          <td><code>JOIN ... ON</code></td>
      </tr>
      <tr>
          <td>半連接</td>
          <td>只有左邊的</td>
          <td>至多左邊的列數</td>
          <td><code>EXISTS</code>、<code>IN</code></td>
      </tr>
      <tr>
          <td>反連接</td>
          <td>只有左邊的</td>
          <td>至多左邊的列數</td>
          <td><code>NOT EXISTS</code></td>
      </tr>
  </tbody>
</table>
<p>右邊那份資料在這兩個運算裡的角色是條件，它的欄位到不了結果。要把右邊的欄位列出來就得用連接，那條判斷標準在 <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：列數、可取用的欄位與 NULL 的處理</a>。</p>
<h2 id="列數上限省掉去重這一步">列數上限省掉去重這一步</h2>
<p>半連接問的是配不配得到，所以配到幾筆不影響結果——一位顧客手上有兩張訂單或二十張訂單，<code>EXISTS</code> 都只讓她出現一次。同一個問題用連接寫，她會照訂單張數各出現一次，要補 <code>DISTINCT</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>DISTINCT</code> 因此是一個訊號：這個查詢問的是有沒有，而連接順便做了它不需要的配對。</p>
<h2 id="反連接有兩條描述路徑">反連接有兩條描述路徑</h2>
<p><code>NOT EXISTS</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>





<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 class="k">FROM</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">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></span></code></pre></div><p>兩段回同一批列，路徑不同——一條由條件直接表達，另一條靠外連接補出來的 <code>NULL</code> 當標記。</p>
<p><code>NOT IN</code> 的行為與這兩條分岔：子查詢的結果裡出現一個 <a href="/blog/backend/01-database/sql/knowledge-cards/null/" data-link-title="NULL（空值）" data-link-desc="查 NULL 在比較、計數與分組裡各自怎麼表現時">NULL</a>，整段就回零列且不報錯。反連接的安全寫法因此落在 <code>NOT EXISTS</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>
<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>的正典運算清單因此多半只列到連接，而「有沒有對應資料」這一族問題全部落在這兩個運算上——找出下過單的顧客、找出從未下單的顧客、找出比前一天熱的日子，題目與寫法各異而運算只有這兩個。</p>
<p>計畫上看不看得到這兩個運算的名字，各家不同。DuckDB 1.5.5 把它印成 <code>Join Type: SEMI</code>；SQLite 3.51.0 沒有對應的運算子，它把 <code>EXISTS</code> 的子查詢編成帶一個 <code>LIMIT 1</code> 計數器的迴圈，找到第一筆就跳出去——語意相同，而<a href="/blog/backend/01-database/sql/knowledge-cards/query-plan/" data-link-title="Query Plan（執行計畫）" data-link-desc="想知道一段查詢實際會怎麼被執行、或被要求「先看計畫」時">執行計畫</a>上只看得到一個子查詢節點。</p>
<p>它們與<a href="/blog/backend/01-database/sql/knowledge-cards/cartesian-product/" data-link-title="Cartesian Product（笛卡兒積）" data-link-desc="看到 CROSS JOIN、或結果的列數遠多於預期時，查這個運算什麼時候會被展開">積</a>分別是連接結果大小的兩個極端。積是條件缺席時的上限，列數是兩邊相乘；半連接把上限壓到左邊的列數，因為右邊配到幾列都只算一次。中間那一段是連接，結果的大小由配對的重複程度決定。</p>
<h2 id="往下走">往下走</h2>
<p><code>IN</code>、<code>EXISTS</code> 與 <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：列數、可取用的欄位與 NULL 的處理</a>。從一句業務描述推到反連接、以及為什麼計數那條路走不通，在 <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>。同一張表的兩列互相比較時這兩個運算長什麼樣，在 <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/cost-lives-in-the-plan/" data-link-title="SQL.17 查詢的代價：資料分布、索引與寫法各自的影響" data-link-desc="找重複值的分組、自連接與 EXISTS 在有無索引與不同重複次數下的實測時間與計畫，以及比較查詢快慢時要補上的資料量、分布、索引與統計資訊">SQL.17 查詢的代價</a>。</p>
]]></content:encoded></item></channel></rss>