<?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>Declarative on Tarragon</title><link>https://tarrragon.github.io/blog/tags/declarative/</link><description>Recent content in Declarative 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/declarative/index.xml" rel="self" type="application/rss+xml"/><item><title>SQL.1 宣告式的紅利與代價：書寫、求值與執行順序各自由誰決定</title><link>https://tarrragon.github.io/blog/backend/01-database/sql/declarative-not-procedural/</link><pubDate>Mon, 31 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/sql/declarative-not-procedural/</guid><description>&lt;p>SQL 寫的是要什麼結果，不是怎麼算出來。這一條是這個語言其餘每個特徵的來源，包括它最容易讓人讀錯的那些。它最直接的一個後果是：同一個問題換兩種寫法送進去回來的結果一模一樣，而引擎在底下做的事可能完全不同——差在哪裡，查詢的文字一個字都沒說。&lt;/p>
&lt;p>&lt;strong>與逐步執行的程式對照一次，這個性質最清楚。&lt;/strong> 用 pandas 或任何一種 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/dataframe/" data-link-title="DataFrame（資料框）" data-link-desc="SQL 教材拿 pandas 或資料框當對照時，查 DataFrame 是什麼、它與資料表差在哪">DataFrame&lt;/a> 寫「找出下過單的顧客」，寫下的是一串動作：先載入這張表、再依條件篩選、再與另一張表合併，每一行都指定了下一步做什麼，而執行就照那個順序發生。SQL 寫下的是一段描述——哪些表參與、配對的條件是什麼、要留下哪些列——至於先讀哪一張、用什麼演算法配對、要不要臨時建索引，一個字都沒說。&lt;/p>
&lt;p>紅利是描述與實作脫鉤。一段查詢在一萬列與一億列的資料上都不必改寫，換一台機器、換一個索引配置也不必改寫，因為怎麼算從來就不在寫下的查詢文字裡面。代價是紅利的另一面——怎麼算不在寫的東西裡面，所以從查詢的文字看不出它要花多少。&lt;/p>
&lt;h2 id="書寫順序求值順序與執行順序">書寫順序、求值順序與執行順序&lt;/h2>
&lt;p>&lt;strong>書寫順序&lt;/strong>是子句在文字上的先後：&lt;code>SELECT&lt;/code> 在最前面，&lt;code>FROM&lt;/code> 跟在後面，&lt;code>WHERE&lt;/code>、&lt;code>GROUP BY&lt;/code>、&lt;code>HAVING&lt;/code>、&lt;code>ORDER BY&lt;/code> 依序往下。這個順序是給人讀的，它的作用是讓查詢讀起來像一句話。&lt;/p>
&lt;p>&lt;strong>求值順序&lt;/strong>是語意模型規定的先後。它與書寫順序不同：先由 &lt;code>FROM&lt;/code> 決定要處理哪個資料集合，再用 &lt;code>WHERE&lt;/code> 篩掉列，接著 &lt;code>GROUP BY&lt;/code> 分組、&lt;code>HAVING&lt;/code> 篩掉組，最後才輪到 &lt;code>SELECT&lt;/code> 把要輸出的欄位算出來、&lt;code>ORDER BY&lt;/code> 排序。寫在最前面的 &lt;code>SELECT&lt;/code> 在語意上排在倒數第二步。&lt;/p>
&lt;p>&lt;strong>執行順序&lt;/strong>是引擎實際做的事，由 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/query-optimizer/" data-link-title="Query Optimizer（查詢最佳化器）" data-link-desc="想知道是誰決定了執行順序、或為什麼同一段查詢換個時間變慢時">query optimizer&lt;/a> 決定，而最佳化器只受一個約束：輸出的列集合要與語意模型算出來的一致。在這個約束之內，它可以任意重排、可以換演算法、可以臨時建資料結構。&lt;/p>
&lt;p>書寫順序、求值順序、執行順序這三者不一致，是宣告式這個選擇本身帶來的。讀查詢時的多數誤判來自把其中兩種當成同一種——最常見的是拿書寫順序去猜執行順序，以為「寫在前面的表會先被讀」。&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="k">count&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">訂單&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">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">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">count&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">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;code>200000&lt;/code>。看它們的 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/query-plan/" data-link-title="Query Plan（執行計畫）" data-link-desc="想知道一段查詢實際會怎麼被執行、或被要求「先看計畫」時">query plan&lt;/a> 要用 &lt;code>EXPLAIN QUERY PLAN&lt;/code>（SQLite 的 &lt;code>EXPLAIN&lt;/code> 給的是逐行 bytecode，不是計畫），而計畫取決於引擎有沒有這兩張表的統計資訊。&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">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="err">顧客&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">顧客編號&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 class="err">姓名&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>&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">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="err">訂單&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="err">訂單編號&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 class="err">顧客編號&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 class="err">金額&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">3&lt;/span>&lt;span class="cl">&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">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">1&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">50&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="k">INSERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">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="s1">&amp;#39;顧客&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">||&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">i&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">6&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&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">1&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">200000&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">8&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="err">訂單&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="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">50&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="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">900&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>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>剛建好、還沒統計過的資料庫&lt;/strong>上，SQLite 給出兩個不同的計畫：&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">-- EXPLAIN QUERY PLAN SELECT count(*) FROM 訂單 JOIN 顧客 ON 訂單.顧客編號 = 顧客.顧客編號;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">|--SCAN 訂單
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">|--BLOOM FILTER ON 顧客 (顧客編號=?)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">`--SEARCH 顧客 USING AUTOMATIC COVERING INDEX (顧客編號=?)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">-- EXPLAIN QUERY PLAN SELECT count(*) FROM 顧客 JOIN 訂單 ON 訂單.顧客編號 = 顧客.顧客編號;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">|--SCAN 顧客
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">|--BLOOM FILTER ON 訂單 (顧客編號=?)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">`--SEARCH 訂單 USING AUTOMATIC COVERING INDEX (顧客編號=?)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>沒有統計就不知道哪張表大，於是它退回去按查詢文字裡兩張表出現的先後來決定計畫：&lt;code>顧客 JOIN 訂單&lt;/code> 那種寫法掃五十列的顧客，為了查找而當場替二十萬列的訂單建一個索引。&lt;/p></description><content:encoded><![CDATA[<p>SQL 寫的是要什麼結果，不是怎麼算出來。這一條是這個語言其餘每個特徵的來源，包括它最容易讓人讀錯的那些。它最直接的一個後果是：同一個問題換兩種寫法送進去回來的結果一模一樣，而引擎在底下做的事可能完全不同——差在哪裡，查詢的文字一個字都沒說。</p>
<p><strong>與逐步執行的程式對照一次，這個性質最清楚。</strong> 用 pandas 或任何一種 <a href="/blog/backend/01-database/sql/knowledge-cards/dataframe/" data-link-title="DataFrame（資料框）" data-link-desc="SQL 教材拿 pandas 或資料框當對照時，查 DataFrame 是什麼、它與資料表差在哪">DataFrame</a> 寫「找出下過單的顧客」，寫下的是一串動作：先載入這張表、再依條件篩選、再與另一張表合併，每一行都指定了下一步做什麼，而執行就照那個順序發生。SQL 寫下的是一段描述——哪些表參與、配對的條件是什麼、要留下哪些列——至於先讀哪一張、用什麼演算法配對、要不要臨時建索引，一個字都沒說。</p>
<p>紅利是描述與實作脫鉤。一段查詢在一萬列與一億列的資料上都不必改寫，換一台機器、換一個索引配置也不必改寫，因為怎麼算從來就不在寫下的查詢文字裡面。代價是紅利的另一面——怎麼算不在寫的東西裡面，所以從查詢的文字看不出它要花多少。</p>
<h2 id="書寫順序求值順序與執行順序">書寫順序、求值順序與執行順序</h2>
<p><strong>書寫順序</strong>是子句在文字上的先後：<code>SELECT</code> 在最前面，<code>FROM</code> 跟在後面，<code>WHERE</code>、<code>GROUP BY</code>、<code>HAVING</code>、<code>ORDER BY</code> 依序往下。這個順序是給人讀的，它的作用是讓查詢讀起來像一句話。</p>
<p><strong>求值順序</strong>是語意模型規定的先後。它與書寫順序不同：先由 <code>FROM</code> 決定要處理哪個資料集合，再用 <code>WHERE</code> 篩掉列，接著 <code>GROUP BY</code> 分組、<code>HAVING</code> 篩掉組，最後才輪到 <code>SELECT</code> 把要輸出的欄位算出來、<code>ORDER BY</code> 排序。寫在最前面的 <code>SELECT</code> 在語意上排在倒數第二步。</p>
<p><strong>執行順序</strong>是引擎實際做的事，由 <a href="/blog/backend/01-database/sql/knowledge-cards/query-optimizer/" data-link-title="Query Optimizer（查詢最佳化器）" data-link-desc="想知道是誰決定了執行順序、或為什麼同一段查詢換個時間變慢時">query optimizer</a> 決定，而最佳化器只受一個約束：輸出的列集合要與語意模型算出來的一致。在這個約束之內，它可以任意重排、可以換演算法、可以臨時建資料結構。</p>
<p>書寫順序、求值順序、執行順序這三者不一致，是宣告式這個選擇本身帶來的。讀查詢時的多數誤判來自把其中兩種當成同一種——最常見的是拿書寫順序去猜執行順序，以為「寫在前面的表會先被讀」。</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="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span 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="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">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></span></code></pre></div><p>兩者都回 <code>200000</code>。看它們的 <a href="/blog/backend/01-database/sql/knowledge-cards/query-plan/" data-link-title="Query Plan（執行計畫）" data-link-desc="想知道一段查詢實際會怎麼被執行、或被要求「先看計畫」時">query plan</a> 要用 <code>EXPLAIN QUERY PLAN</code>（SQLite 的 <code>EXPLAIN</code> 給的是逐行 bytecode，不是計畫），而計畫取決於引擎有沒有這兩張表的統計資訊。</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">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="p">(</span><span class="err">顧客編號</span><span class="w"> </span><span class="nb">INT</span><span class="p">,</span><span class="w"> </span><span class="err">姓名</span><span class="w"> </span><span class="nb">TEXT</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">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="p">(</span><span class="err">訂單編號</span><span class="w"> </span><span class="nb">INT</span><span class="p">,</span><span class="w"> </span><span class="err">顧客編號</span><span class="w"> </span><span class="nb">INT</span><span class="p">,</span><span class="w"> </span><span class="err">金額</span><span class="w"> </span><span class="nb">INT</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="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">1</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">50</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="n">i</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;顧客&#39;</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="n">i</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">6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">7</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">1</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">200000</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="n">i</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">50</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="mi">900</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">n</span><span class="p">;</span></span></span></code></pre></div><p><strong>剛建好、還沒統計過的資料庫</strong>上，SQLite 給出兩個不同的計畫：</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">-- EXPLAIN QUERY PLAN SELECT count(*) FROM 訂單 JOIN 顧客 ON 訂單.顧客編號 = 顧客.顧客編號;
</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">-- EXPLAIN QUERY PLAN SELECT count(*) FROM 顧客 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>顧客 JOIN 訂單</code> 那種寫法掃五十列的顧客，為了查找而當場替二十萬列的訂單建一個索引。</p>
<p>跑一次 <code>ANALYZE</code> 讓引擎去數過兩張表之後，同樣兩段查詢得到同一個計畫：</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">|--SCAN 訂單
</span></span><span class="line"><span class="ln">2</span><span class="cl">|--BLOOM FILTER ON 顧客 (顧客編號=?)
</span></span><span class="line"><span class="ln">3</span><span class="cl">`--SEARCH 顧客 USING AUTOMATIC COVERING INDEX (顧客編號=?)</span></span></code></pre></div><p>兩次都選擇掃二十萬列那張、對五十列那張做查找，寫誰在前面沒有影響。</p>
<p>**書寫順序從來不是引擎的依據，它只是引擎在沒有別的統計資訊時的退路。**真正的依據是<a href="/blog/backend/01-database/sql/knowledge-cards/query-statistics/" data-link-title="Query Statistics（統計資訊）" data-link-desc="看到「先跑 ANALYZE」、或同一段查詢沒改而某天變慢時，查這一份資料是什麼、誰在用它">統計資訊</a>，而那份資訊不在查詢裡、也不在資料裡，它是引擎另外去數出來存著的資料。</p>
<p>計畫裡還有查詢文字完全沒提到的步驟。<code>AUTOMATIC COVERING INDEX</code> 是 SQLite 當場替顧客表建的<a href="/blog/backend/01-database/sql/knowledge-cards/indexing/" data-link-title="Index（索引）" data-link-desc="看到執行計畫寫著走索引或建索引、想知道那是什麼時">索引</a>，<code>BLOOM FILTER</code> 是它加的過濾步驟。查詢裡沒有一個字要求這些。</p>
<p>「先讀的那張表」這個直覺因此對應不到任何固定的一張表——同一段查詢在剛建好、還沒統計過的資料庫上與跑過 <code>ANALYZE</code> 之後，先讀的表不同。文字上的先後只決定語意，不決定執行。它影響語意的位置在外連接：<code>LEFT JOIN</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> 寫那個累積過程，以及在鏈中間放 <code>RIGHT</code> 為什麼會把保護範圍縮到只剩一張表。</p>
<h2 id="求值順序擋下的寫法聚合函數放進-where">求值順序擋下的寫法：聚合函數放進 WHERE</h2>
<p>語意模型摸不著，而它會在引擎拒絕的地方現形：把聚合函數寫進 <code>WHERE</code>，SQLite 與 DuckDB 都拒絕，同一件事移到 <code>HAVING</code> 就通過。拿<a href="/blog/backend/01-database/sql/sample-bookstore-database/" data-link-title="SQL 各篇共用的範例資料庫：一家小書店的顧客、訂單與評價表" data-link-desc="SQL 各篇共用的顧客、訂單與評價表的建表語句與初始資料，以及各篇輸出所用的引擎版本、錯誤訊息與列序的閱讀約定">共用資料庫</a>的訂單表找下過一張以上訂單的顧客：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="err">顧客編號</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">WHERE</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="o">&gt;</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="err">顧客編號</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="c1">-- SQLite：misuse of aggregate: COUNT()
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">-- DuckDB：Binder Error: WHERE clause cannot contain aggregates!
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></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">SELECT</span><span class="w"> </span><span class="err">顧客編號</span><span class="p">,</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">AS</span><span class="w"> </span><span class="err">張數</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">訂單</span><span class="w"> </span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="err">顧客編號</span><span class="w"> </span><span class="k">HAVING</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="o">&gt;</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="c1">-- 兩個引擎都回 (1, 2)：顧客編號 1 的佳穎，兩張訂單</span></span></span></code></pre></div><p><strong>兩則訊息都只說了不准，理由要從求值順序讀出來。</strong> <code>WHERE</code> 那一步分組還沒發生，沒有組就沒有組的計數，聚合函數拿不到它要用的那個值；<code>HAVING</code> 排在 <code>GROUP BY</code> 之後，那時每一組的計數已經算得出來。這條限制由語意模型規定，與寫法好不好看無關。</p>
<p>求值順序與執行順序不同的地方在於它是語意模型規定死的，換引擎不變，所以「每一步能用到哪些欄位與值」是一個有固定答案的問題。<a href="/blog/backend/01-database/sql/clause-evaluation-order/" data-link-title="SQL.2 子句的求值順序：每一步可用的欄位、語意模型擋下的寫法與各家引擎放寬的寫法" data-link-desc="從 FROM 到 LIMIT 每一步可用的欄位與值、聚合放進 WHERE、別名用在 WHERE 與列的條件放進 HAVING 在各家引擎上的結果，以及這幾種寫法換引擎時的表現">SQL.2 子句的求值順序</a> 逐步走過求值順序這一串步驟，並分開模型層擋得住的限制與各家引擎自己放寬的——後者包括一種不報錯而給出錯答案的形態。</p>
<h2 id="代價的驗證輸出與執行計畫各自回答的問題">代價的驗證：輸出與執行計畫各自回答的問題</h2>
<p>宣告式把「怎麼算」交給引擎決定，代價也跟著由引擎決定。查詢的文字裡沒有任何一處表示它會掃全表還是走索引、會不會臨時建一個索引、會不會把中間結果寫到磁碟——上面那段計畫證明了這一點，<code>SCAN</code>、<code>BLOOM FILTER</code> 與那個臨時索引，三者一個都不在查詢裡。</p>
<p>所以驗證分成兩件互相獨立的事。<strong>結果對不對</strong>看輸出，從查詢本身推得出來。<strong>代價可不可以接受</strong>看計畫，非得問引擎不可，而且同一段查詢在不同資料量、不同索引、不同統計資訊底下的答案不一樣。</p>
<p>開發用的小資料只驗得到結果對不對這一件：資料量小到任何寫法都夠快，所以查詢在開發環境通過，只代表描述得對；代價可不可以接受，要到正式的資料量上才量得出來。</p>
<p>跑 <code>ANALYZE</code> 前後計畫跟著改變的那一組對照，在真實系統上有它的故障形態：統計資訊過時、或者欄位之間的相關性沒有被統計到，最佳化器就拿著錯的資訊選計畫。<a href="/blog/backend/01-database/vendors/postgresql/query-optimization/" data-link-title="PostgreSQL Query Optimization：EXPLAIN、auto_explain、pg_hint_plan 與 planner 選錯 plan 的 production case" data-link-desc="PG query 慢的根因常是 *planner 選錯 plan 或 statistics 過時*。本文從 production case 開場（seq scan vs index / hash vs nested loop / 多 column 統計缺 / parallel query 沒觸發）、走 EXPLAIN / EXPLAIN ANALYZE / auto_explain、pg_hint_plan extension 跟 planner GUC 取捨、production 踩雷（ANALYZE 過時 / multi-column statistics / random_page_cost 沿用 HDD 預設 / effective_cache_size 不對齊 RAM / parallel query 配置）、跟 MySQL query-optimization sibling 對比">PostgreSQL Query Optimization</a> 收了這幾種讓計畫選錯的實際案例，以及讀懂 production 計畫要用的 <code>EXPLAIN</code>、<code>EXPLAIN ANALYZE</code>、<code>auto_explain</code> 三層工具。</p>
]]></content:encoded></item><item><title>DataFrame（資料框）</title><link>https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/dataframe/</link><pubDate>Tue, 01 Sep 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/dataframe/</guid><description>&lt;p>DataFrame 是一張表在程式記憶體裡的表示：有具名的欄、有列，整份資料放在執行這段程式的行程的記憶體裡。Python 的 pandas 是最常見的實作，R 與 Julia 各有自己的一套。它與 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/relation/" data-link-title="Relation（關係）" data-link-desc="看到 SQL 教材說「組成一個關係」而不確定它指的是不是資料表時">relation&lt;/a>——一組欄位固定、由若干列構成的資料集合，資料表是它的一種——裝的內容幾乎一樣，而兩者在三個地方分開。&lt;/p>
&lt;h2 id="三個分開的地方">三個分開的地方&lt;/h2>
&lt;p>&lt;strong>列有沒有順序。&lt;/strong> DataFrame 的列從頭到尾有一個位置，可以問「第三列是什麼」。關係沒有順序這個屬性，&lt;code>SELECT&lt;/code> 回來的列要靠 &lt;code>ORDER BY&lt;/code> 才排得出來。&lt;/p>
&lt;p>&lt;strong>誰在算。&lt;/strong> 對 DataFrame 的每一個操作在自己的行程裡執行，資料先整批進記憶體；對關係的操作交給資料庫引擎，程式收到的只有結果。上限因此不同——一邊是這台機器的記憶體，一邊是資料庫的儲存。&lt;/p>
&lt;p>&lt;strong>寫下的是動作還是描述。&lt;/strong> DataFrame 的程式碼是一串照順序發生的動作：載入、篩選、合併。SQL 寫下的是要什麼結果，怎麼算由引擎決定。這一條是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/" data-link-title="SQL：這個語言為什麼長這樣" data-link-desc="SQL 的設計而不是它的語法表：子句為什麼這樣切、一個查詢該怎麼讀，以及代價落在哪裡">SQL 這個分類&lt;/a>每一篇的起點。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>DataFrame 在 SQL 那個分類裡的角色是對照組。宣告式這個性質單獨看不出形狀，跟一段逐步執行的程式並排才清楚——同一件事，一邊每一行指定下一步做什麼，一邊只描述要什麼。&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/declarative-not-procedural/" data-link-title="SQL.1 宣告式的紅利與代價：書寫、求值與執行順序各自由誰決定" data-link-desc="SQL 寫結果而不寫算法帶來的後果：同一個問題換書寫順序之後在有無統計資訊時拿到的計畫、求值順序擋下的寫法，以及代價為什麼要向引擎問">SQL.1 宣告式的紅利與代價&lt;/a>。同一組操作在兩種介面上的四組對應與對應斷掉的三個位置，在 &lt;a href="https://tarrragon.github.io/blog/programming/python/basic/08-data-analysis/same-relational-algebra/" data-link-title="8.4 SQL 子句與 pandas 方法的對應：篩選、連接、分組彙總、視窗函數，以及空值、索引與順序的語意差異" data-link-desc="SQL 與 DataFrame 共用的關聯代數、同一份資料上兩邊寫法的實測比對，以及照著逐句翻譯會靜默失準的位置">python 模組八 8.4&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>DataFrame 是一張表在程式記憶體裡的表示：有具名的欄、有列，整份資料放在執行這段程式的行程的記憶體裡。Python 的 pandas 是最常見的實作，R 與 Julia 各有自己的一套。它與 <a href="/blog/backend/01-database/sql/knowledge-cards/relation/" data-link-title="Relation（關係）" data-link-desc="看到 SQL 教材說「組成一個關係」而不確定它指的是不是資料表時">relation</a>——一組欄位固定、由若干列構成的資料集合，資料表是它的一種——裝的內容幾乎一樣，而兩者在三個地方分開。</p>
<h2 id="三個分開的地方">三個分開的地方</h2>
<p><strong>列有沒有順序。</strong> DataFrame 的列從頭到尾有一個位置，可以問「第三列是什麼」。關係沒有順序這個屬性，<code>SELECT</code> 回來的列要靠 <code>ORDER BY</code> 才排得出來。</p>
<p><strong>誰在算。</strong> 對 DataFrame 的每一個操作在自己的行程裡執行，資料先整批進記憶體；對關係的操作交給資料庫引擎，程式收到的只有結果。上限因此不同——一邊是這台機器的記憶體，一邊是資料庫的儲存。</p>
<p><strong>寫下的是動作還是描述。</strong> DataFrame 的程式碼是一串照順序發生的動作：載入、篩選、合併。SQL 寫下的是要什麼結果，怎麼算由引擎決定。這一條是 <a href="/blog/backend/01-database/sql/" data-link-title="SQL：這個語言為什麼長這樣" data-link-desc="SQL 的設計而不是它的語法表：子句為什麼這樣切、一個查詢該怎麼讀，以及代價落在哪裡">SQL 這個分類</a>每一篇的起點。</p>
<h2 id="概念位置">概念位置</h2>
<p>DataFrame 在 SQL 那個分類裡的角色是對照組。宣告式這個性質單獨看不出形狀，跟一段逐步執行的程式並排才清楚——同一件事，一邊每一行指定下一步做什麼，一邊只描述要什麼。</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/declarative-not-procedural/" data-link-title="SQL.1 宣告式的紅利與代價：書寫、求值與執行順序各自由誰決定" data-link-desc="SQL 寫結果而不寫算法帶來的後果：同一個問題換書寫順序之後在有無統計資訊時拿到的計畫、求值順序擋下的寫法，以及代價為什麼要向引擎問">SQL.1 宣告式的紅利與代價</a>。同一組操作在兩種介面上的四組對應與對應斷掉的三個位置，在 <a href="/blog/programming/python/basic/08-data-analysis/same-relational-algebra/" data-link-title="8.4 SQL 子句與 pandas 方法的對應：篩選、連接、分組彙總、視窗函數，以及空值、索引與順序的語意差異" data-link-desc="SQL 與 DataFrame 共用的關聯代數、同一份資料上兩邊寫法的實測比對，以及照著逐句翻譯會靜默失準的位置">python 模組八 8.4</a>。</p>
]]></content:encoded></item></channel></rss>