<?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>Three-Valued-Logic on Tarragon</title><link>https://tarrragon.github.io/blog/tags/three-valued-logic/</link><description>Recent content in Three-Valued-Logic on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 02 Sep 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/three-valued-logic/index.xml" rel="self" type="application/rss+xml"/><item><title>NULL（空值）</title><link>https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/null/</link><pubDate>Mon, 31 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/null/</guid><description>&lt;p>NULL 表示這個位置沒有值。它與零、空字串都不同——那兩者是值，NULL 是值的缺席。它出現在兩種來源：資料本身就沒填，或 &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;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>&lt;strong>比較：拿 NULL 去比對任何值，答案是第三種——未知。&lt;/strong> 真、假、未知這三種答案合起來叫三值邏輯，SQL 的每一個條件都在這套邏輯裡求值。 &lt;code>NULL = NULL&lt;/code> 的結果既不是真也不是假。而 &lt;code>WHERE&lt;/code> 只留下判斷為真的列，所以「未知」與「假」在那一步的下場相同：列被丟掉。判斷有沒有值要用 &lt;code>IS NULL&lt;/code> 與 &lt;code>IS NOT NULL&lt;/code>，它們問的是位置有沒有值、答案只有真假兩種。&lt;/p>
&lt;p>&lt;strong>計數：&lt;code>count(*)&lt;/code> 數列，&lt;code>count(欄位)&lt;/code> 只數那一欄有值的列。&lt;/strong> 兩者的差額就是空缺的數量。&lt;/p>
&lt;p>&lt;strong>分組：&lt;code>GROUP BY&lt;/code> 把 NULL 當成一個值來分。&lt;/strong> 所有那一欄是 NULL 的列會收進同一組——這是 NULL 少數表現得像一般值的地方。連接產出的那個 &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>LEFT JOIN&lt;/code> 之後宗翰與雅文的訂單欄是 NULL，而用等號去找他們回零列且不報錯：&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">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">訂單編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- 0 列，沒有錯誤訊息
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">訂單編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">IS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- 宗翰、雅文&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>分組那一側的表現相反。按訂單表的顧客編號分組，宗翰與雅文因為都是 NULL 而被收進同一組——MySQL、SQLite 與 DuckDB 的結果一致。&lt;/p>
&lt;p>最難發現的是 &lt;code>NOT IN&lt;/code>：子查詢的結果裡只要有一個 NULL，整段查詢回零列而三個引擎都不報錯。推導、實測與 &lt;code>NOT EXISTS&lt;/code> 的安全範圍在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/join-changes-rows-and-nulls/" data-link-title="SQL.6 連接之後的列數與空缺：列數膨脹、外連接補的 NULL 與三值邏輯" data-link-desc="連接讓聚合多算的機制與計數、加總、平均、中位數各自的修法，以及外連接補出來的 NULL 在 count、等號比較、NOT IN 與 NOT EXISTS 上的行為">SQL.6 連接之後的列數與空缺：列數膨脹、外連接補的 NULL 與三值邏輯&lt;/a>。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>&lt;strong>否定式的成員判斷一律用 &lt;code>NOT EXISTS&lt;/code>&lt;/strong>，除非能保證子查詢那一欄不會有 NULL——而那個保證要來自欄位的&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/constraint/" data-link-title="Constraint（約束）" data-link-desc="看到「這個保證要來自約束而不是來自習慣」，或想確認某一欄會不會出現 NULL 時，查這一層由誰擋">約束&lt;/a>，不是來自習慣。&lt;/p>
&lt;p>&lt;strong>那條約束是建表時的一個決定，而它有代價。&lt;/strong> 允許為空讀起來就是事實的形狀（沒有值就是沒有值），不允許則要替「沒有」發明一個實際的值；兩條在設計當下都合理，而選了前者之後每一段碰到這一欄的查詢都要處理三值邏輯。&lt;a href="https://tarrragon.github.io/blog/backend/01-database/design-decisions-price-every-query/" data-link-title="1.16 Schema 設計決定的查詢代價：可空性、排序鍵唯一性、表寬、一對多的切法、比較規則與外鍵執法" data-link-desc="建表時每一個決定有哪些選法、選了之後查詢要多做什麼、代價在什麼條件下才浮現、改回來要付的遷移成本，以及設計當下就能問的問題；輸出為 SQLite 與 PostgreSQL 的實測">backend 1.16 Schema 設計決定的查詢代價&lt;/a> 的第一節走這個取捨，並給改回來要付的遷移成本。&lt;/p>
&lt;p>判斷空缺的來源也要分開。&lt;code>IS NULL&lt;/code> 為真時，可能是這一列沒配到對象，也可能是配到了而那個欄位本來就空。兩者在結果裡長得一樣，要靠選對欄位來分——用連接鍵那一欄判斷「有沒有配到」最可靠，因為它在配得上的時候必定有值。&lt;/p></description><content:encoded><![CDATA[<p>NULL 表示這個位置沒有值。它與零、空字串都不同——那兩者是值，NULL 是值的缺席。它出現在兩種來源：資料本身就沒填，或 <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>它的行為在三個地方與一般的值不同，而三處各自有一組要記的規則。</p>
<h2 id="概念位置">概念位置</h2>
<p><strong>比較：拿 NULL 去比對任何值，答案是第三種——未知。</strong> 真、假、未知這三種答案合起來叫三值邏輯，SQL 的每一個條件都在這套邏輯裡求值。 <code>NULL = NULL</code> 的結果既不是真也不是假。而 <code>WHERE</code> 只留下判斷為真的列，所以「未知」與「假」在那一步的下場相同：列被丟掉。判斷有沒有值要用 <code>IS NULL</code> 與 <code>IS NOT NULL</code>，它們問的是位置有沒有值、答案只有真假兩種。</p>
<p><strong>計數：<code>count(*)</code> 數列，<code>count(欄位)</code> 只數那一欄有值的列。</strong> 兩者的差額就是空缺的數量。</p>
<p><strong>分組：<code>GROUP BY</code> 把 NULL 當成一個值來分。</strong> 所有那一欄是 NULL 的列會收進同一組——這是 NULL 少數表現得像一般值的地方。連接產出的那個 <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>LEFT JOIN</code> 之後宗翰與雅文的訂單欄是 NULL，而用等號去找他們回零列且不報錯：</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">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="k">NULL</span><span class="w">     </span><span class="c1">-- 0 列，沒有錯誤訊息
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></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="w">    </span><span class="c1">-- 宗翰、雅文</span></span></span></code></pre></div><p>分組那一側的表現相反。按訂單表的顧客編號分組，宗翰與雅文因為都是 NULL 而被收進同一組——MySQL、SQLite 與 DuckDB 的結果一致。</p>
<p>最難發現的是 <code>NOT IN</code>：子查詢的結果裡只要有一個 NULL，整段查詢回零列而三個引擎都不報錯。推導、實測與 <code>NOT EXISTS</code> 的安全範圍在 <a href="/blog/backend/01-database/sql/join-changes-rows-and-nulls/" data-link-title="SQL.6 連接之後的列數與空缺：列數膨脹、外連接補的 NULL 與三值邏輯" data-link-desc="連接讓聚合多算的機制與計數、加總、平均、中位數各自的修法，以及外連接補出來的 NULL 在 count、等號比較、NOT IN 與 NOT EXISTS 上的行為">SQL.6 連接之後的列數與空缺：列數膨脹、外連接補的 NULL 與三值邏輯</a>。</p>
<h2 id="設計責任">設計責任</h2>
<p><strong>否定式的成員判斷一律用 <code>NOT EXISTS</code></strong>，除非能保證子查詢那一欄不會有 NULL——而那個保證要來自欄位的<a href="/blog/backend/01-database/sql/knowledge-cards/constraint/" data-link-title="Constraint（約束）" data-link-desc="看到「這個保證要來自約束而不是來自習慣」，或想確認某一欄會不會出現 NULL 時，查這一層由誰擋">約束</a>，不是來自習慣。</p>
<p><strong>那條約束是建表時的一個決定，而它有代價。</strong> 允許為空讀起來就是事實的形狀（沒有值就是沒有值），不允許則要替「沒有」發明一個實際的值；兩條在設計當下都合理，而選了前者之後每一段碰到這一欄的查詢都要處理三值邏輯。<a href="/blog/backend/01-database/design-decisions-price-every-query/" data-link-title="1.16 Schema 設計決定的查詢代價：可空性、排序鍵唯一性、表寬、一對多的切法、比較規則與外鍵執法" data-link-desc="建表時每一個決定有哪些選法、選了之後查詢要多做什麼、代價在什麼條件下才浮現、改回來要付的遷移成本，以及設計當下就能問的問題；輸出為 SQLite 與 PostgreSQL 的實測">backend 1.16 Schema 設計決定的查詢代價</a> 的第一節走這個取捨，並給改回來要付的遷移成本。</p>
<p>判斷空缺的來源也要分開。<code>IS NULL</code> 為真時，可能是這一列沒配到對象，也可能是配到了而那個欄位本來就空。兩者在結果裡長得一樣，要靠選對欄位來分——用連接鍵那一欄判斷「有沒有配到」最可靠，因為它在配得上的時候必定有值。</p>
]]></content:encoded></item><item><title>SQL.13 查詢的合法性與答案的正確性：引擎檢查的範圍、答案錯掉的成因與查證方法</title><link>https://tarrragon.github.io/blog/backend/01-database/sql/well-formed-is-not-correct/</link><pubDate>Wed, 02 Sep 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/sql/well-formed-is-not-correct/</guid><description>&lt;p>一段查詢送進&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/database-engine/" data-link-title="Database Engine（資料庫引擎）" data-link-desc="文章說「引擎決定」「換一家引擎就不同」時，查那是哪一個層次的決定">引擎&lt;/a>，它檢查的是這段文字合不合法：名字解析得到、型別配得上、子句的組合合乎&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/relational-algebra/" data-link-title="Relational Algebra（關聯代數）" data-link-desc="看到「子句其實是某個運算」或想知道 SQL 的規則從哪裡來時，查這一層在底下的是什麼">關聯代數&lt;/a>的規則。通過之後它照著算，回一批列。&lt;strong>「這批列是不是那個問題的答案」不在引擎檢查的範圍裡&lt;/strong>，因為問題不在查詢裡——查詢是問題的一種表達，而表達得對不對，只有提問的人知道。&lt;/p>
&lt;p>本篇要用到這兩層的名字。&lt;strong>合法&lt;/strong>指上一段那三項檢查都通過——判它不必讀任何一列資料，但要知道是哪一家引擎在判：各家收與不收的界線不同，標準禁止的寫法有的引擎照收，而那條界線劃在哪裡、各家怎麼劃，在 &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;strong>問對了&lt;/strong>指算出來的那份&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/relation/" data-link-title="Relation（關係）" data-link-desc="看到 SQL 教材說「組成一個關係」而不確定它指的是不是資料表時">關係&lt;/a>（欄位固定、由若干列構成的集合，查詢的結果就是一份關係）正好是那個問題的答案，這個詞是本篇為了指認「這批列是不是那個問題的答案」這一層而取的，SQL 的標準術語裡沒有它。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/semantic-model/" data-link-title="Semantic Model（語意模型）" data-link-desc="文章說「模型定死」「符合語意模型」時，查那套規則是什麼、由誰保證">語意模型&lt;/a>指這個語言規定的、由查詢的文字算出結果的那套規則，它的每一項規則都有一處與直覺相反（子句的先後、連接的左右、條件擺哪一邊、分組的鍵各是一例）。本篇處理直覺錯掉之後會發生什麼：引擎照樣算完、照樣回一批結果，而那批結果不帶任何出錯的訊號。&lt;/p>
&lt;h2 id="正解與錯的寫法不等於五漏掉星等與not-in">正解與錯的寫法：「不等於五」「漏掉星等」與「NOT IN」&lt;/h2>
&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>裡訂單 101 有一則五星、訂單 102 沒有評價，所以答案是 102。&lt;/p>
&lt;p>正確的寫法把星等的條件放進 &lt;code>ON&lt;/code>，再挑配不到的那些（&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/on-describes-where-filters/" data-link-title="SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責" data-link-desc="條件寫在連接的配對階段與寫在關係組完之後的篩選階段的差別、外連接的保護在 WHERE 被抵銷的機制，以及判斷一個條件該放哪一邊的測試">SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責&lt;/a> 的「找沒配到的那些列：IS NULL 條件的歸屬」一節）：&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 class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">評價&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">星等&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">5&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">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">評價&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">評價編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">IS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 102&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>「不等於五」的錯法把「沒有五星」寫成「星等不等於五」。&lt;/strong> 其餘不動，只把星等條件挪進 &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="p">...&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">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">評價&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">星等&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 共用資料：0 列
&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">-- 101 唯一那則評價是五星，5 &amp;lt;&amp;gt; 5 判成假
&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">-- 102 沒有評價，外連接替它補一列、星等是 NULL，NULL &amp;lt;&amp;gt; 5 的真值是未知，WHERE 只留判成真的列
&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">-- 訂單 101 多一則三星之後：101&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/knowledge-cards/outer-join/" data-link-title="Outer Join（外連接）" data-link-desc="想確認 LEFT / RIGHT / FULL 各自保護哪一側、或連接之後有些人從名單上消失時">外連接&lt;/a>替配不到評價的訂單補出來、評價欄位全是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/null/" data-link-title="NULL（空值）" data-link-desc="查 NULL 在比較、計數與分組裡各自怎麼表現時">&lt;code>NULL&lt;/code>&lt;/a> 的那一列，MySQL 手冊叫 NULL-complemented row，本篇寫成&lt;strong>補出來的列&lt;/strong>；對補出來的列判不成真的篩選條件叫&lt;strong>拒絕 NULL 的條件&lt;/strong>（null-rejected condition），&lt;code>評價.星等 &amp;lt;&amp;gt; 5&lt;/code> 就是一例（&lt;code>NULL&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> 的「NULL 的比較結果與 IS NULL」一節）。&lt;strong>問錯了問題，加上拒絕 NULL 的條件把補出來的列全數濾掉，兩件疊在一起，所以這段查詢回哪些列由資料決定&lt;/strong>：訂單 101 多一則三星之後，正解仍然是 102，而它交出了恰好相反的那一列。&lt;/p>
&lt;p>補一個 &lt;code>IS NULL&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="p">...&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">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">評價&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">星等&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">OR&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">IS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 共用資料：102，看起來完全正確
&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">-- 訂單 101 多一則三星之後：101 與 102&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>補上的只是補出來的列，問題仍然是「至少有一則非五星評價」。&lt;/p>
&lt;p>&lt;strong>「漏掉星等」的錯法把星等條件整個漏掉，而它在現有資料上回了正確答案。&lt;/strong> 這一種的星等條件哪裡都沒出現：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="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">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">評價&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">評價編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">IS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 共用資料：102
&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">-- 訂單 102 多一則三星評價之後：0 列（正解仍然是 102）&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這一段問的是「沒有任何評價的訂單」。它與正解在共用資料上是同一批列，因為有評價的那張訂單剛好只有五星那一則；兩段查詢要到某張訂單的評價全部不是五星時才分開。&lt;/p></description><content:encoded><![CDATA[<p>一段查詢送進<a href="/blog/backend/01-database/sql/knowledge-cards/database-engine/" data-link-title="Database Engine（資料庫引擎）" data-link-desc="文章說「引擎決定」「換一家引擎就不同」時，查那是哪一個層次的決定">引擎</a>，它檢查的是這段文字合不合法：名字解析得到、型別配得上、子句的組合合乎<a href="/blog/backend/01-database/sql/knowledge-cards/relational-algebra/" data-link-title="Relational Algebra（關聯代數）" data-link-desc="看到「子句其實是某個運算」或想知道 SQL 的規則從哪裡來時，查這一層在底下的是什麼">關聯代數</a>的規則。通過之後它照著算，回一批列。<strong>「這批列是不是那個問題的答案」不在引擎檢查的範圍裡</strong>，因為問題不在查詢裡——查詢是問題的一種表達，而表達得對不對，只有提問的人知道。</p>
<p>本篇要用到這兩層的名字。<strong>合法</strong>指上一段那三項檢查都通過——判它不必讀任何一列資料，但要知道是哪一家引擎在判：各家收與不收的界線不同，標準禁止的寫法有的引擎照收，而那條界線劃在哪裡、各家怎麼劃，在 <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>；<strong>問對了</strong>指算出來的那份<a href="/blog/backend/01-database/sql/knowledge-cards/relation/" data-link-title="Relation（關係）" data-link-desc="看到 SQL 教材說「組成一個關係」而不確定它指的是不是資料表時">關係</a>（欄位固定、由若干列構成的集合，查詢的結果就是一份關係）正好是那個問題的答案，這個詞是本篇為了指認「這批列是不是那個問題的答案」這一層而取的，SQL 的標準術語裡沒有它。</p>
<p><a href="/blog/backend/01-database/sql/knowledge-cards/semantic-model/" data-link-title="Semantic Model（語意模型）" data-link-desc="文章說「模型定死」「符合語意模型」時，查那套規則是什麼、由誰保證">語意模型</a>指這個語言規定的、由查詢的文字算出結果的那套規則，它的每一項規則都有一處與直覺相反（子句的先後、連接的左右、條件擺哪一邊、分組的鍵各是一例）。本篇處理直覺錯掉之後會發生什麼：引擎照樣算完、照樣回一批結果，而那批結果不帶任何出錯的訊號。</p>
<h2 id="正解與錯的寫法不等於五漏掉星等與not-in">正解與錯的寫法：「不等於五」「漏掉星等」與「NOT IN」</h2>
<p>書店要找出沒有五星評價的訂單。<a href="/blog/backend/01-database/sql/sample-bookstore-database/" data-link-title="SQL 各篇共用的範例資料庫：一家小書店的顧客、訂單與評價表" data-link-desc="SQL 各篇共用的顧客、訂單與評價表的建表語句與初始資料，以及各篇輸出所用的引擎版本、錯誤訊息與列序的閱讀約定">共用資料</a>裡訂單 101 有一則五星、訂單 102 沒有評價，所以答案是 102。</p>
<p>正確的寫法把星等的條件放進 <code>ON</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> 的「找沒配到的那些列：IS NULL 條件的歸屬」一節）：</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 class="k">AND</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">星等</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5</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></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 102</span></span></span></code></pre></div><p><strong>「不等於五」的錯法把「沒有五星」寫成「星等不等於五」。</strong> 其餘不動，只把星等條件挪進 <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="p">...</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">2</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="o">&lt;&gt;</span><span class="w"> </span><span class="mi">5</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">-- 共用資料：0 列
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">--   101 唯一那則評價是五星，5 &lt;&gt; 5 判成假
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">--   102 沒有評價，外連接替它補一列、星等是 NULL，NULL &lt;&gt; 5 的真值是未知，WHERE 只留判成真的列
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">-- 訂單 101 多一則三星之後：101</span></span></span></code></pre></div><p>這一段問的是「至少有一則非五星評價的訂單」，與「沒有五星評價的訂單」是兩個不同的問題。<a href="/blog/backend/01-database/sql/knowledge-cards/outer-join/" data-link-title="Outer Join（外連接）" data-link-desc="想確認 LEFT / RIGHT / FULL 各自保護哪一側、或連接之後有些人從名單上消失時">外連接</a>替配不到評價的訂單補出來、評價欄位全是 <a href="/blog/backend/01-database/sql/knowledge-cards/null/" data-link-title="NULL（空值）" data-link-desc="查 NULL 在比較、計數與分組裡各自怎麼表現時"><code>NULL</code></a> 的那一列，MySQL 手冊叫 NULL-complemented row，本篇寫成<strong>補出來的列</strong>；對補出來的列判不成真的篩選條件叫<strong>拒絕 NULL 的條件</strong>（null-rejected condition），<code>評價.星等 &lt;&gt; 5</code> 就是一例（<code>NULL</code> 參與比較時為什麼判成未知，在 <a href="/blog/backend/01-database/sql/join-changes-rows-and-nulls/" data-link-title="SQL.6 連接之後的列數與空缺：列數膨脹、外連接補的 NULL 與三值邏輯" data-link-desc="連接讓聚合多算的機制與計數、加總、平均、中位數各自的修法，以及外連接補出來的 NULL 在 count、等號比較、NOT IN 與 NOT EXISTS 上的行為">SQL.6 連接之後的列數與空缺</a> 的「NULL 的比較結果與 IS NULL」一節）。<strong>問錯了問題，加上拒絕 NULL 的條件把補出來的列全數濾掉，兩件疊在一起，所以這段查詢回哪些列由資料決定</strong>：訂單 101 多一則三星之後，正解仍然是 102，而它交出了恰好相反的那一列。</p>
<p>補一個 <code>IS NULL</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="p">...</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">2</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="o">&lt;&gt;</span><span class="w"> </span><span class="mi">5</span><span class="w"> </span><span class="k">OR</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></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- 共用資料：102，看起來完全正確
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">-- 訂單 101 多一則三星之後：101 與 102</span></span></span></code></pre></div><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="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></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 共用資料：102
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">-- 訂單 102 多一則三星評價之後：0 列（正解仍然是 102）</span></span></span></code></pre></div><p>這一段問的是「沒有任何評價的訂單」。它與正解在共用資料上是同一批列，因為有評價的那張訂單剛好只有五星那一則；兩段查詢要到某張訂單的評價全部不是五星時才分開。</p>
<p><strong>「NOT IN」的錯法用 <code>NOT IN</code> 表達否定的成員判斷。</strong> 星等條件回到<a href="/blog/backend/01-database/sql/knowledge-cards/subquery/" data-link-title="Subquery（子查詢）" data-link-desc="看到括號裡還有一個 SELECT、想知道它什麼時候算、算幾次時">子查詢</a>裡，連接整個省掉：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">訂單編號</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="w">
</span></span></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="k">NOT</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="k">SELECT</span><span class="w"> </span><span class="err">訂單編號</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">評價</span><span class="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="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
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">-- 評價表多一則訂單編號還沒填的五星評價之後：0 列
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">--   子查詢交出 101 與 NULL，NOT IN 對訂單 102 展開成 102 &lt;&gt; 101 AND 102 &lt;&gt; NULL
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">--   前一項為真、後一項的真值是未知，AND 的結果是未知，WHERE 不留
</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="c1">-- 同一個問題寫成 NOT EXISTS，加了那則評價之後仍然回 102
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="err">訂單編號</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</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></span><span class="line"><span class="ln">11</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="o">=</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">訂單編號</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="err">星等</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5</span><span class="p">);</span></span></span></code></pre></div><p><code>NOT IN</code> 展開成一串「不等於」的 AND，子查詢只要交出一個 <code>NULL</code>，那一項對每一列都判成未知，整串因此不會是真（完整推導在 <a href="/blog/backend/01-database/sql/join-changes-rows-and-nulls/" data-link-title="SQL.6 連接之後的列數與空缺：列數膨脹、外連接補的 NULL 與三值邏輯" data-link-desc="連接讓聚合多算的機制與計數、加總、平均、中位數各自的修法，以及外連接補出來的 NULL 在 count、等號比較、NOT IN 與 NOT EXISTS 上的行為">SQL.6 連接之後的列數與空缺：列數膨脹、外連接補的 NULL 與三值邏輯</a> 的「子查詢含 NULL 時的 NOT IN 與 NOT EXISTS」一節）。<code>NOT EXISTS</code> 逐列問「有沒有一則配得上的五星評價」，訂單編號是 <code>NULL</code> 的那則評價配不上任何一張訂單，所以不影響結果。</p>
<h2 id="答案錯掉的成因分類與引擎不報錯的原因">答案錯掉的成因分類與引擎不報錯的原因</h2>
<p>從要問的那一題到算出來的那批列，中間經過三個環節：提問的人把問題寫成查詢文字、語意模型規定這段文字算出什麼、引擎照它的實作去算。答案錯掉的成因按斷在哪一個環節分類：斷在問題寫成文字的，本篇叫<strong>問錯了問題</strong>；寫的人對某條規則的預期與語意模型的規定不同，叫<strong>誤解語意模型</strong>；引擎的行為偏離標準，叫<strong>引擎寬鬆</strong>；另有一類不斷在任何環節，而是要問的那一題本身還沒成形，叫<strong>問題還沒決定</strong>。</p>
<p>「不等於五」與「漏掉星等」問錯了問題。「不等於五」問的是「至少有一則非五星評價」，「漏掉星等」問的是「有沒有任何評價」——兩段查詢都精確地回答了它們自己問的那一題，而那一題不是要問的那一題。問錯了問題與模型的規則無關：換引擎、加約束都碰不到它，因為那段文字沒有任何一處是錯的；靜態分析判得出「不等於五」，憑的是它同時誤解了語意模型（見〈「問對了沒有」的判定：靜態分析、約束與反例資料〉）。問題與查詢文字之間還有反方向的落差，<a href="/blog/backend/01-database/sql/declared-intent-vs-behaviour/" data-link-title="SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證" data-link-desc="LEFT JOIN、CROSS JOIN、DISTINCT、COALESCE 與表的別名各自對讀的人宣告了什麼、宣告落空時下一個修改的人付出什麼代價，以及把關鍵字換成最弱的版本再跑一次的查證動作">SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證</a> 叫它宣告落空：查詢的答案是對的，錯的是文字對下一個讀它的人說的話，例如 <code>LEFT</code> 宣告了一個從未發生的情況。問錯了問題的落差在問題與文字之間，宣告落空在文字與行為之間。</p>
<p>「NOT IN」的錯法誤解語意模型。<code>NOT IN</code> 的展開方式由模型定死，而寫的人預期它表現得像「不在這個集合裡」。誤解語意模型修得掉，改對之後永遠對。分頁是誤解語意模型的另一個實例：同分的列排在哪裡模型沒有規定，而逐頁取的人預期它固定，<a href="/blog/backend/01-database/sql/pagination-needs-a-total-order/" data-link-title="SQL.12 分頁的排序鍵與接續標記：同分的列、全序與 OFFSET 的代價" data-link-desc="每一頁各查一次時同分的列與翻頁途中的寫入怎麼造成重複與遺漏、排序鍵補到兩兩可分的條件、展開式與列值式的接續標記條件在排序方向上的差別，以及 OFFSET 隨頁數成長的代價">SQL.12 分頁的排序鍵與接續標記</a> 寫它怎麼變成重複與遺漏。</p>
<p>「不等於五」的錯法同時屬於問錯了問題與誤解語意模型——它問的那個問題不是要問的，而它的星等條件又是拒絕 NULL 的條件、把補出來的列全數濾掉——所以它回的列隨資料變動，這是它比「漏掉星等」與「NOT IN」難診斷的原因。</p>
<p>引擎寬鬆的實例不在這三種錯法裡。引擎接受了標準禁止的寫法並自己補一個值：分組之後把非分組欄放進 <code>SELECT</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> 與 <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>，換一家引擎就浮現。SQL.2 除了求值順序，也寫了哪些違反求值順序的寫法會被引擎當場報錯、哪些會被照收——同一個放錯邊的條件在一家報錯、在另一家回一份錯的清單。問題還沒決定的實例是「所有訂單都被評價過的顧客」：它在一張訂單都沒下過的人身上為真（邏輯裡叫空真，vacuous truth），而提問的人多半還沒想過那算不算。問題還沒決定連換誰來寫都不會浮現，因為要問的那一題還沒成形。</p>
<p>這些成因有一個共通的地方：引擎回給寫查詢的人的只有兩樣，錯誤訊息與算出來的那批列，而這些錯誤在兩樣裡都不現形。<strong>引擎保證的是兩件事：這段文字合法，而算出來的關係符合語意模型。文字合法與關係符合語意模型這兩項都不決定答案對不對，而答案對不對也不影響這兩項，所以錯誤沒有任何一條經由引擎回報的管道。</strong> 三段查詢一則錯誤訊息都沒有，沒有型別衝突，沒有一列長得奇怪。</p>
<p>回來的那批列本身也看不出答案錯了，而看不出來的情形不只一種。「漏掉星等」的錯法在原本那批資料上回了正確答案，看不出問題；「不等於五」與「NOT IN」兩種回零列，而零列既可能是「確實沒有」也可能是條件整段失效——分不出這兩者，正是 <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</a> 把否定式一律交給 <code>NOT EXISTS</code> 的理由。結果的形狀正確也不構成保證。把訂單接上評價再加總金額：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="k">SUM</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">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="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">-- 共用資料：800（101 是 300、102 是 500）
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">-- 替 101 再加一則評價之後：1100
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">--   101 配到兩則評價，連接後那張訂單變成兩列，300 算了兩次</span></span></span></code></pre></div><p>兩次回來的都是一列一欄的一個數字，形狀與正確答案沒有任何不同（連接為什麼會複製列，在 <a href="/blog/backend/01-database/sql/join-changes-rows-and-nulls/" data-link-title="SQL.6 連接之後的列數與空缺：列數膨脹、外連接補的 NULL 與三值邏輯" data-link-desc="連接讓聚合多算的機制與計數、加總、平均、中位數各自的修法，以及外連接補出來的 NULL 在 count、等號比較、NOT IN 與 NOT EXISTS 上的行為">SQL.6 連接之後的列數與空缺：列數膨脹、外連接補的 NULL 與三值邏輯</a>）。</p>
<p>「漏掉星等」的錯法另有一個性質：它在寫下的當時通得過任何檢驗。拿現有資料驗它，答案是對的；逐句讀它，每一個子句都符合語法與模型。</p>
<h2 id="問對了沒有的判定靜態分析約束與反例資料">「問對了沒有」的判定：靜態分析、約束與反例資料</h2>
<p>「問對了沒有」需要一個答案來比對，而那個答案不在查詢裡，也不在資料裡——它在提問的人手上。所以這一層交不出一條<strong>可以機械執行的判定</strong>——任何規則都得先知道要問的是什麼，才說得出這批列對不對。</p>
<p><strong>「問對了沒有」這整條判定交不出去，而它底下有幾項較小的判斷交得出去。</strong> 一項判斷能不能交給機器，看判斷時要讀的資訊，機器在做這項判斷的那一刻讀不讀得到：引擎收到查詢的時候讀得到查詢的文字與表的結構，資料庫寫入一筆資料的時候讀得到那一筆資料與資料庫當下已有的列，而「要問的是什麼」只有提問的人知道。這些判斷因此按它們要讀什麼分開：只讀查詢文字與表結構的是<strong>靜態分析</strong>，只讀寫入那一刻資料庫裡的資料的寫成<strong>約束</strong>，要讀「要問的是什麼」的，機器能接手的只有比對——提問的人先造一筆<strong>反例資料</strong>、給出它對應的正確答案，機器負責比對查詢回的答案與它是否相同。</p>
<p><strong>靜態分析只讀查詢的文字與表的結構，判斷時不需要任何一列資料，也不需要提問的人。</strong> 「不等於五」的錯法是它判得出來的例子：這段查詢寫了外連接，而 <code>WHERE</code> 裡的星等條件是拒絕 NULL 的條件，補出來的列一列都留不住，結果與內連接在任何資料上都相同。這個等價由三值邏輯決定、與資料裡有什麼無關，所以最佳化器規劃查詢時自己就會用到它，MySQL 手冊把這一步叫 outer join simplification。最佳化器做這個改寫時不產生任何警告，要讀<a href="/blog/backend/01-database/sql/knowledge-cards/query-plan/" data-link-title="Query Plan（執行計畫）" data-link-desc="想知道一段查詢實際會怎麼被執行、或被要求「先看計畫」時">執行計畫</a>才看得到：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 「不等於五」的錯法
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">EXPLAIN</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="err">訂單</span><span class="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"> 4</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"> 5</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="o">&lt;&gt;</span><span class="w"> </span><span class="mi">5</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">-- 星等條件拒絕 NULL：補出來的列星等是 NULL，NULL &lt;&gt; 5 判成未知，全數被濾掉
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">-- PostgreSQL 16 印出 Hash Join：最佳化器已經把 LEFT JOIN 改成內連接
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">-- MySQL 8 的 SHOW WARNINGS 印出改寫後的查詢，連接寫的是 join
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="c1">-- 正解
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="k">EXPLAIN</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</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="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">13</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 class="k">AND</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">星等</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w"></span><span class="k">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></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w"></span><span class="c1">-- 星等條件在 ON 裡；WHERE 的 評價編號 IS NULL 對補出來的列判成真，補出來的列留下
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1">-- PostgreSQL 16 印出 Hash Left Join：外連接保留下來</span></span></span></code></pre></div><p>外連接被最佳化器改成內連接，代表查詢裡的 <code>LEFT</code> 沒有作用，而寫下 <code>LEFT</code> 的人要的正是保留沒有評價的訂單。</p>
<p>「NOT IN」的錯法是另一個例子，靜態分析在它身上判出來的是風險而不是確定的錯。風險由表結構與查詢文字兩處合起來決定，兩處都不必讀任何一列資料：</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">-- 表結構：評價表的訂單編號沒有宣告 NOT NULL，這一欄可能是 NULL
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="err">評價</span><span class="w"> </span><span class="p">(</span><span class="err">評價編號</span><span class="w"> </span><span class="nb">INT</span><span class="p">,</span><span class="w"> </span><span class="err">訂單編號</span><span class="w"> </span><span class="nb">INT</span><span class="p">,</span><span class="w"> </span><span class="err">星等</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">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="c1">-- 查詢文字：NOT IN 的子查詢讀的正是那一欄
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="err">訂單編號</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</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="k">NOT</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="k">SELECT</span><span class="w"> </span><span class="err">訂單編號</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">評價</span><span class="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="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="c1">-- 子查詢只要交出一個 NULL，NOT IN 對每一列都判不成真，整段回零列
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1">-- 現有資料裡沒有訂單編號未填的評價，所以這段此刻回的仍是 102</span></span></span></code></pre></div><p>靜態分析判不出「漏掉星等」：那段查詢與正解是同一種寫法，只差 <code>ON</code> 少了星等的條件，而查詢文字與表結構裡沒有任何一處說那個條件該在。</p>
<p><strong>約束只讀寫入那一刻資料庫裡的資料，所以資料庫在寫入的那一刻就判得完。</strong> <a href="/blog/backend/01-database/sql/knowledge-cards/constraint/" data-link-title="Constraint（約束）" data-link-desc="看到「這個保證要來自約束而不是來自習慣」，或想確認某一欄會不會出現 NULL 時，查這一層由誰擋">約束</a>是寫進資料庫、由它在每次寫入時檢查的規則，每一種讀的範圍由種類固定：<code>NOT NULL</code> 與 <code>CHECK</code> 只讀正要寫入的那一筆，<code>UNIQUE</code> 讀同一欄已有的值，外鍵讀它指向的那一列。<code>NOT NULL</code> 寫下「這一欄不會缺」，外鍵寫下「這個編號一定指得到一列」。讀的範圍超出這幾種的規則（「評價要在出貨之後」要讀另一張表的出貨日）同樣在寫入那一刻判得完，而要改用觸發器或應用層檢查。「NOT IN」的錯法要評價表裡出現一則訂單編號未填的五星評價才會回零列，而評價表的訂單編號那一欄若帶著 <code>NOT NULL</code>，那則評價一開始就寫不進來——這是「否定式一律用 <code>NOT EXISTS</code>」那條規則的豁免條件，不是對 <code>NOT IN</code> 展開方式那個誤解的修法。「不等於五」與「漏掉星等」寫不成約束：兩者錯在問錯了問題，而判斷一個問題有沒有問對要讀「要問的是什麼」，那不是任何一筆資料的性質。<a href="/blog/backend/01-database/sql/foreign-key-and-referential-integrity/" data-link-title="SQL.18 外鍵與參照完整性：宣告、生效與查詢得到的保證" data-link-desc="外鍵擋下哪些寫入與 NULL 的放行、SQLite、PostgreSQL 與 MySQL 讓宣告與生效分開的位置與查法、約束生效後查詢省掉的判斷、父列刪除時子列的去向，以及外鍵涵蓋不到的寫入與規則">SQL.18 外鍵與參照完整性：宣告、生效與查詢得到的保證</a> 拿參照完整性把約束走完一輪——外鍵擋下哪些寫入、宣告與生效在哪幾個位置分開，以及內連接掉列與 <code>NOT IN</code> 回零列這兩種錯各由哪一條約束管掉；五種約束各保證什麼在 <a href="/blog/backend/01-database/sql/knowledge-cards/constraint/" data-link-title="Constraint（約束）" data-link-desc="看到「這個保證要來自約束而不是來自習慣」，或想確認某一欄會不會出現 NULL 時，查這一層由誰擋">Constraint（約束）</a>。</p>
<p><strong>要讀「要問的是什麼」的判斷交不出去，而提問的人的判斷可以記成一筆反例資料。</strong> 反例資料是一筆讓正解與某個錯的寫法回不同答案的資料，名字取自邏輯裡的反例（counterexample）：兩段查詢在某一筆資料上回的答案不同，就證明兩者不等價。上面三種錯法各有一筆——「不等於五」要訂單 101 再多一則非五星評價，「漏掉星等」要訂單 102 多一則非五星評價，「NOT IN」要一則訂單編號未填的五星評價。</p>
<p><strong>反例資料從表的結構與查詢的條件推得出來，不必先知道哪一種寫法是錯的。</strong> 測試領域把這個做法叫<strong>等價類劃分</strong>（equivalence partitioning）：把輸入按程式對它的行為分類，每一類至少放一筆。落在這段查詢上，等價類是一張訂單配到評價的方式——一則都配不到、配到不只一則、配到的全部不是五星——再加上欄位是空值。沒有宣告約束的表全部允許這些等價類，而寫查詢的人容易當成它們不會發生；正解與錯的寫法對這些等價類的假設不同，所以兩者的答案就在這些等價類上分開。</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="c1">-- 從建好的空表開始；101、102 與 9001 同共用資料
</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">101</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="s1">&#39;2026-03-02&#39;</span><span class="p">,</span><span class="mi">300</span><span class="p">),</span><span class="w"> </span><span class="p">(</span><span class="mi">102</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">500</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="p">(</span><span class="mi">103</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="s1">&#39;2026-03-10&#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">104</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="s1">&#39;2026-03-11&#39;</span><span class="p">,</span><span class="mi">400</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="p">(</span><span class="mi">105</span><span class="p">,</span><span class="mi">3</span><span class="p">,</span><span class="s1">&#39;2026-03-12&#39;</span><span class="p">,</span><span class="mi">600</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span><span class="k">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></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="p">(</span><span class="mi">9001</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="w"> </span><span class="p">(</span><span class="mi">9002</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="w">   </span><span class="c1">-- 101：配到不只一則，其中一則五星
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="w">  </span><span class="p">(</span><span class="mi">9003</span><span class="p">,</span><span class="mi">103</span><span class="p">,</span><span class="mi">3</span><span class="p">),</span><span class="w">                 </span><span class="c1">-- 103：配到的全部不是五星
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="w">  </span><span class="p">(</span><span class="mi">9004</span><span class="p">,</span><span class="mi">104</span><span class="p">,</span><span class="mi">5</span><span class="p">),</span><span class="w">                 </span><span class="c1">-- 104：只配到一則五星
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="w">  </span><span class="p">(</span><span class="mi">9005</span><span class="p">,</span><span class="mi">105</span><span class="p">,</span><span class="mi">4</span><span class="p">),</span><span class="w"> </span><span class="p">(</span><span class="mi">9006</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="w">   </span><span class="c1">-- 105：配到不只一則，全部不是五星
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="w">  </span><span class="p">(</span><span class="mi">9007</span><span class="p">,</span><span class="k">NULL</span><span class="p">,</span><span class="mi">5</span><span class="p">);</span><span class="w">                </span><span class="c1">-- 訂單編號是空值的五星評價
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">-- 102 一則評價都配不到
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1">-- 正解：102、103、105
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1">-- 「不等於五」：101、103、105、105（105 配到兩則非五星評價，出現兩次）
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1">-- 「漏掉星等」：102
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1">-- 「NOT IN」：0 列</span></span></span></code></pre></div><p>提問的人要出的仍然只有一件事：這五張訂單裡哪幾張是答案。造得出反例資料，代表提問的人已經把「要問的是什麼」說到分得開兩個問題的程度；造不出來，代表兩段查詢在這張表結構允許的每一種資料上都回同樣的答案，兩種寫法等價、選哪一種都對。反例資料與「問對了沒有」交不出去並不牴觸：反例資料與它對應的正確答案仍然由提問的人給出，交給機器的只有比對這一步。</p>
<p><strong>反例資料與約束改變的對象不同。</strong> 約束改變的是資料能長成什麼樣，不改變查詢——加上 <code>NOT NULL</code> 之後「NOT IN」的錯法不再回零列，是因為觸發它的那則評價寫不進來，而那段查詢照樣合法、照樣是同一個誤解，同一個人在下一個可為空的欄位上會再寫一次 <code>NOT IN</code>。反例資料不改變資料庫允許什麼資料，提問的人的判斷也仍然由他給出，改變的是那個判斷的保存形式：反例資料當輸入、他給的答案當預期值，合起來就是一則測試案例，往後每次改動那段查詢都能再跑一次，而讀者多半已經有跑測試案例的工具。<strong>反例資料只證偽、不證成</strong>——「漏掉星等」的錯法在原本那批資料上通過了一切檢驗，而通過從來不是對的證明。反例資料的射程另有一道邊界：它由提問的人依他當時對問題的理解寫下，所以它分得出「想的對而寫錯了」，分不出「想的就不對」。</p>
<p>反例資料與它的預期答案在測試領域有對應的名字，而測試那邊分得比本篇細：預期值從哪裡取得叫 <a href="/blog/testing/knowledge-cards/test-oracle/" data-link-title="Test Oracle" data-link-desc="一個測試憑什麼判定通過或失敗說不清楚、或被測對象沒有可逐例算出的正確答案時，用來定位判斷標準的來源，以及每一種來源的射程">test oracle</a>，那張卡列了規格、參照實作、人的判斷與現狀四種來源，以及各自抓不到的那些；「寫斷言的人與寫實作的人共用同一份誤解」叫 <a href="/blog/testing/knowledge-cards/test-provenance/" data-link-title="Test Provenance（測試出處）" data-link-desc="測試與被測實作由同一來源產出時（同一次生成、同一輪對話、同一個人同時寫），用來判斷這組測試還剩下多少驗證力">test provenance</a>；而「造不出反例資料」在 <a href="/blog/testing/knowledge-cards/mutation-testing/" data-link-title="Mutation Testing（突變測試）" data-link-desc="行覆蓋率很高卻仍漏掉 bug、或要挑一個比覆蓋率難灌水的測試品質指標時，用來判斷這套測試實際擋得住什麼">Mutation Testing</a> 裡叫 equivalent mutant。本篇只講這三個概念落在 SQL 查詢上的樣子，三張卡各自涵蓋本篇沒有處理的情境。</p>
<h2 id="正確性之外查詢的代價與由引擎資料庫決定的事">正確性之外：查詢的代價與由引擎、資料庫決定的事</h2>
<p>本篇之外還有一條與正確性互不覆蓋的軸：同一段查詢要花多久。一段查詢問對了沒有，與它要花多久，各自的判斷標準管不到對方，而代價那一層有一個機械的做法——向引擎要<a href="/blog/backend/01-database/sql/knowledge-cards/query-plan/" data-link-title="Query Plan（執行計畫）" data-link-desc="想知道一段查詢實際會怎麼被執行、或被要求「先看計畫」時">執行計畫</a>。<a href="/blog/backend/01-database/sql/cost-lives-in-the-plan/" data-link-title="SQL.17 查詢的代價：資料分布、索引與寫法各自的影響" data-link-desc="找重複值的分組、自連接與 EXISTS 在有無索引與不同重複次數下的實測時間與計畫，以及比較查詢快慢時要補上的資料量、分布、索引與統計資訊">SQL.17 查詢的代價</a> 量的是一道重複值的題目：加一個索引，最慢的那一種寫法就掉到與最快的同一個量級。</p>
<p>本篇處理的錯誤都發生在查詢的文字與提問的人之間：引擎照語意模型把結果算對了，而那不是要問的那一題。一段查詢的結果還受幾件不寫在查詢文字裡的事影響，它們由收下查詢的引擎與資料庫決定。<strong>同一段查詢合不合法，由執行它的那一家引擎判定</strong>，而標準禁止的寫法有的引擎照收、有的報錯，<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> 逐個寫法比較各家收不收、收了之後回什麼。查詢裡的表名與欄名送進引擎之後，引擎會照自己的規則處理大小寫與引號之後再去找對應的表與欄，<a href="/blog/backend/01-database/sql/identifier-rules/" data-link-title="SQL.14 識別字的規則：各家的大小寫摺疊、引號與保留字" data-link-desc="表名與欄名送進引擎之後被怎麼改寫、PostgreSQL 與 SQLite、DuckDB 在引號上的差別、MySQL 的反引號，以及全部不加引號、全部加引號與混用各自在換引擎時的後果">SQL.14 識別字的規則：各家的大小寫摺疊、引號與保留字</a> 寫各家的大小寫規則、引號的作用與保留字。執行查詢的帳號被授予哪些權限、最佳化器選了哪一個執行計畫、表上宣告了哪些約束、字串照哪一套 collation 比較，也各自影響同一段查詢回什麼，分別在 <a href="/blog/backend/01-database/sql/privilege-model/" data-link-title="SQL.16 SQL 的權限模型：角色、GRANT 的授權單位與最小權限" data-link-desc="新角色的預設權限、讀寫各自授權與收回的方式、建立物件的權限落在 schema 這一層，以及最小權限在應用程式帳號上成立的前提">SQL.16 SQL 的權限模型：角色、GRANT 的授權單位與最小權限</a>、<a href="/blog/backend/01-database/sql/declarative-not-procedural/" data-link-title="SQL.1 宣告式的紅利與代價：書寫、求值與執行順序各自由誰決定" data-link-desc="SQL 寫結果而不寫算法帶來的後果：同一個問題換書寫順序之後在有無統計資訊時拿到的計畫、求值順序擋下的寫法，以及代價為什麼要向引擎問">SQL.1 宣告式的紅利與代價</a>、<a href="/blog/backend/01-database/sql/foreign-key-and-referential-integrity/" data-link-title="SQL.18 外鍵與參照完整性：宣告、生效與查詢得到的保證" data-link-desc="外鍵擋下哪些寫入與 NULL 的放行、SQLite、PostgreSQL 與 MySQL 讓宣告與生效分開的位置與查法、約束生效後查詢省掉的判斷、父列刪除時子列的去向，以及外鍵涵蓋不到的寫入與規則">SQL.18 外鍵與參照完整性：宣告、生效與查詢得到的保證</a> 與 <a href="/blog/backend/01-database/sql/string-comparison-and-collation/" data-link-title="SQL.15 字串比較與 collation：相等、排序、LIKE 與索引可用性" data-link-desc="同一段等值條件在各家回不同的列的原因、LIKE 與等號何時分岔、索引與條件的比較規則怎麼對上，以及規則該寫在哪一層">SQL.15 字串比較與 collation：相等、排序、LIKE 與索引可用性</a>。</p>
]]></content:encoded></item></channel></rss>