<?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>Alias on Tarragon</title><link>https://tarrragon.github.io/blog/tags/alias/</link><description>Recent content in Alias 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/alias/index.xml" rel="self" type="application/rss+xml"/><item><title>SQL.2 子句的求值順序：每一步可用的欄位、語意模型擋下的寫法與各家引擎放寬的寫法</title><link>https://tarrragon.github.io/blog/backend/01-database/sql/clause-evaluation-order/</link><pubDate>Mon, 31 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/sql/clause-evaluation-order/</guid><description>&lt;p>求值順序是語意模型規定的先後：&lt;code>FROM&lt;/code> 決定要處理哪個資料集合、&lt;code>WHERE&lt;/code> 篩掉列、&lt;code>GROUP BY&lt;/code> 分組、&lt;code>HAVING&lt;/code> 篩掉組、&lt;code>SELECT&lt;/code> 算出要輸出的欄位、&lt;code>ORDER BY&lt;/code> 排序、&lt;code>LIMIT&lt;/code> 取前面幾列。它與書寫順序不同，寫在最前面的 &lt;code>SELECT&lt;/code> 在這裡排在倒數第二。&lt;/p>
&lt;p>求值順序規定的是&lt;strong>每一步能用到哪些欄位與值&lt;/strong>——而不在這一步能用的欄位與值裡的，那一步就用不了，引擎因此會擋下某些寫法。擋下來的理由分兩種，分法看換一家引擎結果會不會變：這一步確實拿不到那個值的，本篇叫&lt;strong>模型層的限制&lt;/strong>，換哪一家引擎都一樣；照模型拿不到、而有的引擎替使用者補上一個決定的，叫&lt;strong>引擎寬鬆&lt;/strong>，換一家就變。在擋下它的那一家引擎上，模型層的限制與引擎寬鬆的錯誤訊息長得一樣。引擎實際照什麼次序做事是另一種順序（執行順序），由最佳化器決定而不在本篇範圍，&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/declarative-not-procedural/" data-link-title="SQL.1 宣告式的紅利與代價：書寫、求值與執行順序各自由誰決定" data-link-desc="SQL 寫結果而不寫算法帶來的後果：同一個問題換書寫順序之後在有無統計資訊時拿到的計畫、求值順序擋下的寫法，以及代價為什麼要向引擎問">SQL.1 宣告式的紅利與代價&lt;/a> 把書寫、求值、執行三種順序分開。&lt;/p>
&lt;h2 id="求值順序各步驟可用的欄位與值">求值順序各步驟可用的欄位與值&lt;/h2>
&lt;p>&lt;code>FROM&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;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;code>ON&lt;/code> 引用得到哪些欄位。&lt;/p>
&lt;p>&lt;code>GROUP BY&lt;/code> 之後中間結果的單位從列換成組，這是整個順序裡變化最大的一步。原本每一列各自獨立，之後每一組是一個單位，而組裡那些列的個別值不再直接可用。&lt;/p>
&lt;p>&lt;code>HAVING&lt;/code> 篩掉的是組，所以它能用&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/aggregate-function/" data-link-title="Aggregate Function（聚合函數）" data-link-desc="想知道為什麼聚合只能寫在某幾個位置、或連接之後的總和算出來不對時">聚合函數&lt;/a>——每一組的計數、總和、最大值到這一步都算得出來了。&lt;code>SELECT&lt;/code> 到這裡才把要輸出的欄位算出來，別名也是在這一步才誕生。&lt;code>ORDER BY&lt;/code> 排的是已經算好的輸出，所以它用得到別名。&lt;code>LIMIT&lt;/code> 在最後，它切的是排序完的結果——&lt;strong>沒有 &lt;code>ORDER BY&lt;/code> 的 &lt;code>LIMIT&lt;/code> 取到哪一列沒有保證&lt;/strong>，因為前面沒有任何一步決定過順序。&lt;/p>
&lt;h2 id="模型層的限制把聚合放進-where">模型層的限制：把聚合放進 WHERE&lt;/h2>
&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="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="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&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">1&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">GROUP&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &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>SQLite 回 &lt;code>misuse of aggregate: COUNT()&lt;/code>，DuckDB 回 &lt;code>WHERE clause cannot contain aggregates&lt;/code>。同一個問題移到 &lt;code>HAVING&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="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&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">GROUP&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">HAVING&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&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">1&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">-- 兩個引擎都回 (1, 2)：顧客編號 1 的佳穎，兩張訂單&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>差別在分組發生了沒有。&lt;code>WHERE&lt;/code> 那一步處理的還是一列一列的訂單，「這一組有幾張」這個值在那個時刻還不存在，所以這是模型層的限制而不是功能缺口。這個限制與引擎的寬鬆度（引擎收下不合標準的寫法、自己補上一個決定）無關；寬鬆度在各家怎麼分、差異在什麼位置發聲，在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/engine-leniency-and-portability/" data-link-title="SQL.19 引擎寬鬆度與可攜性：各家對同一組寫法的差異、分級與處理時機" data-link-desc="SQLite、DuckDB、PostgreSQL 與 MySQL 在未分組欄位、WHERE 用別名、同表未取別名、字串比對與空值排序上的實測對照，差異從當場報錯到完全靜默的分級，以及依目標組態數決定哪幾級差異在寫的當下處理">SQL.19 引擎寬鬆度與可攜性：各家對同一組寫法的差異、分級與處理時機&lt;/a>。&lt;/p>
&lt;h2 id="引擎寬鬆where-用-select-的別名">引擎寬鬆：WHERE 用 SELECT 的別名&lt;/h2>
&lt;p>按求值順序，&lt;code>WHERE&lt;/code> 早於 &lt;code>SELECT&lt;/code>，別名那時候還沒誕生。標準照這個推論禁止，PostgreSQL 也照這個推論拒絕。&lt;/p>
&lt;p>而 SQLite 與 DuckDB 都接受。書店查佳穎這一列，把姓名欄改名之後在 &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="w"> &lt;/span>&lt;span class="k">AS&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="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;佳穎&amp;#39;&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 與 DuckDB 都回 (&amp;#39;佳穎&amp;#39;,)；PostgreSQL 報 column &amp;#34;客戶&amp;#34; does not exist&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>這個放寬的範圍不是照求值順序推出來的。&lt;/strong> &lt;code>GROUP BY&lt;/code> 與 &lt;code>HAVING&lt;/code> 同樣早於 &lt;code>SELECT&lt;/code>，而 SQLite 與 DuckDB 在這兩個位置接受別名：&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="n">substr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">下單日&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="mi">7&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&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="k">count&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&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">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">GROUP&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&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="c1">-- GROUP BY 引用的是 SELECT 那一步才取的別名
&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">-- SQLite：2026-03 | 2&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>所以「這一步還沒算到別名」只推得出候選，推不出答案：各家對哪幾個位置放寬、放寬到什麼程度，要各自查。&lt;/p>
&lt;p>而放寬是疊在語意模型上面的，沒有取代它。可以從一個細節看出疊的位置：來源表本來就有同名欄位的時候，&lt;code>WHERE&lt;/code> 綁的是來源欄位而不是別名。實測用一張只有 &lt;code>x&lt;/code> 與 &lt;code>y&lt;/code> 兩欄的表，把 &lt;code>y&lt;/code> 取名成 &lt;code>x&lt;/code> 之後在 &lt;code>WHERE&lt;/code> 寫 &lt;code>x = 1&lt;/code>：&lt;/p></description><content:encoded><![CDATA[<p>求值順序是語意模型規定的先後：<code>FROM</code> 決定要處理哪個資料集合、<code>WHERE</code> 篩掉列、<code>GROUP BY</code> 分組、<code>HAVING</code> 篩掉組、<code>SELECT</code> 算出要輸出的欄位、<code>ORDER BY</code> 排序、<code>LIMIT</code> 取前面幾列。它與書寫順序不同，寫在最前面的 <code>SELECT</code> 在這裡排在倒數第二。</p>
<p>求值順序規定的是<strong>每一步能用到哪些欄位與值</strong>——而不在這一步能用的欄位與值裡的，那一步就用不了，引擎因此會擋下某些寫法。擋下來的理由分兩種，分法看換一家引擎結果會不會變：這一步確實拿不到那個值的，本篇叫<strong>模型層的限制</strong>，換哪一家引擎都一樣；照模型拿不到、而有的引擎替使用者補上一個決定的，叫<strong>引擎寬鬆</strong>，換一家就變。在擋下它的那一家引擎上，模型層的限制與引擎寬鬆的錯誤訊息長得一樣。引擎實際照什麼次序做事是另一種順序（執行順序），由最佳化器決定而不在本篇範圍，<a href="/blog/backend/01-database/sql/declarative-not-procedural/" data-link-title="SQL.1 宣告式的紅利與代價：書寫、求值與執行順序各自由誰決定" data-link-desc="SQL 寫結果而不寫算法帶來的後果：同一個問題換書寫順序之後在有無統計資訊時拿到的計畫、求值順序擋下的寫法，以及代價為什麼要向引擎問">SQL.1 宣告式的紅利與代價</a> 把書寫、求值、執行三種順序分開。</p>
<h2 id="求值順序各步驟可用的欄位與值">求值順序各步驟可用的欄位與值</h2>
<p><code>FROM</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> 當成一個交出關係的整體；多張表的時候它自己是一個過程，按書寫位置逐個把表併進來，<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> 寫這個累積怎麼決定每個 <code>ON</code> 引用得到哪些欄位。</p>
<p><code>GROUP BY</code> 之後中間結果的單位從列換成組，這是整個順序裡變化最大的一步。原本每一列各自獨立，之後每一組是一個單位，而組裡那些列的個別值不再直接可用。</p>
<p><code>HAVING</code> 篩掉的是組，所以它能用<a href="/blog/backend/01-database/sql/knowledge-cards/aggregate-function/" data-link-title="Aggregate Function（聚合函數）" data-link-desc="想知道為什麼聚合只能寫在某幾個位置、或連接之後的總和算出來不對時">聚合函數</a>——每一組的計數、總和、最大值到這一步都算得出來了。<code>SELECT</code> 到這裡才把要輸出的欄位算出來，別名也是在這一步才誕生。<code>ORDER BY</code> 排的是已經算好的輸出，所以它用得到別名。<code>LIMIT</code> 在最後，它切的是排序完的結果——<strong>沒有 <code>ORDER BY</code> 的 <code>LIMIT</code> 取到哪一列沒有保證</strong>，因為前面沒有任何一步決定過順序。</p>
<h2 id="模型層的限制把聚合放進-where">模型層的限制：把聚合放進 WHERE</h2>
<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="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="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="err">顧客編號</span><span class="p">;</span></span></span></code></pre></div><p>SQLite 回 <code>misuse of aggregate: COUNT()</code>，DuckDB 回 <code>WHERE clause cannot contain aggregates</code>。同一個問題移到 <code>HAVING</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="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">AS</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">GROUP</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">HAVING</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">1</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">-- 兩個引擎都回 (1, 2)：顧客編號 1 的佳穎，兩張訂單</span></span></span></code></pre></div><p>差別在分組發生了沒有。<code>WHERE</code> 那一步處理的還是一列一列的訂單，「這一組有幾張」這個值在那個時刻還不存在，所以這是模型層的限制而不是功能缺口。這個限制與引擎的寬鬆度（引擎收下不合標準的寫法、自己補上一個決定）無關；寬鬆度在各家怎麼分、差異在什麼位置發聲，在 <a href="/blog/backend/01-database/sql/engine-leniency-and-portability/" data-link-title="SQL.19 引擎寬鬆度與可攜性：各家對同一組寫法的差異、分級與處理時機" data-link-desc="SQLite、DuckDB、PostgreSQL 與 MySQL 在未分組欄位、WHERE 用別名、同表未取別名、字串比對與空值排序上的實測對照，差異從當場報錯到完全靜默的分級，以及依目標組態數決定哪幾級差異在寫的當下處理">SQL.19 引擎寬鬆度與可攜性：各家對同一組寫法的差異、分級與處理時機</a>。</p>
<h2 id="引擎寬鬆where-用-select-的別名">引擎寬鬆：WHERE 用 SELECT 的別名</h2>
<p>按求值順序，<code>WHERE</code> 早於 <code>SELECT</code>，別名那時候還沒誕生。標準照這個推論禁止，PostgreSQL 也照這個推論拒絕。</p>
<p>而 SQLite 與 DuckDB 都接受。書店查佳穎這一列，把姓名欄改名之後在 <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="w"> </span><span class="k">AS</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="o">=</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">2</span><span class="cl"><span class="w"></span><span class="c1">-- SQLite 與 DuckDB 都回 (&#39;佳穎&#39;,)；PostgreSQL 報 column &#34;客戶&#34; does not exist</span></span></span></code></pre></div><p><strong>這個放寬的範圍不是照求值順序推出來的。</strong> <code>GROUP BY</code> 與 <code>HAVING</code> 同樣早於 <code>SELECT</code>，而 SQLite 與 DuckDB 在這兩個位置接受別名：</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="n">substr</span><span class="p">(</span><span class="err">下單日</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">7</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="err">月份</span><span class="p">,</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">AS</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">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="err">月份</span><span class="p">;</span><span class="w">   </span><span class="c1">-- GROUP BY 引用的是 SELECT 那一步才取的別名
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">-- SQLite：2026-03 | 2</span></span></span></code></pre></div><p>所以「這一步還沒算到別名」只推得出候選，推不出答案：各家對哪幾個位置放寬、放寬到什麼程度，要各自查。</p>
<p>而放寬是疊在語意模型上面的，沒有取代它。可以從一個細節看出疊的位置：來源表本來就有同名欄位的時候，<code>WHERE</code> 綁的是來源欄位而不是別名。實測用一張只有 <code>x</code> 與 <code>y</code> 兩欄的表，把 <code>y</code> 取名成 <code>x</code> 之後在 <code>WHERE</code> 寫 <code>x = 1</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">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="p">(</span><span class="n">x</span><span class="w"> </span><span class="nb">INT</span><span class="p">,</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="nb">INT</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="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">),</span><span class="w"> </span><span class="p">(</span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">1</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></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">x</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- 2</span></span></span></code></pre></div><p><code>WHERE</code> 綁的若是別名，比對的是 <code>y = 1</code>，會回第二列的 <code>1</code>；實際回的是 <code>2</code>，代表 SQLite 拿來源欄位 <code>x</code> 去比對，DuckDB 也是這樣。<strong>來源表沒有同名欄位的時候，引擎才會讓別名接手這個名字。</strong></p>
<h2 id="列的條件放進-havingduckdbmysql-與-sqlite-各自的反應">列的條件放進 HAVING：DuckDB、MySQL 與 SQLite 各自的反應</h2>
<p>把聚合放進 <code>WHERE</code> 這種放錯邊，每一家都會擋下來。反過來把列的條件放進 <code>HAVING</code>，後果分兩家，而其中一家不報錯。</p>
<p>這一節要多幾則評價才看得出來，所以換掉共用資料裡那一則，並多加一張訂單 103——換完之後訂單 101 有五星與三星各一則，訂單 102 有兩則五星，訂單 103 有一則三星：</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="mi">2</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">400</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="k">DELETE</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="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">1</span><span class="p">,</span><span class="mi">101</span><span class="p">,</span><span class="mi">5</span><span class="p">),(</span><span class="mi">2</span><span class="p">,</span><span class="mi">101</span><span class="p">,</span><span class="mi">3</span><span class="p">),(</span><span class="mi">3</span><span class="p">,</span><span class="mi">102</span><span class="p">,</span><span class="mi">5</span><span class="p">),(</span><span class="mi">4</span><span class="p">,</span><span class="mi">102</span><span class="p">,</span><span class="mi">5</span><span class="p">),(</span><span class="mi">5</span><span class="p">,</span><span class="mi">103</span><span class="p">,</span><span class="mi">3</span><span class="p">);</span></span></span></code></pre></div><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="w"> </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</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="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">GROUP</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">HAVING</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="mi">2</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">-- 102 | 2</span></span></span></code></pre></div><p>只有 102，因為 101 的兩則評價裡只有一則是五星。走一遍就看得到為什麼：<code>WHERE</code> 先砍掉三星那兩列，剩下三列拿去分組，組 101 只剩一列而組 102 有兩列，<code>HAVING</code> 再篩掉組 101。</p>
<p>把星等的條件搬進 <code>HAVING</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="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</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">GROUP</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">HAVING</span><span class="w"> </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="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="mi">2</span><span class="p">;</span></span></span></code></pre></div><p>DuckDB 拒絕，訊息是 <code>column 星等 must appear in the GROUP BY clause</code>——分組之後組裡有兩個不同的星等，「這一組的星等」沒有定義。MySQL 8 也拒絕，而它擋在更前面的一步：</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">ERROR 1054 (42S22): Unknown column &#39;星等&#39; in &#39;having clause&#39;</span></span></code></pre></div><p>MySQL 的 <code>HAVING</code> 只認得分組鍵、聚合，以及 <code>SELECT</code> 裡取好的別名，而 <code>星等</code> 三樣都不是，所以它在解析名字那一步就停了。這一種擋法與設定無關——把 <code>sql_mode</code> 清空再跑，訊息一個字都沒變。</p>
<p>管寬鬆度的那個設定叫 <code>ONLY_FULL_GROUP_BY</code>，從 MySQL 5.7 起是預設值，而它管的是另一個形態：把非分組欄放進 <code>SELECT</code> 的寫法（<code>SELECT 訂單編號, 星等 FROM 評價 GROUP BY 訂單編號</code>）開著時回 <code>ERROR 1055</code> 並在訊息裡點名這個設定，關掉之後就與 SQLite 一樣從組裡任意挑一列。</p>
<p><strong>SQLite 接受，回 101 與 102。</strong> 兩張訂單都被報成「有兩則以上五星」，而 101 只有一則。SQLite 從組裡取了其中一列的星等拿去比對，而它取到的剛好是 101 那組裡五星的那一列，於是整組通過。</p>
<p>至於它會取到哪一列，多數時候規格不保證。唯一的例外是同一個 <code>SELECT</code> 裡只有一個 <code>min()</code> 或 <code>max()</code> 的時候——那時 SQLite 保證非分組欄取自那個極值所在的列，這一點有明文規定。除此之外不要依賴它挑中哪一列。</p>
<p><strong>SQLite 收下並給出錯的清單，比 DuckDB 與 MySQL 當場擋下危害更高，因為輸出看起來完全合理。</strong> 每組一列、欄位齊全、訂單編號都是實際存在的訂單——沒有任何一處會讓人停下來檢查。放錯邊的那個條件在報錯的引擎上寫的當下就會發現，在寬鬆的引擎上會一路帶著錯的清單走下去。</p>
<p>同一個機制還有一個比較溫和的形態：<code>SELECT 顧客編號, 下單日 FROM 訂單 GROUP BY 顧客編號</code>，DuckDB 同樣拒絕、SQLite 回一個從組裡任意挑的日期。非分組欄放進 <code>SELECT</code> 只讓輸出的值不確定，列的條件放進 <code>HAVING</code> 讓整份清單錯掉。</p>
<h2 id="模型層的限制與引擎寬鬆分辨的依據與換引擎時的表現">模型層的限制與引擎寬鬆：分辨的依據與換引擎時的表現</h2>
<p>分開模型層的限制與引擎寬鬆的，是引擎手上有沒有一個值可以補上。分組還沒發生的時候，「這一組有幾張」這個值本身還沒有被算出來，引擎沒有值可補，所以聚合放進 <code>WHERE</code> 是模型層的限制。<code>SELECT</code> 那一步還沒算的時候，別名這個名字還沒誕生，而它要指的值早就算好了——差的只是一個稱呼，有些引擎因此願意替使用者把名字對回去。分組之後「這一組的星等」在模型裡沒有定義，而組裡每一列都有自己的星等，SQLite 從中挑一列的值補上。別名放進 <code>WHERE</code> 與列的條件放進 <code>HAVING</code> 都是引擎寬鬆：模型沒給的決定，由引擎替使用者補上。</p>
<p>模型層的限制與引擎寬鬆在換引擎時的表現不同。碰到模型層的限制的寫法，第一次執行就會被擋下來；靠引擎寬鬆跑起來的寫法會安靜跑過，等到換引擎才出現差異——而其中最麻煩的是從組裡任意挑一列的寬鬆：在兩家都寬鬆的引擎之間搬家（SQLite，以及關掉 <code>ONLY_FULL_GROUP_BY</code> 的 MySQL），連報錯都沒有，只是挑到的那一列可能不同。</p>
<h2 id="求值順序在連接上的實例條件放-on-還是-where">求值順序在連接上的實例：條件放 ON 還是 WHERE</h2>
<p><code>HAVING</code> 那一節已經露出一個問題：同一個條件擺在哪一步，決定的是留下哪些列，而寬鬆的引擎不會替擺錯的那一種報錯。這個問題在連接上重演一次。<code>ON</code> 是 <code>FROM</code> 那一步內部的條件，<code>WHERE</code> 是下一步，所以條件放 <code>ON</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> 用同一組書店資料把兩種擺法的結果並排，並給一個代進 <code>NULL</code> 就看得出差別的測試。兩種擺法的差別由語意模型規定，SQLite、PostgreSQL 與 MySQL 對同一種擺法回同一批列，所以條件擺錯位置的時候，換一家引擎也不會浮現。在 <a href="/blog/backend/01-database/sql/well-formed-is-not-correct/" data-link-title="SQL.13 查詢的合法性與答案的正確性：引擎檢查的範圍、答案錯掉的成因與查證方法" data-link-desc="合法與問對了分別由誰負責、答案錯掉時引擎為什麼不報錯，以及靜態分析、約束與反例資料各自判得出哪些錯">SQL.13 查詢的合法性與答案的正確性：引擎檢查的範圍、答案錯掉的成因與查證方法</a> 按斷在哪一個環節分的成因裡，它屬於誤解語意模型：寫的人預期 <code>WHERE</code> 裡的條件只影響配對，而模型規定它篩的是配對之後的結果。換一家引擎就浮現的是引擎寬鬆，<code>HAVING</code> 那一節 SQLite 收下的列條件是一例。</p>
]]></content:encoded></item><item><title>SQL.7 表的出現與別名：表名的指稱、別名的作用範圍與自連接</title><link>https://tarrragon.github.io/blog/backend/01-database/sql/table-occurrence-and-alias/</link><pubDate>Mon, 31 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/sql/table-occurrence-and-alias/</guid><description>&lt;p>&lt;code>FROM&lt;/code> 裡寫下的表名指的是那張表在這個查詢裡的一次出現，而那個名字是這次出現的稱呼。標準把這件事叫 table reference，關聯演算（relational calculus）裡的傳統名稱是 range variable，各家文件裡也常寫成 correlation name——三個名字說的是同一件事。同一張表因此可以出現兩次、各自扮演不同角色——這就是自連接。&lt;/p>
&lt;p>一張表只出現一次的時候，這個區別沒有作用。出現兩次就有：兩次出現共用同一個名字，引擎分不出某個欄位屬於哪一次，於是擋下來。&lt;/p>
&lt;p>引擎把這件事講得很明白，而擋下來的時機分兩種。PostgreSQL 與 DuckDB 看到重複的名字就停，訊息分別是 &lt;code>table name &amp;quot;訂單&amp;quot; specified more than once&lt;/code> 與 &lt;code>Duplicate alias &amp;quot;訂單&amp;quot; in query!&lt;/code>；SQLite 讓查詢往下走，等到某個欄位引用無從分辨時才回 &lt;code>ambiguous column name&lt;/code>。&lt;strong>表名本身就是預設的別名&lt;/strong>，所以同一張表的兩次出現共用同一個名字時，等於兩次出現叫同一個稱呼，之後每一次引用都無從分辨。&lt;/p>
&lt;h2 id="兩張表共用欄名時的欄位歸屬">兩張表共用欄名時的欄位歸屬&lt;/h2>
&lt;p>欄位的歸屬要靠名字決定，所以兩張不同的表共用一個欄名時同樣分不開。書店的顧客表與訂單表都有 &lt;code>顧客編號&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 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="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">-- MySQL: ERROR 1052: Column &amp;#39;顧客編號&amp;#39; in field list is ambiguous
&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">-- SQLite: ambiguous column name: 顧客編號
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">-- DuckDB: Ambiguous reference to column name &amp;#34;顧客編號&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>id&lt;/code> 是這件事最常撞到的欄位名，因為多數表都有一個。&lt;strong>連接之後每個欄位引用都要能追回它屬於哪一次出現&lt;/strong>，加上表名或別名當前綴就解決。&lt;/p>
&lt;p>自連接只是這個規則的極端情形：兩次出現連表名都一樣，所以連前綴都救不了，非取別名不可。&lt;/p>
&lt;p>表一多，要判讀的就變成每個 &lt;code>ON&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="用別名分開同一張表的兩次出現">用別名分開同一張表的兩次出現&lt;/h2>
&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">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="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="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">AS&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">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">AS&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">&amp;lt;&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 class="c1">-- 101 | 2026-03-02 | 102 | 2026-03-09&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>早&lt;/code> 與 &lt;code>晚&lt;/code> 是同一張表的兩次出現。連接條件在做兩件事：&lt;code>顧客編號&lt;/code> 相同把配對限制在同一個人身上，&lt;code>下單日&lt;/code> 的大小關係決定哪一次出現扮演「早」、哪一次扮演「晚」。&lt;/p>
&lt;p>&lt;strong>條件裡的第二項是這個寫法的重點。&lt;/strong> 少了它，每張訂單會跟同一位顧客的每張訂單配對一次，包括跟自己配。加上嚴格的大小比較之後，每一對只留下一個方向，而同一張訂單跟它自己組成的那一對，因為兩邊日期相等也被排除。同一位顧客有三張訂單時，每兩張各配一次，不多不少：&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="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"> 3&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">AS&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"> 4&lt;/span>&lt;span class="cl">&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">AS&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"> 5&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 共用資料：101|101、101|102、102|101、102|102（兩個方向各一次，加上各自跟自己配）
&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="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="c1">-- 日期的比較放回去
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&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="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"> 9&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">AS&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">10&lt;/span>&lt;span class="cl">&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">AS&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">&amp;lt;&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">11&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;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1">-- 佳穎多一張 103（下單日 2026-03-15）之後：101|102、101|103、102|103&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="自連接的適用問題前後配對同組比較與層級關係">自連接的適用問題：前後配對、同組比較與層級關係&lt;/h2>
&lt;p>自連接處理的是&lt;strong>同一批資料裡兩筆資料之間的關係&lt;/strong>——兩列，不是兩個欄位。一列一列的處理看不到這種關係，因為每一列在被判斷的時候不知道其他列長什麼樣子。&lt;/p>
&lt;p>自連接常見的形態有三種。&lt;strong>前後配對&lt;/strong>：同一位顧客的訂單先後配對，把時間上相鄰或有先後關係的兩列擺在一起。&lt;strong>同組比較&lt;/strong>：找出訂單金額高於同一位顧客其他訂單的那些，比較的對象是同一張表裡的兄弟列。&lt;strong>層級關係&lt;/strong>：一張員工表裡每個人有一個主管欄位指向同一張表的另一列，要把人與他的主管一起列出來時，員工表出現兩次，一次當員工、一次當主管。&lt;/p>
&lt;p>前後配對、同組比較與層級關係三種的共同點是&lt;strong>要比較的兩列住在同一張表裡&lt;/strong>。需不需要自連接由兩個獨立的問題決定：這個問題要不要拿一列去跟同一張表的另一列比——不比的問題不屬於這一族；以及要不要把另一列的欄位輸出出來——這個問題決定族裡選哪一種寫法。&lt;/p>
&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;code>JOIN&lt;/code>，所以它同樣適用。只是判斷「有沒有另一列符合條件」而不輸出它，用 &lt;code>EXISTS&lt;/code> 比自連接省一次配對展開；要把兩列並排列出來（像上面的訂單先後配對），才非自連接不可。&lt;/p></description><content:encoded><![CDATA[<p><code>FROM</code> 裡寫下的表名指的是那張表在這個查詢裡的一次出現，而那個名字是這次出現的稱呼。標準把這件事叫 table reference，關聯演算（relational calculus）裡的傳統名稱是 range variable，各家文件裡也常寫成 correlation name——三個名字說的是同一件事。同一張表因此可以出現兩次、各自扮演不同角色——這就是自連接。</p>
<p>一張表只出現一次的時候，這個區別沒有作用。出現兩次就有：兩次出現共用同一個名字，引擎分不出某個欄位屬於哪一次，於是擋下來。</p>
<p>引擎把這件事講得很明白，而擋下來的時機分兩種。PostgreSQL 與 DuckDB 看到重複的名字就停，訊息分別是 <code>table name &quot;訂單&quot; specified more than once</code> 與 <code>Duplicate alias &quot;訂單&quot; in query!</code>；SQLite 讓查詢往下走，等到某個欄位引用無從分辨時才回 <code>ambiguous column name</code>。<strong>表名本身就是預設的別名</strong>，所以同一張表的兩次出現共用同一個名字時，等於兩次出現叫同一個稱呼，之後每一次引用都無從分辨。</p>
<h2 id="兩張表共用欄名時的欄位歸屬">兩張表共用欄名時的欄位歸屬</h2>
<p>欄位的歸屬要靠名字決定，所以兩張不同的表共用一個欄名時同樣分不開。書店的顧客表與訂單表都有 <code>顧客編號</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 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="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">-- MySQL:  ERROR 1052: Column &#39;顧客編號&#39; in field list is ambiguous
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">-- SQLite: ambiguous column name: 顧客編號
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">-- DuckDB: Ambiguous reference to column name &#34;顧客編號&#34;</span></span></span></code></pre></div><p><code>id</code> 是這件事最常撞到的欄位名，因為多數表都有一個。<strong>連接之後每個欄位引用都要能追回它屬於哪一次出現</strong>，加上表名或別名當前綴就解決。</p>
<p>自連接只是這個規則的極端情形：兩次出現連表名都一樣，所以連前綴都救不了，非取別名不可。</p>
<p>表一多，要判讀的就變成每個 <code>ON</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="用別名分開同一張表的兩次出現">用別名分開同一張表的兩次出現</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="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="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">AS</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">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">AS</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;</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 class="c1">-- 101 | 2026-03-02 | 102 | 2026-03-09</span></span></span></code></pre></div><p><code>早</code> 與 <code>晚</code> 是同一張表的兩次出現。連接條件在做兩件事：<code>顧客編號</code> 相同把配對限制在同一個人身上，<code>下單日</code> 的大小關係決定哪一次出現扮演「早」、哪一次扮演「晚」。</p>
<p><strong>條件裡的第二項是這個寫法的重點。</strong> 少了它，每張訂單會跟同一位顧客的每張訂單配對一次，包括跟自己配。加上嚴格的大小比較之後，每一對只留下一個方向，而同一張訂單跟它自己組成的那一對，因為兩邊日期相等也被排除。同一位顧客有三張訂單時，每兩張各配一次，不多不少：</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="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 class="k">AS</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">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">AS</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"> 5</span><span class="cl"><span class="w"></span><span class="c1">-- 共用資料：101|101、101|102、102|101、102|102（兩個方向各一次，加上各自跟自己配）
</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="c1">-- 日期的比較放回去
</span></span></span><span class="line"><span class="ln"> 8</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="w">
</span></span></span><span class="line"><span class="ln"> 9</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">AS</span><span class="w"> </span><span class="err">早</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="k">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">AS</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;</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">11</span><span class="cl"><span class="w"></span><span class="c1">-- 共用資料：101|102
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1">-- 佳穎多一張 103（下單日 2026-03-15）之後：101|102、101|103、102|103</span></span></span></code></pre></div><h2 id="自連接的適用問題前後配對同組比較與層級關係">自連接的適用問題：前後配對、同組比較與層級關係</h2>
<p>自連接處理的是<strong>同一批資料裡兩筆資料之間的關係</strong>——兩列，不是兩個欄位。一列一列的處理看不到這種關係，因為每一列在被判斷的時候不知道其他列長什麼樣子。</p>
<p>自連接常見的形態有三種。<strong>前後配對</strong>：同一位顧客的訂單先後配對，把時間上相鄰或有先後關係的兩列擺在一起。<strong>同組比較</strong>：找出訂單金額高於同一位顧客其他訂單的那些，比較的對象是同一張表裡的兄弟列。<strong>層級關係</strong>：一張員工表裡每個人有一個主管欄位指向同一張表的另一列，要把人與他的主管一起列出來時，員工表出現兩次，一次當員工、一次當主管。</p>
<p>前後配對、同組比較與層級關係三種的共同點是<strong>要比較的兩列住在同一張表裡</strong>。需不需要自連接由兩個獨立的問題決定：這個問題要不要拿一列去跟同一張表的另一列比——不比的問題不屬於這一族；以及要不要把另一列的欄位輸出出來——這個問題決定族裡選哪一種寫法。</p>
<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> 那條判斷標準——自連接也是一種 <code>JOIN</code>，所以它同樣適用。只是判斷「有沒有另一列符合條件」而不輸出它，用 <code>EXISTS</code> 比自連接省一次配對展開；要把兩列並排列出來（像上面的訂單先後配對），才非自連接不可。</p>
<p><strong>省下的量由配對會不會膨脹決定，所以這條判斷標準要連著連接鍵唯不唯一一起看。</strong> 連接鍵在對面那一次出現裡唯一的時候，一列只配得到一列，兩種寫法產出的列數相同，<code>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> 寫的列數膨脹。判斷的動作是問這個連接鍵在對面那一次出現裡唯不唯一，而唯一與否要由<a href="/blog/backend/01-database/sql/knowledge-cards/constraint/" data-link-title="Constraint（約束）" data-link-desc="看到「這個保證要來自約束而不是來自習慣」，或想確認某一欄會不會出現 NULL 時，查這一層由誰擋">約束</a>回答，不由現在這批資料回答。</p>
<p><strong>同組比較最常見的題型是每組最大值，而它套用「要不要輸出另一列的欄位」這條判斷標準時，答案是不輸出。</strong> 「找出每位顧客金額最高的訂單」輸出的是那張訂單自己的欄位，同顧客的其他訂單只拿來比大小，所以寫成 <code>NOT EXISTS</code> 就夠。前後配對的「找出每筆訂單與同顧客下一筆的差額」則要用到另一列的金額，那就非自連接不可。</p>
<p>每組最大值的條件是比大小，而比大小自己會帶出兩個邊界：<strong>並列的最大值</strong>與<strong>可為空的比較欄</strong>。<code>NOT EXISTS</code> 會把並列的那幾筆全部留下，要只留一筆得另外加一個決勝的條件，通常取編號最小的那筆。<code>NULL</code> 跟誰比都判不成真，所以「同顧客裡沒有比它大的」對金額是 <code>NULL</code> 的那一張恆成立，它會被選成最大；要排除它，<code>IS NOT NULL</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>





<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="n">o</span><span class="p">.</span><span class="err">訂單編號</span><span class="p">,</span><span class="w"> </span><span class="n">o</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 class="n">o</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">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="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </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="n">i</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="n">i</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">i</span><span class="p">.</span><span class="err">金額</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="n">o</span><span class="p">.</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="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="c1">-- 共用資料：102 | 500
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1">-- 佳穎多一張 103（金額 500）之後：102 | 500、103 | 500（並列的兩張都留下）
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">-- 佳穎多一張 104（金額 NULL）之後：102 | 500、104 | NULL（NULL 那一張被選成最大）
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="c1">-- 決勝的條件：金額相同時，編號較小的那一張也算「比它大」
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="err">訂單編號</span><span class="p">,</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="err">金額</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">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="n">o</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</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="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">  </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="n">i</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">  </span><span class="k">WHERE</span><span class="w"> </span><span class="n">i</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="err">顧客編號</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="k">AND</span><span class="w"> </span><span class="p">(</span><span class="n">i</span><span class="p">.</span><span class="err">金額</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="err">金額</span><span class="w"> </span><span class="k">OR</span><span class="w"> </span><span class="p">(</span><span class="n">i</span><span class="p">.</span><span class="err">金額</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="err">金額</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">i</span><span class="p">.</span><span class="err">訂單編號</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="n">o</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">19</span><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w"></span><span class="c1">-- 佳穎多一張 103（金額 500）之後：102 | 500
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w"></span><span class="c1">-- 排除金額是 NULL 的那一張：條件寫在外層
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="err">訂單編號</span><span class="p">,</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="err">金額</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</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="n">o</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">o</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">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">  </span><span class="k">AND</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="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">    </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="n">i</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">    </span><span class="k">WHERE</span><span class="w"> </span><span class="n">i</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">i</span><span class="p">.</span><span class="err">金額</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="err">金額</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">  </span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w"></span><span class="c1">-- 佳穎多一張 104（金額 NULL）之後：102 | 500
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="c1">-- 同一句改寫進子查詢（AND i.金額 IS NOT NULL）：仍回 102 | 500、104 | NULL</span></span></span></code></pre></div><h2 id="表只出現一次時別名的作用">表只出現一次時別名的作用</h2>
<p>同一張表只出現一次的時候，別名的作用是縮短欄位引用。長表名在有三四張表的查詢裡會讓每個欄位引用都變得很長，取一個短別名之後可讀性差很多。</p>
<p>而作用不只是省字。取了別名之後原本的表名在這個查詢裡就不再是它的稱呼，三個引擎都會擋下來：PostgreSQL 回 <code>invalid reference to FROM-clause entry for table &quot;訂單&quot;</code> 並附上一句提示要改用別名，SQLite 回 <code>no such column</code>，DuckDB 回 <code>Referenced table &quot;訂單&quot; not found</code>。<code>FROM 訂單 AS 早</code> 宣告的是「這次出現叫早」，而宣告過後就只有這一個稱呼。</p>
<h2 id="別名的取名標示這次出現扮演的角色">別名的取名：標示這次出現扮演的角色</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="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 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">FROM</span><span class="w"> </span><span class="err">訂單</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="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="err">早</span><span class="p">.</span><span class="err">下單日</span><span class="p">;</span></span></span></code></pre></div><p>把兩個別名換成 <code>a</code> 與 <code>b</code>，查詢的行為一模一樣，而 <code>b.金額 - a.金額</code> 這一行要回頭看 <code>ON</code> 才知道哪一個是後來那張單。判斷標準落在這裡：<strong>讀到用它的那一行時，方向自己說得出來。</strong> 名字取得對的時候，<code>晚.金額 - 早.金額</code> 讀起來就是它的意思。</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="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="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="err">晚</span><span class="p">.</span><span class="err">下單日</span></span></span></code></pre></div><p>叫「早」的那一次出現拿到的是後來那張單。查詢照跑，結果與上面那一段相同，而每一個名字都指反了。別名因此也是一種<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 與表的別名各自對讀的人宣告了什麼、宣告落空時下一個修改的人付出什麼代價，以及把關鍵字換成最弱的版本再跑一次的查證動作">宣告</a>——它說明這次出現代表什麼，兌現與否由寫的人負責。</p>
<h2 id="表的別名與欄位別名各自可用的時機">表的別名與欄位別名各自可用的時機</h2>
<p><code>SELECT</code> 裡的 <code>AS</code> 替算出來的欄位取名，<code>FROM</code> 裡的 <code>AS</code> 替表的一次出現取名。兩者的作用範圍不同，可用的時機也不同。</p>
<p>表的別名在 <code>FROM</code> 那一步就成立，所以 <code>WHERE</code>、<code>GROUP BY</code>、<code>ON</code> 全部用得到它。欄位別名到 <code>SELECT</code> 那一步才誕生，所以 <code>WHERE</code> 照語意模型用不到。兩個 <code>AS</code> 長得一樣而落在求值順序的兩端。</p>
<p>某些引擎讓 <code>WHERE</code> 用得到欄位別名，那是引擎的放寬，不是模型的規定。<a href="/blog/backend/01-database/sql/clause-evaluation-order/" data-link-title="SQL.2 子句的求值順序：每一步可用的欄位、語意模型擋下的寫法與各家引擎放寬的寫法" data-link-desc="從 FROM 到 LIMIT 每一步可用的欄位與值、聚合放進 WHERE、別名用在 WHERE 與列的條件放進 HAVING 在各家引擎上的結果，以及這幾種寫法換引擎時的表現">SQL.2 子句的求值順序</a> 走過每一步能用到哪些欄位與值，並把模型層擋得住的限制與各家引擎自己放寬的分開——表的別名不在它的範圍裡，因為別名在 <code>FROM</code> 那一步就成立了。</p>
]]></content:encoded></item></channel></rss>