<?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>Having on Tarragon</title><link>https://tarrragon.github.io/blog/tags/having/</link><description>Recent content in Having 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/having/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></channel></rss>