<?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>Dataframe on Tarragon</title><link>https://tarrragon.github.io/blog/tags/dataframe/</link><description>Recent content in Dataframe 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/dataframe/index.xml" rel="self" type="application/rss+xml"/><item><title>8.3 ORM 與 DataFrame 的分辨：回傳值的形狀、連線物件與查詢送出的時機</title><link>https://tarrragon.github.io/blog/programming/python/basic/08-data-analysis/orm-and-dataframe/</link><pubDate>Mon, 31 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/programming/python/basic/08-data-analysis/orm-and-dataframe/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/orm/" data-link-title="ORM（Object-Relational Mapping）" data-link-desc="看到程式用類別與方法描述查詢時，查它交出去的是什麼、以及哪些行為在程式碼上讀不出來">ORM&lt;/a> 的產出是一段 SQL 與一組物件，DataFrame 的產出是一張新的表格。兩者的寫法看起來相似，因為都用方法串接描述篩選條件，而底下發生的事完全不同：ORM 是查詢的產生器，DataFrame 是運算的執行者。&lt;/p>
&lt;h2 id="orm-的產出與運算發生的位置">ORM 的產出與運算發生的位置&lt;/h2>
&lt;p>ORM 把資料表對應成類別、把資料列對應成物件。篩選條件寫成屬性的比較式，框架把它翻譯成 SQL 的 &lt;code>WHERE&lt;/code> 子句。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">sqlalchemy&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">select&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">sqlalchemy.orm&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Session&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="n">stmt&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">select&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Customer&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">where&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Customer&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">age&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">18&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">stmt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">compile&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">engine&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1"># SELECT customer.id, customer.name, customer.age FROM customer WHERE customer.age &amp;gt; ?&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="k">with&lt;/span> &lt;span class="n">Session&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">engine&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">s&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="n">rows&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">s&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">scalars&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">stmt&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">all&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="nb">type&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">r&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="vm">__name__&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">r&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">rows&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">r&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">name&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">r&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">rows&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="c1"># [&amp;#39;Customer&amp;#39;] [&amp;#39;A&amp;#39;]&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>stmt&lt;/code> 本身還沒有做任何運算，它是一段待送出的查詢。運算在資料庫那一端完成，回來的每一列被包成一個 &lt;code>Customer&lt;/code> 實例——所以拿到的是物件，可以呼叫它的方法、走訪它關聯到的其他物件。&lt;/p>
&lt;h2 id="dataframe-的產出與運算發生的位置">DataFrame 的產出與運算發生的位置&lt;/h2>
&lt;p>pandas 這一側的同一件事：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">df&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">DataFrame&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;A&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;B&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="s2">&amp;#34;age&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">20&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">]})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="n">df&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">df&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;age&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">18&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># id name age&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"># 0 1 A 20&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>df[&amp;quot;age&amp;quot;] &amp;gt; 18&lt;/code> 先算出一個布林陣列 &lt;code>[True, False]&lt;/code>，外層的取值用這個陣列挑出要留下的列。整段運算發生在這個程序的記憶體裡，沒有連線、沒有 SQL、沒有另一個系統參與。&lt;/p>
&lt;p>回來的是一張新的表格，不是一組物件。它可以繼續被篩選、被合併、被彙總，因為表格正是這些操作接受的輸入。&lt;/p>
&lt;h2 id="分辨-orm-與-dataframe-的訊號回傳值的形狀連線物件查詢送出的時機">分辨 ORM 與 DataFrame 的訊號：回傳值的形狀、連線物件、查詢送出的時機&lt;/h2>
&lt;p>外觀相似的方法串接底下，有三個分得開的訊號。&lt;/p>
&lt;p>&lt;strong>回傳值的形狀。&lt;/strong> 一組各自獨立、可以呼叫方法的實例，是 ORM 那一側；一張有欄有列、可以繼續被篩選與彙總的表格，是 DataFrame 那一側。&lt;/p>
&lt;p>&lt;strong>有沒有連線物件。&lt;/strong> 資料庫端的工具一定要先有一個連線或 session，因為運算要送去別的地方。pandas 的操作不需要：資料一旦載進 DataFrame，之後每一步都在這份記憶體裡的副本上進行，原本那個資料庫或檔案後來有沒有變動都不影響它。&lt;/p>
&lt;p>&lt;strong>查詢送出的時機。&lt;/strong> ORM 允許先把查詢組好、之後才送出，所以「描述」與「執行」是兩個時刻。pandas 的每一行敘述當場就算完，中間沒有待送出的狀態。&lt;/p>
&lt;h2 id="各語言生態的資料庫存取分層手寫-sql查詢建構器orm">各語言生態的資料庫存取分層：手寫 SQL、查詢建構器、ORM&lt;/h2>
&lt;p>自己寫 SQL、用建構器組 SQL、用 ORM 對應物件，這三層在多數語言生態裡都找得到對應的套件，名字不同而分層一致。各語言的代表工具，以及手寫 SQL 再細分成驅動、結果對映、從 SQL 產生程式碼的方式，見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/data-access-layers/" data-link-title="1.17 應用程式存取資料庫的工具分層：驅動、結果對映、從 SQL 產生程式碼、Query Builder 與 ORM" data-link-desc="應用程式存取關聯式資料庫的工作（寫 SQL、取得連線與交易、把結果對映成程式的值），驅動、結果對映、從 SQL 產生程式碼、Query Builder 與 ORM 各接手哪幾件、接手之後哪些行為看不見，ORM 的 N&amp;#43;1 與交易邊界、Go、gunicorn 與 PHP-FPM 各自對資料庫的連線總數與 max_connections、唯一約束違反時各語言讀取約束名稱的位置、讀寫分離時各框架把查詢送到主庫或副本的規則、資料拆到多個資料庫時跨不過的邊界，以及 Go、PHP（Laravel）與 Python 的對應工具與選型軸">1.17 應用程式存取資料庫的工具分層&lt;/a>。&lt;/p>
&lt;p>DataFrame 這一層則是額外的第四種，它不在上面那條線上——它處理的是已經在記憶體裡的表格，與資料庫的存取方式無關。一般的增刪改查應用不使用它，那是 ORM 的位置。&lt;/p>
&lt;h2 id="延伸閱讀sql-與-pandas-的寫法對應運算發生的位置pandas-的記憶體邊界orm-的查詢反模式">延伸閱讀：SQL 與 pandas 的寫法對應、運算發生的位置、pandas 的記憶體邊界、ORM 的查詢反模式&lt;/h2>
&lt;p>&lt;strong>這兩側共用的抽象&lt;/strong>：ORM 產生的 SQL 與 DataFrame 的方法，做的是同一組操作。&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 共用的關聯代數、同一份資料上兩邊寫法的實測比對，以及照著逐句翻譯會靜默失準的位置">8.4 SQL 子句與 pandas 方法的對應：篩選、連接、分組彙總、視窗函數，以及空值、索引與順序的語意差異&lt;/a> 給篩選、連接、分組彙總與視窗函數在兩邊的實測比對，以及空值、索引與順序這幾處對應斷掉的位置。&lt;/p>
&lt;p>&lt;strong>分界本身&lt;/strong>：兩側的差異全部從「運算發生在哪一端」推出來。&lt;a href="https://tarrragon.github.io/blog/programming/python/basic/08-data-analysis/where-computation-runs/" data-link-title="8.2 運算發生的位置：資料庫端與記憶體端的工具、分工條件與 DuckDB" data-link-desc="運算留在資料庫端或搬進記憶體端這條分界，怎麼決定工具的形狀、容量上限與失敗形態">8.2 運算發生的位置&lt;/a> 寫這條軸怎麼決定容量上限與失敗形態，以及選邊時要問的三個條件。&lt;/p>
&lt;p>&lt;strong>選了記憶體端之後的上限&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/programming/python/basic/08-data-analysis/memory-is-the-boundary/" data-link-title="8.5 pandas 的記憶體邊界：資料與運算過程的佔用估算，推回資料庫、換引擎與分散式的代價" data-link-desc="數值欄與字串欄的佔用怎麼得出、合併這類操作的中間結果佔多少，以及記憶體不足與跑得慢這兩種失敗怎麼分辨">8.5 pandas 的記憶體邊界：資料與運算過程的佔用估算，推回資料庫、換引擎與分散式的代價&lt;/a> 給資料量的估算方法與越過之後的三個方向。&lt;/p>
&lt;p>&lt;strong>ORM 那一側的失敗形態&lt;/strong>：延遲載入什麼時候把一次請求變成一百次查詢，在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&amp;#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">backend 模組一的查詢反模式&lt;/a>。那一篇把 N+1 與長交易列成可診斷的清單，並給出每請求的查詢次數預算。&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/backend/knowledge-cards/orm/" data-link-title="ORM（Object-Relational Mapping）" data-link-desc="看到程式用類別與方法描述查詢時，查它交出去的是什麼、以及哪些行為在程式碼上讀不出來">ORM</a> 的產出是一段 SQL 與一組物件，DataFrame 的產出是一張新的表格。兩者的寫法看起來相似，因為都用方法串接描述篩選條件，而底下發生的事完全不同：ORM 是查詢的產生器，DataFrame 是運算的執行者。</p>
<h2 id="orm-的產出與運算發生的位置">ORM 的產出與運算發生的位置</h2>
<p>ORM 把資料表對應成類別、把資料列對應成物件。篩選條件寫成屬性的比較式，框架把它翻譯成 SQL 的 <code>WHERE</code> 子句。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">from</span> <span class="nn">sqlalchemy</span> <span class="kn">import</span> <span class="n">select</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">from</span> <span class="nn">sqlalchemy.orm</span> <span class="kn">import</span> <span class="n">Session</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">stmt</span> <span class="o">=</span> <span class="n">select</span><span class="p">(</span><span class="n">Customer</span><span class="p">)</span><span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="n">Customer</span><span class="o">.</span><span class="n">age</span> <span class="o">&gt;</span> <span class="mi">18</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">stmt</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="n">engine</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># SELECT customer.id, customer.name, customer.age FROM customer WHERE customer.age &gt; ?</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">with</span> <span class="n">Session</span><span class="p">(</span><span class="n">engine</span><span class="p">)</span> <span class="k">as</span> <span class="n">s</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">rows</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">scalars</span><span class="p">(</span><span class="n">stmt</span><span class="p">)</span><span class="o">.</span><span class="n">all</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nb">print</span><span class="p">([</span><span class="nb">type</span><span class="p">(</span><span class="n">r</span><span class="p">)</span><span class="o">.</span><span class="vm">__name__</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">rows</span><span class="p">],</span> <span class="p">[</span><span class="n">r</span><span class="o">.</span><span class="n">name</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">rows</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="c1"># [&#39;Customer&#39;] [&#39;A&#39;]</span></span></span></code></pre></div><p><code>stmt</code> 本身還沒有做任何運算，它是一段待送出的查詢。運算在資料庫那一端完成，回來的每一列被包成一個 <code>Customer</code> 實例——所以拿到的是物件，可以呼叫它的方法、走訪它關聯到的其他物件。</p>
<h2 id="dataframe-的產出與運算發生的位置">DataFrame 的產出與運算發生的位置</h2>
<p>pandas 這一側的同一件事：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">({</span><span class="s2">&#34;id&#34;</span><span class="p">:</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">],</span> <span class="s2">&#34;name&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;A&#34;</span><span class="p">,</span> <span class="s2">&#34;B&#34;</span><span class="p">],</span> <span class="s2">&#34;age&#34;</span><span class="p">:</span> <span class="p">[</span><span class="mi">20</span><span class="p">,</span> <span class="mi">10</span><span class="p">]})</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">df</span><span class="p">[</span><span class="n">df</span><span class="p">[</span><span class="s2">&#34;age&#34;</span><span class="p">]</span> <span class="o">&gt;</span> <span class="mi">18</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">#    id name  age</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 0   1    A   20</span></span></span></code></pre></div><p><code>df[&quot;age&quot;] &gt; 18</code> 先算出一個布林陣列 <code>[True, False]</code>，外層的取值用這個陣列挑出要留下的列。整段運算發生在這個程序的記憶體裡，沒有連線、沒有 SQL、沒有另一個系統參與。</p>
<p>回來的是一張新的表格，不是一組物件。它可以繼續被篩選、被合併、被彙總，因為表格正是這些操作接受的輸入。</p>
<h2 id="分辨-orm-與-dataframe-的訊號回傳值的形狀連線物件查詢送出的時機">分辨 ORM 與 DataFrame 的訊號：回傳值的形狀、連線物件、查詢送出的時機</h2>
<p>外觀相似的方法串接底下，有三個分得開的訊號。</p>
<p><strong>回傳值的形狀。</strong> 一組各自獨立、可以呼叫方法的實例，是 ORM 那一側；一張有欄有列、可以繼續被篩選與彙總的表格，是 DataFrame 那一側。</p>
<p><strong>有沒有連線物件。</strong> 資料庫端的工具一定要先有一個連線或 session，因為運算要送去別的地方。pandas 的操作不需要：資料一旦載進 DataFrame，之後每一步都在這份記憶體裡的副本上進行，原本那個資料庫或檔案後來有沒有變動都不影響它。</p>
<p><strong>查詢送出的時機。</strong> ORM 允許先把查詢組好、之後才送出，所以「描述」與「執行」是兩個時刻。pandas 的每一行敘述當場就算完，中間沒有待送出的狀態。</p>
<h2 id="各語言生態的資料庫存取分層手寫-sql查詢建構器orm">各語言生態的資料庫存取分層：手寫 SQL、查詢建構器、ORM</h2>
<p>自己寫 SQL、用建構器組 SQL、用 ORM 對應物件，這三層在多數語言生態裡都找得到對應的套件，名字不同而分層一致。各語言的代表工具，以及手寫 SQL 再細分成驅動、結果對映、從 SQL 產生程式碼的方式，見 <a href="/blog/backend/01-database/data-access-layers/" data-link-title="1.17 應用程式存取資料庫的工具分層：驅動、結果對映、從 SQL 產生程式碼、Query Builder 與 ORM" data-link-desc="應用程式存取關聯式資料庫的工作（寫 SQL、取得連線與交易、把結果對映成程式的值），驅動、結果對映、從 SQL 產生程式碼、Query Builder 與 ORM 各接手哪幾件、接手之後哪些行為看不見，ORM 的 N&#43;1 與交易邊界、Go、gunicorn 與 PHP-FPM 各自對資料庫的連線總數與 max_connections、唯一約束違反時各語言讀取約束名稱的位置、讀寫分離時各框架把查詢送到主庫或副本的規則、資料拆到多個資料庫時跨不過的邊界，以及 Go、PHP（Laravel）與 Python 的對應工具與選型軸">1.17 應用程式存取資料庫的工具分層</a>。</p>
<p>DataFrame 這一層則是額外的第四種，它不在上面那條線上——它處理的是已經在記憶體裡的表格，與資料庫的存取方式無關。一般的增刪改查應用不使用它，那是 ORM 的位置。</p>
<h2 id="延伸閱讀sql-與-pandas-的寫法對應運算發生的位置pandas-的記憶體邊界orm-的查詢反模式">延伸閱讀：SQL 與 pandas 的寫法對應、運算發生的位置、pandas 的記憶體邊界、ORM 的查詢反模式</h2>
<p><strong>這兩側共用的抽象</strong>：ORM 產生的 SQL 與 DataFrame 的方法，做的是同一組操作。<a href="/blog/programming/python/basic/08-data-analysis/same-relational-algebra/" data-link-title="8.4 SQL 子句與 pandas 方法的對應：篩選、連接、分組彙總、視窗函數，以及空值、索引與順序的語意差異" data-link-desc="SQL 與 DataFrame 共用的關聯代數、同一份資料上兩邊寫法的實測比對，以及照著逐句翻譯會靜默失準的位置">8.4 SQL 子句與 pandas 方法的對應：篩選、連接、分組彙總、視窗函數，以及空值、索引與順序的語意差異</a> 給篩選、連接、分組彙總與視窗函數在兩邊的實測比對，以及空值、索引與順序這幾處對應斷掉的位置。</p>
<p><strong>分界本身</strong>：兩側的差異全部從「運算發生在哪一端」推出來。<a href="/blog/programming/python/basic/08-data-analysis/where-computation-runs/" data-link-title="8.2 運算發生的位置：資料庫端與記憶體端的工具、分工條件與 DuckDB" data-link-desc="運算留在資料庫端或搬進記憶體端這條分界，怎麼決定工具的形狀、容量上限與失敗形態">8.2 運算發生的位置</a> 寫這條軸怎麼決定容量上限與失敗形態，以及選邊時要問的三個條件。</p>
<p><strong>選了記憶體端之後的上限</strong>：<a href="/blog/programming/python/basic/08-data-analysis/memory-is-the-boundary/" data-link-title="8.5 pandas 的記憶體邊界：資料與運算過程的佔用估算，推回資料庫、換引擎與分散式的代價" data-link-desc="數值欄與字串欄的佔用怎麼得出、合併這類操作的中間結果佔多少，以及記憶體不足與跑得慢這兩種失敗怎麼分辨">8.5 pandas 的記憶體邊界：資料與運算過程的佔用估算，推回資料庫、換引擎與分散式的代價</a> 給資料量的估算方法與越過之後的三個方向。</p>
<p><strong>ORM 那一側的失敗形態</strong>：延遲載入什麼時候把一次請求變成一百次查詢，在 <a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">backend 模組一的查詢反模式</a>。那一篇把 N+1 與長交易列成可診斷的清單，並給出每請求的查詢次數預算。</p>
]]></content:encoded></item><item><title>8.4 SQL 子句與 pandas 方法的對應：篩選、連接、分組彙總、視窗函數，以及空值、索引與順序的語意差異</title><link>https://tarrragon.github.io/blog/programming/python/basic/08-data-analysis/same-relational-algebra/</link><pubDate>Mon, 31 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/programming/python/basic/08-data-analysis/same-relational-algebra/</guid><description>&lt;p>SQL 的子句與 pandas 的方法對應得起來，因為兩者操作的是同一種資料結構：由欄與列構成的表格。對表格能做的基本操作就那幾種——挑出符合條件的列、挑出要的欄、把兩張表按鍵值接起來、把列分組後彙總——這組操作叫&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>。SQL 與 &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> 各自是這套代數的一個介面——介面在這裡指的是表達那組操作的那一層，而底下由誰執行、怎麼執行，這一層都蓋住了。&lt;/p>
&lt;p>換介面重寫一次同樣的邏輯因此是划算的練習：邏輯已經想清楚了，剩下的是查另一套語法怎麼寫。&lt;/p>
&lt;h2 id="篩選連接分組彙總與視窗函數的兩邊寫法">篩選、連接、分組彙總與視窗函數的兩邊寫法&lt;/h2>
&lt;p>以下的 &lt;code>emp&lt;/code> 兩邊是同一份資料，一邊在 SQLite 裡、一邊在記憶體的 DataFrame 裡。&lt;/p>
&lt;h3 id="篩選挑出符合條件的列">篩選：挑出符合條件的列&lt;/h3>





&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="n">name&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">emp&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">salary&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">150&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-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">emp&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">emp&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;salary&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">150&lt;/span>&lt;span class="p">][[&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">]]&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="連接把兩張表按鍵值接起來">連接：把兩張表按鍵值接起來&lt;/h3>





&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="n">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">dname&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">emp&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">dept&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">d&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">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">dept&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">dept&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-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">emp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">merge&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">dept&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">on&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;dept&amp;#34;&lt;/span>&lt;span class="p">)[[&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;dname&amp;#34;&lt;/span>&lt;span class="p">]]&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="分組彙總每一組收斂成一列">分組彙總：每一組收斂成一列&lt;/h3>





&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="n">dept&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">MAX&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">salary&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="n">m&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">emp&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">GROUP&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">dept&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-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">emp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">groupby&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;dept&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">as_index&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">)[&lt;/span>&lt;span class="s2">&amp;#34;salary&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">max&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">rename&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">columns&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;salary&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;m&amp;#34;&lt;/span>&lt;span class="p">})&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="視窗函數算出分組的統計值並保留原本的每一列">視窗函數：算出分組的統計值並保留原本的每一列&lt;/h3>





&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="n">name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">salary&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AVG&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">salary&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">OVER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">PARTITION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">dept&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="n">d&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">emp&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-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">emp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">assign&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">d&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">emp&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;salary&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">emp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">groupby&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;dept&amp;#34;&lt;/span>&lt;span class="p">)[&lt;/span>&lt;span class="s2">&amp;#34;salary&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">transform&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;mean&amp;#34;&lt;/span>&lt;span class="p">))[[&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;d&amp;#34;&lt;/span>&lt;span class="p">]]&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>transform&lt;/code> 是這一組裡最值得記的：它與 &lt;code>agg&lt;/code> 的差別正是視窗函數與 &lt;code>GROUP BY&lt;/code> 的差別——&lt;code>agg&lt;/code> 讓每一組收斂成一列，&lt;code>transform&lt;/code> 把組的統計值攤回組內的每一列。&lt;/p>
&lt;p>這四組在同一份資料上跑出來的結果逐格相同。&lt;/p>
&lt;h2 id="對應斷掉的位置分組時的空值索引順序">對應斷掉的位置：分組時的空值、索引、順序&lt;/h2>
&lt;p>有三個地方兩邊的語意本來就不同，照著逐句翻譯會在這裡失準。&lt;/p>
&lt;p>&lt;strong>空值在分組時的去留。&lt;/strong> SQL 的 &lt;code>GROUP BY&lt;/code> 把 &lt;code>NULL&lt;/code> 當成一組，pandas 的 &lt;code>groupby&lt;/code> 預設把分組鍵是缺值的那些列排除掉，於是那一組在結果裡整組消失。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">t&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">groupby&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;k&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">size&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1"># {&amp;#39;x&amp;#39;: 1} 缺值那一組消失了&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="n">t&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">groupby&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;k&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">dropna&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">size&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1"># {&amp;#39;x&amp;#39;: 1, nan: 2} 與 SQL 一致&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>同一段邏輯照著翻譯過來，統計結果會少掉一整組而且不會報錯。這是篩選、連接、分組彙總與視窗函數這幾組對應之外最容易靜默出錯的一項。&lt;/p>
&lt;p>&lt;strong>索引。&lt;/strong> DataFrame 的每一列有一個索引，它獨立於欄位存在，而且會在 &lt;code>groupby&lt;/code>、&lt;code>merge&lt;/code> 之後改變。SQL 沒有對應的概念。翻譯過來的程式碼常常需要 &lt;code>reset_index()&lt;/code> 或 &lt;code>as_index=False&lt;/code>，那幾個呼叫處理的正是索引這個 SQL 沒有的狀態。&lt;/p>
&lt;p>&lt;strong>順序。&lt;/strong> SQL 的查詢結果在沒有 &lt;code>ORDER BY&lt;/code> 時不保證順序。DataFrame 的列有位置順序，而且多數操作會保留它。所以「相同的結果」在順序這一維上兩邊本來就不可比——可比的是集合，而集合的比較要先各自排序才成立。&lt;/p>
&lt;h2 id="延伸閱讀運算發生的位置orm-與-dataframe-的分辨sql-的視窗函數與語言設計pandas-的記憶體邊界">延伸閱讀：運算發生的位置、ORM 與 DataFrame 的分辨、SQL 的視窗函數與語言設計、pandas 的記憶體邊界&lt;/h2>
&lt;p>&lt;strong>這兩個介面為什麼會分家&lt;/strong>：一邊把運算交給資料庫，一邊在自己的記憶體裡算。&lt;a href="https://tarrragon.github.io/blog/programming/python/basic/08-data-analysis/where-computation-runs/" data-link-title="8.2 運算發生的位置：資料庫端與記憶體端的工具、分工條件與 DuckDB" data-link-desc="運算留在資料庫端或搬進記憶體端這條分界，怎麼決定工具的形狀、容量上限與失敗形態">8.2 運算發生的位置&lt;/a> 寫這條分界怎麼決定容量上限，以及選邊要問的三個條件。&lt;/p>
&lt;p>&lt;strong>同一個對比在 SQL 那一側怎麼講&lt;/strong>：&lt;code>transform&lt;/code> 對 &lt;code>agg&lt;/code> 的差別就是視窗函數對 &lt;code>GROUP BY&lt;/code> 的差別。&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/window-keeps-rows-grouping-collapses/" data-link-title="SQL.10 分組與視窗函數：各自的產出、選用的依據與 LAG、LEAD 的相鄰列" data-link-desc="GROUP BY 與視窗函數算完之後原本的列還在不在、依結果的一列代表什麼選用哪一個、視窗函數的值怎麼拿來當篩選條件，以及 LAG 取相鄰列時分區、排序鍵與距離條件各自要確認的事">SQL.10 分組與視窗函數：各自的產出、選用的依據與 LAG、LEAD 的相鄰列&lt;/a> 用輸出的單位當判斷標準，並示範相鄰的定義漏掉分區之後會跨到別的組上。&lt;/p>
&lt;p>&lt;strong>從產出認出手上是哪一個介面&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/programming/python/basic/08-data-analysis/orm-and-dataframe/" data-link-title="8.3 ORM 與 DataFrame 的分辨：回傳值的形狀、連線物件與查詢送出的時機" data-link-desc="ORM 產生查詢、DataFrame 執行運算，以及外觀相似的方法串接底下差了什麼">8.3 ORM 與 DataFrame 的分辨：回傳值的形狀、連線物件與查詢送出的時機&lt;/a> 寫兩者的回傳值形狀差在哪，以及分得開它們的三個訊號。&lt;/p>
&lt;p>&lt;strong>這套代數在 SQL 那一側怎麼設計&lt;/strong>：關聯代數的組合規則決定了子句的先後、連接的左右與分組的鍵，而那些規則各有一處與直覺相反。&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;p>&lt;strong>換介面之後接手的新限制&lt;/strong>：SQL 那一側沒有記憶體上限的問題，換到 DataFrame 就有了。&lt;a href="https://tarrragon.github.io/blog/programming/python/basic/08-data-analysis/memory-is-the-boundary/" data-link-title="8.5 pandas 的記憶體邊界：資料與運算過程的佔用估算，推回資料庫、換引擎與分散式的代價" data-link-desc="數值欄與字串欄的佔用怎麼得出、合併這類操作的中間結果佔多少，以及記憶體不足與跑得慢這兩種失敗怎麼分辨">8.5 pandas 的記憶體邊界：資料與運算過程的佔用估算，推回資料庫、換引擎與分散式的代價&lt;/a> 給估算方法與越過之後的三個方向。&lt;/p></description><content:encoded><![CDATA[<p>SQL 的子句與 pandas 的方法對應得起來，因為兩者操作的是同一種資料結構：由欄與列構成的表格。對表格能做的基本操作就那幾種——挑出符合條件的列、挑出要的欄、把兩張表按鍵值接起來、把列分組後彙總——這組操作叫<a href="/blog/backend/01-database/sql/knowledge-cards/relational-algebra/" data-link-title="Relational Algebra（關聯代數）" data-link-desc="看到「子句其實是某個運算」或想知道 SQL 的規則從哪裡來時，查這一層在底下的是什麼">關聯代數</a>。SQL 與 <a href="/blog/backend/01-database/sql/knowledge-cards/dataframe/" data-link-title="DataFrame（資料框）" data-link-desc="SQL 教材拿 pandas 或資料框當對照時，查 DataFrame 是什麼、它與資料表差在哪">DataFrame</a> 各自是這套代數的一個介面——介面在這裡指的是表達那組操作的那一層，而底下由誰執行、怎麼執行，這一層都蓋住了。</p>
<p>換介面重寫一次同樣的邏輯因此是划算的練習：邏輯已經想清楚了，剩下的是查另一套語法怎麼寫。</p>
<h2 id="篩選連接分組彙總與視窗函數的兩邊寫法">篩選、連接、分組彙總與視窗函數的兩邊寫法</h2>
<p>以下的 <code>emp</code> 兩邊是同一份資料，一邊在 SQLite 裡、一邊在記憶體的 DataFrame 裡。</p>
<h3 id="篩選挑出符合條件的列">篩選：挑出符合條件的列</h3>





<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="n">name</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">emp</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">salary</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">150</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">emp</span><span class="p">[</span><span class="n">emp</span><span class="p">[</span><span class="s2">&#34;salary&#34;</span><span class="p">]</span> <span class="o">&gt;</span> <span class="mi">150</span><span class="p">][[</span><span class="s2">&#34;name&#34;</span><span class="p">]]</span></span></span></code></pre></div><h3 id="連接把兩張表按鍵值接起來">連接：把兩張表按鍵值接起來</h3>





<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="n">e</span><span class="p">.</span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="n">d</span><span class="p">.</span><span class="n">dname</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">emp</span><span class="w"> </span><span class="n">e</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="n">dept</span><span class="w"> </span><span class="n">d</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">e</span><span class="p">.</span><span class="n">dept</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">d</span><span class="p">.</span><span class="n">dept</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">emp</span><span class="o">.</span><span class="n">merge</span><span class="p">(</span><span class="n">dept</span><span class="p">,</span> <span class="n">on</span><span class="o">=</span><span class="s2">&#34;dept&#34;</span><span class="p">)[[</span><span class="s2">&#34;name&#34;</span><span class="p">,</span> <span class="s2">&#34;dname&#34;</span><span class="p">]]</span></span></span></code></pre></div><h3 id="分組彙總每一組收斂成一列">分組彙總：每一組收斂成一列</h3>





<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="n">dept</span><span class="p">,</span><span class="w"> </span><span class="k">MAX</span><span class="p">(</span><span class="n">salary</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">m</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">emp</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="n">dept</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">emp</span><span class="o">.</span><span class="n">groupby</span><span class="p">(</span><span class="s2">&#34;dept&#34;</span><span class="p">,</span> <span class="n">as_index</span><span class="o">=</span><span class="kc">False</span><span class="p">)[</span><span class="s2">&#34;salary&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">max</span><span class="p">()</span><span class="o">.</span><span class="n">rename</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;salary&#34;</span><span class="p">:</span> <span class="s2">&#34;m&#34;</span><span class="p">})</span></span></span></code></pre></div><h3 id="視窗函數算出分組的統計值並保留原本的每一列">視窗函數：算出分組的統計值並保留原本的每一列</h3>





<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="n">name</span><span class="p">,</span><span class="w"> </span><span class="n">salary</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="k">AVG</span><span class="p">(</span><span class="n">salary</span><span class="p">)</span><span class="w"> </span><span class="n">OVER</span><span class="w"> </span><span class="p">(</span><span class="n">PARTITION</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">dept</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">d</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">emp</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">emp</span><span class="o">.</span><span class="n">assign</span><span class="p">(</span><span class="n">d</span><span class="o">=</span><span class="n">emp</span><span class="p">[</span><span class="s2">&#34;salary&#34;</span><span class="p">]</span> <span class="o">-</span> <span class="n">emp</span><span class="o">.</span><span class="n">groupby</span><span class="p">(</span><span class="s2">&#34;dept&#34;</span><span class="p">)[</span><span class="s2">&#34;salary&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">transform</span><span class="p">(</span><span class="s2">&#34;mean&#34;</span><span class="p">))[[</span><span class="s2">&#34;name&#34;</span><span class="p">,</span> <span class="s2">&#34;d&#34;</span><span class="p">]]</span></span></span></code></pre></div><p><code>transform</code> 是這一組裡最值得記的：它與 <code>agg</code> 的差別正是視窗函數與 <code>GROUP BY</code> 的差別——<code>agg</code> 讓每一組收斂成一列，<code>transform</code> 把組的統計值攤回組內的每一列。</p>
<p>這四組在同一份資料上跑出來的結果逐格相同。</p>
<h2 id="對應斷掉的位置分組時的空值索引順序">對應斷掉的位置：分組時的空值、索引、順序</h2>
<p>有三個地方兩邊的語意本來就不同，照著逐句翻譯會在這裡失準。</p>
<p><strong>空值在分組時的去留。</strong> SQL 的 <code>GROUP BY</code> 把 <code>NULL</code> 當成一組，pandas 的 <code>groupby</code> 預設把分組鍵是缺值的那些列排除掉，於是那一組在結果裡整組消失。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">t</span><span class="o">.</span><span class="n">groupby</span><span class="p">(</span><span class="s2">&#34;k&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">size</span><span class="p">()</span>                 <span class="c1"># {&#39;x&#39;: 1}          缺值那一組消失了</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">t</span><span class="o">.</span><span class="n">groupby</span><span class="p">(</span><span class="s2">&#34;k&#34;</span><span class="p">,</span> <span class="n">dropna</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span><span class="o">.</span><span class="n">size</span><span class="p">()</span>   <span class="c1"># {&#39;x&#39;: 1, nan: 2}  與 SQL 一致</span></span></span></code></pre></div><p>同一段邏輯照著翻譯過來，統計結果會少掉一整組而且不會報錯。這是篩選、連接、分組彙總與視窗函數這幾組對應之外最容易靜默出錯的一項。</p>
<p><strong>索引。</strong> DataFrame 的每一列有一個索引，它獨立於欄位存在，而且會在 <code>groupby</code>、<code>merge</code> 之後改變。SQL 沒有對應的概念。翻譯過來的程式碼常常需要 <code>reset_index()</code> 或 <code>as_index=False</code>，那幾個呼叫處理的正是索引這個 SQL 沒有的狀態。</p>
<p><strong>順序。</strong> SQL 的查詢結果在沒有 <code>ORDER BY</code> 時不保證順序。DataFrame 的列有位置順序，而且多數操作會保留它。所以「相同的結果」在順序這一維上兩邊本來就不可比——可比的是集合，而集合的比較要先各自排序才成立。</p>
<h2 id="延伸閱讀運算發生的位置orm-與-dataframe-的分辨sql-的視窗函數與語言設計pandas-的記憶體邊界">延伸閱讀：運算發生的位置、ORM 與 DataFrame 的分辨、SQL 的視窗函數與語言設計、pandas 的記憶體邊界</h2>
<p><strong>這兩個介面為什麼會分家</strong>：一邊把運算交給資料庫，一邊在自己的記憶體裡算。<a href="/blog/programming/python/basic/08-data-analysis/where-computation-runs/" data-link-title="8.2 運算發生的位置：資料庫端與記憶體端的工具、分工條件與 DuckDB" data-link-desc="運算留在資料庫端或搬進記憶體端這條分界，怎麼決定工具的形狀、容量上限與失敗形態">8.2 運算發生的位置</a> 寫這條分界怎麼決定容量上限，以及選邊要問的三個條件。</p>
<p><strong>同一個對比在 SQL 那一側怎麼講</strong>：<code>transform</code> 對 <code>agg</code> 的差別就是視窗函數對 <code>GROUP BY</code> 的差別。<a href="/blog/backend/01-database/sql/window-keeps-rows-grouping-collapses/" data-link-title="SQL.10 分組與視窗函數：各自的產出、選用的依據與 LAG、LEAD 的相鄰列" data-link-desc="GROUP BY 與視窗函數算完之後原本的列還在不在、依結果的一列代表什麼選用哪一個、視窗函數的值怎麼拿來當篩選條件，以及 LAG 取相鄰列時分區、排序鍵與距離條件各自要確認的事">SQL.10 分組與視窗函數：各自的產出、選用的依據與 LAG、LEAD 的相鄰列</a> 用輸出的單位當判斷標準，並示範相鄰的定義漏掉分區之後會跨到別的組上。</p>
<p><strong>從產出認出手上是哪一個介面</strong>：<a href="/blog/programming/python/basic/08-data-analysis/orm-and-dataframe/" data-link-title="8.3 ORM 與 DataFrame 的分辨：回傳值的形狀、連線物件與查詢送出的時機" data-link-desc="ORM 產生查詢、DataFrame 執行運算，以及外觀相似的方法串接底下差了什麼">8.3 ORM 與 DataFrame 的分辨：回傳值的形狀、連線物件與查詢送出的時機</a> 寫兩者的回傳值形狀差在哪，以及分得開它們的三個訊號。</p>
<p><strong>這套代數在 SQL 那一側怎麼設計</strong>：關聯代數的組合規則決定了子句的先後、連接的左右與分組的鍵，而那些規則各有一處與直覺相反。<a href="/blog/backend/01-database/sql/" data-link-title="SQL：這個語言為什麼長這樣" data-link-desc="SQL 的設計而不是它的語法表：子句為什麼這樣切、一個查詢該怎麼讀，以及代價落在哪裡">SQL：這個語言為什麼長這樣</a> 從語言的宣告式性質推導這一整組規則，並另外處理文字之外的四方（識別字規則、引擎的寬鬆度、權限、最佳化器）。</p>
<p><strong>換介面之後接手的新限制</strong>：SQL 那一側沒有記憶體上限的問題，換到 DataFrame 就有了。<a href="/blog/programming/python/basic/08-data-analysis/memory-is-the-boundary/" data-link-title="8.5 pandas 的記憶體邊界：資料與運算過程的佔用估算，推回資料庫、換引擎與分散式的代價" data-link-desc="數值欄與字串欄的佔用怎麼得出、合併這類操作的中間結果佔多少，以及記憶體不足與跑得慢這兩種失敗怎麼分辨">8.5 pandas 的記憶體邊界：資料與運算過程的佔用估算，推回資料庫、換引擎與分散式的代價</a> 給估算方法與越過之後的三個方向。</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>