<?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>Pandas on Tarragon</title><link>https://tarrragon.github.io/blog/tags/pandas/</link><description>Recent content in Pandas 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/pandas/index.xml" rel="self" type="application/rss+xml"/><item><title>8.1 Python 的幾種工作與資料進入程式的單位：Web 後端、資料工程與分析、機器學習、自動化與系統工具</title><link>https://tarrragon.github.io/blog/programming/python/basic/08-data-analysis/data-unit-splits-work/</link><pubDate>Mon, 31 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/programming/python/basic/08-data-analysis/data-unit-splits-work/</guid><description>&lt;p>分開 Python 底下幾種工作的，是資料以什麼單位進入程式。一次進來一筆、還是一次進來一整批，決定了要解的問題是什麼，也決定了工具鏈長什麼樣子。同一個語言標籤底下因此裝著幾組彼此不太相干的技術。&lt;/p>
&lt;h2 id="web-後端以一次請求用到的幾筆資料為單位">Web 後端：以一次請求用到的幾筆資料為單位&lt;/h2>
&lt;p>資料的單位是這次請求要用到的那幾筆。使用者按下一個按鈕，程式取出對應的幾列、改幾個欄位、寫回去，然後這次請求就結束了。&lt;/p>
&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> 把資料列包成物件、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">連線池&lt;/a>管理與資料庫之間的連線、遷移工具讓表結構跟著程式一起演進。Django 與 FastAPI 這類框架的周邊生態幾乎都繞著這件事。&lt;/p>
&lt;p>Web 後端的失敗形態跟著單位走。一次請求裡不小心打了一百次資料庫（每筆各查一次），或是把&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">交易&lt;/a>的範圍包得太大而讓鎖持有太久（一段時間內不讓別人動同一批列，範圍越大擋得越久），都是「一次應該只取幾筆」這個前提被破壞的結果。&lt;/p>
&lt;h2 id="資料工程與分析以整張表或整批事件為單位">資料工程與分析：以整張表或整批事件為單位&lt;/h2>
&lt;p>資料的單位是整張表，或某段期間的全部事件。沒有「這次請求」這種邊界，取而代之的是「這批」。&lt;/p>
&lt;p>資料工程與分析的工具鏈因此分成三組，各解一件事：整批資料怎麼算（pandas 在單機記憶體裡算，Spark 把同一件事拆到多台機器上），轉換邏輯怎麼版本控管（dbt 把每一步轉換寫成可以被審閱與測試的模型），以及什麼時候算、失敗了怎麼重跑（Airflow 這類排程器）。&lt;/p>
&lt;p>失敗形態也整組換掉：這批裝不裝得進記憶體、同一批會不會因為重跑而被算兩次、上游的欄位改了名字而下游整條流程靜默算出錯的結果。&lt;/p>
&lt;h2 id="機器學習接在批次處理後面的數值矩陣">機器學習：接在批次處理後面的數值矩陣&lt;/h2>
&lt;p>模型收的是一個數值矩陣。原始資料很少長成那個樣子，所以把它整理成矩陣的那一段仍然是批次處理——合併幾張表、把類別欄位轉成數值、補掉缺值、切出訓練與驗證。&lt;/p>
&lt;p>pandas 在這裡是前置作業，PyTorch 這類框架接在它後面。機器學習與資料工程共用工具鏈的前半段，分歧在後半段。&lt;/p>
&lt;h2 id="自動化與系統工具以一個檔案或一次執行為單位">自動化與系統工具：以一個檔案或一次執行為單位&lt;/h2>
&lt;p>自動化腳本處理的是檔案、程序與外部命令。單位是一個檔案或一次執行，資料本身是被搬運的內容。工具是 &lt;code>pathlib&lt;/code>、&lt;code>subprocess&lt;/code>、&lt;code>argparse&lt;/code> 這一組，也就是 &lt;a href="https://tarrragon.github.io/blog/programming/python/basic/03-stdlib/" data-link-title="模組三：標準庫實戰" data-link-desc="Python 標準庫的常用模組實戰應用">模組三&lt;/a> 的內容。&lt;/p>
&lt;p>系統與工具開發再往外一層：產出是被別人 import 或執行的程式，資料由使用者帶來。關心的是介面設計、相容性與封裝。&lt;/p>
&lt;h2 id="各組工作的失敗形態判斷材料與檢索關鍵字的差異">各組工作的失敗形態、判斷材料與檢索關鍵字的差異&lt;/h2>
&lt;p>單位決定了什麼事情會出錯，而這幾組的單位不同，出錯的形態就沒有交集。&lt;/p>
&lt;p>把一次請求的查詢次數壓下來，靠的是理解物件與資料列之間的對應關係；判斷一批資料裝不裝得進記憶體，靠的是估算列數乘欄數乘每格佔幾個位元組。這兩種判斷用的材料完全不同，前一種做得再熟也不會讓後一種變容易。&lt;/p>
&lt;p>檢索的結果跟著同一條軸分開。以物件關聯對映與請求生命週期為關鍵字找到的內容，與以 DataFrame 和排程為關鍵字找到的內容幾乎不重疊，因為它們描述的是不同的問題。一個人如果只用其中一組關鍵字搜尋，另一組的內容不會被結果帶到他面前——要看到它得先知道該用哪些詞。&lt;/p>
&lt;p>這幾組工作在職務名稱上也各自分開，Python 只是它們共用的語言。&lt;/p>
&lt;h2 id="延伸閱讀運算發生的位置web-後端的資料庫議題商業分析指標標準庫的檔案與外部命令工具">延伸閱讀：運算發生的位置、Web 後端的資料庫議題、商業分析指標、標準庫的檔案與外部命令工具&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> 寫這條分界怎麼決定工具的形狀，以及 Python 存取資料庫的三種寫法為什麼全部落在資料庫那一側。&lt;/p>
&lt;p>&lt;strong>Web 後端的資料庫議題&lt;/strong>：一次請求該打幾次資料庫、交易邊界包到哪裡、ORM 的延遲載入什麼時候變成問題。&lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">backend 模組一&lt;/a> 從這些形態進去，並給出每請求的查詢次數預算當判讀基準。&lt;/p>
&lt;p>&lt;strong>批次算出來的結果拿去做什麼&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/devops/monitoring/08-business-analytics/" data-link-title="模組八：行為資料的商業利用" data-link-desc="Funnel / Cohort / Attribution / A/B test / 推薦系統 / RFM — 從 debug 工具到商業資產的翻轉">monitoring 模組八&lt;/a> 寫留存、分群與轉換率各自怎麼定義，以及事件當初的屬性設計決定了哪些指標事後算得出來。&lt;/p>
&lt;p>&lt;strong>自動化的工具&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/programming/python/basic/03-stdlib/" data-link-title="模組三：標準庫實戰" data-link-desc="Python 標準庫的常用模組實戰應用">模組三&lt;/a> 收的是處理檔案與外部命令的標準庫，單位是一個檔案或一次執行。&lt;/p></description><content:encoded><![CDATA[<p>分開 Python 底下幾種工作的，是資料以什麼單位進入程式。一次進來一筆、還是一次進來一整批，決定了要解的問題是什麼，也決定了工具鏈長什麼樣子。同一個語言標籤底下因此裝著幾組彼此不太相干的技術。</p>
<h2 id="web-後端以一次請求用到的幾筆資料為單位">Web 後端：以一次請求用到的幾筆資料為單位</h2>
<p>資料的單位是這次請求要用到的那幾筆。使用者按下一個按鈕，程式取出對應的幾列、改幾個欄位、寫回去，然後這次請求就結束了。</p>
<p>工具解的是把那幾筆安全地取出與改回去：<a href="/blog/backend/knowledge-cards/orm/" data-link-title="ORM（Object-Relational Mapping）" data-link-desc="看到程式用類別與方法描述查詢時，查它交出去的是什麼、以及哪些行為在程式碼上讀不出來">ORM</a> 把資料列包成物件、<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">連線池</a>管理與資料庫之間的連線、遷移工具讓表結構跟著程式一起演進。Django 與 FastAPI 這類框架的周邊生態幾乎都繞著這件事。</p>
<p>Web 後端的失敗形態跟著單位走。一次請求裡不小心打了一百次資料庫（每筆各查一次），或是把<a href="/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">交易</a>的範圍包得太大而讓鎖持有太久（一段時間內不讓別人動同一批列，範圍越大擋得越久），都是「一次應該只取幾筆」這個前提被破壞的結果。</p>
<h2 id="資料工程與分析以整張表或整批事件為單位">資料工程與分析：以整張表或整批事件為單位</h2>
<p>資料的單位是整張表，或某段期間的全部事件。沒有「這次請求」這種邊界，取而代之的是「這批」。</p>
<p>資料工程與分析的工具鏈因此分成三組，各解一件事：整批資料怎麼算（pandas 在單機記憶體裡算，Spark 把同一件事拆到多台機器上），轉換邏輯怎麼版本控管（dbt 把每一步轉換寫成可以被審閱與測試的模型），以及什麼時候算、失敗了怎麼重跑（Airflow 這類排程器）。</p>
<p>失敗形態也整組換掉：這批裝不裝得進記憶體、同一批會不會因為重跑而被算兩次、上游的欄位改了名字而下游整條流程靜默算出錯的結果。</p>
<h2 id="機器學習接在批次處理後面的數值矩陣">機器學習：接在批次處理後面的數值矩陣</h2>
<p>模型收的是一個數值矩陣。原始資料很少長成那個樣子，所以把它整理成矩陣的那一段仍然是批次處理——合併幾張表、把類別欄位轉成數值、補掉缺值、切出訓練與驗證。</p>
<p>pandas 在這裡是前置作業，PyTorch 這類框架接在它後面。機器學習與資料工程共用工具鏈的前半段，分歧在後半段。</p>
<h2 id="自動化與系統工具以一個檔案或一次執行為單位">自動化與系統工具：以一個檔案或一次執行為單位</h2>
<p>自動化腳本處理的是檔案、程序與外部命令。單位是一個檔案或一次執行，資料本身是被搬運的內容。工具是 <code>pathlib</code>、<code>subprocess</code>、<code>argparse</code> 這一組，也就是 <a href="/blog/programming/python/basic/03-stdlib/" data-link-title="模組三：標準庫實戰" data-link-desc="Python 標準庫的常用模組實戰應用">模組三</a> 的內容。</p>
<p>系統與工具開發再往外一層：產出是被別人 import 或執行的程式，資料由使用者帶來。關心的是介面設計、相容性與封裝。</p>
<h2 id="各組工作的失敗形態判斷材料與檢索關鍵字的差異">各組工作的失敗形態、判斷材料與檢索關鍵字的差異</h2>
<p>單位決定了什麼事情會出錯，而這幾組的單位不同，出錯的形態就沒有交集。</p>
<p>把一次請求的查詢次數壓下來，靠的是理解物件與資料列之間的對應關係；判斷一批資料裝不裝得進記憶體，靠的是估算列數乘欄數乘每格佔幾個位元組。這兩種判斷用的材料完全不同，前一種做得再熟也不會讓後一種變容易。</p>
<p>檢索的結果跟著同一條軸分開。以物件關聯對映與請求生命週期為關鍵字找到的內容，與以 DataFrame 和排程為關鍵字找到的內容幾乎不重疊，因為它們描述的是不同的問題。一個人如果只用其中一組關鍵字搜尋，另一組的內容不會被結果帶到他面前——要看到它得先知道該用哪些詞。</p>
<p>這幾組工作在職務名稱上也各自分開，Python 只是它們共用的語言。</p>
<h2 id="延伸閱讀運算發生的位置web-後端的資料庫議題商業分析指標標準庫的檔案與外部命令工具">延伸閱讀：運算發生的位置、Web 後端的資料庫議題、商業分析指標、標準庫的檔案與外部命令工具</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> 寫這條分界怎麼決定工具的形狀，以及 Python 存取資料庫的三種寫法為什麼全部落在資料庫那一側。</p>
<p><strong>Web 後端的資料庫議題</strong>：一次請求該打幾次資料庫、交易邊界包到哪裡、ORM 的延遲載入什麼時候變成問題。<a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">backend 模組一</a> 從這些形態進去，並給出每請求的查詢次數預算當判讀基準。</p>
<p><strong>批次算出來的結果拿去做什麼</strong>：<a href="/blog/devops/monitoring/08-business-analytics/" data-link-title="模組八：行為資料的商業利用" data-link-desc="Funnel / Cohort / Attribution / A/B test / 推薦系統 / RFM — 從 debug 工具到商業資產的翻轉">monitoring 模組八</a> 寫留存、分群與轉換率各自怎麼定義，以及事件當初的屬性設計決定了哪些指標事後算得出來。</p>
<p><strong>自動化的工具</strong>：<a href="/blog/programming/python/basic/03-stdlib/" data-link-title="模組三：標準庫實戰" data-link-desc="Python 標準庫的常用模組實戰應用">模組三</a> 收的是處理檔案與外部命令的標準庫，單位是一個檔案或一次執行。</p>
]]></content:encoded></item><item><title>8.2 運算發生的位置：資料庫端與記憶體端的工具、分工條件與 DuckDB</title><link>https://tarrragon.github.io/blog/programming/python/basic/08-data-analysis/where-computation-runs/</link><pubDate>Mon, 31 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/programming/python/basic/08-data-analysis/where-computation-runs/</guid><description>&lt;p>整批資料有兩個地方可以算：留在資料庫裡，或搬進程式的記憶體。這條分界決定了工具的形狀、它的容量上限，以及它的失敗形態。本模組其餘各篇的判斷標準都折算回這一條。&lt;/p>
&lt;h2 id="資料庫端db-api查詢建構器與-orm-的寫法">資料庫端：DB-API、查詢建構器與 ORM 的寫法&lt;/h2>
&lt;p>程式送出的是一段描述要算什麼的文字，資料庫收下之後自己完成篩選、合併與彙總，把結果送回來。原始資料從頭到尾沒有進入程式。&lt;/p>
&lt;p>Python 這一側有三種寫法，差別只在那段 SQL 由誰組出來。&lt;/p>
&lt;p>&lt;strong>自己寫 SQL 字串&lt;/strong>：&lt;code>sqlite3&lt;/code>、&lt;code>psycopg2&lt;/code> 這類套件遵循 DB-API 這個標準介面，程式把 SQL 交給它，它負責送出與取回。&lt;/p>
&lt;p>&lt;strong>用方法串出 SQL&lt;/strong>：SQLAlchemy Core 這類查詢建構器提供 Python 的方法對應 SQL 的子句，組出來的仍然是一段 SQL。&lt;/p>
&lt;p>&lt;strong>用類別描述表&lt;/strong>：&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> 這一類工具（SQLAlchemy、Django ORM）把資料表對應成類別、把資料列對應成物件，篩選條件寫成屬性比較，框架據此組出 SQL，並把回來的每一列包成物件。&lt;/p>
&lt;p>三者的共同點比差異重要：&lt;strong>運算在資料庫那一端發生&lt;/strong>，程式收到的是算完的結果。編譯出來的 SQL 可以直接印出來看：&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="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">3&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">4&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;/code>&lt;/pre>&lt;/div>&lt;h2 id="記憶體端pandas-與-dataframe">記憶體端：pandas 與 DataFrame&lt;/h2>
&lt;p>搬進記憶體端的做法是把資料整批載進程式，運算由程式自己完成。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>。&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="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;/code>&lt;/pre>&lt;/div>&lt;p>這一行沒有連線、沒有查詢語句、沒有資料庫。&lt;code>df&lt;/code> 是一張已經在記憶體裡的表格，這一行做的是對一個布林陣列取值。資料的來源可以是資料庫，也可以是 CSV、日誌檔或某個 API 回傳的內容——載進來之後，接下來的每一步運算都寫成一樣的形式，來源是哪一種不再影響寫法。&lt;/p>
&lt;h2 id="兩端分工的條件資料量sql-的表達能力算完之後的用途">兩端分工的條件：資料量、SQL 的表達能力、算完之後的用途&lt;/h2>
&lt;p>&lt;strong>資料量。&lt;/strong> 記憶體端要把資料整批放進來，所以容量是硬上限；資料庫端沒有這個限制，它一次只需要把符合條件的部分送回來。&lt;/p>
&lt;p>&lt;strong>要算的運算 SQL 表達得出來嗎。&lt;/strong> 篩選、合併、分組彙總、排名，SQL 都寫得出來。統計檢定、時間序列的滑動窗口、把資料餵給模型，SQL 寫起來吃力或根本沒有對應語法，這些落在記憶體端。&lt;/p>
&lt;p>&lt;strong>算完之後還要做什麼。&lt;/strong> 結果如果只是要回傳給使用者或寫回另一張表，留在資料庫端的路徑最短——資料不必先進記憶體再送出去；如果後面還要接繪圖、統計或模型訓練，資料終究要進記憶體，那就沒有理由分兩段做。&lt;/p>
&lt;p>實務上的常見形態是兩端接力：用 SQL 把資料量先壓下來，再把壓縮過的結果搬進記憶體做後面的部分。&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">read_sql&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;SELECT * FROM orders WHERE year = 2024&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">conn&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="c1"># 這一行之後全部在記憶體裡，與資料庫無關&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="duckdb在同一個程序裡用-sql-查-dataframe-與檔案">DuckDB：在同一個程序裡用 SQL 查 DataFrame 與檔案&lt;/h2>
&lt;p>DuckDB 兩邊都站：它接受 SQL，而查詢的對象可以是記憶體裡的 DataFrame，也可以是磁碟上的檔案。&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">duckdb&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sql&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;SELECT dept, count(*) c FROM emp GROUP BY dept&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># emp 是一個 DataFrame&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>它把「資料庫」這個角色從一台獨立的伺服器換成同一個程序裡的運算引擎，而軸本身仍在——差別只在資料要不要複製一份進 Python 的記憶體。&lt;/p>
&lt;h2 id="延伸閱讀orm-與-dataframe-的分辨sql-與-pandas-的寫法對應pandas-的記憶體邊界資料庫端的工程議題">延伸閱讀：ORM 與 DataFrame 的分辨、SQL 與 pandas 的寫法對應、pandas 的記憶體邊界、資料庫端的工程議題&lt;/h2>
&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>兩端共用的那套抽象&lt;/strong>：篩選、合併、分組彙總在兩端都有，而且對應得起來。&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/memory-is-the-boundary/" data-link-title="8.5 pandas 的記憶體邊界：資料與運算過程的佔用估算，推回資料庫、換引擎與分散式的代價" data-link-desc="數值欄與字串欄的佔用怎麼得出、合併這類操作的中間結果佔多少，以及記憶體不足與跑得慢這兩種失敗怎麼分辨">8.5 pandas 的記憶體邊界：資料與運算過程的佔用估算，推回資料庫、換引擎與分散式的代價&lt;/a> 給資料量的估算方法、運算過程額外要的空間，以及越過之後三個方向各自付什麼代價。&lt;/p>
&lt;p>&lt;strong>資料庫端那一側的工程議題&lt;/strong>：連線池、交易邊界、一次請求該打幾次資料庫，在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">backend 模組一&lt;/a>。那個模組從 N+1 與延遲載入這些形態進去，並給出每請求查詢次數預算當判讀基準。&lt;/p></description><content:encoded><![CDATA[<p>整批資料有兩個地方可以算：留在資料庫裡，或搬進程式的記憶體。這條分界決定了工具的形狀、它的容量上限，以及它的失敗形態。本模組其餘各篇的判斷標準都折算回這一條。</p>
<h2 id="資料庫端db-api查詢建構器與-orm-的寫法">資料庫端：DB-API、查詢建構器與 ORM 的寫法</h2>
<p>程式送出的是一段描述要算什麼的文字，資料庫收下之後自己完成篩選、合併與彙總，把結果送回來。原始資料從頭到尾沒有進入程式。</p>
<p>Python 這一側有三種寫法，差別只在那段 SQL 由誰組出來。</p>
<p><strong>自己寫 SQL 字串</strong>：<code>sqlite3</code>、<code>psycopg2</code> 這類套件遵循 DB-API 這個標準介面，程式把 SQL 交給它，它負責送出與取回。</p>
<p><strong>用方法串出 SQL</strong>：SQLAlchemy Core 這類查詢建構器提供 Python 的方法對應 SQL 的子句，組出來的仍然是一段 SQL。</p>
<p><strong>用類別描述表</strong>：<a href="/blog/backend/knowledge-cards/orm/" data-link-title="ORM（Object-Relational Mapping）" data-link-desc="看到程式用類別與方法描述查詢時，查它交出去的是什麼、以及哪些行為在程式碼上讀不出來">ORM</a> 這一類工具（SQLAlchemy、Django ORM）把資料表對應成類別、把資料列對應成物件，篩選條件寫成屬性比較，框架據此組出 SQL，並把回來的每一列包成物件。</p>
<p>三者的共同點比差異重要：<strong>運算在資料庫那一端發生</strong>，程式收到的是算完的結果。編譯出來的 SQL 可以直接印出來看：</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="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">3</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">4</span><span class="cl"><span class="c1"># SELECT customer.id, customer.name, customer.age FROM customer WHERE customer.age &gt; ?</span></span></span></code></pre></div><h2 id="記憶體端pandas-與-dataframe">記憶體端：pandas 與 DataFrame</h2>
<p>搬進記憶體端的做法是把資料整批載進程式，運算由程式自己完成。pandas 屬於這一側，它的容器是 <a href="/blog/backend/01-database/sql/knowledge-cards/dataframe/" data-link-title="DataFrame（資料框）" data-link-desc="SQL 教材拿 pandas 或資料框當對照時，查 DataFrame 是什麼、它與資料表差在哪">DataFrame</a>。</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="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></code></pre></div><p>這一行沒有連線、沒有查詢語句、沒有資料庫。<code>df</code> 是一張已經在記憶體裡的表格，這一行做的是對一個布林陣列取值。資料的來源可以是資料庫，也可以是 CSV、日誌檔或某個 API 回傳的內容——載進來之後，接下來的每一步運算都寫成一樣的形式，來源是哪一種不再影響寫法。</p>
<h2 id="兩端分工的條件資料量sql-的表達能力算完之後的用途">兩端分工的條件：資料量、SQL 的表達能力、算完之後的用途</h2>
<p><strong>資料量。</strong> 記憶體端要把資料整批放進來，所以容量是硬上限；資料庫端沒有這個限制，它一次只需要把符合條件的部分送回來。</p>
<p><strong>要算的運算 SQL 表達得出來嗎。</strong> 篩選、合併、分組彙總、排名，SQL 都寫得出來。統計檢定、時間序列的滑動窗口、把資料餵給模型，SQL 寫起來吃力或根本沒有對應語法，這些落在記憶體端。</p>
<p><strong>算完之後還要做什麼。</strong> 結果如果只是要回傳給使用者或寫回另一張表，留在資料庫端的路徑最短——資料不必先進記憶體再送出去；如果後面還要接繪圖、統計或模型訓練，資料終究要進記憶體，那就沒有理由分兩段做。</p>
<p>實務上的常見形態是兩端接力：用 SQL 把資料量先壓下來，再把壓縮過的結果搬進記憶體做後面的部分。</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">read_sql</span><span class="p">(</span><span class="s2">&#34;SELECT * FROM orders WHERE year = 2024&#34;</span><span class="p">,</span> <span class="n">conn</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 這一行之後全部在記憶體裡，與資料庫無關</span></span></span></code></pre></div><h2 id="duckdb在同一個程序裡用-sql-查-dataframe-與檔案">DuckDB：在同一個程序裡用 SQL 查 DataFrame 與檔案</h2>
<p>DuckDB 兩邊都站：它接受 SQL，而查詢的對象可以是記憶體裡的 DataFrame，也可以是磁碟上的檔案。</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">duckdb</span><span class="o">.</span><span class="n">sql</span><span class="p">(</span><span class="s2">&#34;SELECT dept, count(*) c FROM emp GROUP BY dept&#34;</span><span class="p">)</span>  <span class="c1"># emp 是一個 DataFrame</span></span></span></code></pre></div><p>它把「資料庫」這個角色從一台獨立的伺服器換成同一個程序裡的運算引擎，而軸本身仍在——差別只在資料要不要複製一份進 Python 的記憶體。</p>
<h2 id="延伸閱讀orm-與-dataframe-的分辨sql-與-pandas-的寫法對應pandas-的記憶體邊界資料庫端的工程議題">延伸閱讀：ORM 與 DataFrame 的分辨、SQL 與 pandas 的寫法對應、pandas 的記憶體邊界、資料庫端的工程議題</h2>
<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>兩端共用的那套抽象</strong>：篩選、合併、分組彙總在兩端都有，而且對應得起來。<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/memory-is-the-boundary/" data-link-title="8.5 pandas 的記憶體邊界：資料與運算過程的佔用估算，推回資料庫、換引擎與分散式的代價" data-link-desc="數值欄與字串欄的佔用怎麼得出、合併這類操作的中間結果佔多少，以及記憶體不足與跑得慢這兩種失敗怎麼分辨">8.5 pandas 的記憶體邊界：資料與運算過程的佔用估算，推回資料庫、換引擎與分散式的代價</a> 給資料量的估算方法、運算過程額外要的空間，以及越過之後三個方向各自付什麼代價。</p>
<p><strong>資料庫端那一側的工程議題</strong>：連線池、交易邊界、一次請求該打幾次資料庫，在 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">backend 模組一</a>。那個模組從 N+1 與延遲載入這些形態進去，並給出每請求查詢次數預算當判讀基準。</p>
]]></content:encoded></item><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>8.5 pandas 的記憶體邊界：資料與運算過程的佔用估算，推回資料庫、換引擎與分散式的代價</title><link>https://tarrragon.github.io/blog/programming/python/basic/08-data-analysis/memory-is-the-boundary/</link><pubDate>Mon, 31 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/programming/python/basic/08-data-analysis/memory-is-the-boundary/</guid><description>&lt;p>pandas 把整批資料放進記憶體才開始算，所以記憶體容量是它的硬邊界。這個邊界可以事先估算，而越過之後有三個方向可以走，各自付不同的代價。&lt;/p>
&lt;h2 id="資料本身的佔用量估算">資料本身的佔用量估算&lt;/h2>
&lt;p>數值欄位每一格佔固定的位元組數，所以列數乘欄數乘每格的位元組數就是估算值。&lt;code>int64&lt;/code> 與 &lt;code>float64&lt;/code> 各佔 8 個位元組。&lt;/p>
&lt;p>五百萬列、兩個數值欄的表格實測佔 76 MB，與 5,000,000 × 2 × 8 = 80 MB 相符。這個量級對現在的機器不構成問題，往上推一個量級（五千萬列）到 760 MB 仍然可行，再往上就要看機器。&lt;/p>
&lt;p>字串欄位不適用這個乘法，它的佔用取決於實際的字元內容與 pandas 的字串儲存方式——這一欄只能量，不能算，量的方式是 &lt;code>df.memory_usage(deep=True)&lt;/code>。&lt;/p>
&lt;h2 id="運算過程的中間結果佔用的空間">運算過程的中間結果佔用的空間&lt;/h2>
&lt;p>估算容易漏掉的是中間結果。合併與分組會產生暫時的陣列，而它們與原始資料同時存在於記憶體裡。&lt;/p>
&lt;p>實測一次 &lt;code>merge&lt;/code>：兩張表本身合計 38 MB，合併結果 46 MB，而合併期間的額外配置峰值是 136 MB——大約是那兩張表合計 38 MB 的三倍半。&lt;/p>
&lt;p>所以判斷標準是「資料量的數倍要放得下」，而不是「資料量放得下」。倍數隨操作而變，合併與排序這類要重排全部資料的操作最高。&lt;/p>
&lt;h2 id="越過記憶體邊界之後的方向推回資料庫換引擎分散式">越過記憶體邊界之後的方向：推回資料庫、換引擎、分散式&lt;/h2>
&lt;p>&lt;strong>把運算推回資料庫。&lt;/strong> 資料如果本來就在資料庫裡，先用 SQL 把量壓下來再載入。這條路不需要換工具，代價是那段運算必須用 SQL 表達得出來。&lt;/p>
&lt;p>&lt;strong>換成不需要全部載入的引擎。&lt;/strong> DuckDB 接受 SQL，而查詢的對象可以直接是磁碟上的檔案。實測查一個 129 MB 的 CSV 算出列數與平均值，Python 端的配置峰值是 0.0 MB；同一件事用 pandas 先載入再算，峰值 114.5 MB、載入後的 DataFrame 佔 76 MB。&lt;/p>
&lt;p>這裡的 0.0 MB 要照字面讀：量的是 Python 自己配置的記憶體，DuckDB 的運算發生在它自己的引擎裡、記憶體不經過 Python 的配置器。它逐段讀檔案，任何時刻只持有其中一段。&lt;/p>
&lt;p>Polars 走的是另一條類似的路：介面接近 DataFrame，而它的延遲執行模式讓整段運算先被規劃過再執行，可以只讀用得到的欄位。&lt;/p>
&lt;p>&lt;strong>換成分散式。&lt;/strong> 資料量大到單機放不下時才走這個方向。Spark 把同一份運算拆到多台機器上，代價是叢集本身的維運成本與除錯難度，兩者都比單機高一個量級。&lt;/p>
&lt;h2 id="記憶體不足與跑得慢的分辨">記憶體不足與跑得慢的分辨&lt;/h2>
&lt;p>記憶體不足有它自己的訊號：程序被作業系統終止，或者機器開始把記憶體換出到磁碟而整體速度崩落。這兩種現象都來自同一件事——需要的空間超過實體記憶體，作業系統只能終止程序或改用磁碟頂替。&lt;/p>
&lt;p>跑得慢的成因在別處。逐列套用函式而沒有用向量化的操作，慢的是每一列都要繞一趟 Python 直譯器；反覆複製整張表，慢的是記憶體頻寬。這兩種都與容量無關，所以資料量再小它們也照樣發生，換到容量更大的工具上也照樣發生。&lt;/p>
&lt;p>分開這兩者的意義在於推回資料庫、換引擎與分散式這幾個方向只解記憶體不足那一種。&lt;/p>
&lt;h2 id="延伸閱讀運算發生的位置sql-與-pandas-的寫法對應商業分析指標">延伸閱讀：運算發生的位置、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> 寫兩端的分工由哪三個條件決定，以及 DuckDB 為什麼同時站在兩邊。&lt;/p>
&lt;p>&lt;strong>推回去之後要用的語法對照&lt;/strong>：&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/devops/monitoring/08-business-analytics/" data-link-title="模組八：行為資料的商業利用" data-link-desc="Funnel / Cohort / Attribution / A/B test / 推薦系統 / RFM — 從 debug 工具到商業資產的翻轉">monitoring 模組八&lt;/a> 寫留存、分群與轉換率各自怎麼定義，以及事件當初的屬性設計決定了哪些指標事後算得出來。&lt;/p></description><content:encoded><![CDATA[<p>pandas 把整批資料放進記憶體才開始算，所以記憶體容量是它的硬邊界。這個邊界可以事先估算，而越過之後有三個方向可以走，各自付不同的代價。</p>
<h2 id="資料本身的佔用量估算">資料本身的佔用量估算</h2>
<p>數值欄位每一格佔固定的位元組數，所以列數乘欄數乘每格的位元組數就是估算值。<code>int64</code> 與 <code>float64</code> 各佔 8 個位元組。</p>
<p>五百萬列、兩個數值欄的表格實測佔 76 MB，與 5,000,000 × 2 × 8 = 80 MB 相符。這個量級對現在的機器不構成問題，往上推一個量級（五千萬列）到 760 MB 仍然可行，再往上就要看機器。</p>
<p>字串欄位不適用這個乘法，它的佔用取決於實際的字元內容與 pandas 的字串儲存方式——這一欄只能量，不能算，量的方式是 <code>df.memory_usage(deep=True)</code>。</p>
<h2 id="運算過程的中間結果佔用的空間">運算過程的中間結果佔用的空間</h2>
<p>估算容易漏掉的是中間結果。合併與分組會產生暫時的陣列，而它們與原始資料同時存在於記憶體裡。</p>
<p>實測一次 <code>merge</code>：兩張表本身合計 38 MB，合併結果 46 MB，而合併期間的額外配置峰值是 136 MB——大約是那兩張表合計 38 MB 的三倍半。</p>
<p>所以判斷標準是「資料量的數倍要放得下」，而不是「資料量放得下」。倍數隨操作而變，合併與排序這類要重排全部資料的操作最高。</p>
<h2 id="越過記憶體邊界之後的方向推回資料庫換引擎分散式">越過記憶體邊界之後的方向：推回資料庫、換引擎、分散式</h2>
<p><strong>把運算推回資料庫。</strong> 資料如果本來就在資料庫裡，先用 SQL 把量壓下來再載入。這條路不需要換工具，代價是那段運算必須用 SQL 表達得出來。</p>
<p><strong>換成不需要全部載入的引擎。</strong> DuckDB 接受 SQL，而查詢的對象可以直接是磁碟上的檔案。實測查一個 129 MB 的 CSV 算出列數與平均值，Python 端的配置峰值是 0.0 MB；同一件事用 pandas 先載入再算，峰值 114.5 MB、載入後的 DataFrame 佔 76 MB。</p>
<p>這裡的 0.0 MB 要照字面讀：量的是 Python 自己配置的記憶體，DuckDB 的運算發生在它自己的引擎裡、記憶體不經過 Python 的配置器。它逐段讀檔案，任何時刻只持有其中一段。</p>
<p>Polars 走的是另一條類似的路：介面接近 DataFrame，而它的延遲執行模式讓整段運算先被規劃過再執行，可以只讀用得到的欄位。</p>
<p><strong>換成分散式。</strong> 資料量大到單機放不下時才走這個方向。Spark 把同一份運算拆到多台機器上，代價是叢集本身的維運成本與除錯難度，兩者都比單機高一個量級。</p>
<h2 id="記憶體不足與跑得慢的分辨">記憶體不足與跑得慢的分辨</h2>
<p>記憶體不足有它自己的訊號：程序被作業系統終止，或者機器開始把記憶體換出到磁碟而整體速度崩落。這兩種現象都來自同一件事——需要的空間超過實體記憶體，作業系統只能終止程序或改用磁碟頂替。</p>
<p>跑得慢的成因在別處。逐列套用函式而沒有用向量化的操作，慢的是每一列都要繞一趟 Python 直譯器；反覆複製整張表，慢的是記憶體頻寬。這兩種都與容量無關，所以資料量再小它們也照樣發生，換到容量更大的工具上也照樣發生。</p>
<p>分開這兩者的意義在於推回資料庫、換引擎與分散式這幾個方向只解記憶體不足那一種。</p>
<h2 id="延伸閱讀運算發生的位置sql-與-pandas-的寫法對應商業分析指標">延伸閱讀：運算發生的位置、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> 寫兩端的分工由哪三個條件決定，以及 DuckDB 為什麼同時站在兩邊。</p>
<p><strong>推回去之後要用的語法對照</strong>：<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/devops/monitoring/08-business-analytics/" data-link-title="模組八：行為資料的商業利用" data-link-desc="Funnel / Cohort / Attribution / A/B test / 推薦系統 / RFM — 從 debug 工具到商業資產的翻轉">monitoring 模組八</a> 寫留存、分群與轉換率各自怎麼定義，以及事件當初的屬性設計決定了哪些指標事後算得出來。</p>
]]></content:encoded></item><item><title>模組八：整批資料的處理與分析</title><link>https://tarrragon.github.io/blog/programming/python/basic/08-data-analysis/</link><pubDate>Mon, 31 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/programming/python/basic/08-data-analysis/</guid><description>&lt;p>本模組處理的是資料整批進入程式之後的工具選擇。前七個模組寫的是程式碼本身的結構，這一個模組的軸換成資料：一次進來多少、運算發生在哪一端、超過記憶體之後往哪裡走。&lt;/p>
&lt;h2 id="讀者定位">讀者定位&lt;/h2>
&lt;p>有程式經驗、SQL 大致能寫，但沒有做過批次資料處理的工程師。Web 後端背景的讀者會發現這一側的工具跟熟悉的那一套幾乎沒有交集，而模組的第一篇就在解釋這件事。&lt;/p>
&lt;h2 id="章節">章節&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>篇&lt;/th>
 &lt;th>交付&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/programming/python/basic/08-data-analysis/data-unit-splits-work/" data-link-title="8.1 Python 的幾種工作與資料進入程式的單位：Web 後端、資料工程與分析、機器學習、自動化與系統工具" data-link-desc="資料以一筆還是一整批進入程式，各組工作的工具鏈與失敗形態怎麼跟著這個單位分開">8.1 Python 的幾種工作與資料進入程式的單位&lt;/a>&lt;/td>
 &lt;td>分開幾種 Python 工作的那條軸，以及各自的工具鏈由它推導出來&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&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;/td>
 &lt;td>資料庫端與記憶體端的分界，本模組其餘各篇的推導源頭&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&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;/td>
 &lt;td>兩者各自產出什麼，以及從回傳值認出手上是哪一類的方法&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&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;/td>
 &lt;td>SQL 與 DataFrame 四組對應的實測，以及對應斷掉的位置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&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;/td>
 &lt;td>資料量的估算法，與越過之後三個方向各自付什麼代價&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="推導源頭">推導源頭&lt;/h2>
&lt;p>各篇的判斷標準都折算回 8.2 的那條軸：運算發生在資料庫端還是程式的記憶體端。8.3 是這條軸兩側的產物比對，8.4 是兩側共用的抽象，8.5 是記憶體端那一側的容量上限。8.1 在軸之前，負責說明為什麼會有這條軸要分。&lt;/p>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">backend 模組一 資料庫&lt;/a>：運算留在資料庫端那一側的工程議題，N+1 與交易邊界都在那裡&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/" data-link-title="SQL：這個語言為什麼長這樣" data-link-desc="SQL 的設計而不是它的語法表：子句為什麼這樣切、一個查詢該怎麼讀，以及代價落在哪裡">SQL：這個語言為什麼長這樣&lt;/a>：資料庫端那一側用的語言本身——子句怎麼切、連接的左右決定誰被保護、代價為什麼不在查詢的文字裡&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/devops/monitoring/08-business-analytics/" data-link-title="模組八：行為資料的商業利用" data-link-desc="Funnel / Cohort / Attribution / A/B test / 推薦系統 / RFM — 從 debug 工具到商業資產的翻轉">monitoring 模組八 行為資料的商業利用&lt;/a>：批次算出來的指標怎麼定義，以及事件屬性決定哪些指標算得出來&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/programming/python/basic/03-stdlib/" data-link-title="模組三：標準庫實戰" data-link-desc="Python 標準庫的常用模組實戰應用">python 模組三 標準庫實戰&lt;/a>：自動化那一格用到的工具，處理的單位是檔案與程序而不是資料集&lt;/li>
&lt;/ul>
&lt;h2 id="backlog">Backlog&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>項目&lt;/th>
 &lt;th>類型&lt;/th>
 &lt;th>前置條件&lt;/th>
 &lt;th>規模&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Polars 與 pandas 的 API 差異對照&lt;/td>
 &lt;td>主章&lt;/td>
 &lt;td>8.5 完成&lt;/td>
 &lt;td>中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>dbt 與 Airflow 在批次流程裡的位置&lt;/td>
 &lt;td>主章&lt;/td>
 &lt;td>需先確定與 backend 的分界&lt;/td>
 &lt;td>中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>髒資料處理：型別、缺值、重複列&lt;/td>
 &lt;td>主章&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>中&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<p>本模組處理的是資料整批進入程式之後的工具選擇。前七個模組寫的是程式碼本身的結構，這一個模組的軸換成資料：一次進來多少、運算發生在哪一端、超過記憶體之後往哪裡走。</p>
<h2 id="讀者定位">讀者定位</h2>
<p>有程式經驗、SQL 大致能寫，但沒有做過批次資料處理的工程師。Web 後端背景的讀者會發現這一側的工具跟熟悉的那一套幾乎沒有交集，而模組的第一篇就在解釋這件事。</p>
<h2 id="章節">章節</h2>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>交付</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/programming/python/basic/08-data-analysis/data-unit-splits-work/" data-link-title="8.1 Python 的幾種工作與資料進入程式的單位：Web 後端、資料工程與分析、機器學習、自動化與系統工具" data-link-desc="資料以一筆還是一整批進入程式，各組工作的工具鏈與失敗形態怎麼跟著這個單位分開">8.1 Python 的幾種工作與資料進入程式的單位</a></td>
          <td>分開幾種 Python 工作的那條軸，以及各自的工具鏈由它推導出來</td>
      </tr>
      <tr>
          <td><a href="/blog/programming/python/basic/08-data-analysis/where-computation-runs/" data-link-title="8.2 運算發生的位置：資料庫端與記憶體端的工具、分工條件與 DuckDB" data-link-desc="運算留在資料庫端或搬進記憶體端這條分界，怎麼決定工具的形狀、容量上限與失敗形態">8.2 運算發生的位置</a></td>
          <td>資料庫端與記憶體端的分界，本模組其餘各篇的推導源頭</td>
      </tr>
      <tr>
          <td><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></td>
          <td>兩者各自產出什麼，以及從回傳值認出手上是哪一類的方法</td>
      </tr>
      <tr>
          <td><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></td>
          <td>SQL 與 DataFrame 四組對應的實測，以及對應斷掉的位置</td>
      </tr>
      <tr>
          <td><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></td>
          <td>資料量的估算法，與越過之後三個方向各自付什麼代價</td>
      </tr>
  </tbody>
</table>
<h2 id="推導源頭">推導源頭</h2>
<p>各篇的判斷標準都折算回 8.2 的那條軸：運算發生在資料庫端還是程式的記憶體端。8.3 是這條軸兩側的產物比對，8.4 是兩側共用的抽象，8.5 是記憶體端那一側的容量上限。8.1 在軸之前，負責說明為什麼會有這條軸要分。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">backend 模組一 資料庫</a>：運算留在資料庫端那一側的工程議題，N+1 與交易邊界都在那裡</li>
<li>→ <a href="/blog/backend/01-database/sql/" data-link-title="SQL：這個語言為什麼長這樣" data-link-desc="SQL 的設計而不是它的語法表：子句為什麼這樣切、一個查詢該怎麼讀，以及代價落在哪裡">SQL：這個語言為什麼長這樣</a>：資料庫端那一側用的語言本身——子句怎麼切、連接的左右決定誰被保護、代價為什麼不在查詢的文字裡</li>
<li>→ <a href="/blog/devops/monitoring/08-business-analytics/" data-link-title="模組八：行為資料的商業利用" data-link-desc="Funnel / Cohort / Attribution / A/B test / 推薦系統 / RFM — 從 debug 工具到商業資產的翻轉">monitoring 模組八 行為資料的商業利用</a>：批次算出來的指標怎麼定義，以及事件屬性決定哪些指標算得出來</li>
<li>→ <a href="/blog/programming/python/basic/03-stdlib/" data-link-title="模組三：標準庫實戰" data-link-desc="Python 標準庫的常用模組實戰應用">python 模組三 標準庫實戰</a>：自動化那一格用到的工具，處理的單位是檔案與程序而不是資料集</li>
</ul>
<h2 id="backlog">Backlog</h2>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>類型</th>
          <th>前置條件</th>
          <th>規模</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Polars 與 pandas 的 API 差異對照</td>
          <td>主章</td>
          <td>8.5 完成</td>
          <td>中</td>
      </tr>
      <tr>
          <td>dbt 與 Airflow 在批次流程裡的位置</td>
          <td>主章</td>
          <td>需先確定與 backend 的分界</td>
          <td>中</td>
      </tr>
      <tr>
          <td>髒資料處理：型別、缺值、重複列</td>
          <td>主章</td>
          <td>無</td>
          <td>中</td>
      </tr>
  </tbody>
</table>
]]></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>