<?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>Cross-Join on Tarragon</title><link>https://tarrragon.github.io/blog/tags/cross-join/</link><description>Recent content in Cross-Join 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/cross-join/index.xml" rel="self" type="application/rss+xml"/><item><title>SQL.3 連接的起手式：配對條件的位置與連接種類的選擇</title><link>https://tarrragon.github.io/blog/backend/01-database/sql/join-starts-from-the-relationship/</link><pubDate>Tue, 01 Sep 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/sql/join-starts-from-the-relationship/</guid><description>&lt;p>寫一個連接要先決定的是&lt;strong>哪兩列該配在一起&lt;/strong>，而 SQL 給了這個決定一個專屬的位置：&lt;code>ON&lt;/code>。本篇把這件事叫&lt;strong>配對&lt;/strong>——關係還在組的那個過程，哪一列跟哪一列湊成一列；這是為了指認它而取的稱呼，SQL 的標準術語裡沒有這個詞。條件寫進那裡，配對關係就成為查詢結構的一部分；條件留在 &lt;code>WHERE&lt;/code>，配對關係就退化成一道篩子。&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;/p>
&lt;p>這一篇處理的是&lt;strong>起手式&lt;/strong>——寫連接時第一個下筆的地方，而它決定了條件之後能放哪裡。&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;strong>成品&lt;/strong>（關係組完之後得到的那一批列）並排；本篇兩個詞都會用到，而判斷一個條件屬於配對還是成品的完整判定表在那一篇。順序上這一篇在前：先有配對關係，才有條件該放哪裡的問題。&lt;/p>
&lt;h2 id="從全部配對縮小的起手式與關聯代數的定義">從全部配對縮小的起手式與關聯代數的定義&lt;/h2>
&lt;p>&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;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/cartesian-product/" data-link-title="Cartesian Product（笛卡兒積）" data-link-desc="看到 CROSS JOIN、或結果的列數遠多於預期時，查這個運算什麼時候會被展開">笛卡兒積&lt;/a>，再用條件選取。所以下面這個寫法是把定義翻成 SQL：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">姓名&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">訂單編號&lt;/span>&lt;span class="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">CROSS&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>&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">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>它讀起來自然是有來由的——每一種配對先擺出來，再挑走不要的。而它是&lt;strong>語意層的定義&lt;/strong>：代數用它說明連接的結果集是什麼，沒有規定寫法要照這個順序展開。引擎也沒有照著做，這一段的計畫與 &lt;code>JOIN ... ON&lt;/code> 寫同一個條件逐字相同（&lt;a href="https://tarrragon.github.io/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 關鍵字的宣告與引擎的行為：宣告落空的形態與查證&lt;/a>）。&lt;/p>
&lt;p>這個起手式在內連接底下一直行得通，因為條件放 &lt;code>ON&lt;/code> 與放 &lt;code>WHERE&lt;/code> 在內連接底下回同一批列。行得通的經驗會累積，直到問題需要保留配不到的列。&lt;/p>
&lt;h2 id="從全部配對縮小與寫進-on-在外連接處的差別">從全部配對縮小與寫進 ON 在外連接處的差別&lt;/h2>
&lt;p>書店要一份名單：每位顧客與他 300 元以上的訂單，而沒有這種訂單的顧客也要列出來。&lt;/p>
&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">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">CROSS&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>&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">=&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">&amp;gt;=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">300&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">-- 佳穎 101 / 佳穎 102&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>宗翰與雅文不在名單上。&lt;code>CROSS JOIN&lt;/code> 這個關鍵字沒有外連接的形式，所以要讓宗翰與雅文這種沒有訂單的顧客也留在結果裡（這是外連接做的事，下文叫&lt;strong>保護&lt;/strong>：&lt;strong>被保護的表&lt;/strong>裡配不到對象的列照樣留下，另一張表——下文叫&lt;strong>補 &lt;code>NULL&lt;/code> 的表&lt;/strong>——的欄位在那一列補上 &lt;code>NULL&lt;/code>），這個起手式本身就得換掉。&lt;/p>
&lt;p>把 &lt;code>CROSS JOIN&lt;/code> 換成 &lt;code>LEFT JOIN&lt;/code> 而條件留在原處：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">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">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;gt;=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">300&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">-- 佳穎 101 / 佳穎 102&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>兩人仍然不在。&lt;code>LEFT&lt;/code> 保護下來的那些列右邊全是 &lt;code>NULL&lt;/code>，而 &lt;code>WHERE&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>）。&lt;strong>換掉關鍵字補不回那些列，因為要動的是條件的位置。&lt;/strong>&lt;/p>
&lt;p>把配對關係整個寫進 &lt;code>ON&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">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>&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">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">&amp;gt;=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">300&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">-- 佳穎 101 / 佳穎 102 / 宗翰 （空）/ 雅文 （空）&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>三位顧客都在。&lt;strong>從全部配對縮小與把配對關係寫進 &lt;code>ON&lt;/code> 這兩個起手式，差別在這裡從「寫法偏好」變成「答得出來與答不出來」&lt;/strong>：從全部配對縮小的起手式把所有條件都放進 &lt;code>WHERE&lt;/code>，在成品上篩，而外連接的保護正是在成品那一步被抵銷的。&lt;/p>
&lt;h2 id="連接種類的選擇配不到的列在答案裡有沒有位置">連接種類的選擇：配不到的列在答案裡有沒有位置&lt;/h2>
&lt;p>哪兩列該配在一起說完之後，接著要決定的是配不到對象的列怎麼處理。判斷標準是一句話：&lt;strong>這些列在答案裡有沒有位置。&lt;/strong>&lt;/p>
&lt;p>有位置就用外連接，而哪一側有位置決定寫 &lt;code>LEFT&lt;/code>、&lt;code>RIGHT&lt;/code> 還是 &lt;code>FULL&lt;/code>；沒有位置就用內連接（&lt;code>JOIN&lt;/code>）（&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/outer-join/" data-link-title="Outer Join（外連接）" data-link-desc="想確認 LEFT / RIGHT / FULL 各自保護哪一側、或連接之後有些人從名單上消失時">外連接&lt;/a>那張卡寫三者各保護誰）。「哪些顧客一張單都沒下」這個問題的答案整個在補 &lt;code>NULL&lt;/code> 的那幾列上，內連接會讓它無從回答。&lt;/p>
&lt;p>還有第三種情形：只問配不配得到，右表的欄位一個都不輸出。那時要的運算是半連接——配起來之後只帶走左邊那一份，右邊只當條件（&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/semi-join-and-anti-join/" data-link-title="Semi-join 與 Anti-join（半連接與反連接）" data-link-desc="看到 EXISTS 或 NOT EXISTS 時，查它在代數裡對應哪個運算、以及列數上限為什麼是左邊那張表的列數">Semi-join 與 Anti-join&lt;/a>）。寫成 &lt;code>JOIN&lt;/code> 會讓配得到多列的那些左列重複出現，&lt;a href="https://tarrragon.github.io/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&lt;/a> 寫這三種寫法在列數上的差別，以及選哪一種的判斷標準。&lt;/p></description><content:encoded><![CDATA[<p>寫一個連接要先決定的是<strong>哪兩列該配在一起</strong>，而 SQL 給了這個決定一個專屬的位置：<code>ON</code>。本篇把這件事叫<strong>配對</strong>——關係還在組的那個過程，哪一列跟哪一列湊成一列；這是為了指認它而取的稱呼，SQL 的標準術語裡沒有這個詞。條件寫進那裡，配對關係就成為查詢結構的一部分；條件留在 <code>WHERE</code>，配對關係就退化成一道篩子。</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）。</p>
<p>這一篇處理的是<strong>起手式</strong>——寫連接時第一個下筆的地方，而它決定了條件之後能放哪裡。<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> 處理的是條件寫下之後放哪一邊——那一篇會把配對與<strong>成品</strong>（關係組完之後得到的那一批列）並排；本篇兩個詞都會用到，而判斷一個條件屬於配對還是成品的完整判定表在那一篇。順序上這一篇在前：先有配對關係，才有條件該放哪裡的問題。</p>
<h2 id="從全部配對縮小的起手式與關聯代數的定義">從全部配對縮小的起手式與關聯代數的定義</h2>
<p><a href="/blog/backend/01-database/sql/knowledge-cards/relational-algebra/" data-link-title="Relational Algebra（關聯代數）" data-link-desc="看到「子句其實是某個運算」或想知道 SQL 的規則從哪裡來時，查這一層在底下的是什麼">關聯代數</a>把連接定義成兩個運算的組合：先取<a href="/blog/backend/01-database/sql/knowledge-cards/cartesian-product/" data-link-title="Cartesian Product（笛卡兒積）" data-link-desc="看到 CROSS JOIN、或結果的列數遠多於預期時，查這個運算什麼時候會被展開">笛卡兒積</a>，再用條件選取。所以下面這個寫法是把定義翻成 SQL：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">顧客</span><span class="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">CROSS</span><span class="w"> </span><span class="k">JOIN</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="p">;</span></span></span></code></pre></div><p>它讀起來自然是有來由的——每一種配對先擺出來，再挑走不要的。而它是<strong>語意層的定義</strong>：代數用它說明連接的結果集是什麼，沒有規定寫法要照這個順序展開。引擎也沒有照著做，這一段的計畫與 <code>JOIN ... ON</code> 寫同一個條件逐字相同（<a href="/blog/backend/01-database/sql/declared-intent-vs-behaviour/" data-link-title="SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證" data-link-desc="LEFT JOIN、CROSS JOIN、DISTINCT、COALESCE 與表的別名各自對讀的人宣告了什麼、宣告落空時下一個修改的人付出什麼代價，以及把關鍵字換成最弱的版本再跑一次的查證動作">SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證</a>）。</p>
<p>這個起手式在內連接底下一直行得通，因為條件放 <code>ON</code> 與放 <code>WHERE</code> 在內連接底下回同一批列。行得通的經驗會累積，直到問題需要保留配不到的列。</p>
<h2 id="從全部配對縮小與寫進-on-在外連接處的差別">從全部配對縮小與寫進 ON 在外連接處的差別</h2>
<p>書店要一份名單：每位顧客與他 300 元以上的訂單，而沒有這種訂單的顧客也要列出來。</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">FROM</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></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">=</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="mi">300</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">-- 佳穎 101 / 佳穎 102</span></span></span></code></pre></div><p>宗翰與雅文不在名單上。<code>CROSS JOIN</code> 這個關鍵字沒有外連接的形式，所以要讓宗翰與雅文這種沒有訂單的顧客也留在結果裡（這是外連接做的事，下文叫<strong>保護</strong>：<strong>被保護的表</strong>裡配不到對象的列照樣留下，另一張表——下文叫<strong>補 <code>NULL</code> 的表</strong>——的欄位在那一列補上 <code>NULL</code>），這個起手式本身就得換掉。</p>
<p>把 <code>CROSS JOIN</code> 換成 <code>LEFT JOIN</code> 而條件留在原處：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">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">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">&gt;=</span><span class="w"> </span><span class="mi">300</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">-- 佳穎 101 / 佳穎 102</span></span></span></code></pre></div><p>兩人仍然不在。<code>LEFT</code> 保護下來的那些列右邊全是 <code>NULL</code>，而 <code>WHERE</code> 那一步把它們篩掉了（<a href="/blog/backend/01-database/sql/on-describes-where-filters/" data-link-title="SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責" data-link-desc="條件寫在連接的配對階段與寫在關係組完之後的篩選階段的差別、外連接的保護在 WHERE 被抵銷的機制，以及判斷一個條件該放哪一邊的測試">SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責</a>）。<strong>換掉關鍵字補不回那些列，因為要動的是條件的位置。</strong></p>
<p>把配對關係整個寫進 <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">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></span><span class="line"><span class="ln">2</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="mi">300</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">-- 佳穎 101 / 佳穎 102 / 宗翰 （空）/ 雅文 （空）</span></span></span></code></pre></div><p>三位顧客都在。<strong>從全部配對縮小與把配對關係寫進 <code>ON</code> 這兩個起手式，差別在這裡從「寫法偏好」變成「答得出來與答不出來」</strong>：從全部配對縮小的起手式把所有條件都放進 <code>WHERE</code>，在成品上篩，而外連接的保護正是在成品那一步被抵銷的。</p>
<h2 id="連接種類的選擇配不到的列在答案裡有沒有位置">連接種類的選擇：配不到的列在答案裡有沒有位置</h2>
<p>哪兩列該配在一起說完之後，接著要決定的是配不到對象的列怎麼處理。判斷標準是一句話：<strong>這些列在答案裡有沒有位置。</strong></p>
<p>有位置就用外連接，而哪一側有位置決定寫 <code>LEFT</code>、<code>RIGHT</code> 還是 <code>FULL</code>；沒有位置就用內連接（<code>JOIN</code>）（<a href="/blog/backend/01-database/sql/knowledge-cards/outer-join/" data-link-title="Outer Join（外連接）" data-link-desc="想確認 LEFT / RIGHT / FULL 各自保護哪一側、或連接之後有些人從名單上消失時">外連接</a>那張卡寫三者各保護誰）。「哪些顧客一張單都沒下」這個問題的答案整個在補 <code>NULL</code> 的那幾列上，內連接會讓它無從回答。</p>
<p>還有第三種情形：只問配不配得到，右表的欄位一個都不輸出。那時要的運算是半連接——配起來之後只帶走左邊那一份，右邊只當條件（<a href="/blog/backend/01-database/sql/knowledge-cards/semi-join-and-anti-join/" data-link-title="Semi-join 與 Anti-join（半連接與反連接）" data-link-desc="看到 EXISTS 或 NOT EXISTS 時，查它在代數裡對應哪個運算、以及列數上限為什麼是左邊那張表的列數">Semi-join 與 Anti-join</a>）。寫成 <code>JOIN</code> 會讓配得到多列的那些左列重複出現，<a href="/blog/backend/01-database/sql/in-exists-join/" data-link-title="SQL.8 IN、EXISTS 與 JOIN：列數、可取用的欄位與 NULL 的處理" data-link-desc="同一個「找出有對應資料的那些」的問題用成員判斷、存在性判斷與連接各寫一次的結果，選用的判斷標準，以及子查詢含 NULL 時 NOT IN 與 NOT EXISTS 的分岔">SQL.8 IN、EXISTS 與 JOIN</a> 寫這三種寫法在列數上的差別，以及選哪一種的判斷標準。</p>
<p><strong>配不到的列要不要保護，這個決定由問題決定，不由現有資料決定。</strong> 「翻遍現在的訂單表，每一列都配得到顧客」只證明此刻如此，而查詢要活得比這一批資料久。反過來說，資料保證配得到的時候寫 <code>LEFT</code> 也有代價——它宣告了一個從未發生的情況，下一個讀的人會以為那些列存在（<a href="/blog/backend/01-database/sql/declared-intent-vs-behaviour/" data-link-title="SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證" data-link-desc="LEFT JOIN、CROSS JOIN、DISTINCT、COALESCE 與表的別名各自對讀的人宣告了什麼、宣告落空時下一個修改的人付出什麼代價，以及把關鍵字換成最弱的版本再跑一次的查證動作">SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證</a>）。要判斷配對有沒有保證，去看<a href="/blog/backend/01-database/sql/knowledge-cards/constraint/" data-link-title="Constraint（約束）" data-link-desc="看到「這個保證要來自約束而不是來自習慣」，或想確認某一欄會不會出現 NULL 時，查這一層由誰擋">約束</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>）。</p>
<h2 id="業務描述對應到查詢的位置on連接種類與-where">業務描述對應到查詢的位置：ON、連接種類與 WHERE</h2>
<p>「每位顧客與他 300 元以上的訂單，沒有這種訂單的顧客也要列出來」這句話裡有三個決定，各自對應查詢的一個位置。</p>
<p><strong>哪兩列該配在一起。</strong> 顧客的那一列與訂單的那一列，靠顧客編號對上，而且只跟 300 元以上的訂單配。這一整句進 <code>ON</code>——「只跟 300 元以上的訂單配」描述的是配對，所以它屬於那裡。</p>
<p><strong>配不到的列要不要。</strong> 「沒有這種訂單的顧客也要列出來」說了要，而要保護的是顧客那一側，所以是 <code>LEFT JOIN</code>，左邊放顧客。<strong>兩張表的時候這樣判就夠</strong>；三張以上時「左邊」是前面幾個連接累積出來的結果，鏈中間放一個 <code>RIGHT</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>。</p>
<p><strong>成品裡再篩什麼。</strong> 這句話沒有再提條件，所以 <code>WHERE</code> 是空的。業務描述裡沒有針對顧客那一側的限制時，空的 <code>WHERE</code> 是一個合理的結果。</p>
<p>順序反過來走也能檢查已經寫好的查詢：<code>WHERE</code> 裡引用右表欄位的條件，回頭問它描述的是配對，還是成品——多數形態上它描述的是配對，而它待在那裡正在抵銷某個保護。<strong>有一個形態剛好相反</strong>：<code>WHERE 右表.某欄 IS NULL</code> 挑的正是外連接補上 <code>NULL</code> 的那些列，它描述的是成品，待在 <code>WHERE</code> 是對的位置（<a href="/blog/backend/01-database/sql/on-describes-where-filters/" data-link-title="SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責" data-link-desc="條件寫在連接的配對階段與寫在關係組完之後的篩選階段的差別、外連接的保護在 WHERE 被抵銷的機制，以及判斷一個條件該放哪一邊的測試">SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責</a> 有完整的判定表，<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> 用它示範查孤兒訂單）。</p>
<h2 id="只提到被保護的表的條件該放的位置">只提到被保護的表的條件該放的位置</h2>
<p>上一節那句業務描述裡，「哪兩列該配在一起」與「配不到的列要不要」都在問條件與連接種類該怎麼配。而條件提到哪一側，會讓「哪兩列該配在一起」的答案整個不同。換一對表看得清楚。「每張訂單與它的評價，只要三月五日以後下的單」——被保護的是訂單，而「三月五日以後」只提到訂單。條件寫進 <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="p">,</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">下單日</span><span class="p">,</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">星等</span><span class="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></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="s1">&#39;2026-03-05&#39;</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">-- 101  2026-03-02  （空）
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">-- 102  2026-03-09  （空）</span></span></span></code></pre></div><p>三月二日那張照樣回來，篩選沒有發生——<code>LEFT</code> 的定義是左邊每一列都留著，而 <code>ON</code> 只決定右邊配不配得上，配不上就補 <code>NULL</code>。<strong>這個條件改變的是配對</strong>：101 本來配得上那筆五星評價，多了這個條件之後整個 <code>ON</code> 對它判假，於是連評價一起丟了。同一個條件改放 <code>WHERE</code> 才回到要的那批列：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">訂單編號</span><span class="p">,</span><span class="w"> </span><span class="err">訂單</span><span class="p">.</span><span class="err">下單日</span><span class="p">,</span><span class="w"> </span><span class="err">評價</span><span class="p">.</span><span class="err">星等</span><span class="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="s1">&#39;2026-03-05&#39;</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  2026-03-09  （空）</span></span></span></code></pre></div><p>只提到被保護的表的條件描述的是成品，屬於 <code>WHERE</code>。判斷標準因此要分兩步：先問這個條件提到哪幾張表，只提被保護的表的直接進 <code>WHERE</code>；兩張表都提到、或只提補 <code>NULL</code> 的表的，才問它描述配對還是成品。<strong>自連接寫成外連接時也是這樣</strong>。自連接指同一張表在一段查詢裡出現兩次、各自扮演一個角色，例如同一張訂單表一次代表較早的訂單、一次代表較晚的訂單；兩次出現裡被保護的那一次，它自己的過濾條件寫進 <code>ON</code> 同樣篩不掉列。配對的兩側是同一張表時，兩次出現要各自有名字才分得開，<a href="/blog/backend/01-database/sql/table-occurrence-and-alias/" data-link-title="SQL.7 表的出現與別名：表名的指稱、別名的作用範圍與自連接" data-link-desc="FROM 裡的表名指的是什麼、同名的兩次出現為什麼被引擎擋下、自連接的前後配對、同組比較與層級關係，以及別名取名的依據">SQL.7 表的出現與別名：表名的指稱、別名的作用範圍與自連接</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>。</p>
<p>所以空的 <code>WHERE</code> 不是一個要去追求的形狀。上一節那句業務描述留下空的 <code>WHERE</code>，是因為它確實沒有再提條件；把一個只提被保護的表的條件塞進 <code>ON</code> 去換一個空的 <code>WHERE</code>，換來的是一個篩不掉列、還會丟掉本來成立的配對的條件。</p>
]]></content:encoded></item><item><title>Cartesian Product（笛卡兒積）</title><link>https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/cartesian-product/</link><pubDate>Tue, 01 Sep 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/cartesian-product/</guid><description>&lt;p>笛卡兒積把兩個 &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;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;/p>
&lt;p>SQL 寫成 &lt;code>CROSS JOIN&lt;/code>，也可以在 &lt;code>FROM&lt;/code> 裡用逗號把兩張表隔開。&lt;/p>
&lt;h2 id="條件決定它會不會被展開">條件決定它會不會被展開&lt;/h2>
&lt;p>代數說連接從積開始，而那是語意層的定義，執行方式由&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/query-optimizer/" data-link-title="Query Optimizer（查詢最佳化器）" data-link-desc="想知道是誰決定了執行順序、或為什麼同一段查詢換個時間變慢時">最佳化器&lt;/a>按代價選。條件把兩邊綁起來的時候，它用索引或雜湊直接找出配得上的那些列，全部組合一次都沒有被造出來。&lt;/p>
&lt;p>條件缺席的時候展開才會發生。一張逐日的觀測表跟自己做沒有條件的 &lt;code>CROSS JOIN&lt;/code>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Weather&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INTEGER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">recordDate&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">temperature&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INT&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WITH&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">RECURSIVE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">n&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">i&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="p">(&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">UNION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ALL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">n&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">3890&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="k">INSERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Weather&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">date&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;2015-01-01&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">||&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39; day&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">%&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">40&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">n&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="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">UNIQUE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INDEX&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ix&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Weather&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">recordDate&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">-- SQLite 3.51，Weather 表 3891 列
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">SELECT count(*) FROM Weather a CROSS JOIN Weather b;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">-- 15139881
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">|--SCAN a USING COVERING INDEX ix
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">`--SCAN b USING COVERING INDEX ix&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>15139881 是 3891 的平方。外層每掃一列，內層就把整張表再走一遍，而這一千五百萬對配對在後面的子句拿到資料之前就要先產生出來。這張表加到 13891 列之後，列數變成 3.6 倍而配對數變成 192959881——&lt;strong>將近十三倍&lt;/strong>。成長是平方而非線性，這正是這個運算與其他多出來的列的差別。&lt;/p>
&lt;p>&lt;strong>要看的是條件把不把兩邊綁起來，而不是用了哪個關鍵字。&lt;/strong> &lt;code>CROSS JOIN&lt;/code> 加一個等值條件，與 &lt;code>JOIN ... ON&lt;/code> 寫同一個條件，對引擎是同一段查詢（&lt;a href="https://tarrragon.github.io/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 關鍵字的宣告與引擎的行為：宣告落空的形態與查證&lt;/a>）；反過來，&lt;code>JOIN&lt;/code> 寫了而 &lt;code>ON&lt;/code> 的條件只提到單邊的欄位，展開照樣發生。&lt;/p>
&lt;h2 id="與列數膨脹是兩種不同的多">與列數膨脹是兩種不同的多&lt;/h2>
&lt;p>結果比預期多的時候有兩個成因，分辨它們決定要改什麼。&lt;/p>
&lt;p>&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 上的行為">列數膨脹&lt;/a>是條件正確而一列配到了多列，於是它被複製那麼多次——多出來的量由資料的重複程度決定，改法是把展開的那一層先收成一列，或改用不展開的寫法。笛卡兒積是條件從一開始就沒有把兩邊綁住——多出來的量由兩張表各自的大小決定，改法是補上那個條件。&lt;/p>
&lt;p>分辨的問句落在條件上而非結果上：&lt;strong>這個條件把一列綁到對面幾列。&lt;/strong> 綁到常數量級（幾列、幾十列）是配對條件，多出來的量由重複程度決定，那是膨脹；綁到與表的大小同量級，那個條件篩掉了一些列，而每一列對面掛著的對象數仍然隨表一起長，多出來的量由兩張表的大小決定，那是積。&lt;/p>
&lt;p>「有沒有同時引用兩邊」不足以分辨。&lt;code>ON a.v &amp;gt; b.v&lt;/code> 同時引用了兩邊，而它把左邊每一列綁到右邊將近一半——&lt;code>v&lt;/code> 的值互不相同時，一千列得 499500 對、兩千列得 1999000 對，仍然是平方。低基數的等值鍵同理：&lt;code>ON a.區域 = b.區域&lt;/code> 只有幾個區域時，一千列對一千列得五十萬對。&lt;/p>
&lt;p>這裡刻意不問「這一列是靠哪個條件配起來的」——&lt;code>JOIN&lt;/code> 寫了而 &lt;code>ON&lt;/code> 只提單邊欄位的那個形態，讀者答得出「靠那個 &lt;code>ON&lt;/code> 條件」而它並沒有把兩邊配起來，問句會把積判成膨脹。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>笛卡兒積是連接的下限——條件寫得越少，結果越靠近它，而條件寫滿的時候結果落在兩張表配得上的那些列。所以「連接的結果集會不會整個換一個量級」這個問題等於「條件綁住了多少」，與用了 &lt;code>JOIN&lt;/code> 還是 &lt;code>CROSS JOIN&lt;/code> 無關。而條件綁不綁得住，由它引用到幾個&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 裡的表名指的是什麼、同名的兩次出現為什麼被引擎擋下、自連接的前後配對、同組比較與層級關係，以及別名取名的依據">表的出現&lt;/a>決定——與那個分辨問句是同一件事。&lt;/p>
&lt;p>它在&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;/p>
&lt;h2 id="往下走">往下走&lt;/h2>
&lt;p>條件正確而列數仍然變多的那一種，在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/join-changes-rows-and-nulls/" data-link-title="SQL.6 連接之後的列數與空缺：列數膨脹、外連接補的 NULL 與三值邏輯" data-link-desc="連接讓聚合多算的機制與計數、加總、平均、中位數各自的修法，以及外連接補出來的 NULL 在 count、等號比較、NOT IN 與 NOT EXISTS 上的行為">SQL.6 連接之後的列數與空缺&lt;/a>。同一批配對用自連接與 &lt;code>EXISTS&lt;/code> 兩種寫法的實測對照，在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/cost-lives-in-the-plan/" data-link-title="SQL.17 查詢的代價：資料分布、索引與寫法各自的影響" data-link-desc="找重複值的分組、自連接與 EXISTS 在有無索引與不同重複次數下的實測時間與計畫，以及比較查詢快慢時要補上的資料量、分布、索引與統計資訊">SQL.17 查詢的代價&lt;/a>。條件只用來判斷存在時結果的上限落在哪裡，在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/semi-join-and-anti-join/" data-link-title="Semi-join 與 Anti-join（半連接與反連接）" data-link-desc="看到 EXISTS 或 NOT EXISTS 時，查它在代數裡對應哪個運算、以及列數上限為什麼是左邊那張表的列數">Semi-join 與 Anti-join（半連接與反連接）&lt;/a>。&lt;code>CROSS JOIN&lt;/code> 這個關鍵字對讀的人宣告了什麼，在 &lt;a href="https://tarrragon.github.io/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 關鍵字的宣告與引擎的行為：宣告落空的形態與查證&lt;/a>。production 查詢的結果集大小怎麼治理，在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cardinality-explosion/" data-link-title="Query Cardinality Explosion" data-link-desc="Query 結果行數因 join / cross product / 條件缺失爆炸性放大的反模式">Cardinality Explosion&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>笛卡兒積把兩個 <a href="/blog/backend/01-database/sql/knowledge-cards/relation/" data-link-title="Relation（關係）" data-link-desc="看到 SQL 教材說「組成一個關係」而不確定它指的是不是資料表時">relation</a> 的每一列與另一邊的每一列各配一次，得到的關係列數是兩邊列數的乘積。它是<a href="/blog/backend/01-database/sql/knowledge-cards/relational-algebra/" data-link-title="Relational Algebra（關聯代數）" data-link-desc="看到「子句其實是某個運算」或想知道 SQL 的規則從哪裡來時，查這一層在底下的是什麼">關聯代數</a>的一個運算，而連接由它與選取組合而成：全部組合先在語意上成立，條件再從裡面挑出要留的那些。</p>
<p>SQL 寫成 <code>CROSS JOIN</code>，也可以在 <code>FROM</code> 裡用逗號把兩張表隔開。</p>
<h2 id="條件決定它會不會被展開">條件決定它會不會被展開</h2>
<p>代數說連接從積開始，而那是語意層的定義，執行方式由<a href="/blog/backend/01-database/sql/knowledge-cards/query-optimizer/" data-link-title="Query Optimizer（查詢最佳化器）" data-link-desc="想知道是誰決定了執行順序、或為什麼同一段查詢換個時間變慢時">最佳化器</a>按代價選。條件把兩邊綁起來的時候，它用索引或雜湊直接找出配得上的那些列，全部組合一次都沒有被造出來。</p>
<p>條件缺席的時候展開才會發生。一張逐日的觀測表跟自己做沒有條件的 <code>CROSS JOIN</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">Weather</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="w"> </span><span class="nb">INTEGER</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w"> </span><span class="n">recordDate</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">,</span><span class="w"> </span><span class="n">temperature</span><span class="w"> </span><span class="nb">INT</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">WITH</span><span class="w"> </span><span class="k">RECURSIVE</span><span class="w"> </span><span class="n">n</span><span class="p">(</span><span class="n">i</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="p">(</span><span class="k">SELECT</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="k">UNION</span><span class="w"> </span><span class="k">ALL</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">n</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">3890</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">Weather</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="n">i</span><span class="p">,</span><span class="w"> </span><span class="nb">date</span><span class="p">(</span><span class="s1">&#39;2015-01-01&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="s1">&#39; day&#39;</span><span class="p">),</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="mi">40</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">n</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">UNIQUE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">ix</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">Weather</span><span class="p">(</span><span class="n">recordDate</span><span class="p">);</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">-- SQLite 3.51，Weather 表 3891 列
</span></span><span class="line"><span class="ln">2</span><span class="cl">SELECT count(*) FROM Weather a CROSS JOIN Weather b;
</span></span><span class="line"><span class="ln">3</span><span class="cl">-- 15139881
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl">|--SCAN a USING COVERING INDEX ix
</span></span><span class="line"><span class="ln">6</span><span class="cl">`--SCAN b USING COVERING INDEX ix</span></span></code></pre></div><p>15139881 是 3891 的平方。外層每掃一列，內層就把整張表再走一遍，而這一千五百萬對配對在後面的子句拿到資料之前就要先產生出來。這張表加到 13891 列之後，列數變成 3.6 倍而配對數變成 192959881——<strong>將近十三倍</strong>。成長是平方而非線性，這正是這個運算與其他多出來的列的差別。</p>
<p><strong>要看的是條件把不把兩邊綁起來，而不是用了哪個關鍵字。</strong> <code>CROSS JOIN</code> 加一個等值條件，與 <code>JOIN ... ON</code> 寫同一個條件，對引擎是同一段查詢（<a href="/blog/backend/01-database/sql/declared-intent-vs-behaviour/" data-link-title="SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證" data-link-desc="LEFT JOIN、CROSS JOIN、DISTINCT、COALESCE 與表的別名各自對讀的人宣告了什麼、宣告落空時下一個修改的人付出什麼代價，以及把關鍵字換成最弱的版本再跑一次的查證動作">SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證</a>）；反過來，<code>JOIN</code> 寫了而 <code>ON</code> 的條件只提到單邊的欄位，展開照樣發生。</p>
<h2 id="與列數膨脹是兩種不同的多">與列數膨脹是兩種不同的多</h2>
<p>結果比預期多的時候有兩個成因，分辨它們決定要改什麼。</p>
<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 上的行為">列數膨脹</a>是條件正確而一列配到了多列，於是它被複製那麼多次——多出來的量由資料的重複程度決定，改法是把展開的那一層先收成一列，或改用不展開的寫法。笛卡兒積是條件從一開始就沒有把兩邊綁住——多出來的量由兩張表各自的大小決定，改法是補上那個條件。</p>
<p>分辨的問句落在條件上而非結果上：<strong>這個條件把一列綁到對面幾列。</strong> 綁到常數量級（幾列、幾十列）是配對條件，多出來的量由重複程度決定，那是膨脹；綁到與表的大小同量級，那個條件篩掉了一些列，而每一列對面掛著的對象數仍然隨表一起長，多出來的量由兩張表的大小決定，那是積。</p>
<p>「有沒有同時引用兩邊」不足以分辨。<code>ON a.v &gt; b.v</code> 同時引用了兩邊，而它把左邊每一列綁到右邊將近一半——<code>v</code> 的值互不相同時，一千列得 499500 對、兩千列得 1999000 對，仍然是平方。低基數的等值鍵同理：<code>ON a.區域 = b.區域</code> 只有幾個區域時，一千列對一千列得五十萬對。</p>
<p>這裡刻意不問「這一列是靠哪個條件配起來的」——<code>JOIN</code> 寫了而 <code>ON</code> 只提單邊欄位的那個形態，讀者答得出「靠那個 <code>ON</code> 條件」而它並沒有把兩邊配起來，問句會把積判成膨脹。</p>
<h2 id="概念位置">概念位置</h2>
<p>笛卡兒積是連接的下限——條件寫得越少，結果越靠近它，而條件寫滿的時候結果落在兩張表配得上的那些列。所以「連接的結果集會不會整個換一個量級」這個問題等於「條件綁住了多少」，與用了 <code>JOIN</code> 還是 <code>CROSS JOIN</code> 無關。而條件綁不綁得住，由它引用到幾個<a href="/blog/backend/01-database/sql/table-occurrence-and-alias/" data-link-title="SQL.7 表的出現與別名：表名的指稱、別名的作用範圍與自連接" data-link-desc="FROM 裡的表名指的是什麼、同名的兩次出現為什麼被引擎擋下、自連接的前後配對、同組比較與層級關係，以及別名取名的依據">表的出現</a>決定——與那個分辨問句是同一件事。</p>
<p>它在<a href="/blog/backend/01-database/sql/knowledge-cards/relational-algebra/" data-link-title="Relational Algebra（關聯代數）" data-link-desc="看到「子句其實是某個運算」或想知道 SQL 的規則從哪裡來時，查這一層在底下的是什麼">關聯代數</a>裡的位置說明了漏寫連接條件的後果為什麼這麼大：查詢退回它的定義起點，全部組合重新成立。這是結果集大小的整個量級換掉，與逐步變慢是不同的一件事。</p>
<h2 id="往下走">往下走</h2>
<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 連接之後的列數與空缺</a>。同一批配對用自連接與 <code>EXISTS</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>。條件只用來判斷存在時結果的上限落在哪裡，在 <a href="/blog/backend/01-database/sql/knowledge-cards/semi-join-and-anti-join/" data-link-title="Semi-join 與 Anti-join（半連接與反連接）" data-link-desc="看到 EXISTS 或 NOT EXISTS 時，查它在代數裡對應哪個運算、以及列數上限為什麼是左邊那張表的列數">Semi-join 與 Anti-join（半連接與反連接）</a>。<code>CROSS JOIN</code> 這個關鍵字對讀的人宣告了什麼，在 <a href="/blog/backend/01-database/sql/declared-intent-vs-behaviour/" data-link-title="SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證" data-link-desc="LEFT JOIN、CROSS JOIN、DISTINCT、COALESCE 與表的別名各自對讀的人宣告了什麼、宣告落空時下一個修改的人付出什麼代價，以及把關鍵字換成最弱的版本再跑一次的查證動作">SQL.20 關鍵字的宣告與引擎的行為：宣告落空的形態與查證</a>。production 查詢的結果集大小怎麼治理，在 <a href="/blog/backend/knowledge-cards/cardinality-explosion/" data-link-title="Query Cardinality Explosion" data-link-desc="Query 結果行數因 join / cross product / 條件缺失爆炸性放大的反模式">Cardinality Explosion</a>。</p>
]]></content:encoded></item></channel></rss>