<?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>Subquery on Tarragon</title><link>https://tarrragon.github.io/blog/tags/subquery/</link><description>Recent content in Subquery on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 31 Aug 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/subquery/index.xml" rel="self" type="application/rss+xml"/><item><title>Subquery（子查詢）</title><link>https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/subquery/</link><pubDate>Mon, 31 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/subquery/</guid><description>&lt;p>Subquery 是寫在另一個查詢裡面的查詢。它分兩種，而分法只看一件事：&lt;strong>它有沒有引用外層的欄位。&lt;/strong> 這個差別決定它算幾次，也決定了它的代價——完整的量測在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/query-plan/" data-link-title="Query Plan（執行計畫）" data-link-desc="想知道一段查詢實際會怎麼被執行、或被要求「先看計畫」時">query plan&lt;/a> 那一層。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>&lt;strong>獨立子查詢&lt;/strong>與外層無關，可以整段剪下來單獨執行。它算一次，把結果交給外層。&lt;code>WHERE 顧客編號 IN (SELECT 顧客編號 FROM 訂單)&lt;/code> 裡的括號就是這一種——訂單表那一份清單先算好，外層再拿它比對。&lt;/p>
&lt;p>&lt;strong>相關子查詢&lt;/strong>引用了外層的欄位，離開外層就跑不動。它對外層的每一列各執行一次。&lt;code>WHERE EXISTS (SELECT 1 FROM 訂單 WHERE 訂單.顧客編號 = 顧客.顧客編號)&lt;/code> 是這一種——括號裡的 &lt;code>顧客.顧客編號&lt;/code> 來自外層，所以每一位顧客各問一次。&lt;/p>
&lt;p>讀陌生查詢時這是一個可以直接看的訊號：&lt;strong>括號裡出現外層的表名，它就是逐列被問一次的&lt;/strong>。兩種子查詢交出的都是值而不是一個 &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;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>子查詢交出的是值，不是表。&lt;code>SELECT 姓名, 訂單.金額 FROM 顧客 WHERE 顧客編號 IN (SELECT ... FROM 訂單)&lt;/code> 會回 &lt;code>no such column: 訂單.金額&lt;/code>——訂單這張表沒有進到外層的 &lt;code>FROM&lt;/code> 裡。&lt;/p>
&lt;p>要帶欄位出來有另一條路：把相關子查詢寫在 &lt;code>SELECT&lt;/code> 裡，&lt;code>SELECT 姓名, (SELECT 金額 FROM 訂單 WHERE 訂單.顧客編號 = 顧客.顧客編號 ORDER BY 訂單編號 LIMIT 1) FROM 顧客&lt;/code>。它一次只交得出一個值，所以要多欄或多列時仍然回到連接。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>相關子查詢的逐列執行在沒有索引時代價很高——外層每一列都讓內層掃一次全表。有索引之後每一次從掃全表變成定位，同一段查詢可以差幾百倍。&lt;/p>
&lt;p>獨立子查詢有另一個陷阱：它的結果裡出現任何一個 NULL，外層的 &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></description><content:encoded><![CDATA[<p>Subquery 是寫在另一個查詢裡面的查詢。它分兩種，而分法只看一件事：<strong>它有沒有引用外層的欄位。</strong> 這個差別決定它算幾次，也決定了它的代價——完整的量測在 <a href="/blog/backend/01-database/sql/knowledge-cards/query-plan/" data-link-title="Query Plan（執行計畫）" data-link-desc="想知道一段查詢實際會怎麼被執行、或被要求「先看計畫」時">query plan</a> 那一層。</p>
<h2 id="概念位置">概念位置</h2>
<p><strong>獨立子查詢</strong>與外層無關，可以整段剪下來單獨執行。它算一次，把結果交給外層。<code>WHERE 顧客編號 IN (SELECT 顧客編號 FROM 訂單)</code> 裡的括號就是這一種——訂單表那一份清單先算好，外層再拿它比對。</p>
<p><strong>相關子查詢</strong>引用了外層的欄位，離開外層就跑不動。它對外層的每一列各執行一次。<code>WHERE EXISTS (SELECT 1 FROM 訂單 WHERE 訂單.顧客編號 = 顧客.顧客編號)</code> 是這一種——括號裡的 <code>顧客.顧客編號</code> 來自外層，所以每一位顧客各問一次。</p>
<p>讀陌生查詢時這是一個可以直接看的訊號：<strong>括號裡出現外層的表名，它就是逐列被問一次的</strong>。兩種子查詢交出的都是值而不是一個 <a href="/blog/backend/01-database/sql/knowledge-cards/relation/" data-link-title="Relation（關係）" data-link-desc="看到 SQL 教材說「組成一個關係」而不確定它指的是不是資料表時">relation</a>，這一點決定了下一節那個錯誤訊息。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>子查詢交出的是值，不是表。<code>SELECT 姓名, 訂單.金額 FROM 顧客 WHERE 顧客編號 IN (SELECT ... FROM 訂單)</code> 會回 <code>no such column: 訂單.金額</code>——訂單這張表沒有進到外層的 <code>FROM</code> 裡。</p>
<p>要帶欄位出來有另一條路：把相關子查詢寫在 <code>SELECT</code> 裡，<code>SELECT 姓名, (SELECT 金額 FROM 訂單 WHERE 訂單.顧客編號 = 顧客.顧客編號 ORDER BY 訂單編號 LIMIT 1) FROM 顧客</code>。它一次只交得出一個值，所以要多欄或多列時仍然回到連接。</p>
<h2 id="設計責任">設計責任</h2>
<p>相關子查詢的逐列執行在沒有索引時代價很高——外層每一列都讓內層掃一次全表。有索引之後每一次從掃全表變成定位，同一段查詢可以差幾百倍。</p>
<p>獨立子查詢有另一個陷阱：它的結果裡出現任何一個 NULL，外層的 <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>
]]></content:encoded></item><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></channel></rss>