<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mercy</title>
    <description>The latest articles on DEV Community by Mercy (@qq5yu).</description>
    <link>https://dev.to/qq5yu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3189362%2F5f52cdf8-a8c8-4027-9503-be09efbe0f19.jpeg</url>
      <title>DEV Community: Mercy</title>
      <link>https://dev.to/qq5yu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/qq5yu"/>
    <language>en</language>
    <item>
      <title>[🗄️DataBase] ACID - Consistency</title>
      <dc:creator>Mercy</dc:creator>
      <pubDate>Mon, 15 Jun 2026 12:03:35 +0000</pubDate>
      <link>https://dev.to/qq5yu/database-acid-consistency-e5p</link>
      <guid>https://dev.to/qq5yu/database-acid-consistency-e5p</guid>
      <description>&lt;h2&gt;
  
  
  目錄
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;ACID 簡單介紹&lt;/li&gt;
&lt;li&gt;Consistency 是什麼&lt;/li&gt;
&lt;li&gt;Consistency 的兩個層面：資料庫/應用層&lt;/li&gt;
&lt;li&gt;如何達到 Consistency&lt;/li&gt;
&lt;li&gt;總結&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1. ACID 簡單介紹
&lt;/h2&gt;

&lt;p&gt;想像你要從 A 銀行轉帳 $100 到 B 銀行：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A 帳戶：-$100
B 帳戶：+$100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;如果轉帳到一半伺服器當機，會發生什麼事？&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;如果只扣了 A 沒加 B → A 少了 $100，B 沒收到 → 錢消失
如果扣了 A 也加了 B → 正常，但當機沒影響
如果兩邊都扣了 → 明顯不對
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ACID 就是用來保證這種操作&lt;strong&gt;不會出錯&lt;/strong&gt;的四個規則：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A = Atomicity（原子性）&lt;/strong&gt;：全部成功或全部失敗，沒有「只做一半」&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C = Consistency（一致性）&lt;/strong&gt;：操作前後，所有的規則都沒有被打破&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I = Isolation（隔離性）&lt;/strong&gt;：同時發生的操作，互相不要干擾&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;D = Durability（持久性）&lt;/strong&gt;：一旦成功，資料不會無故消失&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  要注意 Atomicity ≠ Consistency
&lt;/h3&gt;

&lt;p&gt;回到轉帳的故事：&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Atomicity 保證了：A 扣 $100 和 B 加 $100 要嘛一起成功，要嘛一起失敗
Consistency 保證了：轉帳前 A+B 的總額 = 轉帳後 A+B 的總額
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;strong&gt;看出差別了嗎？&lt;/strong&gt;&lt;br&gt;
Atomicity 管的是「操作有沒有完整執行」，Consistency 管的是「操作完結果合不合理」。&lt;br&gt;
&lt;strong&gt;即使 Atomicity、Isolation、Durability 都滿足了，還是可能違反Consistency。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  2. Consistency 到底是什麼？
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;指的是交易開始前是合法的狀態，交易結束後也必須是合法的狀態。&lt;/strong&gt;&lt;br&gt;
這個「合法」不是法律上的合法，是「你定義的所有規則都沒有被打破」。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  舉例來說
&lt;/h3&gt;

&lt;p&gt;你規定：「冰箱裡的飲料不能少於 5 瓶。」&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;開始前：冰箱有 8 瓶 ✅&lt;/li&gt;
&lt;li&gt;你拿走 2 瓶 → 還剩 6 瓶 ✅&lt;/li&gt;
&lt;li&gt;你拿走 4 瓶 → 還剩 4 瓶 ❌（規則被打破）&lt;/li&gt;
&lt;li&gt;你把冰箱洗劫一空 → 0 瓶 ❌（規則被打破）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Consistency 就是在檢查：&lt;/strong&gt;&lt;br&gt;
每一次你開關冰箱後，那個「不能少於 5 瓶」的規則還有沒有被遵守&lt;/p&gt;
&lt;h3&gt;
  
  
  同樣的道理，在資料庫裡就是：
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;規則類型&lt;/th&gt;
&lt;th&gt;範例&lt;/th&gt;
&lt;th&gt;誰來檢查&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;資料庫內建規則&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Primary Key 不能重複、ForeignKey 不能指向不存在的資料、NOT NULL 欄位不能空、UNIQUE 不能重複&lt;/td&gt;
&lt;td&gt;資料庫自動擋掉&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;應用層業務規則&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;「餘額不能為負」、「每天只能發 5 篇文」、「購物車總金額不得超過 $10,000」&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;你自己寫程式檢查&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  常見的誤區
&lt;/h3&gt;

&lt;p&gt;很多人以為：「資料庫會自動保證一致性。」&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;資料庫&lt;strong&gt;只會&lt;/strong&gt;保證 Primary Key 不重複、ForeignKey 正確指向這些「內建規則」。&lt;br&gt;
但你寫的&lt;strong&gt;業務規則&lt;/strong&gt;，像「餘額不能負的」、「每天提款上限 $30,000」，資料庫根本不知道。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Consistency 的最終責任在開發者身上。&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Consistency 的兩個層面: 資料庫/應用層
&lt;/h2&gt;
&lt;h3&gt;
  
  
  3.1. 資料庫層級的 Consistency
&lt;/h3&gt;

&lt;p&gt;這是資料庫幫你做的事，不用寫額外程式碼：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Entity Integrity&lt;/strong&gt;：Primary Key 不能是 NULL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Referential Integrity&lt;/strong&gt;：ForeignKey 必須指向存在的資料&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain Integrity&lt;/strong&gt;：欄位的型別、長度、格式必須正確（例如 INT 欄位不能存文字）&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unique Constraints&lt;/strong&gt;：不能有重複值
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 資料庫會幫你擋掉這些：&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'test@test.com'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- ❌ Primary Key 不能為 NULL&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;999&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- ❌ 如果 user_id 999 不存在，FK 約束會拒絕&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- ❌ 如果有 CHECK(price &amp;gt;= 0) 約束&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;違反了資料庫直接拒絕操作，回傳錯誤&lt;/strong&gt;。&lt;br&gt;
你不需要寫任何業務邏輯來防止這種情況。&lt;/p&gt;
&lt;h3&gt;
  
  
  3.2. 應用層級的 Consistency
&lt;/h3&gt;

&lt;p&gt;這是你自己要寫程式處理的，像是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;業務邏輯規則&lt;/strong&gt;：例如「用戶餘額不能為負」、「訂單折扣不能超過 30%」&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;跨表狀態一致性&lt;/strong&gt;：例如「A 表扣庫存時，B 表的訂單明細也要寫入」&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;衍生資料一致性&lt;/strong&gt;：例如「快取裡的資料必須和資料庫一致」&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;操作紀錄的追蹤&lt;/strong&gt;：例如「每次修改權限都要有操作記錄」&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;以業務邏輯規則的案例來看為何會違反應用層級的 Consistency :&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;違反的規則：用戶餘額不能為負（餘額必須 ≥ 0）&lt;br&gt;
&lt;/p&gt;


&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 讀取和寫入之間有時間窗口&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;WithdrawAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Balance&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

    &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Balance&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;這段程式碼的問題是，想像兩個人同時提款：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;帳戶餘額 $100

Request 1（提款 $80）：讀到 Balance = $100 → 通過 → 寫入 $20
Request 2（提款 $70）：讀到 Balance = $100（髒資料）→ 通過 → 寫入 $30 ❌

最終餘額：$30（R1 扣的 $80 被覆蓋了，且 R2 基於過期資料做了錯誤決定）
正確應該要是：R1 扣成 $20 → R2 發現 $20 &amp;lt; $70，拒絕提款 → 最終 $20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;這不是 Atomicity 的問題（每個扣款都完整執行），也不是資料庫層級的問題（沒有違反 PK/FK），&lt;strong&gt;而是應用層的 Consistency 被違反了。&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;這其實就是典型的 &lt;strong&gt;Lost Update（遺失更新）&lt;/strong&gt;&lt;br&gt;
指的是：&lt;strong&gt;兩個交易先後讀取了同一筆資料，各自做了判斷後寫入，後寫的那個把前一個的修改覆蓋掉了&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  4. 如何達到 Consistency
&lt;/h2&gt;

&lt;p&gt;Consistency 沒有單一解法，需要在&lt;strong&gt;資料庫約束、隔離層級、鎖機制、業務邏輯檢查&lt;/strong&gt;等多方面做處理。&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1. 資料庫層級：用 Constraints 建立防火牆
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;把你能想到的規則，盡可能用資料庫的 Constraints 表達出來，讓資料庫自動幫你檢查&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constraint&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;th&gt;範例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PRIMARY KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;每筆資料都要有唯一識別&lt;/td&gt;
&lt;td&gt;&lt;code&gt;id INT PRIMARY KEY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FOREIGN KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;確保關聯存在&lt;/td&gt;
&lt;td&gt;&lt;code&gt;user_id INT REFERENCES users(id)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UNIQUE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;值不能重複&lt;/td&gt;
&lt;td&gt;&lt;code&gt;email VARCHAR(255) UNIQUE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CHECK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;自訂條件&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CHECK (price &amp;gt;= 0)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NOT NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;欄位不能為空&lt;/td&gt;
&lt;td&gt;&lt;code&gt;name VARCHAR(100) NOT NULL&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ✅ CHECK 約束：價格不能為負&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- ✅ 違反時資料庫直接拒絕&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- ❌ ERROR: CHECK constraint violation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4.2. 應用層級：Consistency 需要 Isolation 來保護
&lt;/h3&gt;

&lt;p&gt;前面 3.2 的提款範例有一個關鍵問題：&lt;strong&gt;沒有違反原子性 Atomicity&lt;/strong&gt;，每個扣款都完整執行，但還是違反 Consistency 。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;為什麼？因為 &lt;strong&gt;Isolation 不夠&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  問題的本質
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 這段程式碼有 Transaction，也有 Atomicity，但還是錯了&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;WithdrawAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// 讀取&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Balance&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// 檢查&lt;/span&gt;
    &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Balance&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                               &lt;span class="c1"&gt;// 寫入&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;兩個請求同時進來：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;餘額 $100
R1（取 $80）：讀到 $100 → 檢查通過 → 寫入 $20
R2（取 $70）：讀到 $100（還沒被 R1 提交蓋掉）→ 檢查通過 → 寫入 $30 ❌
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;問題出在「讀取」和「寫入」之間，&lt;strong&gt;另一個交易插了進來，讀到了舊資料&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;要防止這種情況，就要靠&lt;strong&gt;隔離層級&lt;/strong&gt;來控制一個交易能不能看到別人&lt;strong&gt;「還沒提交」&lt;/strong&gt;的變更。&lt;/p&gt;

&lt;h4&gt;
  
  
  什麼是隔離層級 (Isolation Level)？
&lt;/h4&gt;

&lt;p&gt;資料庫為了效能，不會讓交易真的排隊一個一個執行，而是讓它們&lt;strong&gt;同時跑&lt;/strong&gt;。隔離層級就是在控制一件事：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;一個交易能不能看到別人「還沒提交」的變更？&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;先搞懂「提交」是什麼意思：&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;在資料庫裡，你對資料做完修改後，要下 COMMIT 指令資料才算真的寫進去。
執行 COMMIT 之前，你的修改叫「還沒提交」。
這時候如果執行 ROLLBACK，修改就會取消，像沒發生過一樣。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;回到提款的例子，R1 執行了 &lt;code&gt;account.Balance -= amount&lt;/code&gt; 但還沒下 COMMIT：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;R1 的修改：$100 → $20（還沒提交，隨時可以 ROLLBACK 取消）
R2 能不能看到這個 $20？ → 看隔離層級決定
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;各隔離層級對 Consistency 的保護程度：&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;隔離層級&lt;/th&gt;
&lt;th&gt;白話解釋&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read Uncommitted&lt;/td&gt;
&lt;td&gt;別人還沒 COMMIT 的資料你也看得到&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read Committed&lt;/td&gt;
&lt;td&gt;只能看到別人已經 COMMIT 的資料&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeatable Read&lt;/td&gt;
&lt;td&gt;同一筆資料在這筆交易內，不管讀幾次結果都不變&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serializable&lt;/td&gt;
&lt;td&gt;所有交易像排隊一個一個執行，互不干擾&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  舉例來說
&lt;/h4&gt;

&lt;p&gt;原本餘額 $100，R1 取 $80，R2 取 $70：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read Uncommitted（最弱）
  R1：讀到 $100 → 扣成 $20（還沒提交）
  R2：讀到 $20 ⚠️（看到 R1 還沒提交的變更）
       → $20 &amp;lt; $70 → 拒絕提款 ✅
  但如果 R1 最後 Rollback，R2 看到的 $20 就是髒資料 (Dirty Read)

Read Committed（通常資料庫的預設都是這個層級）
  R1：讀到 $100 → 扣成 $20（還沒提交）
  R2：讀到 $100（看不到 R1 未提交的 $20）
       → 檢查通過 → 扣成 $30 ❌ 結果超賣

Repeatable Read
  R1：讀到 $100 → 扣成 $20
  R2：讀到 $100 → 檢查通過 → 開始扣...
       └ 實際結果依資料庫而異：
         MySQL / SQL Server：UPDATE 會讀最新值 → 餘額變 -$50 ❌
         PostgreSQL：發現資料被改過 → 中止交易，報錯 ✅

Serializable
  R1：讀到 $100 → 扣成 $20
  R2：準備扣成 $30
       ❌ 資料庫發現衝突 → 中止 R2，請他重試
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;看到這邊你可能會想：「所以只有 Serializable 能解決問題？」&lt;br&gt;
對，如果只靠隔離層級，確實只有 Serializable 保證安全。&lt;br&gt;
但 Serializable 會把所有交易排隊執行，&lt;strong&gt;效能很差&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;所以我們常常會搭配下面幾種方法一同做保護。&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3. 原子操作：讓資料庫一次做完
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;為什麼前面 3.2 的寫法會出事？因為程式碼是&lt;strong&gt;分三步走&lt;/strong&gt;的：&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// 步驟 1：讀取&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Balance&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// 步驟 2：檢查&lt;/span&gt;
&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Balance&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                              &lt;span class="c1"&gt;// 步驟 3：寫入&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;步驟 1 和步驟 3 之間，別人可以趁機插進來修改資料。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;這個&lt;strong&gt;從「讀取」到「寫入」之間的空檔&lt;/strong&gt;，就是所謂的 &lt;strong&gt;時間窗口&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;時間窗口越長，別的交易插進來的機會就越大。&lt;/p&gt;

&lt;h4&gt;
  
  
  原子操作的思路：三步變一步
&lt;/h4&gt;

&lt;p&gt;不要把「讀取 → 檢查 → 寫入」拆成三段程式碼分開執行，而是用&lt;strong&gt;一條 SQL&lt;/strong&gt; 讓資料庫一次做完：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ✅ 庫存夠才扣，不夠則影響 0 筆，全程一條 SQL&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;這條 SQL 做的事情等於：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. 讀取 stock          ← 資料庫內部一次完成
2. 檢查 stock &amp;gt;= 1     ← 資料庫內部一次完成
3. 夠的話就扣 1        ← 資料庫內部一次完成
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;因為&lt;strong&gt;檢查和寫入發生在同一條 SQL 裡&lt;/strong&gt;，資料庫在執行這條 SQL 時不會讓其他人插隊，所以根本沒有時間窗口。&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ 對應的 EF Core 寫法&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;affected&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Products&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stock&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteUpdateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stock&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;affected&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"庫存不足"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  原子操作的優點
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;不需要 Transaction 也能保證一致性。&lt;/strong&gt; 檢查和寫入在同一條 SQL 中完成，沒有時間差。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;當你的操作可以濃縮成&lt;strong&gt;一條 SQL&lt;/strong&gt; 時，等於資料庫幫你擋住了所有並發干擾。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  什麼時候不該用
&lt;/h4&gt;

&lt;p&gt;原子操作雖然簡單，但它有極限：&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;不適合的場景&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;th&gt;該用什麼&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;需要先讀取舊值做複雜判斷（eg. 計算每日提款總額、檢查折扣上限）&lt;/td&gt;
&lt;td&gt;一條 SQL 塞不下這麼多邏輯&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;樂觀鎖（4.4）&lt;/strong&gt; 或 &lt;strong&gt;悲觀鎖（4.5）&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;需要跨多個 table 保持同步（eg. 扣庫存 + 同時寫訂單明細）&lt;/td&gt;
&lt;td&gt;單條 SQL 只能動一個 table&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Transaction + 適當隔離層級&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;寫入後需要寫 Log 或發送通知&lt;/td&gt;
&lt;td&gt;資料庫只管資料，不管 side effect&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;應用層處理 + Transaction&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  4.4. 樂觀鎖（Optimistic Locking）
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;適合&lt;strong&gt;衝突率低&lt;/strong&gt;的場景，主要是在讀取資料時記下那筆資料的&lt;strong&gt;版本號&lt;/strong&gt;，寫入時檢查版本號有沒有被別人改過。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  舉例來說
&lt;/h4&gt;

&lt;p&gt;想像兩位管理員同時打開商品編輯頁面：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;管理員 A：讀到價格 $50 (version=1) → 改成 $100 → 存檔 → version 變 2 ✅
管理員 B：讀到價格 $50 (version=1) → 改成 $90 → 存檔 → version 已經是 2 了 → 拒絕 ❌
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;管理員 B 在&lt;strong&gt;開啟頁面到按下存檔之間&lt;/strong&gt;，別人已經改過了。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;樂觀鎖的做法很簡單：&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;每筆資料帶一個 &lt;code&gt;version&lt;/code&gt; 欄位，寫入時檢查版本。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  SQL 範例
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;newPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;oldVersion&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- 管理員 B 傳入 @oldVersion = 1，但資料庫中的 version 已被 A 改成 2&lt;/span&gt;
&lt;span class="c1"&gt;-- WHERE version = 1 找不到資料 → 影響 0 筆 → 拋出 DbUpdateConcurrencyException&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  EF Core 實作
&lt;/h4&gt;

&lt;p&gt;在 Entity 上加一個版本欄位，標記為 &lt;code&gt;ConcurrencyToken&lt;/code&gt;：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Price&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;// 樂觀鎖欄位&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newPrice&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// EF Core 自動生成類似這樣的 SQL：&lt;/span&gt;
&lt;span class="c1"&gt;// UPDATE products SET price = @p0, version = version + 1&lt;/span&gt;
&lt;span class="c1"&gt;// WHERE id = @p1 AND version = @p2&lt;/span&gt;
&lt;span class="c1"&gt;// 如果影響 0 筆 → 拋出 DbUpdateConcurrencyException&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  衝突發生後怎麼辦
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DbUpdateConcurrencyException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 重新載入最新資料&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReloadAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// 告訴使用者「資料已被別人修改，請重新確認」&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"最新價格是 &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;，請重新填寫"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;樂觀鎖的特色是：&lt;strong&gt;不阻止並發，只偵測衝突&lt;/strong&gt; &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;意思是讀取時不鎖資料，只是在&lt;strong&gt;寫入時才檢查&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;適用場景&lt;/th&gt;
&lt;th&gt;不適用場景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;一般 CRUD、後台管理操作&lt;/td&gt;
&lt;td&gt;秒殺搶票&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;同一個人編輯自己的資料&lt;/td&gt;
&lt;td&gt;高頻扣庫存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;使用者很少同時改同一筆資料&lt;/td&gt;
&lt;td&gt;會大量重試且重試成本高&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  4.5. 悲觀鎖（Pessimistic Locking）
&lt;/h3&gt;

&lt;p&gt;如果今天的場景不是「兩個管理員修改商品價格」，&lt;br&gt;
而是&lt;strong&gt;一千個人同時搶最後一件商品&lt;/strong&gt;，樂觀鎖就不適用了。&lt;br&gt;
&lt;strong&gt;因為每個人都會衝突，所以每個人都在 Retry，系統反而更慢&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;悲觀鎖的邏輯與樂觀所相反：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;讀取時直接鎖起來，不讓別人碰，做完才解鎖&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;
  
  
  舉例來說
&lt;/h4&gt;

&lt;p&gt;想像一個線上購物系統在特賣活動中，只剩最後一台筆電：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request A（小明）：SELECT ... FOR UPDATE → 鎖住這筆資料 → 扣庫存 → Commit → 解鎖
Request B（小華）：                                             等到 A 解鎖才能讀 → 發現沒貨了
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;小華不是「讀到舊庫存然後扣失敗」，他是&lt;strong&gt;根本讀不到&lt;/strong&gt;，直到 A 做完。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  SQL 範例
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- 鎖住 id=1 這筆&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- 解鎖&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FOR UPDATE&lt;/code&gt; 的意思是：「我要鎖這筆，其他人要讀取或修改都請排隊。」&lt;/p&gt;

&lt;h4&gt;
  
  
  EF Core 實作
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BeginTransactionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IsolationLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadCommitted&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 鎖住這筆資料，其他交易要讀同一筆時必須等&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Products&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSqlRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT * FROM products WHERE id = {0} FOR UPDATE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stock&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stock&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CommitAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// Transaction 結束 → 自動解鎖&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  代價：並行度降低
&lt;/h4&gt;

&lt;blockquote&gt;
&lt;p&gt;A 鎖住資料時，B 必須排隊等&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;所以要確保 Transaction 短小輕快&lt;/strong&gt;，千萬不要在使用者填表單的過程中鎖資料，不然使用者填 10 分鐘，資料就鎖 10 分鐘。&lt;/p&gt;

&lt;p&gt;而且如果 Transaction A 鎖了商品等訂單，Transaction B 鎖了訂單等商品，兩邊都在等對方釋放，就會造成&lt;strong&gt;死鎖&lt;/strong&gt;。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;有這種情況時，資料庫會自動偵測並中止其中一個，但你的程式碼要做好重試的處理。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  樂觀鎖 vs 悲觀鎖
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;樂觀鎖&lt;/th&gt;
&lt;th&gt;悲觀鎖&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;什麼時候檢查衝突&lt;/td&gt;
&lt;td&gt;寫入時才檢查&lt;/td&gt;
&lt;td&gt;讀取時就直接鎖&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;衝突時的代價&lt;/td&gt;
&lt;td&gt;重試一次&lt;/td&gt;
&lt;td&gt;排隊等待&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;適合情境&lt;/td&gt;
&lt;td&gt;衝突率低&lt;/td&gt;
&lt;td&gt;衝突率高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;讀取效能&lt;/td&gt;
&lt;td&gt;高（不鎖）&lt;/td&gt;
&lt;td&gt;低（要等鎖）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;典型場景&lt;/td&gt;
&lt;td&gt;修改文章、更新個人資料&lt;/td&gt;
&lt;td&gt;搶購、扣庫存、扣款&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  4.6. 業務規則檢查：自己寫程式碼檢查
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;資料庫不知道你的業務規則，例如「每日提款上限 $30,000」、「折扣不能超過 30%」。&lt;/p&gt;

&lt;p&gt;這些&lt;strong&gt;必須你自己寫程式碼檢查&lt;/strong&gt;。&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ❌ 資料庫不會幫你擋這個：&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;35000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- 即使超過日限額，資料庫照樣寫入，因為它不知道你的規則&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ 你要自己檢查：&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;todayTotal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Transactions&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Date&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SumAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todayTotal&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"每日提款上限 $30,000"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. 總結
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Consistency（一致性）的核心問題只有一個：&lt;strong&gt;交易結束後，你定義的規則還有沒有被遵守？&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Consistency 不是資料庫單方面能保證的&lt;/strong&gt;，它需要幾個層級來做保護：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;資料庫 Constraints&lt;/strong&gt;，例如 PK、FK、CHECK、UNIQUE 的基本防護&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction + 隔離層級&lt;/strong&gt; ，確保同時發生的操作不互相干擾&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;鎖機制（樂觀鎖 / 悲觀鎖）&lt;/strong&gt;， 精確控制並發存取&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;業務規則檢查&lt;/strong&gt;，只有開發者知道的業務規則，要自己寫程式檢查&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;沒有哪一招能搞定所有情況，應該要在不同的情境下，選用不同的工具組合。&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>database</category>
      <category>webdev</category>
      <category>backend</category>
      <category>learning</category>
    </item>
    <item>
      <title>[🗄️DataBase] Database Transactions 底層到底做了什麼：從記憶體到磁碟</title>
      <dc:creator>Mercy</dc:creator>
      <pubDate>Sat, 13 Jun 2026 02:24:48 +0000</pubDate>
      <link>https://dev.to/qq5yu/database-database-transactions-di-ceng-dao-di-zuo-liao-shi-mo-cong-ji-yi-ti-dao-ci-die-4cnc</link>
      <guid>https://dev.to/qq5yu/database-database-transactions-di-ceng-dao-di-zuo-liao-shi-mo-cong-ji-yi-ti-dao-ci-die-4cnc</guid>
      <description>&lt;h2&gt;
  
  
  目錄
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;為什麼要理解這件事&lt;/li&gt;
&lt;li&gt;電腦儲存層級：從 Register 到 SSD&lt;/li&gt;
&lt;li&gt;資料庫引擎核心架構：Page Cache、WAL、Checkpoint&lt;/li&gt;
&lt;li&gt;實際範例&lt;/li&gt;
&lt;li&gt;總結&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1. 為什麼要理解這件事
&lt;/h2&gt;

&lt;p&gt;不管用哪個資料庫、哪種語言，你每天都在做類似的事&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Python + psycopg2 (PostgreSQL)
&lt;/span&gt;&lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO roles (name) VALUES (%s)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// C# + EF Core&lt;/span&gt;
&lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Roles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CommitAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Node.js + pg (PostgreSQL)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INSERT INTO roles (name) VALUES ($1)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;COMMIT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;但你真的知道這些 API 背後發生了什麼事嗎？&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ExecuteNonQuery()&lt;/code&gt; / &lt;code&gt;SaveChangesAsync()&lt;/code&gt; 執行後，資料到底在哪？&lt;/li&gt;
&lt;li&gt;什麼叫「在記憶體裡」？&lt;/li&gt;
&lt;li&gt;Flush 和 Commit 有什麼差別？&lt;/li&gt;
&lt;li&gt;Transaction 是怎麼保護你的資料的？&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;這篇文章會從 CPU 如何存取資料開始，一步步講到資料庫引擎怎麼保證你的資料不遺失。&lt;/p&gt;




&lt;h2&gt;
  
  
  2. 電腦儲存層級：從 Register 到 SSD
&lt;/h2&gt;

&lt;h3&gt;
  
  
  2.1 CPU Register（暫存器）
&lt;/h3&gt;

&lt;p&gt;CPU 晶片內部有幾十個暫存器，每個 64-bit（以現代 x64 CPU 來說）。資料要&lt;strong&gt;被運算&lt;/strong&gt;（加減乘除、比對大小），一定要先載到暫存器。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mov rax, [memory_address]   ; 從 RAM 搬到 CPU 暫存器
add rax, 1                  ; 在 CPU 裡面加 1
mov [memory_address], rax   ; 存回 RAM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;前面例子寫的 &lt;code&gt;role.name = "新名稱"&lt;/code&gt;，編譯器編譯後最終會變成好幾條這種機器指令，把值從一個 RAM 位址搬到 CPU，改完再搬回去。&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;屬性&lt;/th&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;容量&lt;/td&gt;
&lt;td&gt;~幾十 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;速度&lt;/td&gt;
&lt;td&gt;~0.3 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;斷電是否消失&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  2.2 RAM（主記憶體）
&lt;/h3&gt;

&lt;p&gt;RAM（Random Access Memory）是&lt;strong&gt;電容 + 電晶體&lt;/strong&gt;做成的儲存陣列，每個 cell 儲存 1 bit 的電荷。優點是讀寫極快，缺點是&lt;strong&gt;斷電就全部消失&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;256 GB RAM 大概有 2 兆個這樣的 cell。&lt;/p&gt;

&lt;p&gt;你的變數全部存在這裡。&lt;br&gt;
當你修改一個變數，你修改的是 RAM 中某個位址上的 byte。&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;屬性&lt;/th&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;容量&lt;/td&gt;
&lt;td&gt;~GB ~ TB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;速度&lt;/td&gt;
&lt;td&gt;~50-100 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;斷電是否消失&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  2.3 SSD / HDD（硬碟）
&lt;/h3&gt;

&lt;p&gt;硬碟是最終儲存資料的地方。HDD 用磁碟片，SSD 用 NAND Flash 晶片。&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;屬性&lt;/th&gt;
&lt;th&gt;HDD&lt;/th&gt;
&lt;th&gt;SSD&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;速度&lt;/td&gt;
&lt;td&gt;~5-10 ms&lt;/td&gt;
&lt;td&gt;~10-100 μs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;比 RAM 慢&lt;/td&gt;
&lt;td&gt;~100,000 倍&lt;/td&gt;
&lt;td&gt;~1,000 倍&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;斷電是否消失&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  3. 資料庫引擎核心架構：Page Cache、WAL、Checkpoint
&lt;/h2&gt;

&lt;p&gt;不管哪個資料庫，設計目標都一樣：&lt;strong&gt;提供 ACID 保證，同時要有夠好的效能&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;直接讀寫磁碟太慢了（比 RAM 慢 1000~100000 倍），所以所有資料庫都有一個核心機制：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;你的程式 (Application)                    Database Engine
┌─────────────────────────┐        ┌──────────────────────────────────┐
│  Application Layer      │  SQL   │         Page Cache (RAM)          │
│  ┌───────────────────┐  │ ─────→ │  ┌──────┬──────┬──────┬───────┐  │
│  │ 資料 (物件/row)    │  │        │  │Page 1│Page 2│Page 3│ ...   │  │
│  │ SQL 語句           │  │        │  │ dirty│ clean│ dirty│       │  │
│  └───────────────────┘  │        │  └──┬───┴──────┴──┬───┴───────┘  │
│                         │        │     │             │               │
│                         │        │     ↓             ↓               │
│                         │        │  Background    Background         │
│                         │        │  Writer        Checkpoint         │
│                         │        │     │             │               │
└─────────────────────────┘        │     ↓             ↓               │
                                    │  Write-Ahead Log  Data Files     │
                                    │  (WAL / Redo Log / Tx Log)       │
                                    │  (磁碟)           (磁碟)          │
                                    └──────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;假設你寫了一行：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Alice'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;你的程式把這行 SQL 丟給資料庫引擎。然後呢？&lt;/p&gt;

&lt;p&gt;--&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第一站：Application RAM（你的程式記憶體）&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;當你的程式執行：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Role&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"管理員"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Roles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;這筆資料只在你程式的記憶體裡（.NET managed heap / Python heap / 等等），還沒透過任何 API 送出去。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;這時如果斷電，這筆資料一定消失。&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;第二站：Flush（送出 SQL）&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;          &lt;span class="c1"&gt;// EF Core&lt;/span&gt;
&lt;span class="c1"&gt;// 或&lt;/span&gt;
&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteNonQuery&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;          &lt;span class="c1"&gt;// ADO.NET&lt;/span&gt;
&lt;span class="c1"&gt;// 或&lt;/span&gt;
&lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INSERT ..."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// psycopg2 (Python)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;這步做了：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;應用程式把 SQL 或資料透過 TCP/IP（或共用記憶體）送到 Database Engine&lt;/li&gt;
&lt;li&gt;Database Engine 開始接手處理&lt;/li&gt;
&lt;li&gt;資料正式離開你的行程記憶體，進入資料庫的管轄範圍&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;第三站：Page Cache（Buffer Pool）&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;資料庫不直接讀寫磁碟，原因很簡單，因為磁碟太慢了。&lt;br&gt;
所以它在啟動時會跟 OS 要一大塊 &lt;strong&gt;RAM&lt;/strong&gt;，叫 &lt;strong&gt;Page Cache&lt;/strong&gt;。&lt;br&gt;
每個資料庫叫法不同，做的事一模一樣：&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;資料庫&lt;/th&gt;
&lt;th&gt;Page Cache 名稱&lt;/th&gt;
&lt;th&gt;位置&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;shared_buffers&lt;/td&gt;
&lt;td&gt;資料庫自己的記憶體&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL/InnoDB&lt;/td&gt;
&lt;td&gt;innodb_buffer_pool&lt;/td&gt;
&lt;td&gt;資料庫自己的記憶體&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Server&lt;/td&gt;
&lt;td&gt;Buffer Pool&lt;/td&gt;
&lt;td&gt;資料庫自己的記憶體&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;Page Cache&lt;/td&gt;
&lt;td&gt;行程內記憶體&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;資料庫收到你的 UPDATE 後，第一步是找到那筆資料所在的 Page：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;先在 Page Cache 找有沒有這個 Page&lt;/li&gt;
&lt;li&gt;有 → 直接拿來用（Cache Hit）&lt;/li&gt;
&lt;li&gt;沒有 → 從磁碟載入 Page Cache（Cache Miss）&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;然後&lt;strong&gt;直接在這個 Page 上修改&lt;/strong&gt;，再把該 Page 標記為 &lt;strong&gt;Dirty Page&lt;/strong&gt;（跟磁碟版本不一致）。&lt;/p&gt;

&lt;p&gt;但 Dirty Page 不能一直待在記憶體裡，不然機器斷電資料就不見了。&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;第四站：WAL（Write-Ahead Log）&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;所以資料庫做了一件非常重要的事，&lt;br&gt;
&lt;strong&gt;在把 Dirty Page 寫回磁碟的資料檔之前，先把修改紀錄寫到 WAL。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;流程有四步：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. 收到 UPDATE / INSERT / DELETE SQL
2. 在 Page Cache 修改對應的 Page（Dirty）
3. 把「我做了什麼修改」寫到 Transaction Log（磁碟）
4. 回傳給用戶「完成了」
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WAL 是&lt;strong&gt;順序寫入&lt;/strong&gt;（sequential write），速度快。&lt;br&gt;
它只是 append-only 的日誌，不會回頭修改舊紀錄。&lt;/p&gt;

&lt;p&gt;一旦 WAL 寫完，這筆修改就算安全了，就算下一秒機器崩潰或是斷電，重啟時只要 replay WAL，就能復原到你剛修改完的狀態。&lt;/p&gt;

&lt;p&gt;這時候資料檔可能還沒有被更新，但 Transaction Log 已經寫了。如果斷電，資料庫重新啟動時會讀 Transaction Log：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;有 COMMIT 記錄 → &lt;strong&gt;Redo&lt;/strong&gt;：把還沒寫回磁碟的修改重新套用&lt;/li&gt;
&lt;li&gt;沒有 COMMIT 記錄 → &lt;strong&gt;Undo&lt;/strong&gt;：把已做的修改還原&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;&lt;strong&gt;第五站：Commit&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CommitAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;       &lt;span class="c1"&gt;// EF Core&lt;/span&gt;
&lt;span class="c1"&gt;// 或&lt;/span&gt;
&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                   &lt;span class="c1"&gt;// ADO.NET&lt;/span&gt;
&lt;span class="c1"&gt;// 或&lt;/span&gt;
&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                 &lt;span class="c1"&gt;// Python DB-API&lt;/span&gt;
&lt;span class="c1"&gt;// 或&lt;/span&gt;
&lt;span class="n"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                        &lt;span class="c1"&gt;// CLI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;這行在 Transaction Log 中寫入一筆 &lt;strong&gt;COMMIT&lt;/strong&gt; 記錄。&lt;/p&gt;

&lt;p&gt;從這一刻起：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;如果斷電，資料庫 Recovery 會把這筆資料還原（Redo）&lt;/li&gt;
&lt;li&gt;其他連線可以看到這筆資料（隔離級別決定何時看到）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Flush vs Commit：差別在哪&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;用生活比喻，想像你是一個廚師，要做一道菜給客人：&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;動作&lt;/th&gt;
&lt;th&gt;對應的程式操作&lt;/th&gt;
&lt;th&gt;比喻&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;備料切菜&lt;/td&gt;
&lt;td&gt;建立物件 / 組裝 SQL&lt;/td&gt;
&lt;td&gt;在流理台上準備材料&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;下鍋煮&lt;/td&gt;
&lt;td&gt;Flush（送出 SQL / SaveChanges）&lt;/td&gt;
&lt;td&gt;煮好了，放在廚房檯面上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;決定上菜&lt;/td&gt;
&lt;td&gt;Commit&lt;/td&gt;
&lt;td&gt;端出去給客人吃&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;覺得太鹹倒掉&lt;/td&gt;
&lt;td&gt;Rollback&lt;/td&gt;
&lt;td&gt;倒掉重做，反正客人沒看到&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flush&lt;/strong&gt;: 把菜做好放在廚房檯面上，菜已經熟了（資料庫已執行），但還沒端出去（還沒 Commit），客人看不到，萬一覺得不好吃還可以倒掉（Rollback）。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit&lt;/strong&gt;:  端出去給客人吃，一旦端出去了就來不及了（永久的）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;strong&gt;第六站：Background Writer&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Dirty Page 還是要寫回資料檔的，但資料檔是&lt;strong&gt;隨機寫入&lt;/strong&gt;（random write），慢很多。&lt;br&gt;
如果一次大量寫，使用者的查詢就會被卡住。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;所以 Background Writer 在後台&lt;strong&gt;默默、慢慢地&lt;/strong&gt;把 Dirty Page 刷回磁碟的資料檔。每次只刷一點點，不搶頻寬，讓使用者感覺不到它的存在。&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;第七站：Checkpoint&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Background Writer 平常一直在刷，但總有一些 Page 它來不及刷，或累積太多 Dirty Page。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Checkpoint 的工作就是&lt;strong&gt;定時強制把所有髒 Page 刷回磁碟&lt;/strong&gt;。&lt;br&gt;
做完之後告訴 WAL：「這之前的日誌都已經同步了，可以砍掉了。」&lt;/p&gt;

&lt;p&gt;這樣 WAL 不會無限膨脹，也縮短了崩潰後 recovery 的時間，只需要 replay checkpoint 之後的 WAL 就夠了。&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;崩潰怎麼辦：Recovery&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;機器永遠會 crash，這時就需要 ARIES 演算法來救。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;ARIES（Algorithm for Recovery and Isolation Exploiting Semantics）是資料庫崩潰後&lt;strong&gt;如何自動救回來&lt;/strong&gt;的標準流程，所有現代關聯式資料庫（InnoDB、SQL Server、PostgreSQL、Oracle）都在用：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Analysis&lt;/strong&gt; — 掃 WAL，查出哪些 Page 是髒的、哪些交易還沒結束&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redo&lt;/strong&gt; — 從上一個 checkpoint 開始，把所有修改重新做一遍（不管有沒有寫回資料檔），確保資料檔跟 crash 瞬間一致&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Undo&lt;/strong&gt; — 把 crash 時還沒 commit 的交易全部還原&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;不管你 crash 前做過什麼，redo 全部重做一次，再把沒做完的 undo 掉，結果就是對的。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;之所以叫 ARIES，是因為它利用 WAL 裡的語義資訊（知道每筆修改對應哪個 page、哪個 transaction），不需要猜測。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;總結流程：&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;先寫 WAL（按順序寫，保證崩潰安全）→ Background Worker 慢慢刷 Dirty Page 回資料檔（隨機寫）→ Checkpoint 定時總整理、截斷 WAL&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  4. 實際範例
&lt;/h2&gt;

&lt;p&gt;用一個典型的業務情境：建立一個新角色，同步身分認證資料，記錄稽核日誌。&lt;/p&gt;
&lt;h3&gt;
  
  
  4.1 沒有 Transaction
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Python pseudo code
&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;# 隱含 AUTO COMMIT
&lt;/span&gt;
&lt;span class="nf"&gt;ensure_identity_role&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# 內部也有自己的 AUTO COMMIT
&lt;/span&gt;
&lt;span class="n"&gt;audit_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RoleCreated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# 隱含 AUTO COMMIT
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;資料流向：&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;第 1 次 flush →  DB Page Cache: ✅ INSERT INTO roles
                 Transaction Log:  ✅ INSERT + AUTO COMMIT
                 → 已「永久」

第 2 次 flush →  DB Page Cache: ✅ INSERT INTO identity_roles
                 Transaction Log:  ✅ INSERT + AUTO COMMIT
                 → 又「永久」

第 3 次 flush →  DB Page Cache: ✅ INSERT INTO audit_logs
                 Transaction Log:  ✅ INSERT + AUTO COMMIT
                 → 又「永久」
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;如果第 1 次 flush 後、第 2 次 flush 前斷電：&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;資料表 (Table)&lt;/th&gt;
&lt;th&gt;DB Recovery 後的狀態&lt;/th&gt;
&lt;th&gt;關鍵原因說明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;roles&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;存在&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;第一次 Flush 時，觸發 &lt;strong&gt;AUTO COMMIT&lt;/strong&gt; 永久落地&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;identity_roles&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;不存在&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;寫入前系統已崩潰，Transaction Log 無此記錄&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;audit_logs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;不存在&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;寫入前系統已崩潰，Transaction Log 無此記錄&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;資料庫處於不一致狀態，而且無法恢復。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  4.2 有 Transaction
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin_transaction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nf"&gt;ensure_identity_role&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;audit_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RoleCreated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;資料流向：&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;執行動作 / 程式碼&lt;/th&gt;
&lt;th&gt;DB Page Cache 狀態&lt;/th&gt;
&lt;th&gt;Transaction Log 狀態&lt;/th&gt;
&lt;th&gt;結果說明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;db.begin_transaction()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;無變更&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;BEGIN TRAN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;交易正式開始，開啟安全保護傘&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;roles.insert()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;INSERT roles&lt;/td&gt;
&lt;td&gt;INSERT &lt;strong&gt;(無 COMMIT)&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;資料暫存於快取，&lt;strong&gt;尚未真正生效&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ensure_identity_role()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;INSERT identity_roles&lt;/td&gt;
&lt;td&gt;INSERT &lt;strong&gt;(無 COMMIT)&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;資料暫存於快取，&lt;strong&gt;尚未真正生效&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;audit_service.log()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;INSERT audit_logs&lt;/td&gt;
&lt;td&gt;INSERT &lt;strong&gt;(無 COMMIT)&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;資料暫存於快取，&lt;strong&gt;尚未真正生效&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tx.commit()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;保持不變 (等待 Checkpoint)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;COMMIT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;日誌強制落地，&lt;strong&gt;三筆資料同時正式生效&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;如果 commit 前斷電：&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DB Recovery 後：
  Transaction Log 中沒有 COMMIT 記錄
  → 全部三筆 INSERT 都被 Rollback（Undo）
  → 資料庫像沒發生過一樣
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;重新執行一次即可，不會有任何殘留資料。&lt;/p&gt;




&lt;h2&gt;
  
  
  5. 總結
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;資料從應用程式到磁碟，並不是一步到位&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;它會&lt;strong&gt;先存在 Application RAM&lt;/strong&gt;，接著透過 &lt;strong&gt;Flush 送進 Database Engine&lt;/strong&gt;，在 Page Cache 中修改資料頁，並&lt;strong&gt;寫入 WAL / Transaction Log&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;只有當 Transaction Log 中出現 &lt;strong&gt;COMMIT 記錄&lt;/strong&gt;時，這筆交易才正式成立。&lt;/p&gt;

&lt;p&gt;至於 Dirty Page 什麼時候真正寫回 Data Files，通常是之後由 &lt;strong&gt;Checkpoint&lt;/strong&gt; 或 &lt;strong&gt;背景程序&lt;/strong&gt; 完成。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;整個流程為：&lt;/strong&gt;&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application RAM → Flush → Page Cache + WAL → Commit → Checkpoint → Data Files
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

</description>
      <category>architecture</category>
      <category>computerscience</category>
      <category>database</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>[🗄️DataBase] ACID - Atomicity</title>
      <dc:creator>Mercy</dc:creator>
      <pubDate>Thu, 11 Jun 2026 08:02:12 +0000</pubDate>
      <link>https://dev.to/qq5yu/database-acid-atomicity-54lb</link>
      <guid>https://dev.to/qq5yu/database-acid-atomicity-54lb</guid>
      <description>&lt;h2&gt;
  
  
  什麼是 ACID？
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ACID&lt;/strong&gt; 是資料庫交易（Transaction）的四個基本特性，確保資料操作的正確性與可靠性：&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;中文&lt;/th&gt;
&lt;th&gt;說明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;A&lt;/strong&gt;tomicity&lt;/td&gt;
&lt;td&gt;原子性&lt;/td&gt;
&lt;td&gt;交易內所有操作全部成功或全部失敗，不允許部分完成&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;C&lt;/strong&gt;onsistency&lt;/td&gt;
&lt;td&gt;一致性&lt;/td&gt;
&lt;td&gt;交易完成後，資料庫必須遵守所有原先定義的規則與約束 Ex.錢包餘額不可為負&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;I&lt;/strong&gt;solation&lt;/td&gt;
&lt;td&gt;隔離性&lt;/td&gt;
&lt;td&gt;多個並行交易的執行結果，彼此不能互相干擾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;D&lt;/strong&gt;urability&lt;/td&gt;
&lt;td&gt;持久性&lt;/td&gt;
&lt;td&gt;已提交的交易結果，即使系統崩潰也必須永久保存&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  什麼是 Atomicity？為什麼重要？
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Atomicity&lt;/strong&gt; 指的是：一個 transaction 內的所有操作，&lt;strong&gt;要馬全部成功，要馬全部當作沒發生過&lt;/strong&gt;。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;白話來說：資料處理不能做到一半失敗，然後資料庫留下一半髒資料。&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  違反 Atomicity 的真實案例
&lt;/h3&gt;

&lt;p&gt;舉一個我在自己專案犯的錯誤&lt;/p&gt;

&lt;p&gt;想像一筆玩家儲值交易因金流服務異常被標記為「異常交易」&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;系統修復後執行結案流程：&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;把異常交易狀態改成「已解決」&lt;/li&gt;
&lt;li&gt;把儲值金額加到玩家的遊戲錢包&lt;/li&gt;
&lt;li&gt;更新該玩家的累積儲值總額&lt;/li&gt;
&lt;li&gt;記錄這筆操作到日誌（Audit Log）&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;如果步驟 2 失敗了，但步驟 1 已經存進資料庫 &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;結果就是：&lt;strong&gt;異常交易顯示已解決，但錢根本沒進玩家錢包&lt;/strong&gt;。玩家儲值不到帳，系統卻顯示已完成，這就是違反 Atomicity (原子性)。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  以程式碼來看
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveAbnormalTopUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topUpOrder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;找不到儲值訂單&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 1. 先把異常交易狀態改成「已解決」&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topUpOrder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RESOLVED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;resolvedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. 把儲值金額加到玩家錢包&lt;/span&gt;
  &lt;span class="c1"&gt;// 假設這一步因為 DB timeout、資料鎖定、網路問題而失敗&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playerId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. 更新玩家累積儲值總額&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playerId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;totalTopUpAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// 4. 記錄 Audit Log&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auditLog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RESOLVE_ABNORMAL_TOP_UP&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;異常儲值訂單已結案並補發金額&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;這段程式碼的問題是：&lt;/strong&gt;每一步都直接寫入資料庫，但沒有包在同一個 transaction 裡。&lt;br&gt;
只要某個步驟失敗了，&lt;strong&gt;資料就會變成不一致的狀態&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  正確做法：用 Transaction 包起來
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;因為這整個流程本來應該被視為 &lt;strong&gt;「同一筆交易」&lt;/strong&gt; ：&lt;/p&gt;


&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;更新訂單狀態&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;補發錢包金額&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;更新累積儲值&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;寫入日誌&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;br&gt;
typescript&lt;br&gt;
要嘛全部成功，要嘛全部失敗，不能只有第一步成功，後面失敗。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  以程式碼來看
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveAbnormalTopUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topUpOrder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;找不到儲值訂單&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. 更新異常交易狀態&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topUpOrder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RESOLVED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;resolvedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. 增加玩家錢包餘額&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playerId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. 更新玩家累積儲值總額&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playerId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;totalTopUpAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. 寫入 Audit Log&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auditLog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RESOLVE_ABNORMAL_TOP_UP&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;異常儲值訂單已結案並補發金額&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;使用 transaction 之後，只要其中任何一步失敗，例如錢包更新失敗：&lt;/p&gt;


&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;整個 transaction 就會 rollback。&lt;br&gt;
也就是前面已經執行過的步驟都會被還原，最後&lt;strong&gt;資料庫會保持在原本狀態&lt;/strong&gt;&lt;br&gt;
這樣就&lt;strong&gt;符合 Atomicity&lt;/strong&gt;：這筆流程，要嘛完整成功，要嘛完全不發生。&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>database</category>
      <category>webdev</category>
      <category>backend</category>
      <category>learning</category>
    </item>
    <item>
      <title>[🗄️DataBase] N+1 Query Problem</title>
      <dc:creator>Mercy</dc:creator>
      <pubDate>Wed, 10 Jun 2026 11:38:28 +0000</pubDate>
      <link>https://dev.to/qq5yu/database-n1-query-problem-56kd</link>
      <guid>https://dev.to/qq5yu/database-n1-query-problem-56kd</guid>
      <description>&lt;p&gt;在做後端開發時，一定遇過這種情況：&lt;br&gt;
👉 API 很單純&lt;br&gt;
👉 查的資料也不多&lt;br&gt;
👉 但效能就是「莫名其妙很慢」&lt;/p&gt;

&lt;p&gt;認真查 log 才發現，SQL 被打了幾十次、幾百次，但你明明只寫了一個查詢&lt;br&gt;
這種問題，通常不是你寫錯邏輯，而是踩到了&lt;u&gt;&lt;strong&gt;N+1 Query Problem&lt;/strong&gt;&lt;/u&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  所以什麼是 N+1 Query Problem 呢?
&lt;/h2&gt;

&lt;p&gt;指的就是&lt;br&gt;
為了取得一組資料，系統額外又發出了 N 次資料庫查詢&lt;/p&gt;

&lt;p&gt;準確一點來說是&lt;br&gt;
我們為了取得一組資料，會做兩件事&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;先執行 1 次查詢取得主資料&lt;/li&gt;
&lt;li&gt;然後對每一筆資料，再各自發出 1 次查詢&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;以生活化的例子來看，想像你要查全班同學的成績：&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;你先去教務處問：「請問 301 班有哪些學生？」→ 拿到 30 個名字&lt;/li&gt;
&lt;li&gt;然後你對每一個同學，都跑去教務處問一次：「XXX 的成績是多少？」&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;第 1 次：問全班名單&lt;br&gt;
第 2 次：問 小明 的成績&lt;br&gt;
第 3 次：問 小華 的成績&lt;br&gt;
第 4 次：問 小美 的成績&lt;br&gt;
...&lt;br&gt;
第 31 次：問第 30 個同學 的成績&lt;br&gt;
總共 1 + 30 = 31 次 跑教務處&lt;/p&gt;

&lt;p&gt;這就是 N+1 —— N=30 個同學，多出 30 次額外查詢&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;比較聰明做法是&lt;/strong&gt;&lt;br&gt;
第 1 次就一次把全部同學的成績單拿回來，只要跑 1 趟&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  以程式碼來看
&lt;/h2&gt;

&lt;p&gt;假設今天我們要做一個 API 回傳「商店列表 + 每家店的機器數量」&lt;br&gt;
前端會拿到像這樣的資料：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Store A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"machineCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"B"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Store B"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"machineCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Store C"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"machineCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;第 1 次查詢：取得所有商店&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetStoresAsync&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;    &lt;span class="c1"&gt;// → SELECT * FROM Stores&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;第 2～N+1 次查詢：對每家店逐一問「你有幾台機器？」&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CountMachinesByStoreAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// → SELECT COUNT(*) FROM Machines WHERE StoreId = @p&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;如果分頁回傳 20 家店，那就是：&lt;br&gt;
1 + 20 = 21 次 SQL 查詢 (N=20 家店，每家多 1 次)&lt;/p&gt;


&lt;h2&gt;
  
  
  那應該怎麼解決呢?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;第一種解決方式: JOIN&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;這種情境，其實可以用一條 SQL 就解決&lt;br&gt;
先把 Stores 跟 Machines join 起來，並用 COUNT 算出每家店的機器數量&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;MachineCount&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Stores&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Machines&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StoreId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;對應到 C#（EF / LINQ）&lt;/strong&gt;&lt;br&gt;
第 1 次：取得所有商店（不變）&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetStoresAsync&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;    &lt;span class="c1"&gt;// → SELECT * FROM Stores&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;第 2 次：一次把所有商店的機器數量算完&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Skip&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GroupJoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Machines&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StoreId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;machines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;StoreDto&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;MachineCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;machines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;第二種解決方式: batch query（批量查詢)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;透過 GROUP+COUNT，一次把所有商店的機器數量算完&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;StoreId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Machines&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;StoreId&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'A'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'B'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'C'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;StoreId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;對應到 C#&lt;/strong&gt;&lt;br&gt;
第 1 次：取得所有商店（不變）&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetStoresAsync&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;    &lt;span class="c1"&gt;// → SELECT * FROM Stores&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;第 2 次：一次把所有商店的機器數量算完&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;machineCounts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Machines&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;storeIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StoreId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;    &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;WHERE&lt;/span&gt; &lt;span class="s"&gt;"StoreId"&lt;/span&gt; &lt;span class="nf"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GroupBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StoreId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                    &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;GROUP&lt;/span&gt; &lt;span class="n"&gt;BY&lt;/span&gt; &lt;span class="s"&gt;"StoreId"&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;StoreId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;  &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;SELECT&lt;/span&gt; &lt;span class="s"&gt;"StoreId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(*)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToDictionaryAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StoreId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;不需要第 3 次了，直接用 Dictionary 查出答案&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;machineCounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetValueOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;someStoreId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;變成：1 + 1 = 2 次 SQL 查詢&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;等同於，用 SQL 來看：&lt;/strong&gt;&lt;br&gt;
原本（逐筆）：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Machines&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;StoreId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'A'&lt;/span&gt;   &lt;span class="c1"&gt;-- 第 1 次&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Machines&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;StoreId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'B'&lt;/span&gt;   &lt;span class="c1"&gt;-- 第 2 次&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Machines&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;StoreId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'C'&lt;/span&gt;   &lt;span class="c1"&gt;-- 第 3 次&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;改 batch 後（一次問完）：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nv"&gt;"StoreId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Machines&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nv"&gt;"StoreId"&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'A'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'B'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'C'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;    &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="err"&gt;一次問全部&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nv"&gt;"StoreId"&lt;/span&gt;                         &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="err"&gt;分組各自算&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;為什麼 JOIN 與 Batch 更快？&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;網路只來回 1 次而不是 N 次&lt;/li&gt;
&lt;li&gt;每次資料庫查詢都有固定成本（連線、解析 SQL、交易開銷），N 次就把這些成本放大 N 倍&lt;/li&gt;
&lt;li&gt;資料庫對一次問很多筆有優化處理（索引、快取）&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;JOIN 與 Batch 的核心優化，其實都在做同一件事：&lt;/strong&gt;&lt;br&gt;
👉 把 N 次查詢，變成一次（或少數幾次）查詢&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;strong&gt;下次在撈資料前可以先想想：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;這段會不會產生 N 次 SQL？&lt;/li&gt;
&lt;li&gt;能不能在進迴圈前就一次查完？&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>database</category>
      <category>backend</category>
      <category>learning</category>
      <category>webdev</category>
    </item>
    <item>
      <title>📄Paper: RORA-VLM: Robust Retrieval Augmentation for Vision Language Models</title>
      <dc:creator>Mercy</dc:creator>
      <pubDate>Fri, 29 May 2026 04:13:04 +0000</pubDate>
      <link>https://dev.to/qq5yu/paper-rora-vlm-robust-retrieval-augmentation-for-vision-language-models-5b4l</link>
      <guid>https://dev.to/qq5yu/paper-rora-vlm-robust-retrieval-augmentation-for-vision-language-models-5b4l</guid>
      <description>&lt;p&gt;&lt;strong&gt;Public At&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;International Conference on Learning Representations (ICLR) 2025&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Why I read this&lt;/strong&gt;&lt;br&gt;
最近在找論文的 idea 剛好找到這篇，發表在 ICLR 2025，不過被 Reject 了有點可惜&lt;br&gt;
這篇主要是把 RAG 應用到 VLM ，讓模型在回答問題時可以利用外部知識&lt;br&gt;
在很多 VQA 的任務中，答案其實不在圖片裡面，而是需要額外的背景知識&lt;br&gt;
例如一張圖顯示一種鳥，問題是：「這種鳥主要分布在哪裡？」&lt;br&gt;
圖片只能讓你看出鳥長什麼樣，但像棲地這種資訊一定要查資料才知道&lt;br&gt;
這篇主要在解決：「當 retrieved knowledge 有 noise 時，VLM 怎麼還能穩定推理？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;🧠 Core idea&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;作者提出一個 robust retrieval framework 給 VLM：&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft3il3c5naor2j0hzrmym.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft3il3c5naor2j0hzrmym.png" alt=" " width="799" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Two-stage retrieval&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;先用 image retrieve 相似 entity，再用 entity expansion 做 text retrieval。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;在第一個階段，他們把 query image 當作一個「anchor」，去資料庫裡找很多長得很像的圖片。&lt;/p&gt;

&lt;p&gt;他們用的資料庫叫 &lt;a href="https://github.com/google-research-datasets/wit" rel="noopener noreferrer"&gt;WIT&lt;/a&gt;，裡面有 3700 萬張圖片，每張圖片都搭配一個 entity 的名字跟描述。&lt;/p&gt;

&lt;p&gt;在第二個階段，他們把在第一個階段拿到的 entity 名稱、描述加進原本的問題裡面，變成一個更具體的 query，再去用 google 查知識(call api)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;✨ For Example&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;原本的問句: 

&lt;ul&gt;
&lt;li&gt;which year was this building built?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;找到的 Entity

&lt;ul&gt;
&lt;li&gt;Castle of Good Hope&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;新的 Query (原本的問句 + entity)

&lt;ul&gt;
&lt;li&gt;which year was Castle of Good Hope built?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs12o4enxk65xkayllalz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs12o4enxk65xkayllalz.png" alt=" " width="532" height="704"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Query-oriented visual token refinement&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;只保留和 query 最相關的 visual tokens，減少 image background noise。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;一開始有兩個輸入：問題和圖片。&lt;br&gt;
在 VLM 裡面，一張圖片會被切成很多塊，每個區塊會變成一個 visual token。&lt;/p&gt;

&lt;p&gt;接下來，模型會根據問題的內容，計算每一塊(image patch) 和 query 的相關性。&lt;br&gt;
與問題比較相關的區塊會被保留下來，不相關的就被忽略。&lt;/p&gt;

&lt;p&gt;對於每一張檢索到的圖片，也會做一樣的篩選，用「query image 的比較重要的幾個 patch」來判斷，只留下和 query image 相關的區塊。&lt;/p&gt;

&lt;p&gt;最後留下的這些區塊，會轉成對應的 visual tokens，並以 sequence 的形式排列(refined visual tokens)，作為 VLM 的 Input&lt;/p&gt;

&lt;p&gt;也就是模型最後看到的圖片資訊，其實已經被篩選過了。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fep02egk7za1el49ln4yi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fep02egk7za1el49ln4yi.png" alt=" " width="800" height="264"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;中間那些綠色的區塊，其實代表的是，每個 patch 和問題之間的相關性分數。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;3. Noise-resilient RAG&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;training 時故意加入錯誤 retrieval，讓 model 學會忽略 irrelevant knowledge。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;VLM 會同時看到：原始圖片、問題、還有多筆查到的知識（圖片 + 文字）&lt;/p&gt;

&lt;p&gt;這些 retrieval 結果裡面，有些是正確的，有些是錯的。&lt;br&gt;
模型要做的事就是根據相關程度(每張圖片與 query 到的 image)，決定要相信哪一段資訊。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F41egow2oh6lzmm5jnom3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F41egow2oh6lzmm5jnom3.png" alt=" " width="800" height="634"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;👉 綠色 = 高 attention &lt;br&gt;
👉 紅色 = 忽略&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;經過這個過程，模型可以回答問題，例如這個建築是在 1666–1679 年建造的。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;✨ Full Workflow&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F22cwt4kpjzufweq9mjh4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F22cwt4kpjzufweq9mjh4.png" alt="Full Workflow" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📄Soure&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://openreview.net/pdf/1dff65b976d44f89183d623a8d26842e17ed51da.pdf" rel="noopener noreferrer"&gt;https://openreview.net/pdf/1dff65b976d44f89183d623a8d26842e17ed51da.pdf&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>vlm</category>
      <category>rag</category>
      <category>paper</category>
    </item>
  </channel>
</rss>
