<?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>Window-Function on Tarragon</title><link>https://tarrragon.github.io/blog/tags/window-function/</link><description>Recent content in Window-Function 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/window-function/index.xml" rel="self" type="application/rss+xml"/><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>