<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Trigger on Tarragon</title><link>https://tarrragon.github.io/blog/tags/trigger/</link><description>Recent content in Trigger on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 02 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/trigger/index.xml" rel="self" type="application/rss+xml"/><item><title>Cosmos DB Stored Procedure / Trigger（JavaScript）：partition-scoped 交易、server-side 邏輯邊界、何時用何時讓 application 層處理</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/stored-procedure-trigger/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/stored-procedure-trigger/</guid><description>&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB&lt;/a> overview 的 deep article、寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。Cosmos DB 的 stored procedure、trigger 與 user-defined function 是用 JavaScript 寫、執行在 Cosmos DB engine 內的 server-side 邏輯。它最有價值的能力是把同一 logical partition 內的多個操作包成一個原子交易 — 這是 application 層無法用 SDK 單獨做到的。本文先講這層 server-side 邏輯的精確語義與限制、再進操作流程、最後重點放在「何時用、何時不用」的判準 — 因為多數應用邏輯放在 application 層更好維護、stored procedure 應該是少數有明確理由的場景。&lt;/p>
&lt;p>本文沒有專屬 production case anchor：stored procedure 的設計取捨在公開 case 庫覆蓋稀薄、機制以 Azure vendor 規格與通用工程展開、情境用 partition 內原子交易這個具體需求驅動。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Scope warning&lt;/strong>：本文涉及的 script 大小上限、執行時間上限、bounded execution 行為等具體限制屬時間敏感、不同 account 配置可能不同、實作前以 &lt;a href="https://learn.microsoft.com/azure/cosmos-db/nosql/stored-procedures-triggers-udfs">Cosmos DB stored procedure 官方文件&lt;/a> cross-verify。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>典型觸發場景：業務需要「讀一筆庫存、檢查數量、扣減、寫一筆扣減記錄」這四步必須原子完成 — 中間不能被別的請求插入。用 application 層 SDK 做、四步是四個獨立 round-trip、中間有 race window；兩個請求同時扣同一筆庫存、可能都讀到 10、各扣 1、結果是 9 而非 8。這類 read-modify-write 在同一 partition 內、需要 server-side 原子性。&lt;/p>
&lt;p>讀者徵兆：&lt;/p>
&lt;ul>
&lt;li>「同一 partition 內的 read-modify-write 有 race、想要原子交易」&lt;/li>
&lt;li>「想做批次 upsert、減少 round-trip 與 RU」&lt;/li>
&lt;li>「想在寫入時自動加 timestamp / 算衍生欄位、用 pre-trigger 行不行」&lt;/li>
&lt;li>「stored procedure 能不能跨 partition 做交易」（不行 — 這是常見誤解）&lt;/li>
&lt;/ul>
&lt;p>真實壓力：Cosmos DB 的 transaction 邊界是 &lt;em>single logical partition&lt;/em>、跨 partition 沒有原生 ACID 交易。partition 內需要原子性時、SDK 多次 round-trip 無法保證、stored procedure 是 vendor 提供的 partition-scoped transaction 機制。但這個能力有強約束、且容易被濫用成「把業務邏輯都搬進 DB」。&lt;/p>
&lt;h2 id="核心機制partition-scoped-javascript-execution">核心機制：partition-scoped JavaScript execution&lt;/h2>
&lt;p>Cosmos DB 的 server-side 邏輯有三類、責任不同。&lt;/p>
&lt;p>Stored procedure 是執行在單一 logical partition 內的 JavaScript 函式、它內部對該 partition 的所有 document 操作包在一個 &lt;em>隱式交易&lt;/em> 裡 — 全部成功 commit、任一失敗整個 rollback。呼叫時必須指定 partition key、procedure 的所有操作都限定在那個 partition。&lt;/p>
&lt;p>Trigger 分 pre-trigger 與 post-trigger、綁在 create / replace / delete 等操作上、但 &lt;em>不會自動觸發&lt;/em> — 必須在 request 明確指定要跑哪個 trigger（這跟關聯式 DB 的 trigger 自動執行不同）。pre-trigger 在操作前跑（常用來補欄位、驗證）、post-trigger 在操作後跑（常用來更新同 partition 的彙總 document）。&lt;/p></description><content:encoded><![CDATA[<p>本文是 <a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB</a> overview 的 deep article、寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。Cosmos DB 的 stored procedure、trigger 與 user-defined function 是用 JavaScript 寫、執行在 Cosmos DB engine 內的 server-side 邏輯。它最有價值的能力是把同一 logical partition 內的多個操作包成一個原子交易 — 這是 application 層無法用 SDK 單獨做到的。本文先講這層 server-side 邏輯的精確語義與限制、再進操作流程、最後重點放在「何時用、何時不用」的判準 — 因為多數應用邏輯放在 application 層更好維護、stored procedure 應該是少數有明確理由的場景。</p>
<p>本文沒有專屬 production case anchor：stored procedure 的設計取捨在公開 case 庫覆蓋稀薄、機制以 Azure vendor 規格與通用工程展開、情境用 partition 內原子交易這個具體需求驅動。</p>
<blockquote>
<p><strong>Scope warning</strong>：本文涉及的 script 大小上限、執行時間上限、bounded execution 行為等具體限制屬時間敏感、不同 account 配置可能不同、實作前以 <a href="https://learn.microsoft.com/azure/cosmos-db/nosql/stored-procedures-triggers-udfs">Cosmos DB stored procedure 官方文件</a> cross-verify。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>典型觸發場景：業務需要「讀一筆庫存、檢查數量、扣減、寫一筆扣減記錄」這四步必須原子完成 — 中間不能被別的請求插入。用 application 層 SDK 做、四步是四個獨立 round-trip、中間有 race window；兩個請求同時扣同一筆庫存、可能都讀到 10、各扣 1、結果是 9 而非 8。這類 read-modify-write 在同一 partition 內、需要 server-side 原子性。</p>
<p>讀者徵兆：</p>
<ul>
<li>「同一 partition 內的 read-modify-write 有 race、想要原子交易」</li>
<li>「想做批次 upsert、減少 round-trip 與 RU」</li>
<li>「想在寫入時自動加 timestamp / 算衍生欄位、用 pre-trigger 行不行」</li>
<li>「stored procedure 能不能跨 partition 做交易」（不行 — 這是常見誤解）</li>
</ul>
<p>真實壓力：Cosmos DB 的 transaction 邊界是 <em>single logical partition</em>、跨 partition 沒有原生 ACID 交易。partition 內需要原子性時、SDK 多次 round-trip 無法保證、stored procedure 是 vendor 提供的 partition-scoped transaction 機制。但這個能力有強約束、且容易被濫用成「把業務邏輯都搬進 DB」。</p>
<h2 id="核心機制partition-scoped-javascript-execution">核心機制：partition-scoped JavaScript execution</h2>
<p>Cosmos DB 的 server-side 邏輯有三類、責任不同。</p>
<p>Stored procedure 是執行在單一 logical partition 內的 JavaScript 函式、它內部對該 partition 的所有 document 操作包在一個 <em>隱式交易</em> 裡 — 全部成功 commit、任一失敗整個 rollback。呼叫時必須指定 partition key、procedure 的所有操作都限定在那個 partition。</p>
<p>Trigger 分 pre-trigger 與 post-trigger、綁在 create / replace / delete 等操作上、但 <em>不會自動觸發</em> — 必須在 request 明確指定要跑哪個 trigger（這跟關聯式 DB 的 trigger 自動執行不同）。pre-trigger 在操作前跑（常用來補欄位、驗證）、post-trigger 在操作後跑（常用來更新同 partition 的彙總 document）。</p>
<p>UDF（user-defined function）是 query 內可呼叫的純函式、用來在 query projection / filter 階段做自訂計算、沒有寫入能力。</p>
<h3 id="交易邊界與-bounded-execution">交易邊界與 bounded execution</h3>
<p>交易嚴格限 single logical partition。stored procedure 不能跨 partition 寫、傳不同 partition key 的操作會失敗。跨 partition 的原子需求要改 workflow（saga / 補償）或重新設計 partition key 讓相關資料同 partition、見 <a href="../partition-key-design/">partition-key-design</a>。</p>
<p>執行有 bounded execution 限制：每次呼叫有時間與 resource 上限（時間敏感、查文件）、跑太久 Cosmos DB 會中止。處理大量 document 的 stored procedure 必須自己檢查每個操作的回傳、發現「快到上限」時停下、回傳一個 continuation 標記、讓 client 帶著標記再呼叫一次 — 這個 continuation 模式是寫批次 stored procedure 的必備 pattern。</p>
<h3 id="ru-成本">RU 成本</h3>
<p>stored procedure 內每個 document 操作都吃 RU、整個 procedure 的 RU 是內部所有操作的總和、由 response header 回報。一個掃很多 document 的 procedure 可能很貴、且因為 bounded execution 要分多次呼叫、成本與複雜度都比想像高、見 <a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a>。</p>
<h2 id="操作流程">操作流程</h2>
<h3 id="寫一個-partition-scoped-原子扣減">寫一個 partition-scoped 原子扣減</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// deductStock.js — 在單一 partition 內原子扣減庫存
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">deductStock</span><span class="p">(</span><span class="nx">productId</span><span class="p">,</span> <span class="nx">qty</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="kd">var</span> <span class="nx">context</span> <span class="o">=</span> <span class="nx">getContext</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="kd">var</span> <span class="nx">container</span> <span class="o">=</span> <span class="nx">context</span><span class="p">.</span><span class="nx">getCollection</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="kd">var</span> <span class="nx">response</span> <span class="o">=</span> <span class="nx">context</span><span class="p">.</span><span class="nx">getResponse</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="kd">var</span> <span class="nx">query</span> <span class="o">=</span> <span class="s2">&#34;SELECT * FROM c WHERE c.id = &#39;&#34;</span> <span class="o">+</span> <span class="nx">productId</span> <span class="o">+</span> <span class="s2">&#34;&#39;&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="kd">var</span> <span class="nx">accepted</span> <span class="o">=</span> <span class="nx">container</span><span class="p">.</span><span class="nx">queryDocuments</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nx">container</span><span class="p">.</span><span class="nx">getSelfLink</span><span class="p">(),</span> <span class="nx">query</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="kd">function</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">docs</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="k">throw</span> <span class="nx">err</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">docs</span> <span class="o">||</span> <span class="nx">docs</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">                <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">&#34;product not found&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">            <span class="kd">var</span> <span class="nx">product</span> <span class="o">=</span> <span class="nx">docs</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="nx">product</span><span class="p">.</span><span class="nx">stock</span> <span class="o">&lt;</span> <span class="nx">qty</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">                <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">&#34;insufficient stock&#34;</span><span class="p">);</span>  <span class="c1">// 整個交易 rollback
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">            <span class="nx">product</span><span class="p">.</span><span class="nx">stock</span> <span class="o">-=</span> <span class="nx">qty</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">            <span class="kd">var</span> <span class="nx">ok</span> <span class="o">=</span> <span class="nx">container</span><span class="p">.</span><span class="nx">replaceDocument</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">                <span class="nx">product</span><span class="p">.</span><span class="nx">_self</span><span class="p">,</span> <span class="nx">product</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">                <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="k">throw</span> <span class="nx">e</span><span class="p">;</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">ok</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">&#34;replace not accepted&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">            <span class="nx">response</span><span class="p">.</span><span class="nx">setBody</span><span class="p">({</span> <span class="nx">remaining</span><span class="o">:</span> <span class="nx">product</span><span class="p">.</span><span class="nx">stock</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">        <span class="p">});</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">accepted</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">&#34;query not accepted&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>註冊與呼叫（C# SDK）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">await</span> <span class="n">container</span><span class="p">.</span><span class="n">Scripts</span><span class="p">.</span><span class="n">CreateStoredProcedureAsync</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">new</span> <span class="n">StoredProcedureProperties</span><span class="p">(</span><span class="s">&#34;deductStock&#34;</span><span class="p">,</span> <span class="n">File</span><span class="p">.</span><span class="n">ReadAllText</span><span class="p">(</span><span class="s">&#34;deductStock.js&#34;</span><span class="p">)));</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="kt">var</span> <span class="n">result</span> <span class="p">=</span> <span class="k">await</span> <span class="n">container</span><span class="p">.</span><span class="n">Scripts</span><span class="p">.</span><span class="n">ExecuteStoredProcedureAsync</span><span class="p">&lt;</span><span class="kt">dynamic</span><span class="p">&gt;(</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="s">&#34;deductStock&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">new</span> <span class="n">PartitionKey</span><span class="p">(</span><span class="n">productId</span><span class="p">),</span>   <span class="c1">// 必須指定 partition key</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="k">new</span> <span class="kt">dynamic</span><span class="p">[]</span> <span class="p">{</span> <span class="n">productId</span><span class="p">,</span> <span class="m">1</span> <span class="p">});</span></span></span></code></pre></div><p>驗證：兩個並行請求扣同一筆、總扣減量等於兩次之和、不會 lost update（交易原子性）。庫存不足時拋例外、整個 procedure rollback、stock 不變。回傳 header 的 <code>x-ms-request-charge</code> 是這次交易的總 RU。</p>
<h3 id="批次操作的-continuation-模式">批次操作的 continuation 模式</h3>
<p>掃多筆 document 的 procedure 要在 callback 內檢查回傳的 <code>accepted</code>、為 false（快到上限）時停下並回傳已處理數量、由 client loop 呼叫直到全部處理完。驗證：對一個大 partition 跑、觀察需要多次呼叫、每次回傳的已處理數累加到總數。</p>
<h3 id="pre-trigger-補欄位">pre-trigger 補欄位</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">addTimestamp</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="kd">var</span> <span class="nx">doc</span> <span class="o">=</span> <span class="nx">getContext</span><span class="p">().</span><span class="nx">getRequest</span><span class="p">().</span><span class="nx">getBody</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">doc</span><span class="p">.</span><span class="nx">createdAt</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">().</span><span class="nx">toISOString</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">getContext</span><span class="p">().</span><span class="nx">getRequest</span><span class="p">().</span><span class="nx">setBody</span><span class="p">(</span><span class="nx">doc</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>呼叫時要明確指定 trigger、否則不執行：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">await</span> <span class="n">container</span><span class="p">.</span><span class="n">CreateItemAsync</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="k">new</span> <span class="n">PartitionKey</span><span class="p">(</span><span class="n">item</span><span class="p">.</span><span class="n">pk</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">new</span> <span class="n">ItemRequestOptions</span> <span class="p">{</span> <span class="n">PreTriggers</span> <span class="p">=</span> <span class="k">new</span><span class="p">[]</span> <span class="p">{</span> <span class="s">&#34;addTimestamp&#34;</span> <span class="p">}</span> <span class="p">});</span></span></span></code></pre></div><p>驗證：帶 trigger 的寫入有 <code>createdAt</code>、不帶 trigger 的寫入沒有 — 確認 trigger 非自動。</p>
<h3 id="rollback-boundary">Rollback boundary</h3>
<p>stored procedure 本身的交易是 all-or-nothing、procedure 內拋例外即整個 rollback。部署層面：stored procedure / trigger 是 container 內的 resource、replace 即更新、delete 即移除、不影響 data。</p>
<h2 id="何時用何時不用">何時用、何時不用</h2>
<p>這是本文的主判讀段：多數應用邏輯放在 application 層更好、stored procedure 只有少數場景值得。</p>
<p>值得用 stored procedure 的條件：</p>
<ul>
<li><em>partition 內的多步原子交易</em> — read-modify-write、需要 all-or-nothing、且相關資料確實在同一 partition。這是 stored procedure 不可替代的能力。</li>
<li><em>省 round-trip 的批次操作</em> — 一次寫入幾百筆同 partition document、用 stored procedure 比幾百次 SDK 呼叫省 latency 與部分 RU overhead。</li>
</ul>
<p>讓 application 層處理的條件（多數情況）：</p>
<ul>
<li>業務邏輯複雜、會頻繁變動 — JavaScript stored procedure 的版本管理、測試、debug、observability 都比 application 層差；邏輯放 DB 內、CI / 單元測試 / log / APM 都接不上。</li>
<li>不需要原子性、或跨 partition — 跨 partition 的協調用 application 層 workflow 或 saga、stored procedure 做不到。</li>
<li>寫入後的非同步工作（投影、通知、同步）— 用 <a href="../change-feed-cdc/">Change Feed</a> 解耦、不要塞進 stored procedure 拖長寫入路徑。</li>
<li>衍生欄位 / 計算 — 簡單的放 application 層或 pre-trigger、複雜的不要進 DB 邏輯。</li>
</ul>
<p>判讀句：stored procedure 的正當理由幾乎只有「partition-scoped atomicity」與「批次 round-trip 縮減」。看到「想把業務規則集中到 DB」「想讓 DB 自動做某件事」這類動機、優先回 application 層 — server-side JavaScript 的維護成本長期高於它省下的東西。</p>
<h2 id="失敗模式">失敗模式</h2>
<h3 id="期待跨-partition-交易">期待跨 partition 交易</h3>
<p>team 把多個不同 partition key 的寫入放進一個 stored procedure、期待原子性。procedure 對非當前 partition 的操作會失敗。徵兆是「跨用戶 / 跨類別的原子操作報錯或部分寫入」。修法是重新設計 partition key 讓相關資料同 partition（若業務允許）、或改用 application 層補償 / saga workflow 處理跨 partition 一致性。</p>
<h3 id="沒處理-bounded-execution">沒處理 bounded execution</h3>
<p>批次 stored procedure 假設「一次呼叫處理完所有 document」、資料量大時被中止、只處理了一部分、client 以為全做完。徵兆是大 partition 上批次操作結果不完整、且沒有錯誤（procedure 被 bounded execution 截斷但回傳了部分成功）。修法是實作 continuation 模式、每個操作檢查 <code>accepted</code>、回傳已處理數、client loop 直到完成。</p>
<h3 id="把可變業務邏輯固化進-stored-procedure">把可變業務邏輯固化進 stored procedure</h3>
<p>把定價規則、折扣計算、狀態機這類會變的邏輯寫進 JavaScript stored procedure、之後每次改規則都要改 DB resource、無法走正常 application CI / code review / 測試流程、且 production debug 缺 log。徵兆是「改一個業務規則要動 DB、且改完不確定對不對」。修法是把邏輯搬回 application 層、stored procedure 只保留無法在 application 層做的 partition-scoped atomicity。</p>
<h3 id="依賴-trigger-自動執行">依賴 trigger 自動執行</h3>
<p>從關聯式 DB 過來的 team 假設 trigger 像 SQL trigger 一樣自動跑、寫了 audit / 補欄位的 trigger 卻發現大部分寫入沒觸發 — 因為 Cosmos DB trigger 必須 per-request 指定。徵兆是「trigger 有時跑有時不跑」、實際是只有明確帶 trigger 的 request 才跑。修法是確認所有相關寫入路徑都指定 trigger、或把「必須每次都做」的邏輯放 application 層 / pre-trigger 並在 SDK wrapper 統一帶上。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<ul>
<li>必看 metric：stored procedure 執行的 <code>x-ms-request-charge</code>（整個交易的總 RU）、執行例外率、bounded execution 中止比例</li>
<li>成本：一個掃多 document 的 procedure 可能比等量單筆操作貴、且 continuation 多次呼叫累加 — 把它當「一個複合操作的總 RU」進容量公式、見 <a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a></li>
<li>observability gap：stored procedure 內部沒有 application APM / structured log、debug 靠回傳 body 與例外訊息 — 這個 gap 本身是「邏輯不該放這裡」的訊號之一</li>
<li>回 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>：partition-scoped transaction 的 RU 要算進該 partition 的 budget、熱門 partition 上跑重 procedure 會放大 hot partition、見 <a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a></li>
<li>Alert：stored procedure 例外率上升、執行 RU 異常偏高、bounded execution 截斷比例升高</li>
</ul>
<h2 id="邊界與整合">邊界與整合</h2>
<ul>
<li>Sibling deep articles：<a href="../change-feed-cdc/">change-feed-cdc</a>（寫入後的非同步工作走 Change Feed、不要塞 stored procedure）、<a href="../partition-key-design/">partition-key-design</a>（transaction 邊界 = partition 邊界、跨 partition 原子需求要重設計 partition key）、<a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a>（複合交易的 RU 估算）、<a href="../consistency-levels-engineering/">consistency-levels-engineering</a>（partition 內原子性 vs 跨 session consistency 是兩個不同議題）</li>
<li>跟 Spanner 對照：需要 <em>跨 partition / 全域</em> ACID 交易時、Cosmos DB stored procedure 做不到 — 轉 <a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner vendor</a> 或 Aurora DSQL</li>
<li>跟 DynamoDB 對照：DynamoDB 的 TransactWriteItems 提供跨 item（含跨 partition、有上限）的交易、語義跟 Cosmos DB 的 single-partition stored procedure 不同 — 從 DynamoDB transaction 過來的 team 要注意 Cosmos DB 沒有等價的開箱跨 partition 交易、見 <a href="/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB vendor</a></li>
<li>回 overview：<a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor overview</a> 的「跨 partition transaction 要改 workflow / stored procedure 邊界」</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor overview</a> — 本文是該頁尾 stored procedure / trigger backlog 的深度展開</li>
<li><a href="../change-feed-cdc/">change-feed-cdc</a> — 寫入後非同步工作的對照路徑</li>
<li><a href="../partition-key-design/">partition-key-design</a> — transaction 邊界 = partition 邊界</li>
<li><a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a> — 複合交易 RU 估算</li>
<li><a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner vendor</a> / <a href="/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB vendor</a> — 跨 partition 交易能力對照</li>
<li><a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition 卡片</a> — 熱 partition 上的重交易放大效應</li>
<li>官方：<a href="https://learn.microsoft.com/azure/cosmos-db/nosql/stored-procedures-triggers-udfs">Stored procedures, triggers, and UDFs</a></li>
</ul>
]]></content:encoded></item></channel></rss>