<?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>Group-By on Tarragon</title><link>https://tarrragon.github.io/blog/tags/group-by/</link><description>Recent content in Group-By on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 01 Sep 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/group-by/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>Aggregate Function（聚合函數）</title><link>https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/aggregate-function/</link><pubDate>Mon, 31 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/aggregate-function/</guid><description>&lt;p>Aggregate function 把一組列收成一個值：&lt;code>count&lt;/code> 數幾列、&lt;code>sum&lt;/code> 加總、&lt;code>avg&lt;/code> 取平均、&lt;code>max&lt;/code> 與 &lt;code>min&lt;/code> 取極值。它的輸入是一組列，輸出是一個值——這個形狀決定了它能寫在哪裡，也決定了它在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/outer-join/" data-link-title="Outer Join（外連接）" data-link-desc="想確認 LEFT / RIGHT / FULL 各自保護哪一側、或連接之後有些人從名單上消失時">outer join&lt;/a> 之後為什麼容易算錯。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>&lt;strong>「一組」是由 &lt;code>GROUP BY&lt;/code> 的鍵決定的，所以聚合要等到分組發生之後才有意義。&lt;/strong> 這解釋了一個看起來像限制的行為：聚合寫進 &lt;code>WHERE&lt;/code> 會被拒絕，因為那一步分組還沒發生，沒有組就沒有組的計數。同一件事寫進 &lt;code>HAVING&lt;/code> 通過，因為那一步分組已經完成。&lt;/p>
&lt;p>沒有 &lt;code>GROUP BY&lt;/code> 的時候整張表算一組，所以 &lt;code>SELECT count(*) 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;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>&lt;code>count(*)&lt;/code> 與 &lt;code>count(欄位)&lt;/code> 數的不是同一件事：前者數列，後者只數那一欄有值的列。書店的兩張訂單接上評價之後有三列，而其中一列的星等是空的，於是 &lt;code>count(*)&lt;/code> 回 3、&lt;code>count(評價.星等)&lt;/code> 回 2。差額就是空缺的數量（見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/null/" data-link-title="NULL（空值）" data-link-desc="查 NULL 在比較、計數與分組裡各自怎麼表現時">NULL&lt;/a>）。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>&lt;strong>連接之後做聚合，算的是連接結果的列，不是來源表的列。&lt;/strong> 一列配到多列就被複製一次，所以 &lt;code>count&lt;/code> 會多、&lt;code>sum&lt;/code> 會重複加。&lt;/p>
&lt;p>處理方式依聚合的性質分兩類。&lt;strong>可加的&lt;/strong>（&lt;code>count&lt;/code>、&lt;code>sum&lt;/code>）可以用 &lt;code>count(DISTINCT 鍵)&lt;/code> 把單位拉回來，或先把展開的那一層各自彙總成一列再接上去。&lt;strong>不可加的&lt;/strong>（&lt;code>avg&lt;/code>、中位數、百分位）兩條路都不通——先彙總再平均會變成平均的平均，權重錯了。這一類要把分子與分母一起帶出來（子查詢裡同時算 &lt;code>sum&lt;/code> 與 &lt;code>count&lt;/code>，接上來之後再相除）。&lt;/p>
&lt;p>&lt;code>max&lt;/code> 與 &lt;code>min&lt;/code> 是例外：它們對重複不敏感，複製幾次結果都一樣。&lt;/p></description><content:encoded><![CDATA[<p>Aggregate function 把一組列收成一個值：<code>count</code> 數幾列、<code>sum</code> 加總、<code>avg</code> 取平均、<code>max</code> 與 <code>min</code> 取極值。它的輸入是一組列，輸出是一個值——這個形狀決定了它能寫在哪裡，也決定了它在 <a href="/blog/backend/01-database/sql/knowledge-cards/outer-join/" data-link-title="Outer Join（外連接）" data-link-desc="想確認 LEFT / RIGHT / FULL 各自保護哪一側、或連接之後有些人從名單上消失時">outer join</a> 之後為什麼容易算錯。</p>
<h2 id="概念位置">概念位置</h2>
<p><strong>「一組」是由 <code>GROUP BY</code> 的鍵決定的，所以聚合要等到分組發生之後才有意義。</strong> 這解釋了一個看起來像限制的行為：聚合寫進 <code>WHERE</code> 會被拒絕，因為那一步分組還沒發生，沒有組就沒有組的計數。同一件事寫進 <code>HAVING</code> 通過，因為那一步分組已經完成。</p>
<p>沒有 <code>GROUP BY</code> 的時候整張表算一組，所以 <code>SELECT count(*) FROM 訂單</code> 回一列。分組之後結果的單位從列換成組，而每一組仍然是一個 <a href="/blog/backend/01-database/sql/knowledge-cards/relation/" data-link-title="Relation（關係）" data-link-desc="看到 SQL 教材說「組成一個關係」而不確定它指的是不是資料表時">relation</a> 的子集。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p><code>count(*)</code> 與 <code>count(欄位)</code> 數的不是同一件事：前者數列，後者只數那一欄有值的列。書店的兩張訂單接上評價之後有三列，而其中一列的星等是空的，於是 <code>count(*)</code> 回 3、<code>count(評價.星等)</code> 回 2。差額就是空缺的數量（見 <a href="/blog/backend/01-database/sql/knowledge-cards/null/" data-link-title="NULL（空值）" data-link-desc="查 NULL 在比較、計數與分組裡各自怎麼表現時">NULL</a>）。</p>
<h2 id="設計責任">設計責任</h2>
<p><strong>連接之後做聚合，算的是連接結果的列，不是來源表的列。</strong> 一列配到多列就被複製一次，所以 <code>count</code> 會多、<code>sum</code> 會重複加。</p>
<p>處理方式依聚合的性質分兩類。<strong>可加的</strong>（<code>count</code>、<code>sum</code>）可以用 <code>count(DISTINCT 鍵)</code> 把單位拉回來，或先把展開的那一層各自彙總成一列再接上去。<strong>不可加的</strong>（<code>avg</code>、中位數、百分位）兩條路都不通——先彙總再平均會變成平均的平均，權重錯了。這一類要把分子與分母一起帶出來（子查詢裡同時算 <code>sum</code> 與 <code>count</code>，接上來之後再相除）。</p>
<p><code>max</code> 與 <code>min</code> 是例外：它們對重複不敏感，複製幾次結果都一樣。</p>
]]></content:encoded></item><item><title>SQL.9 分組鍵的選擇：鍵與一組所代表的對象</title><link>https://tarrragon.github.io/blog/backend/01-database/sql/grouping-key-decides-the-unit/</link><pubDate>Mon, 31 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/sql/grouping-key-decides-the-unit/</guid><description>&lt;p>&lt;code>GROUP BY&lt;/code> 把鍵的值相同的列收成一組，所以鍵決定了&lt;strong>一組代表什麼&lt;/strong>，而後續的 &lt;code>HAVING&lt;/code> 與 &lt;code>SELECT&lt;/code> 全部是對那個對象提問。&lt;/p>
&lt;p>選錯鍵的時候查詢照樣跑得動，只是每一組代表的對象與寫查詢的人預期的不同。&lt;/p>
&lt;h2 id="外連接之後用右表的欄位當分組鍵">外連接之後用右表的欄位當分組鍵&lt;/h2>
&lt;p>書店的三位顧客裡只有佳穎下過單，兩張都是她的。要找出一張單都沒下的顧客，用 &lt;code>LEFT JOIN&lt;/code> 接起來之後按訂單那一側的顧客編號分組：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">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="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="k">count&lt;/span>&lt;span class="p">(&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="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="n">group_concat&lt;/span>&lt;span class="p">(&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="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="c1">-- group_concat 把一組裡的名字串起來
&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">&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="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">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="err">顧客編號&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>MySQL、SQLite 與 DuckDB 的答案一致：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">分組鍵 這組幾列 有值幾列 裝了誰
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">NULL 2 0 宗翰,雅文
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">1 2 2 佳穎,佳穎&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>宗翰與雅文被併進同一組。&lt;/strong> 他們沒有配到訂單，外連接給他們的 &lt;code>訂單.顧客編號&lt;/code> 都是 &lt;code>NULL&lt;/code>，而 &lt;code>GROUP BY&lt;/code> 把值相同的列收成一組——&lt;code>GROUP BY&lt;/code> 對 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/null/" data-link-title="NULL（空值）" data-link-desc="查 NULL 在比較、計數與分組裡各自怎麼表現時">NULL&lt;/a> 的處理與比較運算不同：鍵同樣是 &lt;code>NULL&lt;/code> 的那些列會被收進同一組，所有沒配到的列因此共用同一個鍵。分組是少數 &lt;code>NULL&lt;/code> 表現得像一般值的地方，它在比較與 &lt;code>NOT IN&lt;/code> 上的表現剛好相反，那兩處的表現寫在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/join-changes-rows-and-nulls/" data-link-title="SQL.6 連接之後的列數與空缺：列數膨脹、外連接補的 NULL 與三值邏輯" data-link-desc="連接讓聚合多算的機制與計數、加總、平均、中位數各自的修法，以及外連接補出來的 NULL 在 count、等號比較、NOT IN 與 NOT EXISTS 上的行為">SQL.6 連接之後的列數與空缺&lt;/a>；這些 &lt;code>NULL&lt;/code> 是連接補上的、不是資料裡本來的空值，兩種來源的分別在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/outer-join/" data-link-title="Outer Join（外連接）" data-link-desc="想確認 LEFT / RIGHT / FULL 各自保護哪一側、或連接之後有些人從名單上消失時">Outer Join（外連接）&lt;/a>。&lt;/p>
&lt;p>接著用 &lt;code>HAVING count(訂單.顧客編號) = 0&lt;/code> 挑出沒下過單的那一組，這個條件本身是對的：它確實選中了那一組，因為 &lt;code>count(欄位)&lt;/code> 只數有值的列而那一組一列都沒有。&lt;strong>選中的是一組，而這一組底下是宗翰與雅文兩列。&lt;/strong>&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="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">LEFT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">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="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="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="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>MySQL 8 預設拒絕：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">ERROR 1055 (42000): Expression #1 of SELECT list is not in GROUP BY clause and contains
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">nonaggregated column ... incompatible with sql_mode=only_full_group_by&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>DuckDB 同樣拒絕。SQLite 接受，從分組鍵是 &lt;code>NULL&lt;/code> 的那一組裡任意挑一列回傳——只回宗翰，&lt;strong>雅文消失了，而且沒有任何錯誤訊息&lt;/strong>。&lt;/p>
&lt;p>而拒絕的 MySQL 與 DuckDB 擋下的是輸出非分組欄這個寫法，不是選錯的鍵：把姓名補進 &lt;code>GROUP BY&lt;/code> 之後三家都照跑，在共用資料上答案也是對的，因為三位顧客不同名。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">姓名&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">LEFT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">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="err">顧客編號&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">姓名&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- 鍵仍是訂單表的顧客編號，只補上姓名
&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">&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="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="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&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">-- 共用資料：宗翰、雅文
&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="k">INSERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">,&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 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>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="c1">-- 沒下過單的顧客有三位而結果只有兩列：兩位宗翰的鍵都是 (NULL, 宗翰)，被併成一組&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>鍵錯在它讓不同的顧客共用一個值，而這件事要等資料裡出現同名的人才看得到。選錯鍵的查詢在語法上完全合法，&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/well-formed-is-not-correct/" data-link-title="SQL.13 查詢的合法性與答案的正確性：引擎檢查的範圍、答案錯掉的成因與查證方法" data-link-desc="合法與問對了分別由誰負責、答案錯掉時引擎為什麼不報錯，以及靜態分析、約束與反例資料各自判得出哪些錯">SQL.13 查詢的合法性與答案的正確性：引擎檢查的範圍、答案錯掉的成因與查證方法&lt;/a> 把它歸進問錯了問題的那一類。&lt;/p></description><content:encoded><![CDATA[<p><code>GROUP BY</code> 把鍵的值相同的列收成一組，所以鍵決定了<strong>一組代表什麼</strong>，而後續的 <code>HAVING</code> 與 <code>SELECT</code> 全部是對那個對象提問。</p>
<p>選錯鍵的時候查詢照樣跑得動，只是每一組代表的對象與寫查詢的人預期的不同。</p>
<h2 id="外連接之後用右表的欄位當分組鍵">外連接之後用右表的欄位當分組鍵</h2>
<p>書店的三位顧客裡只有佳穎下過單，兩張都是她的。要找出一張單都沒下的顧客，用 <code>LEFT JOIN</code> 接起來之後按訂單那一側的顧客編號分組：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="k">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="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">count</span><span class="p">(</span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</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="n">group_concat</span><span class="p">(</span><span class="err">顧客</span><span class="p">.</span><span class="err">姓名</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="c1">-- group_concat 把一組裡的名字串起來
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">顧客編號</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</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="p">.</span><span class="err">顧客編號</span><span class="p">;</span></span></span></code></pre></div><p>MySQL、SQLite 與 DuckDB 的答案一致：</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">分組鍵  這組幾列  有值幾列  裝了誰
</span></span><span class="line"><span class="ln">2</span><span class="cl">NULL    2         0         宗翰,雅文
</span></span><span class="line"><span class="ln">3</span><span class="cl">1       2         2         佳穎,佳穎</span></span></code></pre></div><p><strong>宗翰與雅文被併進同一組。</strong> 他們沒有配到訂單，外連接給他們的 <code>訂單.顧客編號</code> 都是 <code>NULL</code>，而 <code>GROUP BY</code> 把值相同的列收成一組——<code>GROUP BY</code> 對 <a href="/blog/backend/01-database/sql/knowledge-cards/null/" data-link-title="NULL（空值）" data-link-desc="查 NULL 在比較、計數與分組裡各自怎麼表現時">NULL</a> 的處理與比較運算不同：鍵同樣是 <code>NULL</code> 的那些列會被收進同一組，所有沒配到的列因此共用同一個鍵。分組是少數 <code>NULL</code> 表現得像一般值的地方，它在比較與 <code>NOT IN</code> 上的表現剛好相反，那兩處的表現寫在 <a href="/blog/backend/01-database/sql/join-changes-rows-and-nulls/" data-link-title="SQL.6 連接之後的列數與空缺：列數膨脹、外連接補的 NULL 與三值邏輯" data-link-desc="連接讓聚合多算的機制與計數、加總、平均、中位數各自的修法，以及外連接補出來的 NULL 在 count、等號比較、NOT IN 與 NOT EXISTS 上的行為">SQL.6 連接之後的列數與空缺</a>；這些 <code>NULL</code> 是連接補上的、不是資料裡本來的空值，兩種來源的分別在 <a href="/blog/backend/01-database/sql/knowledge-cards/outer-join/" data-link-title="Outer Join（外連接）" data-link-desc="想確認 LEFT / RIGHT / FULL 各自保護哪一側、或連接之後有些人從名單上消失時">Outer Join（外連接）</a>。</p>
<p>接著用 <code>HAVING count(訂單.顧客編號) = 0</code> 挑出沒下過單的那一組，這個條件本身是對的：它確實選中了那一組，因為 <code>count(欄位)</code> 只數有值的列而那一組一列都沒有。<strong>選中的是一組，而這一組底下是宗翰與雅文兩列。</strong></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="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">顧客編號</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">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="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="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="p">)</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span></span></span></code></pre></div><p>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 1055 (42000): Expression #1 of SELECT list is not in GROUP BY clause and contains
</span></span><span class="line"><span class="ln">2</span><span class="cl">nonaggregated column ... incompatible with sql_mode=only_full_group_by</span></span></code></pre></div><p>DuckDB 同樣拒絕。SQLite 接受，從分組鍵是 <code>NULL</code> 的那一組裡任意挑一列回傳——只回宗翰，<strong>雅文消失了，而且沒有任何錯誤訊息</strong>。</p>
<p>而拒絕的 MySQL 與 DuckDB 擋下的是輸出非分組欄這個寫法，不是選錯的鍵：把姓名補進 <code>GROUP BY</code> 之後三家都照跑，在共用資料上答案也是對的，因為三位顧客不同名。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">姓名</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">顧客編號</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">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="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="c1">-- 鍵仍是訂單表的顧客編號，只補上姓名
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="k">HAVING</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="p">)</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</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">-- 共用資料：宗翰、雅文
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="k">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">4</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;宗翰&#39;</span><span class="p">);</span><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></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1">-- 沒下過單的顧客有三位而結果只有兩列：兩位宗翰的鍵都是 (NULL, 宗翰)，被併成一組</span></span></span></code></pre></div><p>鍵錯在它讓不同的顧客共用一個值，而這件事要等資料裡出現同名的人才看得到。選錯鍵的查詢在語法上完全合法，<a href="/blog/backend/01-database/sql/well-formed-is-not-correct/" data-link-title="SQL.13 查詢的合法性與答案的正確性：引擎檢查的範圍、答案錯掉的成因與查證方法" data-link-desc="合法與問對了分別由誰負責、答案錯掉時引擎為什麼不報錯，以及靜態分析、約束與反例資料各自判得出哪些錯">SQL.13 查詢的合法性與答案的正確性：引擎檢查的範圍、答案錯掉的成因與查證方法</a> 把它歸進問錯了問題的那一類。</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="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">顧客編號</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">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="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">4</span><span class="cl"><span class="w"></span><span class="k">HAVING</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="p">)</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</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">-- 宗翰、雅文</span></span></span></code></pre></div><p>現在三家引擎都給出兩列。分組鍵換掉之後，沒有任何一組的鍵是 <code>NULL</code> 了——宗翰與雅文各自有不同的顧客編號，所以各自成組，而每一組的訂單計數都是零。</p>
<p><strong>判斷方法：先說出「我要每一組代表什麼」，鍵就是那個對象的識別欄位。</strong> 說不出來的時候，多半是這一題根本不需要分組。</p>
<p>鍵還要涵蓋一件事：<strong><code>SELECT</code> 裡沒有被聚合的欄位全部要進 <code>GROUP BY</code></strong>。上面補的 <code>姓名</code> 就是為此——少了它 MySQL 與 DuckDB 會拒絕（正是上一節那個錯誤）。</p>
<p><strong>而補進去的欄位必須由鍵決定——鍵的每一個值只對到這個欄位的一個值，否則分組的粒度會跟著變。</strong> 這種關係在資料庫理論裡叫函數相依（functional dependency），MySQL 的 <code>ERROR 1055</code> 完整訊息寫的 <code>not functionally dependent on columns in GROUP BY clause</code> 指的就是它。一個顧客編號只對到一個姓名，所以補姓名不影響分組；要求的方向是從鍵到欄位，反方向不必成立——兩位宗翰就是同一個姓名對到兩個顧客編號，而按顧客編號分組時他們仍然各自成組。下單日不由顧客編號決定：把它補進 <code>GROUP BY</code>，同一位顧客的每一個下單日各自成組，計數從「這位顧客有幾張單」變成「這位顧客在這一天下了幾張單」，而引擎不會報錯：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">姓名</span><span class="p">,</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">下單日</span><span class="p">,</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</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></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">顧客編號</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">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="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></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 佳穎 | 2026-03-02 | 1
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">-- 佳穎 | 2026-03-09 | 1      佳穎被拆成兩組
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">-- 宗翰 |            | 0
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1">-- 雅文 |            | 0
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1">-- 拿掉下單日、只按顧客編號與姓名分組時：佳穎 | 2、宗翰 | 0、雅文 | 0</span></span></span></code></pre></div><p>所以不由鍵決定的欄位不能靠補進鍵來輸出，要用聚合包起來（<code>max(下單日)</code> 這種），讓它回到「這一組的某個值」而不是「這一組的分法」。</p>
<p>鍵只決定一組由誰構成；算完之後原本的列還留不留，是分組與視窗函數分開的地方，<a href="/blog/backend/01-database/sql/window-keeps-rows-grouping-collapses/" data-link-title="SQL.10 分組與視窗函數：各自的產出、選用的依據與 LAG、LEAD 的相鄰列" data-link-desc="GROUP BY 與視窗函數算完之後原本的列還在不在、依結果的一列代表什麼選用哪一個、視窗函數的值怎麼拿來當篩選條件，以及 LAG 取相鄰列時分區、排序鍵與距離條件各自要確認的事">SQL.10 分組與視窗函數：各自的產出、選用的依據與 LAG、LEAD 的相鄰列</a> 用同一批訂單並排兩種產出。</p>
<h2 id="不需要分組的問題存在性判斷">不需要分組的問題：存在性判斷</h2>
<p>把分組鍵換成顧客編號、再補上姓名的那個修正讓查詢回出正確答案，而更前面還有一個問題沒被問：這一題需要分組嗎。</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="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 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></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="c1">-- (1, 2)</span></span></span></code></pre></div><p>只有佳穎。宗翰與雅文不在結果裡，因為訂單表沒有他們的列，所以沒有一組可以代表他們，也就沒有計數可以是零。</p>
<p><strong>那個零是外連接造出來的。</strong> <code>LEFT JOIN</code> 替他們各生一列全是 <code>NULL</code> 的假訂單，<code>count(欄位)</code> 數那一列得到零。所以「計數等於零」是查詢自己製造出來再讀回去的產物，而不是資料本身帶著的性質。</p>
<p>計數預設有列可以數，而這一題的對象恰好是什麼都沒有的那些人。<strong>這就是分組那條路從一開始就對不上的來源</strong>，比鍵選錯更上一層。</p>
<p>翻成 SQL 最貼字面的形式是存在性判斷：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">姓名</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">EXISTS</span><span class="w"> </span><span class="p">(</span><span class="k">SELECT</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">顧客編號</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- 宗翰、雅文</span></span></span></code></pre></div><p>一句話對一句話——「找不到對應訂單的顧客」。沒有分組、沒有計數。同一件事也寫得成外連接，兩者都對：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">姓名</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">顧客編號</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">訂單編號</span><span class="w"> </span><span class="k">IS</span><span class="w"> </span><span class="k">NULL</span><span class="p">;</span><span class="w">   </span><span class="c1">-- 訂單編號只有在外連接補出來的列上是 NULL
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">-- 宗翰、雅文</span></span></span></code></pre></div><p>差別在描述的路徑——<code>NOT EXISTS</code> 直接說「找不到」，外連接那一版先讓每位顧客都保留下來、再挑出補了 <code>NULL</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> 從條件該放哪一邊的角度走同一題，兩篇的結論一致。</p>
<p><strong><code>GROUP BY</code> 是換單位的工具</strong>，用在單看一列答不出來的問題上——「這位顧客下了幾張單」「哪張訂單被評價超過兩次」那一類。判斷沿用 <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>WHERE</code>，連 <code>GROUP BY</code> 都不必寫。求值順序那一篇走過從 <code>FROM</code> 到 <code>LIMIT</code> 每一步處理的單位，分組前後從列換成組是其中變化最大的一步。</p>
<p>三種寫法各自描述什麼、以及 <code>NOT IN</code> 為什麼在這裡是危險的那一個，在 <a href="/blog/backend/01-database/sql/in-exists-join/" data-link-title="SQL.8 IN、EXISTS 與 JOIN：列數、可取用的欄位與 NULL 的處理" data-link-desc="同一個「找出有對應資料的那些」的問題用成員判斷、存在性判斷與連接各寫一次的結果，選用的判斷標準，以及子查詢含 NULL 時 NOT IN 與 NOT EXISTS 的分岔">SQL.8 IN、EXISTS 與 JOIN：列數、可取用的欄位與 NULL 的處理</a>。</p>
]]></content:encoded></item><item><title>SQL.10 分組與視窗函數：各自的產出、選用的依據與 LAG、LEAD 的相鄰列</title><link>https://tarrragon.github.io/blog/backend/01-database/sql/window-keeps-rows-grouping-collapses/</link><pubDate>Tue, 01 Sep 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/sql/window-keeps-rows-grouping-collapses/</guid><description>&lt;p>&lt;code>GROUP BY&lt;/code> 與視窗函數都對一組列算出一個值。分開兩者的是算完之後原本那些列還在不在：&lt;strong>分組把一組收成一列，視窗函數把值貼回每一列上。&lt;/strong>&lt;/p>
&lt;p>這一篇接在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/grouping-key-decides-the-unit/" data-link-title="SQL.9 分組鍵的選擇：鍵與一組所代表的對象" data-link-desc="外連接之後拿右表欄位當鍵的後果、輸出非分組欄時 MySQL、DuckDB 與 SQLite 的反應、補進鍵的欄位要滿足的函數相依，以及改用 NOT EXISTS 回答的那一類問題">SQL.9 分組鍵的選擇：鍵與一組所代表的對象&lt;/a> 後面。那一篇的判斷標準是「一組要代表誰」，而選好了鍵之後還有一個問題——組算出來的那個值，要不要跟原本的列擺在一起。&lt;/p>
&lt;p>本篇在&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/sample-bookstore-database/" data-link-title="SQL 各篇共用的範例資料庫：一家小書店的顧客、訂單與評價表" data-link-desc="SQL 各篇共用的顧客、訂單與評價表的建表語句與初始資料，以及各篇輸出所用的引擎版本、錯誤訊息與列序的閱讀約定">共用的訂單表&lt;/a>上多加兩列，讓兩位顧客各有兩張訂單。共用的那兩列是佳穎（顧客 1）的 101 與 102，下單日 3/02 與 3/09；加的兩列屬於宗翰（顧客 2）：&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">INSERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">105&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s1">&amp;#39;2026-03-05&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="mi">200&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">106&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s1">&amp;#39;2026-03-20&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="mi">450&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="同一個總額用分組與用視窗函數寫的輸出">同一個總額用分組與用視窗函數寫的輸出&lt;/h2>
&lt;p>「每位顧客的總消費」用分組寫，四列訂單收成兩列：&lt;/p>





&lt;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">sum&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="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="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">1 800
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">2 650&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>同一個總額用視窗函數寫，四列訂單仍然是四列，各自帶著自己那位顧客的總額：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單編號&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">金額&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">sum&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="n">OVER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">PARTITION&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="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">FROM&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;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">101 1 300 800
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">102 1 500 800
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">105 2 200 650
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">106 2 450 650&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>PARTITION BY&lt;/code> 承擔的是 &lt;code>GROUP BY&lt;/code> 那個鍵的角色——它說明一組由誰構成。差別全在輸出：&lt;code>GROUP BY&lt;/code> 那一段的結果只剩兩列，而每一張訂單的金額已經被收進總額裡取不回來了。&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="w"> &lt;/span>&lt;span class="err">金額&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">round&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">金額&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">sum&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="n">OVER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">PARTITION&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="mi">1&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">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="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">101 300 37.5
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">102 500 62.5
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">105 200 30.8
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">106 450 69.2&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>分組要算出同一份結果，得把同一批資料讀兩次再接起來——一次算總額，一次拿明細去跟它連接：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-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>&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="n">round&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">金額&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">0&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 class="mi">1&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"> 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>&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="p">(&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">sum&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="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="c1">-- 讀第一次：收成每位顧客一列總額
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">&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="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"> 6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">各顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- 讀第二次：每一張訂單接上它那位顧客的總額
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1">-- 101 300 37.5
&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">-- 102 500 62.5
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">-- 105 200 30.8
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">-- 106 450 69.2（與視窗函數那一段的結果相同）&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>視窗函數把這兩次寫成一次，明細與組的值在同一趟裡就都算出來。&lt;strong>這裡說的是結構省了一次連接，不是它一定跑得比較快&lt;/strong>：兩者的代價差多少由資料量、分組數與可用的索引決定，而那要向引擎要計畫——&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/cost-lives-in-the-plan/" data-link-title="SQL.17 查詢的代價：資料分布、索引與寫法各自的影響" data-link-desc="找重複值的分組、自連接與 EXISTS 在有無索引與不同重複次數下的實測時間與計畫，以及比較查詢快慢時要補上的資料量、分布、索引與統計資訊">SQL.17 查詢的代價&lt;/a> 量了同一道題三種寫法在有無索引下的排名對調。&lt;/p>
&lt;h2 id="選用分組或視窗函數的依據結果的一列代表什麼">選用分組或視窗函數的依據：結果的一列代表什麼&lt;/h2>
&lt;p>判斷標準是一句話：&lt;strong>結果的一列代表什麼。&lt;/strong> 一列代表一位顧客，用 &lt;code>GROUP BY&lt;/code>；一列代表一張訂單而順便帶著顧客層的值，用視窗函數。&lt;/p>
&lt;p>這一句對得上 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/grouping-key-decides-the-unit/" data-link-title="SQL.9 分組鍵的選擇：鍵與一組所代表的對象" data-link-desc="外連接之後拿右表欄位當鍵的後果、輸出非分組欄時 MySQL、DuckDB 與 SQLite 的反應、補進鍵的欄位要滿足的函數相依，以及改用 NOT EXISTS 回答的那一類問題">SQL.9 分組鍵的選擇：鍵與一組所代表的對象&lt;/a> 的鍵判斷標準，鍵的判斷標準決定一組由誰構成，輸出單位決定這一組收不收成一列；一段用到分組或視窗函數的查詢，兩者都要先回答。&lt;/p></description><content:encoded><![CDATA[<p><code>GROUP BY</code> 與視窗函數都對一組列算出一個值。分開兩者的是算完之後原本那些列還在不在：<strong>分組把一組收成一列，視窗函數把值貼回每一列上。</strong></p>
<p>這一篇接在 <a href="/blog/backend/01-database/sql/grouping-key-decides-the-unit/" data-link-title="SQL.9 分組鍵的選擇：鍵與一組所代表的對象" data-link-desc="外連接之後拿右表欄位當鍵的後果、輸出非分組欄時 MySQL、DuckDB 與 SQLite 的反應、補進鍵的欄位要滿足的函數相依，以及改用 NOT EXISTS 回答的那一類問題">SQL.9 分組鍵的選擇：鍵與一組所代表的對象</a> 後面。那一篇的判斷標準是「一組要代表誰」，而選好了鍵之後還有一個問題——組算出來的那個值，要不要跟原本的列擺在一起。</p>
<p>本篇在<a href="/blog/backend/01-database/sql/sample-bookstore-database/" data-link-title="SQL 各篇共用的範例資料庫：一家小書店的顧客、訂單與評價表" data-link-desc="SQL 各篇共用的顧客、訂單與評價表的建表語句與初始資料，以及各篇輸出所用的引擎版本、錯誤訊息與列序的閱讀約定">共用的訂單表</a>上多加兩列，讓兩位顧客各有兩張訂單。共用的那兩列是佳穎（顧客 1）的 101 與 102，下單日 3/02 與 3/09；加的兩列屬於宗翰（顧客 2）：</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">105</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="s1">&#39;2026-03-05&#39;</span><span class="p">,</span><span class="mi">200</span><span class="p">),</span><span class="w"> </span><span class="p">(</span><span class="mi">106</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="s1">&#39;2026-03-20&#39;</span><span class="p">,</span><span class="mi">450</span><span class="p">);</span></span></span></code></pre></div><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="w"> </span><span class="k">sum</span><span class="p">(</span><span class="err">金額</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="p">;</span></span></span></code></pre></div>




<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">1  800
</span></span><span class="line"><span class="ln">2</span><span class="cl">2  650</span></span></code></pre></div><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="w"> </span><span class="err">顧客編號</span><span class="p">,</span><span class="w"> </span><span class="err">金額</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">       </span><span class="k">sum</span><span class="p">(</span><span class="err">金額</span><span class="p">)</span><span class="w"> </span><span class="n">OVER</span><span class="w"> </span><span class="p">(</span><span class="n">PARTITION</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="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">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="p">;</span></span></span></code></pre></div>




<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">101  1  300  800
</span></span><span class="line"><span class="ln">2</span><span class="cl">102  1  500  800
</span></span><span class="line"><span class="ln">3</span><span class="cl">105  2  200  650
</span></span><span class="line"><span class="ln">4</span><span class="cl">106  2  450  650</span></span></code></pre></div><p><code>PARTITION BY</code> 承擔的是 <code>GROUP BY</code> 那個鍵的角色——它說明一組由誰構成。差別全在輸出：<code>GROUP BY</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="w"> </span><span class="err">金額</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">       </span><span class="n">round</span><span class="p">(</span><span class="err">金額</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">100</span><span class="p">.</span><span class="mi">0</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="k">sum</span><span class="p">(</span><span class="err">金額</span><span class="p">)</span><span class="w"> </span><span class="n">OVER</span><span class="w"> </span><span class="p">(</span><span class="n">PARTITION</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="mi">1</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">3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="p">;</span></span></span></code></pre></div>




<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">101  300  37.5
</span></span><span class="line"><span class="ln">2</span><span class="cl">102  500  62.5
</span></span><span class="line"><span class="ln">3</span><span class="cl">105  200  30.8
</span></span><span class="line"><span class="ln">4</span><span class="cl">106  450  69.2</span></span></code></pre></div><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></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">       </span><span class="n">round</span><span class="p">(</span><span class="err">訂單</span><span class="p">.</span><span class="err">金額</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">100</span><span class="p">.</span><span class="mi">0</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 class="mi">1</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"> 3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="k">JOIN</span><span class="w"> </span><span class="p">(</span><span class="k">SELECT</span><span class="w"> </span><span class="err">顧客編號</span><span class="p">,</span><span class="w"> </span><span class="k">sum</span><span class="p">(</span><span class="err">金額</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="c1">-- 讀第一次：收成每位顧客一列總額
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></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="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"> 6</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="p">;</span><span class="w">         </span><span class="c1">-- 讀第二次：每一張訂單接上它那位顧客的總額
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">-- 101 300 37.5
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">-- 102 500 62.5
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1">-- 105 200 30.8
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">-- 106 450 69.2（與視窗函數那一段的結果相同）</span></span></span></code></pre></div><p>視窗函數把這兩次寫成一次，明細與組的值在同一趟裡就都算出來。<strong>這裡說的是結構省了一次連接，不是它一定跑得比較快</strong>：兩者的代價差多少由資料量、分組數與可用的索引決定，而那要向引擎要計畫——<a href="/blog/backend/01-database/sql/cost-lives-in-the-plan/" data-link-title="SQL.17 查詢的代價：資料分布、索引與寫法各自的影響" data-link-desc="找重複值的分組、自連接與 EXISTS 在有無索引與不同重複次數下的實測時間與計畫，以及比較查詢快慢時要補上的資料量、分布、索引與統計資訊">SQL.17 查詢的代價</a> 量了同一道題三種寫法在有無索引下的排名對調。</p>
<h2 id="選用分組或視窗函數的依據結果的一列代表什麼">選用分組或視窗函數的依據：結果的一列代表什麼</h2>
<p>判斷標準是一句話：<strong>結果的一列代表什麼。</strong> 一列代表一位顧客，用 <code>GROUP BY</code>；一列代表一張訂單而順便帶著顧客層的值，用視窗函數。</p>
<p>這一句對得上 <a href="/blog/backend/01-database/sql/grouping-key-decides-the-unit/" data-link-title="SQL.9 分組鍵的選擇：鍵與一組所代表的對象" data-link-desc="外連接之後拿右表欄位當鍵的後果、輸出非分組欄時 MySQL、DuckDB 與 SQLite 的反應、補進鍵的欄位要滿足的函數相依，以及改用 NOT EXISTS 回答的那一類問題">SQL.9 分組鍵的選擇：鍵與一組所代表的對象</a> 的鍵判斷標準，鍵的判斷標準決定一組由誰構成，輸出單位決定這一組收不收成一列；一段用到分組或視窗函數的查詢，兩者都要先回答。</p>
<p>後續的子句跟著這個選擇走。<code>GROUP BY</code> 算出來的值可以放進 <code>HAVING</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>SELECT</code> 那一步才誕生，<code>WHERE</code> 與 <code>HAVING</code> 都拿不到它——要拿它當條件，得把整段包成一層子查詢再從外面篩（部分引擎另有 <code>QUALIFY</code> 這個子句直接做這件事，標準 SQL 沒有）。算 <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="c1">-- 直接在同一層寫 WHERE 佔該顧客百分比 &gt; 50，SQLite 3.51 拒絕：
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">--   misuse of aliased window function 佔該顧客百分比
</span></span></span><span class="line"><span class="ln"> 3</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="w"> </span><span class="err">佔該顧客百分比</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </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="k">SELECT</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"> 5</span><span class="cl"><span class="w">         </span><span class="n">round</span><span class="p">(</span><span class="err">金額</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">100</span><span class="p">.</span><span class="mi">0</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="k">sum</span><span class="p">(</span><span class="err">金額</span><span class="p">)</span><span class="w"> </span><span class="n">OVER</span><span class="w"> </span><span class="p">(</span><span class="n">PARTITION</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="mi">1</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"> 6</span><span class="cl"><span class="w">  </span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="p">)</span><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">WHERE</span><span class="w"> </span><span class="err">佔該顧客百分比</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">50</span><span class="p">;</span><span class="w">       </span><span class="c1">-- 外層的 WHERE 讀得到它
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1">-- 102 62.5
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">-- 106 69.2</span></span></span></code></pre></div><h2 id="lag-與-lead取同一組裡相鄰的列">LAG 與 LEAD：取同一組裡相鄰的列</h2>
<p>明細列還在，於是「同一位顧客的上一張訂單」這個概念才存在——分組把明細收掉之後，能取的相鄰只剩組與組之間的。所以視窗函數除了對整組算值，還能直接取同一組裡相鄰的某一列。<code>LAG</code> 往前取、<code>LEAD</code> 往後取，而相鄰的定義由 <code>ORDER BY</code> 給：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">訂單編號</span><span class="p">,</span><span class="w"> </span><span class="err">顧客編號</span><span class="p">,</span><span class="w"> </span><span class="err">金額</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">       </span><span class="err">金額</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">LAG</span><span class="p">(</span><span class="err">金額</span><span class="p">)</span><span class="w"> </span><span class="n">OVER</span><span class="w"> </span><span class="p">(</span><span class="n">PARTITION</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">ORDER</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="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">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="p">;</span></span></span></code></pre></div>




<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">101  1  300  （空）
</span></span><span class="line"><span class="ln">2</span><span class="cl">102  1  500  200
</span></span><span class="line"><span class="ln">3</span><span class="cl">105  2  200  （空）
</span></span><span class="line"><span class="ln">4</span><span class="cl">106  2  450  250</span></span></code></pre></div><p>每一位顧客的第一張訂單前面沒有列可取，值是 <code>NULL</code>。取同一組裡相鄰一列的問題用自連接也寫得出來，差別在自連接要把兩列配起來，而 <code>LAG</code> 沿著已經排好的順序直接往前數一列。<a href="/blog/backend/01-database/sql/table-occurrence-and-alias/" data-link-title="SQL.7 表的出現與別名：表名的指稱、別名的作用範圍與自連接" data-link-desc="FROM 裡的表名指的是什麼、同名的兩次出現為什麼被引擎擋下、自連接的前後配對、同組比較與層級關係，以及別名取名的依據">SQL.7 表的出現與別名：表名的指稱、別名的作用範圍與自連接</a> 走的是配起來那條路，並給了配對會不會膨脹這個條件。</p>
<h2 id="排序相鄰與問題要的相鄰分區排序鍵與距離條件">排序相鄰與問題要的相鄰：分區、排序鍵與距離條件</h2>
<p><code>LAG</code> 交出的是<strong>排序相鄰</strong>——照 <code>ORDER BY</code> 排好之後緊鄰的那一列，而<strong>問題要的相鄰</strong>往往是另一種：同一位顧客的上一張、前一天、上一個月。排序相鄰與問題要的相鄰對不上的時候，查詢照樣回結果。</p>
<p>把上面那段的 <code>PARTITION BY</code> 拿掉，其餘不動。少了分區之後 <code>LAG</code> 沿全表的下單日排序。兩個版本並排在同一段查詢裡：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">訂單編號</span><span class="p">,</span><span class="w"> </span><span class="err">顧客編號</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">       </span><span class="err">金額</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">LAG</span><span class="p">(</span><span class="err">金額</span><span class="p">)</span><span class="w"> </span><span class="n">OVER</span><span class="w"> </span><span class="p">(</span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="err">下單日</span><span class="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="c1">-- 沿全表的下單日往前取一列
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="w">       </span><span class="err">金額</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">LAG</span><span class="p">(</span><span class="err">金額</span><span class="p">)</span><span class="w"> </span><span class="n">OVER</span><span class="w"> </span><span class="p">(</span><span class="n">PARTITION</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">ORDER</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="k">AS</span><span class="w"> </span><span class="err">分區後的差額</span><span class="w">   </span><span class="c1">-- 只在同一位顧客之內往前取
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></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">5</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="err">下單日</span><span class="p">;</span><span class="w">                  </span><span class="c1">-- 列序照全表的下單日：3/02、3/05、3/09、3/20</span></span></span></code></pre></div>




<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">訂單  顧客  沒分區的差額   分區後的差額
</span></span><span class="line"><span class="ln">2</span><span class="cl">101   1     （空）         （空）
</span></span><span class="line"><span class="ln">3</span><span class="cl">105   2     -100           （空）
</span></span><span class="line"><span class="ln">4</span><span class="cl">102   1     300            200
</span></span><span class="line"><span class="ln">5</span><span class="cl">106   2     -50            250</span></span></code></pre></div><p>非首列的三列全部錯了。宗翰的第一張訂單拿佳穎的訂單當「上一張」算出 -100，佳穎的第二張拿宗翰的算出 300。<strong>這些數字沒有一個報錯，而它們都是拿兩位不同顧客的訂單相減得來的。</strong></p>
<p>排序相鄰與問題要的相鄰對不上，除了跨到另一位顧客，也會發生在時間上：<code>ORDER BY 下單日</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">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="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">SELECT</span><span class="w"> </span><span class="err">訂單編號</span><span class="p">,</span><span class="w"> </span><span class="err">金額</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">         </span><span class="n">LAG</span><span class="p">(</span><span class="err">金額</span><span class="p">)</span><span class="w"> </span><span class="n">OVER</span><span class="w"> </span><span class="n">w</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></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">         </span><span class="n">julianday</span><span class="p">(</span><span class="err">下單日</span><span class="p">)</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">julianday</span><span class="p">(</span><span class="n">LAG</span><span class="p">(</span><span class="err">下單日</span><span class="p">)</span><span class="w"> </span><span class="n">OVER</span><span class="w"> </span><span class="n">w</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">5</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">WINDOW</span><span class="w"> </span><span class="n">w</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="p">(</span><span class="n">PARTITION</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">ORDER</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></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="err">金額</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="err">前一筆</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="err">相隔天數</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">8</span><span class="cl"><span class="w"></span><span class="c1">-- 本篇的四列訂單：零列（102 與佳穎的上一張相隔 7 天，106 與宗翰的上一張相隔 15 天）
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1">-- 拿掉 AND 相隔天數 = 1：102、106</span></span></span></code></pre></div><p>少了距離條件，102 與 106 都會被當成「前一天」的比較。</p>
<p>這個缺口在逐日資料上更難察覺。逐日觀測只要偶爾缺一天，少了距離檢查的版本就會把斷點兩側當成相鄰而多算幾十次比較，而多出來的量相對於總列數小到不會有任何一處顯得異常——查詢照跑，結果集的規模也看不出差別。</p>
<p>所以 <code>OVER</code> 括號裡有三件事要分開確認：<strong><code>PARTITION BY</code> 說明相鄰限定在哪個範圍之內，<code>ORDER BY</code> 說明沿著什麼排，而那個排序鍵要能把同一組裡的列排成唯一的次序。</strong> 排序唯一最容易漏——排序鍵有並列時「前一列」是哪一列沒有定義，同一組資料與同一個窗規格因此可以給出不同的答案，而同一段 <code>SELECT</code> 裡另有一個排序不同的視窗就足以讓它換一個：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 本篇的四列訂單再加一張與 102 同一天下單的 107（只在這一段成立）
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></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">107</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="s1">&#39;2026-03-09&#39;</span><span class="p">,</span><span class="mi">100</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="err">訂單編號</span><span class="p">,</span><span class="w"> </span><span class="n">LAG</span><span class="p">(</span><span class="err">訂單編號</span><span class="p">)</span><span class="w"> </span><span class="n">OVER</span><span class="w"> </span><span class="p">(</span><span class="n">PARTITION</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">ORDER</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="k">AS</span><span class="w"> </span><span class="err">上一張</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span><span class="k">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"> 6</span><span class="cl"><span class="w"></span><span class="c1">-- 102 的上一張是 101，107 的上一張是 102
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="err">訂單編號</span><span class="p">,</span><span class="w"> </span><span class="n">LAG</span><span class="p">(</span><span class="err">訂單編號</span><span class="p">)</span><span class="w"> </span><span class="n">OVER</span><span class="w"> </span><span class="p">(</span><span class="n">PARTITION</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">ORDER</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="k">AS</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"> 9</span><span class="cl"><span class="w">       </span><span class="n">row_number</span><span class="p">()</span><span class="w"> </span><span class="n">OVER</span><span class="w"> </span><span class="p">(</span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="err">金額</span><span class="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="c1">-- 多一個按金額排序的視窗；上一張那一欄的寫法沒動
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></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">11</span><span class="cl"><span class="w"></span><span class="c1">-- 107 的上一張變成 101，102 的上一張變成 107（SQLite 3.51）</span></span></span></code></pre></div><p>並列時要補一個決勝的次鍵：窗規格寫成 <code>ORDER BY 下單日, 訂單編號</code> 之後，兩段查詢的上一張那一欄都是 102 接在 101 後面、107 接在 102 後面。分區、排序鍵與排序唯一都確認之後，還要問排序相鄰是不是就等於問題要的相鄰——答案是否定的時候，那個距離條件得自己補上。</p>
<h2 id="over-括號裡的分區與排序作為宣告">OVER 括號裡的分區與排序作為宣告</h2>
<p><code>PARTITION BY 顧客編號</code> 宣告的是「相鄰限定在同一位顧客之內」。寫了它而它沒在做事也是可能的——每位顧客都只有一張單的時候，拿掉它結果不變，那一類「宣告在、行為不需要它」的情形在 <a href="/blog/backend/01-database/sql/declared-intent-vs-behaviour/" data-link-title="SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證" data-link-desc="LEFT JOIN、CROSS JOIN、DISTINCT、COALESCE 與表的別名各自對讀的人宣告了什麼、宣告落空時下一個修改的人付出什麼代價，以及把關鍵字換成最弱的版本再跑一次的查證動作">SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證</a>。漏寫它則是另一回事：文字裡沒有那份宣告，錯的是答案。上一節的兩個缺口（漏分區、漏距離條件）共同的形狀是查詢照跑、結果集的規模也看不出差別，<a href="/blog/backend/01-database/sql/well-formed-is-not-correct/" data-link-title="SQL.13 查詢的合法性與答案的正確性：引擎檢查的範圍、答案錯掉的成因與查證方法" data-link-desc="合法與問對了分別由誰負責、答案錯掉時引擎為什麼不報錯，以及靜態分析、約束與反例資料各自判得出哪些錯">SQL.13 查詢的合法性與答案的正確性：引擎檢查的範圍、答案錯掉的成因與查證方法</a> 把輸出不帶訊號的幾種形態並排：回了正確答案的、回零列的、以及列數與欄位都看不出異常的。</p>
]]></content:encoded></item></channel></rss>