<?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>Distinct on Tarragon</title><link>https://tarrragon.github.io/blog/tags/distinct/</link><description>Recent content in Distinct 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/distinct/index.xml" rel="self" type="application/rss+xml"/><item><title>SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證</title><link>https://tarrragon.github.io/blog/backend/01-database/sql/declared-intent-vs-behaviour/</link><pubDate>Tue, 01 Sep 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/sql/declared-intent-vs-behaviour/</guid><description>&lt;p>一段查詢有兩個讀者。引擎讀的是行為——這段文字要求它算出哪一批列；人讀的是宣告——寫的人選了 &lt;code>LEFT&lt;/code> 而不選 &lt;code>INNER&lt;/code>，選了 &lt;code>CROSS&lt;/code> 而不選 &lt;code>JOIN ... ON&lt;/code>，每一個選擇都在說明他預期資料長什麼樣。&lt;strong>引擎執行行為，而它從不檢查宣告有沒有兌現。&lt;/strong>&lt;/p>
&lt;p>宣告與行為分岔的時候，查詢照常回正確答案，測試照常通過，&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/query-plan/" data-link-title="Query Plan（執行計畫）" data-link-desc="想知道一段查詢實際會怎麼被執行、或被要求「先看計畫」時">執行計畫&lt;/a>上也看不出異常。付出代價的是下一個改它的人：他讀到的是宣告，於是他手上的前提從一開始就是錯的。宣告與行為之外，還有一種落差在查詢與要問的問題之間：查詢在語法上完全合法、引擎照實執行，而它回答的不是要問的那一題，那一種在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/well-formed-is-not-correct/" data-link-title="SQL.13 查詢的合法性與答案的正確性：引擎檢查的範圍、答案錯掉的成因與查證方法" data-link-desc="合法與問對了分別由誰負責、答案錯掉時引擎為什麼不報錯，以及靜態分析、約束與反例資料各自判得出哪些錯">SQL.13 查詢的合法性與答案的正確性：引擎檢查的範圍、答案錯掉的成因與查證方法&lt;/a>；同一段文字換一家引擎會做什麼，是跨引擎的那一種，在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/engine-leniency-and-portability/" data-link-title="SQL.19 引擎寬鬆度與可攜性：各家對同一組寫法的差異、分級與處理時機" data-link-desc="SQLite、DuckDB、PostgreSQL 與 MySQL 在未分組欄位、WHERE 用別名、同表未取別名、字串比對與空值排序上的實測對照，差異從當場報錯到完全靜默的分級，以及依目標組態數決定哪幾級差異在寫的當下處理">SQL.19 引擎寬鬆度與可攜性：各家對同一組寫法的差異、分級與處理時機&lt;/a>。本篇的宣告落空時答案仍然正確，錯的是文字對下一個讀它的人說的話。&lt;/p>
&lt;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，金額 300 與 500）。讀這一篇需要先有三件事：&lt;code>LEFT&lt;/code> 保護了哪些列在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/join-left-operand-accumulates/" data-link-title="SQL.4 鏈式連接的左運算元：累積方式與外連接的保護範圍" data-link-desc="每個 JOIN 的左運算元由哪些表合成、ON 因此引用得到哪些欄位、LEFT 與 RIGHT 各自保護哪些列，以及 WHERE 引用鏈尾的表時哪些列會被撤掉保護">SQL.4 鏈式連接的左運算元&lt;/a>，它與條件位置的互動在 &lt;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>，連接怎麼把一列變成多列在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/join-changes-rows-and-nulls/" data-link-title="SQL.6 連接之後的列數與空缺：列數膨脹、外連接補的 NULL 與三值邏輯" data-link-desc="連接讓聚合多算的機制與計數、加總、平均、中位數各自的修法，以及外連接補出來的 NULL 在 count、等號比較、NOT IN 與 NOT EXISTS 上的行為">SQL.6 連接之後的列數與空缺&lt;/a>。那三篇答的是「這個關鍵字做什麼」，這一篇不重寫那些機制，它問的是同一段查詢對下一個讀它的人說了什麼。&lt;/p>
&lt;h2 id="各個關鍵字的宣告與落空的形態">各個關鍵字的宣告與落空的形態&lt;/h2>
&lt;p>&lt;code>LEFT JOIN&lt;/code> 說的是「右邊配不到對象的左邊那些列也要留著」。寫的人選它，等於宣告他預期有配不到的列，而且那些列對答案有意義。&lt;/p>
&lt;p>&lt;code>CROSS JOIN&lt;/code> 說的是「我要每一種配對」。&lt;code>DISTINCT&lt;/code> 說的是「這一組值本身會重複，而重複對我沒有意義」。&lt;code>COALESCE&lt;/code> 說的是「這裡會有空值」。表的別名也是一種宣告：&lt;code>FROM 訂單 AS 早&lt;/code> 說的是「這一次出現代表較早的那張單」。這些宣告是免費的，寫下去沒有人會查證。&lt;/p>
&lt;p>落空的樣子不只一種。宣告可能&lt;strong>被同一段查詢的別的部分撤銷&lt;/strong>（&lt;code>LEFT&lt;/code> 加上一個問右表的 &lt;code>WHERE&lt;/code>）、可能&lt;strong>描述一個引擎並沒有執行的動作&lt;/strong>（&lt;code>CROSS&lt;/code> 加等值條件）、可能&lt;strong>描述一個從一開始就不存在的情況&lt;/strong>（配對已有保證時的 &lt;code>LEFT&lt;/code>），也可能&lt;strong>指向錯的成因而答案仍然正確&lt;/strong>（&lt;code>DISTINCT&lt;/code> 收掉連接製造的重複），還可能&lt;strong>名字與它代表的那一次出現對調&lt;/strong>（叫「早」的別名拿到後來那張單，條件跟著對調，結果不變，這個例子在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/table-occurrence-and-alias/" data-link-title="SQL.7 表的出現與別名：表名的指稱、別名的作用範圍與自連接" data-link-desc="FROM 裡的表名指的是什麼、同名的兩次出現為什麼被引擎擋下、自連接的前後配對、同組比較與層級關係，以及別名取名的依據">SQL.7 表的出現與別名：表名的指稱、別名的作用範圍與自連接&lt;/a>）。這幾種落空在現有資料上都回正確答案，代價都落在下一個照著宣告動手的人身上：&lt;code>DISTINCT&lt;/code> 指向錯成因，他多要一個欄位就回出錯的結果；被撤銷的 &lt;code>LEFT&lt;/code> 讓他改了 &lt;code>SELECT&lt;/code> 之後仍然少了沒有訂單的顧客，而原因在他沒有動的 &lt;code>WHERE&lt;/code>；其餘幾種連錯的結果都不會出現，只讓他下錯判斷。&lt;/p>
&lt;h2 id="被-where-撤銷的-left-join">被 WHERE 撤銷的 LEFT JOIN&lt;/h2>
&lt;p>書店想找出下過大單的顧客。查詢寫成這樣：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">姓名&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">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="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">200&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">-- 佳穎、佳穎
&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>&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="c1">-- 同一段把 LEFT JOIN 換成 JOIN，其餘不動
&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">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">姓名&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">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"> 9&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;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">200&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">10&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 佳穎、佳穎&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>LEFT&lt;/code> 在這裡宣告「沒有訂單的顧客也要留著」。而下一行的 &lt;code>WHERE&lt;/code> 問了一個右表欄位，宗翰與雅文那兩列右邊全是 &lt;code>NULL&lt;/code>，&lt;code>NULL &amp;gt; 200&lt;/code> 答不出真，於是被篩掉，外連接與內連接兩段因此回同樣兩列。&lt;/p>
&lt;p>機制在 &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>：條件放 &lt;code>ON&lt;/code> 參與配對，放 &lt;code>WHERE&lt;/code> 篩成品，而外連接補上的那些列過不了成品這一關。這裡要看的是另一件事——&lt;strong>這段查詢的行為是內連接，而它的文字說的是外連接&lt;/strong>。&lt;/p>
&lt;p>下一個維護的人讀到 &lt;code>LEFT&lt;/code>，會以為沒下單的顧客在結果裡。他想加一個「顯示零元」的欄位：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">姓名&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">COALESCE&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">金額&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&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">-- 他加的欄位：沒有訂單的顧客顯示 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">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">LEFT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">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="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">200&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">4&lt;/span>&lt;span class="cl">&lt;span class="c1">-- 佳穎 300
&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">-- 佳穎 500&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>跑出來仍然只有佳穎，而他找不出原因——因為問題在他沒有動的那一行。&lt;/p></description><content:encoded><![CDATA[<p>一段查詢有兩個讀者。引擎讀的是行為——這段文字要求它算出哪一批列；人讀的是宣告——寫的人選了 <code>LEFT</code> 而不選 <code>INNER</code>，選了 <code>CROSS</code> 而不選 <code>JOIN ... ON</code>，每一個選擇都在說明他預期資料長什麼樣。<strong>引擎執行行為，而它從不檢查宣告有沒有兌現。</strong></p>
<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/well-formed-is-not-correct/" data-link-title="SQL.13 查詢的合法性與答案的正確性：引擎檢查的範圍、答案錯掉的成因與查證方法" data-link-desc="合法與問對了分別由誰負責、答案錯掉時引擎為什麼不報錯，以及靜態分析、約束與反例資料各自判得出哪些錯">SQL.13 查詢的合法性與答案的正確性：引擎檢查的範圍、答案錯掉的成因與查證方法</a>；同一段文字換一家引擎會做什麼，是跨引擎的那一種，在 <a href="/blog/backend/01-database/sql/engine-leniency-and-portability/" data-link-title="SQL.19 引擎寬鬆度與可攜性：各家對同一組寫法的差異、分級與處理時機" data-link-desc="SQLite、DuckDB、PostgreSQL 與 MySQL 在未分組欄位、WHERE 用別名、同表未取別名、字串比對與空值排序上的實測對照，差異從當場報錯到完全靜默的分級，以及依目標組態數決定哪幾級差異在寫的當下處理">SQL.19 引擎寬鬆度與可攜性：各家對同一組寫法的差異、分級與處理時機</a>。本篇的宣告落空時答案仍然正確，錯的是文字對下一個讀它的人說的話。</p>
<p>本篇的查詢跑在<a href="/blog/backend/01-database/sql/sample-bookstore-database/" data-link-title="SQL 各篇共用的範例資料庫：一家小書店的顧客、訂單與評價表" data-link-desc="SQL 各篇共用的顧客、訂單與評價表的建表語句與初始資料，以及各篇輸出所用的引擎版本、錯誤訊息與列序的閱讀約定">共用的範例資料庫</a>上：三位顧客佳穎、宗翰、雅文，而只有佳穎下過兩張訂單（101 與 102，金額 300 與 500）。讀這一篇需要先有三件事：<code>LEFT</code> 保護了哪些列在 <a href="/blog/backend/01-database/sql/join-left-operand-accumulates/" data-link-title="SQL.4 鏈式連接的左運算元：累積方式與外連接的保護範圍" data-link-desc="每個 JOIN 的左運算元由哪些表合成、ON 因此引用得到哪些欄位、LEFT 與 RIGHT 各自保護哪些列，以及 WHERE 引用鏈尾的表時哪些列會被撤掉保護">SQL.4 鏈式連接的左運算元</a>，它與條件位置的互動在 <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>，連接怎麼把一列變成多列在 <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>。那三篇答的是「這個關鍵字做什麼」，這一篇不重寫那些機制，它問的是同一段查詢對下一個讀它的人說了什麼。</p>
<h2 id="各個關鍵字的宣告與落空的形態">各個關鍵字的宣告與落空的形態</h2>
<p><code>LEFT JOIN</code> 說的是「右邊配不到對象的左邊那些列也要留著」。寫的人選它，等於宣告他預期有配不到的列，而且那些列對答案有意義。</p>
<p><code>CROSS JOIN</code> 說的是「我要每一種配對」。<code>DISTINCT</code> 說的是「這一組值本身會重複，而重複對我沒有意義」。<code>COALESCE</code> 說的是「這裡會有空值」。表的別名也是一種宣告：<code>FROM 訂單 AS 早</code> 說的是「這一次出現代表較早的那張單」。這些宣告是免費的，寫下去沒有人會查證。</p>
<p>落空的樣子不只一種。宣告可能<strong>被同一段查詢的別的部分撤銷</strong>（<code>LEFT</code> 加上一個問右表的 <code>WHERE</code>）、可能<strong>描述一個引擎並沒有執行的動作</strong>（<code>CROSS</code> 加等值條件）、可能<strong>描述一個從一開始就不存在的情況</strong>（配對已有保證時的 <code>LEFT</code>），也可能<strong>指向錯的成因而答案仍然正確</strong>（<code>DISTINCT</code> 收掉連接製造的重複），還可能<strong>名字與它代表的那一次出現對調</strong>（叫「早」的別名拿到後來那張單，條件跟著對調，結果不變，這個例子在 <a href="/blog/backend/01-database/sql/table-occurrence-and-alias/" data-link-title="SQL.7 表的出現與別名：表名的指稱、別名的作用範圍與自連接" data-link-desc="FROM 裡的表名指的是什麼、同名的兩次出現為什麼被引擎擋下、自連接的前後配對、同組比較與層級關係，以及別名取名的依據">SQL.7 表的出現與別名：表名的指稱、別名的作用範圍與自連接</a>）。這幾種落空在現有資料上都回正確答案，代價都落在下一個照著宣告動手的人身上：<code>DISTINCT</code> 指向錯成因，他多要一個欄位就回出錯的結果；被撤銷的 <code>LEFT</code> 讓他改了 <code>SELECT</code> 之後仍然少了沒有訂單的顧客，而原因在他沒有動的 <code>WHERE</code>；其餘幾種連錯的結果都不會出現，只讓他下錯判斷。</p>
<h2 id="被-where-撤銷的-left-join">被 WHERE 撤銷的 LEFT JOIN</h2>
<p>書店想找出下過大單的顧客。查詢寫成這樣：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">姓名</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">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="o">&gt;</span><span class="w"> </span><span class="mi">200</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">-- 佳穎、佳穎
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></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">-- 同一段把 LEFT JOIN 換成 JOIN，其餘不動
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">姓名</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">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"> 9</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">&gt;</span><span class="w"> </span><span class="mi">200</span><span class="p">;</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></code></pre></div><p><code>LEFT</code> 在這裡宣告「沒有訂單的顧客也要留著」。而下一行的 <code>WHERE</code> 問了一個右表欄位，宗翰與雅文那兩列右邊全是 <code>NULL</code>，<code>NULL &gt; 200</code> 答不出真，於是被篩掉，外連接與內連接兩段因此回同樣兩列。</p>
<p>機制在 <a href="/blog/backend/01-database/sql/on-describes-where-filters/" data-link-title="SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責" data-link-desc="條件寫在連接的配對階段與寫在關係組完之後的篩選階段的差別、外連接的保護在 WHERE 被抵銷的機制，以及判斷一個條件該放哪一邊的測試">SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責</a>：條件放 <code>ON</code> 參與配對，放 <code>WHERE</code> 篩成品，而外連接補上的那些列過不了成品這一關。這裡要看的是另一件事——<strong>這段查詢的行為是內連接，而它的文字說的是外連接</strong>。</p>
<p>下一個維護的人讀到 <code>LEFT</code>，會以為沒下單的顧客在結果裡。他想加一個「顯示零元」的欄位：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">姓名</span><span class="p">,</span><span class="w"> </span><span class="n">COALESCE</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="mi">0</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">-- 他加的欄位：沒有訂單的顧客顯示 0
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">顧客編號</span><span class="w">
</span></span></span><span class="line"><span class="ln">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="o">&gt;</span><span class="w"> </span><span class="mi">200</span><span class="p">;</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">-- 佳穎 300
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">-- 佳穎 500</span></span></span></code></pre></div><p>跑出來仍然只有佳穎，而他找不出原因——因為問題在他沒有動的那一行。</p>
<h2 id="cross-join-加等值條件與內連接的計畫">CROSS JOIN 加等值條件與內連接的計畫</h2>
<p>要比較同一批資料的兩列時，常見的寫法是把表跟自己配對再篩：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">今天</span><span class="p">.</span><span class="err">訂單編號</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="err">昨天</span><span class="w"> </span><span class="k">CROSS</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="err">今天</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">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="p">.</span><span class="err">金額</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="err">昨天</span><span class="p">.</span><span class="err">金額</span><span class="p">;</span></span></span></code></pre></div><p><code>CROSS JOIN</code> 宣告「我要每一種配對」，讀起來像是先造出所有配對再挑。而等值條件寫進去之後，<a href="/blog/backend/01-database/sql/knowledge-cards/query-optimizer/" data-link-title="Query Optimizer（查詢最佳化器）" data-link-desc="想知道是誰決定了執行順序、或為什麼同一段查詢換個時間變慢時">最佳化器</a>產出的計畫與內連接的計畫一模一樣。改寫成 <code>JOIN ... ON</code> 的版本把同一個條件搬進 <code>ON</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">今天</span><span class="p">.</span><span class="err">訂單編號</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="err">昨天</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="err">今天</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="k">ON</span><span class="w"> </span><span class="err">今天</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">昨天</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="err">今天</span><span class="p">.</span><span class="err">金額</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="err">昨天</span><span class="p">.</span><span class="err">金額</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 102（CROSS JOIN 那一段也回 102）</span></span></span></code></pre></div><p>同一組資料上兩種寫法的計畫逐字相同：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">-- CROSS JOIN 加 WHERE 等值（SQLite 3.51，共用的範例資料庫，沒有另外建索引）
</span></span><span class="line"><span class="ln">2</span><span class="cl">|--SCAN 昨天
</span></span><span class="line"><span class="ln">3</span><span class="cl">|--BLOOM FILTER ON 今天 (顧客編號=?)
</span></span><span class="line"><span class="ln">4</span><span class="cl">`--SEARCH 今天 USING AUTOMATIC COVERING INDEX (顧客編號=?)
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl">-- JOIN ... ON 同一個條件
</span></span><span class="line"><span class="ln">7</span><span class="cl">|--SCAN 昨天
</span></span><span class="line"><span class="ln">8</span><span class="cl">|--BLOOM FILTER ON 今天 (顧客編號=?)
</span></span><span class="line"><span class="ln">9</span><span class="cl">`--SEARCH 今天 USING AUTOMATIC COVERING INDEX (顧客編號=?)</span></span></code></pre></div><p><code>AUTOMATIC</code> 表示這個索引是引擎為了這一次查詢臨時建的——訂單表上沒有 <code>顧客編號</code> 的索引，而它判斷建一個比逐列掃描划算。兩段查詢連這個決定都一樣。</p>
<p><code>CROSS JOIN</code> 在代數上確實對應<a href="/blog/backend/01-database/sql/knowledge-cards/cartesian-product/" data-link-title="Cartesian Product（笛卡兒積）" data-link-desc="看到 CROSS JOIN、或結果的列數遠多於預期時，查這個運算什麼時候會被展開">笛卡兒積</a>，而那張卡寫明展開由條件決定：條件把兩邊綁起來時全部組合一次都沒有被造出來。</p>
<p><code>CROSS JOIN</code> 這個宣告的誤導方向與 <code>LEFT</code> 相反。<code>LEFT</code> 那一節是宣告承諾了引擎沒做的事，這一節是宣告讓讀的人誤判了代價——看到 <code>CROSS</code> 就以為它是列數的平方，於是花力氣改寫一段本來就是內連接的查詢。兩段文字比不出快慢，快慢要向引擎要計畫——<a href="/blog/backend/01-database/sql/cost-lives-in-the-plan/" data-link-title="SQL.17 查詢的代價：資料分布、索引與寫法各自的影響" data-link-desc="找重複值的分組、自連接與 EXISTS 在有無索引與不同重複次數下的實測時間與計畫，以及比較查詢快慢時要補上的資料量、分布、索引與統計資訊">SQL.17 查詢的代價</a> 在有無索引的兩種狀態下各量一次；而在這裡要先分清楚：<code>CROSS JOIN</code> 加等值條件與 <code>JOIN ... ON</code> 是同一段查詢的兩種寫法，換掉它省下的是讀的人的疑慮，不是執行時間。</p>
<h2 id="配對已有保證時的-left-join">配對已有保證時的 LEFT JOIN</h2>
<p>每一張訂單都帶著一個有效的顧客編號時，這樣寫也會過：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">訂單編號</span><span class="p">,</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">姓名</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">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></span></code></pre></div><p><code>LEFT</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">count</span><span class="p">(</span><span class="o">*</span><span class="p">),</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="err">顧客</span><span class="p">.</span><span class="err">姓名</span><span class="p">)</span><span class="w">   </span><span class="c1">-- count(*) 數列數，count(欄位) 只數那一欄不是 NULL 的列
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="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">-- 2  2：沒有任何一列是 LEFT 補出來的</span></span></span></code></pre></div><p>它的代價要等到有人相信它才出現。下一個人想查有沒有孤兒訂單（指不到任何顧客的訂單），順手接上這段查詢：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">訂單編號</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">姓名</span><span class="w"> </span><span class="k">IS</span><span class="w"> </span><span class="k">NULL</span><span class="p">;</span><span class="w">            </span><span class="c1">-- 配不到顧客的訂單，姓名會是 LEFT 補上的 NULL
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">-- 零列</span></span></span></code></pre></div><p>拿到零列，於是回報「資料乾淨」。零列在這裡有兩種來源：確實沒有孤兒訂單，或者這個宣告本來就沒有內容。這段查詢分不出兩者。</p>
<p>真正保證配對的是<a href="/blog/backend/01-database/sql/knowledge-cards/constraint/" data-link-title="Constraint（約束）" data-link-desc="看到「這個保證要來自約束而不是來自習慣」，或想確認某一欄會不會出現 NULL 時，查這一層由誰擋">約束</a>——外鍵存在時每一張訂單都指得到顧客，那時 <code>LEFT</code> 描述的是一個資料庫已經排除掉的情況。宣告與約束衝突時，讀的人多半相信宣告，因為它就在眼前，而約束在另一個檔案裡。</p>
<h2 id="收掉連接重複的-distinct">收掉連接重複的 DISTINCT</h2>
<p>顧客與訂單連起來之後，佳穎出現兩次，因為她有兩張訂單。要一份「下過單的顧客」名單，加 <code>DISTINCT</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">DISTINCT</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">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">顧客編號</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="c1">-- 佳穎</span></span></span></code></pre></div><p><code>DISTINCT</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>
<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="k">DISTINCT</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="err">顧客</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">顧客編號</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="err">顧客編號</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="c1">-- 佳穎 300
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">-- 佳穎 500</span></span></span></code></pre></div><p><code>DISTINCT</code> 作用在整列上，兩列的金額不同，於是它一列都收不掉。上一個人的修法在這裡失效，而失效的理由要回到「重複從哪裡來」這個問題——那個問題當初被 <code>DISTINCT</code> 跳過了。</p>
<h2 id="查證宣告的動作把關鍵字換成最弱的版本再跑一次">查證宣告的動作：把關鍵字換成最弱的版本再跑一次</h2>
<p>文字本身看不出宣告有沒有兌現——<code>LEFT</code> 寫在那裡，而它保護下來的列可能一列都不剩。差別要現形，得讓 <code>LEFT</code> 這類關鍵字消失一次：<strong>把它換成最弱的版本再跑一次，結果變不變就是它有沒有在做事。</strong> 這個動作在軟體測試裡叫 <a href="/blog/testing/knowledge-cards/mutation-testing/" data-link-title="Mutation Testing（突變測試）" data-link-desc="行覆蓋率很高卻仍漏掉 bug、或要挑一個比覆蓋率難灌水的測試品質指標時，用來判斷這套測試實際擋得住什麼">mutation testing</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>
<table>
  <thead>
      <tr>
          <th>宣告</th>
          <th>換成</th>
          <th>結果不變表示</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>LEFT JOIN</code> / <code>RIGHT JOIN</code></td>
          <td><code>JOIN</code></td>
          <td>在現有資料上行為是內連接</td>
      </tr>
      <tr>
          <td><code>CROSS JOIN</code> 加 <code>WHERE</code> 等值</td>
          <td><code>JOIN ... ON</code> 同一個條件</td>
          <td>這段本來就是內連接（限等值）</td>
      </tr>
      <tr>
          <td><code>DISTINCT</code></td>
          <td>拿掉</td>
          <td>這一組值原本就各不相同</td>
      </tr>
      <tr>
          <td><code>COALESCE(欄位, 預設值)</code></td>
          <td>直接用該欄位</td>
          <td>這一欄在現有資料上沒有空值</td>
      </tr>
  </tbody>
</table>
<p>結果變了的時候，換掉關鍵字這個動作只證明被換掉的那個關鍵字在做事，還沒證明宣告是對的。<code>DISTINCT</code> 那一格就是這種：拿掉之後結果會變，而變的理由是連接把列複製了，與「這一欄有重複值」無關。所以結果變了之後還要問：<strong>變化的成因與宣告說的是不是同一件事</strong>。</p>
<p>配對已有保證時的 <code>LEFT</code>（〈配對已有保證時的 LEFT JOIN〉那一節）是換掉關鍵字再跑一次這個動作測不出來的：配對由外鍵保證的時候，<code>LEFT</code> 與 <code>JOIN</code> 在現有資料上結果相同，而換掉之後仍然看不出宣告當初有沒有意義。那一種要去查<a href="/blog/backend/01-database/sql/knowledge-cards/constraint/" data-link-title="Constraint（約束）" data-link-desc="看到「這個保證要來自約束而不是來自習慣」，或想確認某一欄會不會出現 NULL 時，查這一層由誰擋">約束</a>，判斷標準是資料庫有沒有替這個配對背書——而約束寫進 schema 也不一定生效，各家在哪一步把寫下與生效分開，在 <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>。</p>
<p>對讀別人查詢的人來說，這個寫下與生效是否一致的差別回答的是「我可以相信這段文字說的話嗎」——而那個問題在讀的當下沒有別的入口。文字該先為哪一個讀者而寫——人還是引擎——是 <a href="/blog/backend/01-database/sql/readable-and-fast-mostly-align/" data-link-title="SQL.21 可讀性與執行代價：兩者一致與分岔的情形" data-link-desc="寫法差異在什麼情形下免費、什麼情形下分岔，以及分岔時該動查詢還是動 schema">SQL.21 可讀性與執行代價：兩者一致與分岔的情形</a> 的題目。</p>
]]></content:encoded></item></channel></rss>