<?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>Left-Join on Tarragon</title><link>https://tarrragon.github.io/blog/tags/left-join/</link><description>Recent content in Left-Join on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 31 Aug 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/left-join/index.xml" rel="self" type="application/rss+xml"/><item><title>SQL.4 鏈式連接的左運算元：累積方式與外連接的保護範圍</title><link>https://tarrragon.github.io/blog/backend/01-database/sql/join-left-operand-accumulates/</link><pubDate>Mon, 31 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/sql/join-left-operand-accumulates/</guid><description>&lt;p>&lt;code>FROM&lt;/code> 子句由上往下把表併進來，每一個 &lt;code>JOIN&lt;/code> 的左運算元是它上面全部合起來的結果。第一個 &lt;code>JOIN&lt;/code> 的左邊剛好是一張表，從第二個開始就不是了。&lt;/p>
&lt;p>左運算元會累積這件事決定三件事：每個 &lt;code>ON&lt;/code> 引用得到哪些欄位、每個 &lt;code>LEFT&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>LEFT&lt;/code> 左邊配不到對象的列也留在結果裡、右邊那幾欄補 &lt;code>NULL&lt;/code>）、以及在鏈中間換成 &lt;code>RIGHT&lt;/code> 會發生什麼。&lt;/p>
&lt;p>累積這件事在只有一個 &lt;code>JOIN&lt;/code> 的查詢裡不現形，從第二個開始才有分量：某個 &lt;code>LEFT&lt;/code> 保護了哪些列，取決於它上面累積出來的是什麼。&lt;/p>
&lt;h2 id="左運算元的累積與判讀方法">左運算元的累積與判讀方法&lt;/h2>
&lt;p>書店的顧客有三位——佳穎下過兩張單，宗翰與雅文一張都沒有，而兩張單裡只有一張被評價過。要一份完整名單：每位顧客連同他的訂單與那張訂單的評價，沒下單的顧客與沒被評價的訂單也要出現。三張表往下接：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">姓名&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">訂單編號&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span 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>&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">LEFT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">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="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>回來四列：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">佳穎 101 5
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">佳穎 102 NULL
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">宗翰 NULL NULL
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">雅文 NULL NULL&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第二個 &lt;code>LEFT JOIN&lt;/code> 的左邊是顧客與訂單併出來的結果，包含宗翰與雅文那兩列補 NULL 的訂單欄。問第二個 &lt;code>LEFT JOIN&lt;/code>「左邊是哪一張表」得不到答案，因為左邊已經不是一張表。&lt;strong>判讀的作法回到查詢的文字：在要判讀的那個 &lt;code>JOIN&lt;/code> 所在的那一行上方畫一條橫線，橫線以上寫到的表合起來是左運算元，那一行自己掛的那張表是右運算元。&lt;/strong>&lt;/p>
&lt;p>由此得到兩個可以直接用的結論。第二個 &lt;code>ON&lt;/code> 寫得出顧客表的欄位，因為顧客在左運算元裡面。第二個 &lt;code>JOIN&lt;/code> 若換成 &lt;code>INNER&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="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>&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">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="c1">-- 橫線以上：佳穎 101、佳穎 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">&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="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">5&lt;/span>&lt;span class="cl">&lt;span class="c1">-- 佳穎 101 5&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>他們那兩列訂單欄裡的 &lt;code>NULL&lt;/code> 是連接的產物，不是資料裡本來的空值，而後續對這欄做的計數與比較分不出這件事。&lt;code>LEFT&lt;/code> 保護左側、&lt;code>RIGHT&lt;/code> 保護右側、&lt;code>FULL&lt;/code> 兩側都保護，這三種方向與這些 &lt;code>NULL&lt;/code> 的來源定義在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/outer-join/" data-link-title="Outer Join（外連接）" data-link-desc="想確認 LEFT / RIGHT / FULL 各自保護哪一側、或連接之後有些人從名單上消失時">Outer Join（外連接）&lt;/a>；這些 &lt;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> 接手。&lt;/p>
&lt;h2 id="鏈中間的-right-join-保護的對象">鏈中間的 RIGHT JOIN 保護的對象&lt;/h2>
&lt;p>把最後一個 &lt;code>LEFT&lt;/code> 換成 &lt;code>RIGHT&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>&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">RIGHT&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>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>結果從四列變成一列，只剩 &lt;code>佳穎 101 5&lt;/code>。&lt;/p>
&lt;p>&lt;code>RIGHT&lt;/code> 保護的是右運算元，而右運算元是評價這一張表。左運算元——顧客與訂單累積出來的那四列——整批降級成可選的，配不上評價的就丟掉。宗翰、雅文、以及佳穎那張沒被評價的訂單，全部消失。&lt;/p>
&lt;p>&lt;strong>語意上這等於宣告「每一則評價都要留著，而顧客與訂單可有可無」。&lt;/strong> 書店大概不會想問這個問題。它幾乎不會是任何人在鏈尾接上評價那個 &lt;code>JOIN&lt;/code> 時想表達的事，而它讀起來只像是把 &lt;code>LEFT&lt;/code> 打成了 &lt;code>RIGHT&lt;/code>。&lt;/p>
&lt;h2 id="只用-left-join-的鏈保護方向的判讀與-full-join-的例外">只用 LEFT JOIN 的鏈：保護方向的判讀與 FULL JOIN 的例外&lt;/h2>
&lt;p>不用 &lt;code>RIGHT&lt;/code> 的話，鏈裡每個 &lt;code>JOIN&lt;/code> 都是同一個方向：要保護的那張表寫在最上面，往下一行一行掛可選的表。要判斷查詢裡任何一個 &lt;code>JOIN&lt;/code> 保護了什麼，在它那一行上方畫橫線就夠了。&lt;/p>
&lt;p>真正需要保護的表放到 &lt;code>FROM&lt;/code> 之後、寫成鏈最上面那一張就好。&lt;/p>
&lt;p>&lt;strong>兩張表都需要保護的時候這個做法不適用&lt;/strong>，那時要的是 &lt;code>FULL JOIN&lt;/code>——它兩側都保護，配不上的兩邊各補一次 &lt;code>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="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">姓名&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">訂單編號&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span 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>&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">LEFT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">訂單編號&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">評價&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="err">訂單編號&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">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="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 佳穎 101 5，與鏈尾換成 RIGHT 的那段查詢是同一個列集合&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段查詢的每個 &lt;code>JOIN&lt;/code> 都是 &lt;code>LEFT&lt;/code>，鏈的方向沒有中途反轉。&lt;/p></description><content:encoded><![CDATA[<p><code>FROM</code> 子句由上往下把表併進來，每一個 <code>JOIN</code> 的左運算元是它上面全部合起來的結果。第一個 <code>JOIN</code> 的左邊剛好是一張表，從第二個開始就不是了。</p>
<p>左運算元會累積這件事決定三件事：每個 <code>ON</code> 引用得到哪些欄位、每個 <code>LEFT</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>LEFT</code> 左邊配不到對象的列也留在結果裡、右邊那幾欄補 <code>NULL</code>）、以及在鏈中間換成 <code>RIGHT</code> 會發生什麼。</p>
<p>累積這件事在只有一個 <code>JOIN</code> 的查詢裡不現形，從第二個開始才有分量：某個 <code>LEFT</code> 保護了哪些列，取決於它上面累積出來的是什麼。</p>
<h2 id="左運算元的累積與判讀方法">左運算元的累積與判讀方法</h2>
<p>書店的顧客有三位——佳穎下過兩張單，宗翰與雅文一張都沒有，而兩張單裡只有一張被評價過。要一份完整名單：每位顧客連同他的訂單與那張訂單的評價，沒下單的顧客與沒被評價的訂單也要出現。三張表往下接：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">顧客</span><span class="p">.</span><span class="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></span><span class="line"><span class="ln">3</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">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="p">;</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">佳穎  101   5
</span></span><span class="line"><span class="ln">2</span><span class="cl">佳穎  102   NULL
</span></span><span class="line"><span class="ln">3</span><span class="cl">宗翰  NULL  NULL
</span></span><span class="line"><span class="ln">4</span><span class="cl">雅文  NULL  NULL</span></span></code></pre></div><p>第二個 <code>LEFT JOIN</code> 的左邊是顧客與訂單併出來的結果，包含宗翰與雅文那兩列補 NULL 的訂單欄。問第二個 <code>LEFT JOIN</code>「左邊是哪一張表」得不到答案，因為左邊已經不是一張表。<strong>判讀的作法回到查詢的文字：在要判讀的那個 <code>JOIN</code> 所在的那一行上方畫一條橫線，橫線以上寫到的表合起來是左運算元，那一行自己掛的那張表是右運算元。</strong></p>
<p>由此得到兩個可以直接用的結論。第二個 <code>ON</code> 寫得出顧客表的欄位，因為顧客在左運算元裡面。第二個 <code>JOIN</code> 若換成 <code>INNER</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></span><span class="line"><span class="ln">3</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="c1">-- 橫線以上：佳穎 101、佳穎 102、宗翰、雅文四列
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></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 class="c1">-- 內連接：四列裡配不到評價的三列一起砍掉
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">-- 佳穎  101  5</span></span></span></code></pre></div><p>他們那兩列訂單欄裡的 <code>NULL</code> 是連接的產物，不是資料裡本來的空值，而後續對這欄做的計數與比較分不出這件事。<code>LEFT</code> 保護左側、<code>RIGHT</code> 保護右側、<code>FULL</code> 兩側都保護，這三種方向與這些 <code>NULL</code> 的來源定義在 <a href="/blog/backend/01-database/sql/knowledge-cards/outer-join/" data-link-title="Outer Join（外連接）" data-link-desc="想確認 LEFT / RIGHT / FULL 各自保護哪一側、或連接之後有些人從名單上消失時">Outer Join（外連接）</a>；這些 <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> 接手。</p>
<h2 id="鏈中間的-right-join-保護的對象">鏈中間的 RIGHT JOIN 保護的對象</h2>
<p>把最後一個 <code>LEFT</code> 換成 <code>RIGHT</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></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">RIGHT</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></span></code></pre></div><p>結果從四列變成一列，只剩 <code>佳穎 101 5</code>。</p>
<p><code>RIGHT</code> 保護的是右運算元，而右運算元是評價這一張表。左運算元——顧客與訂單累積出來的那四列——整批降級成可選的，配不上評價的就丟掉。宗翰、雅文、以及佳穎那張沒被評價的訂單，全部消失。</p>
<p><strong>語意上這等於宣告「每一則評價都要留著，而顧客與訂單可有可無」。</strong> 書店大概不會想問這個問題。它幾乎不會是任何人在鏈尾接上評價那個 <code>JOIN</code> 時想表達的事，而它讀起來只像是把 <code>LEFT</code> 打成了 <code>RIGHT</code>。</p>
<h2 id="只用-left-join-的鏈保護方向的判讀與-full-join-的例外">只用 LEFT JOIN 的鏈：保護方向的判讀與 FULL JOIN 的例外</h2>
<p>不用 <code>RIGHT</code> 的話，鏈裡每個 <code>JOIN</code> 都是同一個方向：要保護的那張表寫在最上面，往下一行一行掛可選的表。要判斷查詢裡任何一個 <code>JOIN</code> 保護了什麼，在它那一行上方畫橫線就夠了。</p>
<p>真正需要保護的表放到 <code>FROM</code> 之後、寫成鏈最上面那一張就好。</p>
<p><strong>兩張表都需要保護的時候這個做法不適用</strong>，那時要的是 <code>FULL JOIN</code>——它兩側都保護，配不上的兩邊各補一次 <code>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="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></span><span class="line"><span class="ln">3</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">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="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- 佳穎  101  5，與鏈尾換成 RIGHT 的那段查詢是同一個列集合</span></span></span></code></pre></div><p>這段查詢的每個 <code>JOIN</code> 都是 <code>LEFT</code>，鏈的方向沒有中途反轉。</p>
<h2 id="書寫的左右與引擎的執行順序">書寫的左右與引擎的執行順序</h2>
<p>左右是書寫位置決定的語意，與引擎實際先處理誰無關。同一個連接寫成 <code>FROM 訂單 JOIN 顧客</code> 與 <code>FROM 顧客 JOIN 訂單</code>，在跑過 <code>ANALYZE</code> 的資料庫上，SQLite 兩次給出完全相同的 <a href="/blog/backend/01-database/sql/knowledge-cards/query-plan/" data-link-title="Query Plan（執行計畫）" data-link-desc="想知道一段查詢實際會怎麼被執行、或被要求「先看計畫」時">query plan</a>——它掃二十萬列的訂單、對五十列的顧客做查找，與誰寫在前面無關。沒有統計時它會改照文字順序走，而那只是它沒有別的資訊時的退路——<a href="/blog/backend/01-database/sql/declarative-not-procedural/" data-link-title="SQL.1 宣告式的紅利與代價：書寫、求值與執行順序各自由誰決定" data-link-desc="SQL 寫結果而不寫算法帶來的後果：同一個問題換書寫順序之後在有無統計資訊時拿到的計畫、求值順序擋下的寫法，以及代價為什麼要向引擎問">SQL.1 宣告式的紅利與代價</a> 對照有無統計資訊時引擎怎麼選計畫。</p>
<p><strong>左右決定哪一側的列被保護，執行順序決定誰先被掃，兩者互不相干。</strong> 混用這兩個直覺會讓人以為調換書寫順序可以調效能，或以為從查詢文字看得出引擎的動作。</p>
<h2 id="where-引用鏈尾那張表時被撤掉保護的列">WHERE 引用鏈尾那張表時被撤掉保護的列</h2>
<p><code>WHERE</code> 篩的是整條鏈累積完的那四列。條件引用鏈尾評價表的欄位時，佳穎的 102、宗翰與雅文那三列的評價欄是補出來的 <code>NULL</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">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></span><span class="line"><span class="ln">3</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">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">=</span><span class="w"> </span><span class="mi">5</span><span class="p">;</span><span class="w">   </span><span class="c1">-- 補出來的 NULL 判成未知，那三列被篩掉
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">-- 佳穎  101  5</span></span></span></code></pre></div><p>在這份資料上，消失的正好是把第二個 <code>JOIN</code> 換成 <code>INNER</code> 時消失的同一批列。所以判讀一個 <code>WHERE</code> 條件撤掉了誰，同樣回到累積：它引用的表掛在鏈的哪一行，那一行以上累積進來、在那張表上配不到的列都在它的射程裡。條件對 <code>NULL</code> 判成什麼決定撤不撤得掉，哪幾種條件形態保得住保護，在 <a href="/blog/backend/01-database/sql/on-describes-where-filters/" data-link-title="SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責" data-link-desc="條件寫在連接的配對階段與寫在關係組完之後的篩選階段的差別、外連接的保護在 WHERE 被抵銷的機制，以及判斷一個條件該放哪一邊的測試">SQL.5 ON 與 WHERE：連接條件與篩選條件各自的職責</a>。</p>
]]></content:encoded></item></channel></rss>