<?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>Posts on 愷的大冒險 Kai's Adventure</title><link>https://kaiadv.com/posts/</link><description>Recent content in Posts on 愷的大冒險 Kai's Adventure</description><generator>Hugo</generator><language>zh-tw</language><lastBuildDate>Sun, 14 Jun 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://kaiadv.com/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>當 Shopify Webhook 漏接時：分散式系統的資料一致性問題</title><link>https://kaiadv.com/posts/202606014/</link><pubDate>Sun, 14 Jun 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/202606014/</guid><description>&lt;p&gt;本文延續上一篇「防止 Shopify 線上商店與實體門市超賣」的討論，探討反向情境：當 Shopify 的 Webhook Event 漏接，導致線上訂單無法即時同步進內部資料庫時，應該如何設計系統來保障資料一致性，以及避免先下單的顧客受到損失。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="問題情境"&gt;問題情境&lt;/h1&gt;
&lt;p&gt;承接上篇的架構背景：線上訂單透過 Shopify Webhook 即時回傳至內部 DB，並有一支 Timer Program 每小時定期補漏。&lt;/p&gt;
&lt;p&gt;當 Webhook 漏接時，空窗期內可能發生以下情境：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;線上顧客成功下單（Shopify 已收款）
→ Webhook 發送失敗或漏接
→ 內部 DB 不知道這筆訂單的存在
→ 內部 DB 庫存仍顯示舊的（較高）數字
同一時間，實體門市售出同一商品
→ 扣減內部 DB 庫存
Timer 補跑，終於處理到那筆線上訂單
→ 此時庫存已不足
→ 先下單的線上顧客反而無法出貨 ❌
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;這個問題的核心是：Shopify 與內部 DB 是兩個獨立的系統，Webhook 是主要的同步橋樑，一旦橋樑出現延遲，兩端就會產生資料落差。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="解法方向"&gt;解法方向&lt;/h1&gt;
&lt;h2 id="方案一縮短-timer-間隔"&gt;方案一：縮短 Timer 間隔&lt;/h2&gt;
&lt;p&gt;最直覺的做法是將 Timer 的同步週期從一小時縮短，例如改為每五分鐘一次，讓系統更快發現未處理的線上訂單並補上；這個方案不需要改動架構，成本最低且能夠立即縮短風險窗口。&lt;/p&gt;
&lt;p&gt;然而它存在一個根本性的隱憂：當系統因負荷過高而開始變慢，頻繁的 Timer 仍會不斷觸發新的同步任務，不斷消耗系統資源，反而讓系統雪上加霜，最終可能完全卡死；這在系統設計上是一種 Thundering Herd（驚群效應）的變體，意指用更頻繁的 Polling 來彌補事件驅動的不足，但 Polling 本身在系統壓力大時會成為壓垮駱駝的稻草。&lt;/p&gt;
&lt;p&gt;縮短間隔治標不治本，空窗期的長短仍然取決於 Timer 的頻率，只是縮小了問題的規模。&lt;/p&gt;
&lt;h2 id="方案二timer-補跑時訂單同步優先於庫存校正"&gt;方案二：Timer 補跑時，訂單同步優先於庫存校正&lt;/h2&gt;
&lt;p&gt;這是一個執行順序的調整；Timer 補跑時應先將所有未同步的線上訂單補進內部 DB，再進行庫存比對與扣減，避免實體銷售的庫存扣減蓋過尚未被認知的線上訂單。&lt;/p&gt;</description></item><item><title>如何防止 Shopify 線上商店與實體門市的超賣問題</title><link>https://kaiadv.com/posts/202606013/</link><pubDate>Sat, 13 Jun 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/202606013/</guid><description>&lt;p&gt;本文源自一次技術面試的討論題目，紀錄了問題的成因分析與逐步推導出的解決方案，可以作為電商系統設計的學習參考：假設公司同時擁有 Shopify 線上商店與實體銷售門市，兩個渠道共用同一套內部資料庫（庫存 DB 與訂單 DB）。&lt;/p&gt;
&lt;p&gt;現有架構如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;線上訂單&lt;/strong&gt;：Shopify 透過 Webhook Event 即時回傳至訂單資料庫。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;補漏機制&lt;/strong&gt;：由於擔心 Webhook 漏接，系統另有一支 Timer Program，定期（約每小時一次）與 Shopify 比對並同步訂單與庫存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;實體訂單&lt;/strong&gt;：門市銷售的訂單會寫入內部訂單 DB，但不會同步至 Shopify。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;資料庫的語意定義如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;內部 DB 庫存 = 全渠道實際庫存總量
內部 DB 訂單 = 線上訂單 ＋ 實體訂單（所有訂單）
Shopify 庫存 = 僅反映線上銷售，對實體銷售一無所知
&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h1 id="問題根源"&gt;問題根源&lt;/h1&gt;
&lt;p&gt;由於 Timer Program 每小時才同步一次，在同步之前存在一個空窗期：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;實體門市售出商品
→ 扣減內部 DB 庫存 正確
→ Shopify 庫存尚未更新 數字虛高
線上顧客在空窗期內下單
→ Shopify 顯示庫存充足 錯誤資訊
→ 顧客成功下單 實際已超賣
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;矛盾在於：內部 DB 才是庫存的 Single Source of Truth，但 Shopify 並不知道實體門市的銷售情況，導致其顯示的庫存數字在空窗期內是虛高的。&lt;/p&gt;</description></item><item><title>用 ASP.NET + Redis + PostgreSQL 實作購票系統的併發控制</title><link>https://kaiadv.com/posts/20260607/</link><pubDate>Sun, 07 Jun 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260607/</guid><description>&lt;p&gt;前兩篇分別談了樂觀鎖與悲觀鎖的理論，以及購票系統各環節的設計決策，這篇將直接進入程式碼實作，把設計思路轉化成可以實際運行的實作。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="資料模型"&gt;資料模型&lt;/h1&gt;
&lt;p&gt;先定義三個核心 Entity，各自對應不同的鎖策略。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Ticket&lt;/code&gt; 代表票種與庫存，Entity 上掛的 &lt;code&gt;RowVersion&lt;/code&gt;（對應 PostgreSQL 的 &lt;code&gt;xmin&lt;/code&gt; 系統欄位）是&lt;a href="https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=data-annotations#optimistic-concurrency"&gt;樂觀鎖機制&lt;/a&gt;，用於後台修改票種名稱、調整票價等一般更新場景。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Order&lt;/code&gt; 代表訂單，其中狀態流轉使用樂觀鎖（Optimistic Lock），手動維護 &lt;code&gt;Version&lt;/code&gt; 整數欄位，方便在 SQL 層直接用條件更新防止重複付款。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Seat&lt;/code&gt; 代表對號座的每一個座位，記錄目前是否被鎖定、被誰鎖定、以及鎖定到什麼時間；它的鎖定狀態由 Redis 的分散式鎖（Distributed Lock）主導，&lt;code&gt;LockedByUserId&lt;/code&gt; 和 &lt;code&gt;LockedUntil&lt;/code&gt; 則作為資料庫層的備份紀錄，用於查詢座位狀態或在 Redis 異常時做補救。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 1&lt;/span&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;// Entities/Ticket.cs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 2&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;class&lt;/span&gt; &lt;span style="color:#f0883e;font-weight:bold"&gt;Ticket&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 3&lt;/span&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 4&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;int&lt;/span&gt; Id { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 5&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;int&lt;/span&gt; EventId { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 6&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;string&lt;/span&gt; TicketType { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; } = &lt;span style="color:#ff7b72"&gt;default&lt;/span&gt;!;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 7&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;int&lt;/span&gt; AvailableQty { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 8&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 9&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// EF Core 樂觀鎖：使用 PostgreSQL xmin 系統欄位，免額外欄位&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;10&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// xmin 是每次 row 被更新時自動遞增的系統欄位，完全不需要手動維護&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;11&lt;/span&gt;&lt;span&gt; [Timestamp]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;12&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;byte&lt;/span&gt;[] RowVersion { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; } = &lt;span style="color:#ff7b72"&gt;default&lt;/span&gt;!;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;13&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;14&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;15&lt;/span&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;// Entities/Order.cs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;16&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;class&lt;/span&gt; &lt;span style="color:#f0883e;font-weight:bold"&gt;Order&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;17&lt;/span&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;18&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;int&lt;/span&gt; Id { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;19&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;string&lt;/span&gt; UserId { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; } = &lt;span style="color:#ff7b72"&gt;default&lt;/span&gt;!;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;20&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; OrderStatus Status { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;21&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;22&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// 手動版本號，用於訂單狀態流轉的樂觀鎖&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;23&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;int&lt;/span&gt; Version { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;24&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;25&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;26&lt;/span&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;// Entities/Seat.cs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;27&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;class&lt;/span&gt; &lt;span style="color:#f0883e;font-weight:bold"&gt;Seat&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;28&lt;/span&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;29&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;string&lt;/span&gt; SeatId { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; } = &lt;span style="color:#ff7b72"&gt;default&lt;/span&gt;!; &lt;span style="color:#8b949e;font-style:italic"&gt;// e.g. &amp;#34;A-12&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;30&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;int&lt;/span&gt; EventId { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;31&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; SeatStatus Status { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;32&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;string?&lt;/span&gt; LockedByUserId { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;33&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; DateTime? LockedUntil { &lt;span style="color:#ff7b72"&gt;get&lt;/span&gt;; &lt;span style="color:#ff7b72"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;34&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;35&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;36&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;enum&lt;/span&gt; OrderStatus { Pending, Paid, Cancelled, Expired }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;37&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;enum&lt;/span&gt; SeatStatus { Available, Locked, Sold }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 1&lt;/span&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;// Data/AppDbContext.cs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 2&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;class&lt;/span&gt; &lt;span style="color:#f0883e;font-weight:bold"&gt;AppDbContext&lt;/span&gt; : DbContext
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 3&lt;/span&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 4&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; DbSet&amp;lt;Ticket&amp;gt; Tickets =&amp;gt; Set&amp;lt;Ticket&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 5&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; DbSet&amp;lt;Order&amp;gt; Orders =&amp;gt; Set&amp;lt;Order&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 6&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; DbSet&amp;lt;Seat&amp;gt; Seats =&amp;gt; Set&amp;lt;Seat&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 7&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 8&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;protected&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;override&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;void&lt;/span&gt; OnModelCreating(ModelBuilder builder)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 9&lt;/span&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;10&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// 將 RowVersion 對應到 PostgreSQL 的 xmin 欄位&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;11&lt;/span&gt;&lt;span&gt; builder.Entity&amp;lt;Ticket&amp;gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;12&lt;/span&gt;&lt;span&gt; .Property(t =&amp;gt; t.RowVersion)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;13&lt;/span&gt;&lt;span&gt; .IsRowVersion()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;14&lt;/span&gt;&lt;span&gt; .HasColumnName(&lt;span style="color:#a5d6ff"&gt;&amp;#34;xmin&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;15&lt;/span&gt;&lt;span&gt; .HasColumnType(&lt;span style="color:#a5d6ff"&gt;&amp;#34;xid&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;16&lt;/span&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;17&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h1 id="redis-層庫存預扣"&gt;Redis 層：庫存預扣&lt;/h1&gt;
&lt;p&gt;單純使用 &lt;code&gt;DECR&lt;/code&gt; 可能會有 Race Condition 的風險，在「讀取 → 判斷 → 扣減」三步驟之間可能插入其他操作；使用 &lt;a href="https://redis.io/docs/latest/develop/programmability/eval-intro/"&gt;Lua Script&lt;/a&gt; 則能夠確保 Redis 的原子執行，使得整個 check-and-decrement 變得不可分割；更多詳情可以參考 &lt;a href="https://stackexchange.github.io/StackExchange.Redis/"&gt;StackExchange.Redis&lt;/a&gt;。&lt;/p&gt;</description></item><item><title>為什麼購票系統需要兩種不同的鎖</title><link>https://kaiadv.com/posts/20260606/</link><pubDate>Sat, 06 Jun 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260606/</guid><description>&lt;p&gt;上一篇談了悲觀鎖（Pessimistic Lock）和樂觀鎖（Optimistic Lock）的基本概念，而這篇要把焦點放在一個具體的場景：演唱會購票系統。&lt;/p&gt;
&lt;p&gt;為什麼會選這個場景呢？因為它的併發特性非常極端：開賣瞬間可能湧入數十萬個請求，庫存有限、不能超賣、用戶又非常敏感，幾乎把所有併發控制（Concurrency Control）的挑戰都壓縮在一起了，適合用來思考鎖策略的設計。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="系統的核心矛盾"&gt;系統的核心矛盾&lt;/h1&gt;
&lt;p&gt;購票系統面臨一個根本的矛盾：速度和正確性永遠在對抗。&lt;/p&gt;
&lt;p&gt;為了正確性，你想讓每個請求排隊、一個一個處理，但這樣系統會慢到讓人放棄；為了速度，你想讓所有請求並行處理，但這樣很可能超賣或出現資料不一致；而解決這個矛盾的方式，不是選擇其中一邊，而是在不同的環節用不同的策略。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="把系統拆成幾個關鍵環節"&gt;把系統拆成幾個關鍵環節&lt;/h1&gt;
&lt;h2 id="庫存扣減這是整個系統最不能出錯的地方"&gt;庫存扣減：這是整個系統最不能出錯的地方&lt;/h2&gt;
&lt;p&gt;票券超賣是商業損失，無法事後補救；這裡需要的是一致性而不是高效能，寧願讓請求排隊等待，也必須保證每次扣減前看到的都是真實的剩餘數量。&lt;/p&gt;
&lt;p&gt;這是悲觀鎖（Pessimistic Lock）典型的使用場景；在 PostgreSQL 裡，用 &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; 鎖住那行資料，讓後來的交易（Transaction）必須等待；這個鎖從查詢的當下持有，直到 Transaction 提交（Commit）或回滾（Rollback）才釋放。&lt;/p&gt;
&lt;p&gt;代價是明確的：高併發下這裡會成為瓶頸，請求會大量排隊，但在「超賣後果不可逆」的前提下，這個代價值得付。&lt;/p&gt;
&lt;h2 id="座位鎖定互斥資源必須強佔"&gt;座位鎖定：互斥資源，必須強佔&lt;/h2&gt;
&lt;p&gt;對號座的選位，是另一個互斥資源（Exclusive Resource）的場景，用戶 A 選了 A-12，在她付款完成之前，A-12 不能被用戶 B 選走。&lt;/p&gt;
&lt;p&gt;這裡的問題不只是資料庫併發，更是跨請求（Request）的狀態持有；用戶進入付款頁面後，這把鎖需要持續存在 10 分鐘，若超時便自動釋放；資料庫鎖的生命週期只存在於一個 Transaction 裡，無法做到跨 Request 持有。&lt;/p&gt;
&lt;p&gt;這時可以改用 &lt;a href="https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/"&gt;Redis 的分散式鎖（Distributed Lock）&lt;/a&gt;：&lt;code&gt;SET NX EX&lt;/code&gt;（不存在才設置，並附上過期時間），第一個選到這個座位的請求成功鎖定，後來的請求看到 key 已存在，直接被拒絕；過了 10 分鐘之後，Redis 的 TTL 自動刪除這個 key，座位自動釋放回可選狀態。&lt;/p&gt;
&lt;h2 id="庫存預扣在資料庫前擋掉大多數請求"&gt;庫存預扣：在資料庫前擋掉大多數請求&lt;/h2&gt;
&lt;p&gt;純粹依賴 PostgreSQL 的 &lt;code&gt;FOR UPDATE&lt;/code&gt;，在演唱會開賣瞬間會承受巨大的資料庫壓力。大量請求湧進來，每個都要等待前一個 Transaction 釋放鎖，資料庫連線很快就會耗盡。&lt;/p&gt;
&lt;p&gt;這時可以在資料庫前面加一層 Redis 的原子操作（Atomic Operation）：先在 Redis 裡做庫存預扣，用 &lt;a href="https://redis.io/docs/latest/develop/programmability/eval-intro/"&gt;Lua Script&lt;/a&gt; 確保「檢查庫存是否足夠」和「扣減庫存」這兩步是符合原子操作且不可分割；因此大多數「票已售罄」的請求，在這一層就被擋掉了，不需要進入資料庫，只有成功預扣的請求才會繼續往下，讓 PostgreSQL 做最終確認。&lt;/p&gt;</description></item><item><title>樂觀鎖與悲觀鎖：併發的控制</title><link>https://kaiadv.com/posts/20260605/</link><pubDate>Fri, 05 Jun 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260605/</guid><description>&lt;p&gt;併發問題的根源，大多來自「多個操作同時想動同一份資料」；當系統只有一個使用者時，這不是問題，然而流量一旦上來，很多我們以為不可能同時發生的事，就會同時發生；面對這個問題，思路分成了兩個截然不同的方向。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="悲觀鎖先鎖再說"&gt;悲觀鎖：先鎖再說&lt;/h1&gt;
&lt;p&gt;悲觀鎖（Pessimistic Lock）的世界觀很直接：衝突一定會發生，所以在操作資料之前就先把它鎖住，其他人等我用完再說；它像一個很謹慎的人，進房間之前先把門鎖上，做完事情才開鎖讓別人進來；在這段期間，任何人想進來都必須等待。&lt;/p&gt;
&lt;p&gt;資料庫層面最常見的實作是 &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;，在查詢的當下就對那幾行資料加上排他鎖，其他 transaction 如果也想修改同一行，就必須等待前一個 transaction 結束：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 1&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;BEGIN&lt;/span&gt;;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 2&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 3&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;SELECT&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;available_qty&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 4&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;FROM&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;tickets&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 5&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;WHERE&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;event_id&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;1&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;AND&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;ticket_type&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#39;VIP&amp;#39;&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 6&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;FOR&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;UPDATE&lt;/span&gt;;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;-- 從這一刻起，這行資料被鎖住
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 7&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 8&lt;/span&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;-- 確認有票後才扣減
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 9&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;UPDATE&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;tickets&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;10&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;SET&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;available_qty&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;available_qty&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;1&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;11&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;WHERE&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;event_id&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;1&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;AND&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;ticket_type&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#39;VIP&amp;#39;&lt;/span&gt;;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;12&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;13&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;COMMIT&lt;/span&gt;;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;-- 鎖在這裡釋放
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這個做法的代價很明顯：併發效能較低，因為後來的請求都必須排隊等待；而且如果兩個 transaction 互相等待對方釋放鎖，就會發生死鎖（Deadlock）；不過在某些場景下，這是正確的選擇，因為有些錯誤是無法事後補救的。&lt;/p&gt;</description></item><item><title>依賴反轉原則（Dependency Inversion Principle, DIP）</title><link>https://kaiadv.com/posts/20260603/</link><pubDate>Wed, 03 Jun 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260603/</guid><description>&lt;p&gt;Robert C. Martin 為這個原則提出兩條互相呼應的規則：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;High-level modules should not import anything from low-level modules. Both should depend on abstractions（高階模組不應依賴低階模組，兩者都應依賴抽象）。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Abstractions should not depend on details. Details should depend on abstractions（抽象不應依賴細節，細節應依賴抽象）。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;這兩條規則合在一起，描述的是系統中依賴關係的方向性應該如何安排，要理解它們需要先理解「高階模組」和「低階模組」的差別。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="高階模組與低階模組"&gt;高階模組與低階模組&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;高階模組（High-level modules）&lt;/strong&gt;：包含應用程式核心業務邏輯的模組，它們描述的是「這個系統要做什麼」：訂單需要被驗證、付款需要被處理、報表需要被產生；這些邏輯代表了系統存在的根本原因，是具有業務價值的部分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;低階模組（Low-level modules）&lt;/strong&gt;：提供具體技術實作的模組，它們描述的是「這件事具體怎麼做」：資料存到 SQL Server、Email 用 SMTP 發送、圖片存到 S3；這些模組是高階模組的工具，本身通常不含業務價值，可以被替換。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在傳統的依賴方向中，高階模組直接使用（依賴）低階模組：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;OrderService（高階）→ SqlServerRepository（低階）
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;這意味著業務邏輯層與具體的資料庫技術「綁死」在一起；若今天想把 SQL Server 換成 PostgreSQL，就必須修改 OrderService 這個本來應該只關心業務規則的類別，DIP 要「反轉」的正是這個依賴方向。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="與-oop-的關係多型polymorphism與抽象abstraction"&gt;與 OOP 的關係：多型（Polymorphism）與抽象（Abstraction）&lt;/h1&gt;
&lt;p&gt;DIP 的實現依賴抽象與多型，具體的做法是在高階模組與低階模組之間插入一層抽象（介面），讓兩者都依賴這個介面，而非彼此直接依賴。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;傳統方向（高耦合）：
OrderService ─────────────────→ SqlServerRepository
（高階） （低階，具體實作）
DIP 方向（低耦合）：
OrderService ──→ IOrderRepository ←── SqlServerRepository
（高階） （抽象介面） （低階，具體實作）
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在 DIP 的架構下，依賴箭頭的方向發生了「反轉」：&lt;/p&gt;</description></item><item><title>介面隔離原則（Interface Segregation Principle, ISP）</title><link>https://kaiadv.com/posts/20260602/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260602/</guid><description>&lt;p&gt;&lt;strong&gt;Robert C. Martin 對這個原則的定義是：Clients should not be forced to depend upon interface methods that they do not use.（不應強迫客戶端依賴它不使用的方法）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;這裡的「客戶端」不是指使用者介面的終端用戶，而是指在程式碼中使用某個介面的類別或模組；當一個介面提供了十個方法，但某個實作類別只需要其中三個，ISP 說這樣的設計是有問題的。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="與-oop-的關係抽象abstraction"&gt;與 OOP 的關係：抽象（Abstraction）&lt;/h1&gt;
&lt;p&gt;介面（Interface）是 OOP 抽象機制的體現，抽象的目的是「只暴露必要的能力，隱藏不必要的細節」；一個設計良好的介面應該像是一份精確的「能力聲明」：「持有這個介面的物件，保證能做 X、Y、Z。」&lt;/p&gt;
&lt;p&gt;然而，若一個介面同時聲明了十幾個能力，而這些能力並非總是同時需要，這個介面就失去了精準性；實作者不得不提供所有能力的實作，即使其中有些對它毫無意義；呼叫端也無法從介面名稱判斷「這個物件究竟擅長做什麼」，因為它什麼都做。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ISP 的本質是對抽象品質的要求：介面應精簡、專一，只描述一個「角色（role）」。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="肥大介面的危害"&gt;「肥大介面」的危害&lt;/h1&gt;
&lt;p&gt;假設系統有一個 &lt;code&gt;IWorker&lt;/code&gt; 介面：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;1&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;interface&lt;/span&gt; &lt;span style="color:#f0883e;font-weight:bold"&gt;IWorker&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;2&lt;/span&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;3&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;void&lt;/span&gt; Work();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;4&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;void&lt;/span&gt; Eat();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;5&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;void&lt;/span&gt; TakeBreak();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;6&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;void&lt;/span&gt; ReceiveSalary();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;7&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;對於人類員工來說，這四個方法都有意義；但如果在系統中引入了「工業機器人」（自動化設備），它同樣需要實作 &lt;code&gt;IWorker&lt;/code&gt;（因為它也需要執行工作），但機器人不吃飯、不休息、也不領薪水，這時機器人類別就被迫實作它根本不支援的方法：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;1&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;class&lt;/span&gt; &lt;span style="color:#f0883e;font-weight:bold"&gt;IndustrialRobot&lt;/span&gt; : IWorker
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;2&lt;/span&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;3&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;void&lt;/span&gt; Work() { Console.WriteLine(&lt;span style="color:#a5d6ff"&gt;&amp;#34;機器人執行工序中...&amp;#34;&lt;/span&gt;); }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;4&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;5&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// 機器人不需要這些，但被強迫實作&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;6&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;void&lt;/span&gt; Eat() =&amp;gt; &lt;span style="color:#ff7b72"&gt;throw&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;new&lt;/span&gt; NotSupportedException(&lt;span style="color:#a5d6ff"&gt;&amp;#34;機器人不需要進食&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;7&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;void&lt;/span&gt; TakeBreak() =&amp;gt; &lt;span style="color:#ff7b72"&gt;throw&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;new&lt;/span&gt; NotSupportedException(&lt;span style="color:#a5d6ff"&gt;&amp;#34;機器人不需要休息&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;8&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;void&lt;/span&gt; ReceiveSalary() =&amp;gt; &lt;span style="color:#ff7b72"&gt;throw&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;new&lt;/span&gt; NotSupportedException(&lt;span style="color:#a5d6ff"&gt;&amp;#34;機器人不領薪水&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;9&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這個設計同時違反了 ISP 和 LSP：&lt;code&gt;IndustrialRobot&lt;/code&gt; 宣稱自己是一個 &lt;code&gt;IWorker&lt;/code&gt;，但呼叫 &lt;code&gt;Eat()&lt;/code&gt; 會在執行期間報錯，呼叫端如果不特別防範就會有潛在風險。&lt;/p&gt;</description></item><item><title>里氏替換原則（Liskov Substitution Principle, LSP）</title><link>https://kaiadv.com/posts/20260601/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260601/</guid><description>&lt;p&gt;&lt;strong&gt;Barbara Liskov 提出的定義是： If S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program（若 S 是 T 的子型別，則程式中使用 T 物件的地方可以使用 S 物件來替換，而不改變程式的任何預期屬性）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Barbara Liskov 是麻省理工學院（MIT）的電腦科學教授，這個原則由他在 1987 年的一篇論文中首次提出，後來被 Robert C. Martin 納入 SOLID 體系。&lt;/p&gt;
&lt;p&gt;用更白話的方式說：凡是可以使用父類別物件的地方，換成任何子類別的物件，程式應該要能正確執行，結果也應該要符合預期；如果換了子類別之後行為變了，那就是違反了 LSP。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="與-oop-的關係繼承inheritance"&gt;與 OOP 的關係：繼承（Inheritance）&lt;/h1&gt;
&lt;p&gt;繼承是 OOP 中強大但也容易被濫用的機制，把繼承當作「程式碼複用」的工具：「子類別可以繼承父類別的方法，這樣就不用重寫了。」這個想法本身沒有錯，但它遺漏了繼承更深層的語義。&lt;/p&gt;
&lt;p&gt;在物件導向系統中，繼承不只是程式碼的共享機制，它同時建立了一種型別關係（is-a relationship）；當寫了 &lt;code&gt;class Dog : Animal&lt;/code&gt; 時，不只是讓 &lt;code&gt;Dog&lt;/code&gt; 複用了 &lt;code&gt;Animal&lt;/code&gt; 的程式碼，同時宣告了「&lt;code&gt;Dog&lt;/code&gt; 是一種 &lt;code&gt;Animal&lt;/code&gt;」；這個宣告有一個重要的隱含意義：在任何需要 &lt;code&gt;Animal&lt;/code&gt; 的場合，都可以放入一隻 &lt;code&gt;Dog&lt;/code&gt;，而程式的行為不應改變。&lt;/p&gt;</description></item><item><title>開放封閉原則（Open–Closed Principle, OCP）</title><link>https://kaiadv.com/posts/20260531/</link><pubDate>Sun, 31 May 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260531/</guid><description>&lt;p&gt;&lt;strong&gt;Bertrand Meyer 在 1988 年提出、後經 Robert C. Martin 推廣的定義是：Software entities should be open for extension, but closed for modification（軟體實體應該對擴展開放，對修改封閉）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;「對擴展開放」意味著當需求出現新的變化時，我們可以為系統加入新的行為；「對修改封閉」意味著加入新行為時，我們不需要改動已經存在且正常運作的程式碼；這兩個要求乍聽矛盾，既然不能改動既有程式碼，又怎麼增加新行為呢？OCP 的概念是：透過多型與抽象，讓新行為以「新增類別」的方式注入系統，而非以「修改現有邏輯」的方式侵入系統。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="與-oop-的關係多型polymorphism與抽象abstraction"&gt;與 OOP 的關係：多型（Polymorphism）與抽象（Abstraction）&lt;/h1&gt;
&lt;p&gt;OCP 的實現幾乎完全仰賴 OOP 的多型機制，想理解這個關係要先從多型的本質說起；多型讓我們可以寫出這樣的程式碼：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;1&lt;/span&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;// 呼叫端只知道 shape 是某種 Shape，不知道它具體是哪一種&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;2&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;void&lt;/span&gt; DrawShape(IShape shape)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;3&lt;/span&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;4&lt;/span&gt;&lt;span&gt; shape.Draw(); &lt;span style="color:#8b949e;font-style:italic"&gt;// 執行時才決定呼叫哪個 Draw()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;5&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;DrawShape&lt;/code&gt; 這個方法依賴的是抽象（&lt;code&gt;IShape&lt;/code&gt; 介面），而不是任何具體的形狀類別；未來若要新增「六邊形」，只需要新增一個實作 &lt;code&gt;IShape&lt;/code&gt; 的 &lt;code&gt;Hexagon&lt;/code&gt; 類別，不需要改動 &lt;code&gt;DrawShape&lt;/code&gt;；這就是 OCP 的運作原理：抽象是穩定的，具體實作是可以擴展的。&lt;/p&gt;
&lt;p&gt;換言之，OCP 是多型概念重要的應用場景：用「新增類別」取代「修改現有邏輯」，讓已通過測試的程式碼保持穩定。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="修改的代價"&gt;「修改」的代價&lt;/h1&gt;
&lt;p&gt;為什麼要這麼費心避免修改既有程式碼？每次修改都有潛在的風險：&lt;/p&gt;</description></item><item><title>單一職責原則（Single-Responsibility Principle, SRP）</title><link>https://kaiadv.com/posts/20260530/</link><pubDate>Sat, 30 May 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260530/</guid><description>&lt;p&gt;&lt;strong&gt;Robert C. Martin 對這個原則的定義是：A class should have only one reason to change（一個類別應該只有一個改變的理由）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;乍看之下這句話的意思相當直觀：一個類別只做一件事，但「改變的理由」這個措辭或許比表面上更深刻，也是 SRP 常被誤解的地方。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="與-oop-的關係封裝encapsulation"&gt;與 OOP 的關係：封裝（Encapsulation）&lt;/h1&gt;
&lt;p&gt;封裝是 OOP 的第一個基石，它的目的是將資料與操作資料的行為包裝在一起，並對外隱藏實作細節；這讓類別可以像一個「黑盒子」，使用者只需要知道「它能做什麼」，不需要知道「它是怎麼做的」。&lt;/p&gt;
&lt;p&gt;然而，封裝只解決了「如何包裝」的問題，沒有回答「應該把哪些東西包在一起」；一個把所有功能都塞進同一個類別的設計，在語法上是合法的封裝，但在設計上卻是災難性的。&lt;/p&gt;
&lt;p&gt;SRP 為封裝提供了邊界判斷的標準：同一個職責（responsibility）的程式碼才應該被封裝在同一個類別裡；換句話說，類別的邊界不應由技術上的便利性決定（這幾個 method 放在一起比較方便），而應由職責的邊界來決定（這幾個 method 服務於同一個目的、同一個改變來源）。&lt;/p&gt;
&lt;p&gt;也就是說，SRP 是封裝的昇華：不只把資料藏起來，更要讓「職責」成為劃分類別邊界的判斷依據。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="改變的理由的含義"&gt;「改變的理由」的含義&lt;/h1&gt;
&lt;p&gt;Uncle Bob 在後來的詮釋中進一步說明：一個模組應該對一個且只對一個 actor（行為者） 負責。&lt;/p&gt;
&lt;p&gt;這裡的 actor 不是指使用者介面上的角色，而是指會要求這個模組發生改變的利害關係人（stakeholder），可能是一個部門、一個業務單位、一個外部系統，或任何一個有能力提出需求變更的人或團隊。&lt;/p&gt;
&lt;p&gt;用一個具體例子來說明：假設有一個 &lt;code&gt;Employee&lt;/code&gt; 類別，它提供三個方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CalculatePay()&lt;/code&gt;：計算薪資，財務部門關心這個邏輯。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ReportHours()&lt;/code&gt;：回報工時，HR 部門關心這個邏輯。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Save()&lt;/code&gt;：儲存員工資料，DBA 或資料庫團隊關心這個邏輯。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;三個方法，三個不同的 actor；財務部門可能要求調整薪資計算的演算法，HR 部門可能要求修改工時的統計方式，資料庫團隊可能要求更換 ORM 框架或調整儲存格式；這三類需求彼此獨立，但因為它們全都落在同一個 &lt;code&gt;Employee&lt;/code&gt; 類別裡，任何一方的修改都有可能意外影響其他兩方，即使改動本身只涉及其中一個方法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;這就是「有多個改變的理由」的意涵：一個類別因為服務多個 actor，所以有多個獨立的、互不相關的原因可能促使它發生改變。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="程式碼範例"&gt;程式碼範例&lt;/h1&gt;
&lt;h2 id="違反-srp三種職責擠在一個類別"&gt;違反 SRP：三種職責擠在一個類別&lt;/h2&gt;
&lt;p&gt;以下是一個很常見的反模式，在實際專案中幾乎隨處可見：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 1&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;class&lt;/span&gt; &lt;span style="color:#f0883e;font-weight:bold"&gt;ReportService&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 2&lt;/span&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 3&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;private&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;readonly&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;string&lt;/span&gt; _connectionString;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 4&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 5&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; ReportService(&lt;span style="color:#ff7b72"&gt;string&lt;/span&gt; connectionString)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 6&lt;/span&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 7&lt;/span&gt;&lt;span&gt; _connectionString = connectionString;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 8&lt;/span&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 9&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;10&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// 職責一：取得業務資料（屬於業務邏輯層）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;11&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// actor：業務分析師、後端工程師&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;12&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; List&amp;lt;Order&amp;gt; GetOrders()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;13&lt;/span&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;14&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;using&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;var&lt;/span&gt; conn = &lt;span style="color:#ff7b72"&gt;new&lt;/span&gt; SqlConnection(_connectionString);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;15&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// 查詢並回傳訂單清單...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;16&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;return&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;new&lt;/span&gt; List&amp;lt;Order&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;17&lt;/span&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;18&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;19&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// 職責二：格式化報表（屬於呈現層）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;20&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// actor：前端工程師、設計師&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;21&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;string&lt;/span&gt; FormatAsHtml(List&amp;lt;Order&amp;gt; orders)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;22&lt;/span&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;23&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;var&lt;/span&gt; sb = &lt;span style="color:#ff7b72"&gt;new&lt;/span&gt; StringBuilder();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;24&lt;/span&gt;&lt;span&gt; sb.Append(&lt;span style="color:#a5d6ff"&gt;&amp;#34;&amp;lt;table&amp;gt;&amp;lt;thead&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;ID&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Total&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;/thead&amp;gt;&amp;lt;tbody&amp;gt;&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;25&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;foreach&lt;/span&gt; (&lt;span style="color:#ff7b72"&gt;var&lt;/span&gt; o &lt;span style="color:#ff7b72"&gt;in&lt;/span&gt; orders)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;26&lt;/span&gt;&lt;span&gt; sb.Append(&lt;span style="color:#a5d6ff"&gt;$&amp;#34;&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;{o.Id}&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;{o.Total:C}&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;27&lt;/span&gt;&lt;span&gt; sb.Append(&lt;span style="color:#a5d6ff"&gt;&amp;#34;&amp;lt;/tbody&amp;gt;&amp;lt;/table&amp;gt;&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;28&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;return&lt;/span&gt; sb.ToString();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;29&lt;/span&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;30&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;31&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// 職責三：傳送通知（屬於基礎設施層）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;32&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// actor：維運團隊、產品經理&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;33&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;void&lt;/span&gt; SendByEmail(&lt;span style="color:#ff7b72"&gt;string&lt;/span&gt; recipient, &lt;span style="color:#ff7b72"&gt;string&lt;/span&gt; content)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;34&lt;/span&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;35&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;var&lt;/span&gt; client = &lt;span style="color:#ff7b72"&gt;new&lt;/span&gt; SmtpClient(&lt;span style="color:#a5d6ff"&gt;&amp;#34;smtp.example.com&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;36&lt;/span&gt;&lt;span&gt; client.Send(&lt;span style="color:#a5d6ff"&gt;&amp;#34;noreply@example.com&amp;#34;&lt;/span&gt;, recipient, &lt;span style="color:#a5d6ff"&gt;&amp;#34;Weekly Report&amp;#34;&lt;/span&gt;, content);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;37&lt;/span&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;38&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這個設計的問題不只是「感覺不太對」，而是會帶來非常具體的麻煩：&lt;/p&gt;</description></item><item><title>什麼是 SOLID？與 OOP 的根源</title><link>https://kaiadv.com/posts/20260529/</link><pubDate>Fri, 29 May 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260529/</guid><description>&lt;p&gt;許多工程師都有過類似的經驗：接手一份「能跑就好」的舊專案，或只是想改一個小功能，卻發現牽一髮動全身，改了 A 壞了 B，修了 B 又壞了 C；幾輪下來，程式碼愈來愈難以理解，沒有人敢輕易動它。&lt;/p&gt;
&lt;p&gt;這種現象有個名字，叫做程式碼腐化（Software Rot）；它的成因不是因為工程師不夠努力，也不是因為程式語言本身的限制，而是因為程式碼在設計上缺乏清晰的邊界與結構，隨著時間累積，複雜度像滾雪球一樣不斷擴大。&lt;/p&gt;
&lt;p&gt;程式碼腐化常見的三種症狀：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fragility（脆弱性）&lt;/strong&gt;：系統在意想不到的地方發生錯誤；改了一處邏輯之後，看似無關的另一個模組卻開始出錯，原因是它們之間存在隱藏的耦合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rigidity（僵化性）&lt;/strong&gt;：每一個改動都需要修改大量的地方，工程師必須追蹤一長串的連鎖反應，導致任何變更的成本都極高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Immobility（不動性）&lt;/strong&gt;：某段邏輯明明可以在其他地方複用，卻因為它與太多其他元件糾纏在一起，根本無法單獨抽出來。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SOLID 就是為了系統性地對抗這三種症狀而誕生的。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="solid-的由來"&gt;SOLID 的由來&lt;/h1&gt;
&lt;p&gt;Robert C. Martin（人稱 Uncle Bob）是《Clean Code》與《Clean Architecture》的作者，也是敏捷宣言的共同簽署人之一，他在 2000 年代初期，將多年在軟體工程領域的觀察與實踐整理成文，提出了五個核心設計原則。&lt;/p&gt;
&lt;p&gt;這五個原則各自在過去幾十年間分散出現於不同的學術論文與工程討論中，Uncle Bob 的貢獻在於將它們系統化，並賦予一致的框架；之後 Michael Feathers（《Working Effectively with Legacy Code》的作者）發現這五個原則的英文首字母恰好組成「SOLID」，這個縮寫從此沿用至今。&lt;/p&gt;
&lt;p&gt;五個原則各自對應的英文全名與中文名稱：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;S&lt;/strong&gt; — Single-Responsibility Principle：單一職責原則&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;O&lt;/strong&gt; — Open-Closed Principle：開放封閉原則&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;L&lt;/strong&gt; — Liskov Substitution Principle：里氏替換原則&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I&lt;/strong&gt; — Interface Segregation Principle：介面隔離原則&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;D&lt;/strong&gt; — Dependency Inversion Principle：依賴反轉原則&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="oop-的四個基石以及它們的局限"&gt;OOP 的四個基石，以及它們的局限&lt;/h1&gt;
&lt;p&gt;物件導向程式設計（OOP）提供了四個強大的工具：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;封裝（Encapsulation）&lt;/strong&gt;：將資料與操作資料的方法包裝在一起，隱藏實作細節，只對外暴露必要的介面；這讓模組之間的邊界更清楚，也保護了資料不被任意存取。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;繼承（Inheritance）&lt;/strong&gt;：讓子類別繼承父類別的屬性與行為，實現程式碼的複用；它也建立了「is-a」的型別關係，讓一個子類別的物件可以被當作父類別使用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多型（Polymorphism）&lt;/strong&gt;：同一個介面或方法名稱，可以在不同的類別中有不同的實作；呼叫端只需要知道「這個物件能做什麼」，不需要知道「它是怎麼做的」，讓程式碼更具彈性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抽象（Abstraction）&lt;/strong&gt;：透過介面（Interface）或抽象類別（Abstract Class），只定義行為的「輪廓」，而不指定實作，這讓高階的業務邏輯可以與低階的實作細節解耦。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然而這四個工具只是手段，本身並不保證良好的設計；一個大量使用繼承、卻讓子類別行為不一致的系統，可能比不用繼承更糟糕；一個把所有邏輯封裝在同一個「萬能類別」裡的設計，只是用封装帶來更大的隱患；OOP 告訴你可以做什麼，SOLID 告訴你應該怎麼做。&lt;/p&gt;</description></item><item><title>Cursor 可以怎麼改善效能？從自我參照到集合運算的幾種改善方式</title><link>https://kaiadv.com/posts/20260525/</link><pubDate>Mon, 25 May 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260525/</guid><description>&lt;p&gt;上篇提到，一段看起來沒有問題的 Cursor，因為同時「從同一張表讀資料，又寫回同一張表」，最後讓資料量一路膨脹，效能也跟著快速惡化；理解原因之後，接下來要探討的是：&lt;strong&gt;如果接手這樣的問題，該怎麼調整或修改&lt;/strong&gt;？&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="可以怎麼調整"&gt;可以怎麼調整？&lt;/h1&gt;
&lt;h2 id="拆出獨立的來源表把自我參照分開"&gt;拆出獨立的「來源表」，把自我參照分開&lt;/h2&gt;
&lt;p&gt;修改幅度最小的做法；把母體先複製到另一張暫存表，Cursor 從來源表讀、寫入目標表，兩者不會混在一起：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 1&lt;/span&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;-- 先把「會員母體」獨立出來
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 2&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;SELECT&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;DISTINCT&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;member_id,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;member_name,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;member_level&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 3&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;INTO&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;#&lt;/span&gt;member_source&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 4&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;FROM&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;#&lt;/span&gt;member_base;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 5&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 6&lt;/span&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;-- 清空目標表（如果需要的話）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 7&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;TRUNCATE&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;TABLE&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;#&lt;/span&gt;member_base;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 8&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 9&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;DECLARE&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;month_cursor&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;CURSOR&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;FOR&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;SELECT&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;month_id&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;FROM&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;dim_month;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;10&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;OPEN&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;month_cursor;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;11&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;FETCH&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;NEXT&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;FROM&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;month_cursor&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;INTO&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;@&lt;/span&gt;month_id;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;12&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;13&lt;/span&gt;&lt;span&gt;WHILE&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;@@&lt;/span&gt;FETCH_STATUS&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;14&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;BEGIN&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;15&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;INSERT&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;INTO&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;#&lt;/span&gt;member_base&lt;span style="color:#6e7681"&gt; &lt;/span&gt;(member_id,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;member_name,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;member_level,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;month_id,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;amount)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;16&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;SELECT&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;member_id,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;member_name,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;member_level,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;@&lt;/span&gt;month_id,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;17&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;FROM&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;#&lt;/span&gt;member_source;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;-- 來源固定，不會膨脹
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;18&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;19&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;FETCH&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;NEXT&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;FROM&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;month_cursor&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;INTO&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;@&lt;/span&gt;month_id;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;20&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;END&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;21&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;22&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;CLOSE&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;month_cursor;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;23&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;DEALLOCATE&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;month_cursor;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;24&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;DROP&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;TABLE&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;#&lt;/span&gt;member_source;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這個改法只處理了「資料爆炸」的部分，Cursor 本身的效能成本還在；不過如果業務邏輯比較複雜、確實需要逐筆處理，這算是相對安全的最小改動。&lt;/p&gt;</description></item><item><title>一段「看起來會跑」的 SQL，為什麼跑起來這麼慢？聊聊 Cursor 的自我參照</title><link>https://kaiadv.com/posts/20260524/</link><pubDate>Sun, 24 May 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260524/</guid><description>&lt;p&gt;最近在看一段比較舊的 T-SQL 程式碼，遇到一個還算滿常見的效能議題：用 Cursor 逐筆寫入時，從同一張暫存表讀資料、又寫回去同一張表；乍看之下邏輯沒問題、結果也正確，但只要資料量稍微一多，整段 SQL 就會跑得非常慢；這篇筆記想整理一下背後的原理，以及為什麼這種寫法在 T-SQL 中通常不太建議。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="簡單情境"&gt;簡單情境&lt;/h1&gt;
&lt;p&gt;用一個跟業務無關的例子來說明，假設有一個「會員 × 月份的消費紀錄初始化」的需求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有一張 &lt;code&gt;#member_base&lt;/code&gt;，裡面是會員基本資料（會員編號、姓名、等級）。&lt;/li&gt;
&lt;li&gt;有 12 個月份，需要幫每個會員產生 12 筆「初始為 0」的消費紀錄。&lt;/li&gt;
&lt;li&gt;預期結果：會員數 × 12 筆資料。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一個比較直覺、但其實會有問題的寫法可能會長這樣：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 1&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;DECLARE&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;month_cursor&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;CURSOR&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;FOR&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 2&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;SELECT&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;month_id&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;FROM&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;dim_month;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;-- 1 ~ 12 月
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 3&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 4&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;OPEN&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;month_cursor;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 5&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;FETCH&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;NEXT&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;FROM&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;month_cursor&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;INTO&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;@&lt;/span&gt;month_id;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 6&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 7&lt;/span&gt;&lt;span&gt;WHILE&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;@@&lt;/span&gt;FETCH_STATUS&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 8&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;BEGIN&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 9&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;INSERT&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;INTO&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;#&lt;/span&gt;member_base&lt;span style="color:#6e7681"&gt; &lt;/span&gt;(member_id,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;member_name,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;member_level,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;month_id,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;amount)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;10&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;SELECT&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;DISTINCT&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;member_id,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;member_name,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;member_level,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;@&lt;/span&gt;month_id,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;11&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;FROM&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;#&lt;/span&gt;member_base;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;-- 注意：從 #member_base 讀，又寫回 #member_base
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;12&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;13&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;FETCH&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;NEXT&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;FROM&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;month_cursor&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;INTO&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;@&lt;/span&gt;month_id;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;14&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;END&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;直覺上看起來是：「就是跑 12 圈，每圈把所有會員複製一份、補上月份而已。」但實際跑起來，資料量會以&lt;strong&gt;指數的速度成長&lt;/strong&gt;。&lt;/p&gt;</description></item><item><title>Index 建立的時機：先建還是後建？</title><link>https://kaiadv.com/posts/20260523/</link><pubDate>Sat, 23 May 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260523/</guid><description>&lt;p&gt;前陣子在跟同事討論一段 SP（Stored Procedure）的效能問題，話題漸漸轉到了 Table 建立時的設計策略。&lt;/p&gt;
&lt;p&gt;同事的想法是這樣的：「建 Table 的時候就把欄位結構跟 Index 都設定好，之後 Insert 資料就一步到位，感覺比較有效率。」&lt;/p&gt;
&lt;p&gt;而我的直覺剛好相反：「不對吧，Index 本身會影響 Insert 的速度，應該是先把資料塞完，最後再建 Index？」&lt;/p&gt;
&lt;p&gt;兩個人都有點不確定，但認知方向剛好相反，就決定一起查清楚；結果，我的印象是對的，但要加一些但書。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="為什麼-index-會讓-insert-變慢"&gt;為什麼 Index 會讓 Insert 變慢？&lt;/h1&gt;
&lt;p&gt;要解釋這件事，得先聊聊資料庫最常用的 Index 結構：B-Tree。&lt;/p&gt;
&lt;h2 id="b-tree-是什麼"&gt;B-Tree 是什麼？&lt;/h2&gt;
&lt;p&gt;B-Tree 是大多數關聯式資料庫（PostgreSQL、MySQL、SQL Server）預設使用的 Index 結構，它長這樣：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; [50]
/ \
[25] [75]
/ \ / \
[10] [40] [60] [90]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;重點有三個：&lt;/p&gt;
&lt;ul&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;li&gt;&lt;strong&gt;查詢速度快&lt;/strong&gt;：找一筆資料需要 &lt;code&gt;O(log n)&lt;/code&gt; 次比較。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因為 B-Tree 要維持排序與平衡的特性，每次 Insert 一筆資料，都必須同時更新 B-Tree 的結構。&lt;/p&gt;
&lt;h2 id="insert-時到底發生什麼事"&gt;Insert 時到底發生什麼事？&lt;/h2&gt;
&lt;p&gt;每當 &lt;code&gt;INSERT&lt;/code&gt; 一筆新資料，資料庫不只是把那筆資料寫進去而已，它還要作以下這些事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;找到這筆資料在 B-Tree 中對應的位置。&lt;/li&gt;
&lt;li&gt;把新資料插入正確的節點。&lt;/li&gt;
&lt;li&gt;如果節點滿了，觸發「節點分裂（node split）」，重新調整樹的結構。&lt;/li&gt;
&lt;li&gt;必要時，更新上層節點的資訊。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你有 3 個 Index，就得重複這個過程 3 次；資料量越大，這個效能開銷就越明顯。&lt;/p&gt;</description></item><item><title>SQL Server 效能調校之五：從發現問題到解決問題的步驟</title><link>https://kaiadv.com/posts/20260519/</link><pubDate>Tue, 19 May 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260519/</guid><description>&lt;p&gt;經過前面四篇文章，能夠知道 Execution Plan 的四大核心：閱讀方向（粗細箭頭）、存取方式（Seek 與 Scan）、關聯策略（三大 Join），以及警告標誌（四大陷阱）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kaiadv.com/posts/20260515/"&gt;SQL Server 效能調校之一：不迷路的導航，Execution Plan 的閱讀方向與指標&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kaiadv.com/posts/20260516/"&gt;SQL Server 效能調校之二：看懂 Index Seek、Scan 與 Key Loop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kaiadv.com/posts/20260517/"&gt;SQL Server 效能調校之三：三大 Join 運算子解密&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kaiadv.com/posts/20260518/"&gt;SQL Server 效能調校之四：四個常見陷阱解析&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然而，當正式面對一張包含幾十個、甚至上百個節點的巨大執行計畫時，很容易再度感到不知所措；這時候，需要的是一套系統化的除錯 SOP；效能調校的心法「一次只動一個地方，並且永遠以數據驗證差異。」以下是從一團亂麻中找出解方的步驟：&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="步驟一鎖定最貴的目標-find-the-most-expensive"&gt;步驟一：鎖定「最貴」的目標 (Find the Most Expensive)&lt;/h1&gt;
&lt;p&gt;面對龐大的執行計畫，不要試圖由右至左把每個節點都看懂，需要「擒賊先擒王」。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;尋找 Cost % 最高的節點&lt;/strong&gt;：每個節點下方都會標示該步驟佔整句查詢成本的百分比；直接掃視全圖，把目光鎖定在那些標示 40%、60% 甚至 90% 的高成本節點上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;追蹤「最粗的箭頭」&lt;/strong&gt;：尋找哪兩個節點之間傳遞了異常龐大的資料量，特別是「漏斗效應」；如果一個節點右邊進來很粗的箭頭，左邊出去卻變得很細，這代表它浪費了大量資源在過濾不必要的資料。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;限縮範圍，挑出 1 到 2 個效能最差的局部節點作為第一階段的開刀對象，不要急著對整句 SQL 做全域（Global）的改寫。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="步驟二辨識症狀與警告-identify-symptoms"&gt;步驟二：辨識症狀與警告 (Identify Symptoms)&lt;/h1&gt;
&lt;p&gt;鎖定可疑節點後，把滑鼠懸停（Hover）在該節點上，開始進行「健康檢查」：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;有沒有黃色驚嘆號&lt;/strong&gt;：如果是 &lt;code&gt;CONVERT_IMPLICIT&lt;/code&gt;，就去檢查程式端參數型別是不是和資料庫不符（尤其是字串型別）；如果是 Spill to TempDB，先去檢查統計資料是否過期。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;它是怎麼存取資料的&lt;/strong&gt;：如果是 Table Scan 或 Clustered Index Scan，檢查 &lt;code&gt;WHERE&lt;/code&gt; 條件欄位是不是漏建了索引，或者條件寫法讓索引失效了（例如在欄位上套用函數）；如果是 Index Seek，點開屬性檢查有沒有發生了「殘餘篩選條件（Residual Predicate）」，導致讀取了一堆資料卻被拋棄。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有沒有跟著 Key Lookup&lt;/strong&gt;：如果高成本節點是 Key Lookup，去看看 &lt;code&gt;SELECT&lt;/code&gt; 了哪些多餘的欄位，或者把它們加入現有索引的 &lt;code&gt;INCLUDE&lt;/code&gt; 清單中。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="步驟三戳破-optimizer-的幻覺-check-row-estimates"&gt;步驟三：戳破 Optimizer 的幻覺 (Check Row Estimates)&lt;/h1&gt;
&lt;p&gt;很多時候，SQL Server 選了極差的執行計畫（例如：不該用 Hash Match 卻用了），是因為它「猜錯了資料量」：&lt;/p&gt;</description></item><item><title>SQL Server 效能調校之四：四個常見陷阱解析</title><link>https://kaiadv.com/posts/20260518/</link><pubDate>Mon, 18 May 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260518/</guid><description>&lt;p&gt;在調校 SQL Server 查詢效能時，有些問題容易被忽略，卻也容易造成嚴重的效能退化：Missing Index、Implicit Conversion、Spill to TempDB、以及 Residual Predicate；本篇整理這四種問題的成因、症狀與修正方式，並說明彼此之間容易混淆的關鍵差異。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="missing-index缺少索引"&gt;Missing Index（缺少索引）&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;成因&lt;/strong&gt;：查詢的篩選欄位（&lt;code&gt;WHERE&lt;/code&gt;、&lt;code&gt;JOIN&lt;/code&gt; 條件）未建立對應索引，導致 SQL Server 必須掃描整張資料表（Table Scan 或 Clustered Index Scan）才能找到符合的資料列。&lt;/li&gt;
&lt;li&gt;症狀：
&lt;ul&gt;
&lt;li&gt;執行計畫出現 Table Scan 或 Clustered Index Scan 節點。&lt;/li&gt;
&lt;li&gt;Logical reads 數量遠超預期。&lt;/li&gt;
&lt;li&gt;CPU 與磁碟 IO 長期偏高。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;偵測方式&lt;/strong&gt;：執行計畫中會出現綠色的「遺漏索引」提示文字；亦可查詢 &lt;code&gt;sys.dm_db_missing_index_details&lt;/code&gt; 取得建議清單。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修正建議&lt;/strong&gt;：針對高頻查詢的篩選欄位建立索引，並善用 &lt;code&gt;INCLUDE&lt;/code&gt; 子句將查詢所需的非索引鍵欄位一併納入，形成 Covering Index，避免額外的 Key Lookup。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;1&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;CREATE&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;INDEX&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;IX_Orders_CustDate&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;2&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;ON&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;Orders&lt;span style="color:#6e7681"&gt; &lt;/span&gt;(CustomerID,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;OrderDate)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;3&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;INCLUDE&lt;span style="color:#6e7681"&gt; &lt;/span&gt;(TotalAmount);&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h1 id="implicit-conversion隱含型別轉換"&gt;Implicit Conversion（隱含型別轉換）&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;成因&lt;/strong&gt;：當查詢參數或比較值的資料型別與欄位型別不符時，SQL Server 會自動進行隱含型別轉換；轉換作業發生在欄位端，使得索引的 Seek 能力喪失，退化為 Scan。&lt;/li&gt;
&lt;li&gt;症狀：
&lt;ul&gt;
&lt;li&gt;明明已建立索引，執行計畫仍顯示 Index Scan 而非 Index Seek。&lt;/li&gt;
&lt;li&gt;執行計畫節點出現黃色警告，標示 &lt;code&gt;CONVERT_IMPLICIT&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;偵測方式&lt;/strong&gt;：檢查執行計畫節點上的警告圖示，或透過 &lt;code&gt;SET STATISTICS IO ON&lt;/code&gt; 觀察 Logical reads 是否異常偏高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修正建議&lt;/strong&gt;：確保應用程式傳入的參數型別與資料表欄位型別完全一致；使用 ORM 框架時須特別注意 &lt;code&gt;nvarchar&lt;/code&gt; 與 &lt;code&gt;varchar&lt;/code&gt; 的混用，以及資料庫 Collation 設定是否統一。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;1&lt;/span&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;-- 錯誤：欄位為 int，傳入字串導致隱含轉換
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;2&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;WHERE&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;CustomerID&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#39;12345&amp;#39;&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;3&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;4&lt;/span&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;-- 正確：型別一致，索引可正常 Seek
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;5&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;WHERE&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;CustomerID&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;12345&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h1 id="spill-to-tempdb記憶體溢出至暫存資料庫"&gt;Spill to TempDB（記憶體溢出至暫存資料庫）&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;成因&lt;/strong&gt;：執行 Sort、Hash Match、或 Window Function 等需要工作記憶體的運算時，若 SQL Server 低估了資料量（通常源自過期的統計資料），分配的 Memory Grant 不足，多餘的中間資料便會溢出（Spill）至 TempDB 磁碟。&lt;/li&gt;
&lt;li&gt;症狀：
&lt;ul&gt;
&lt;li&gt;執行計畫的 Sort 或 Hash Match 節點出現警告圖示。&lt;/li&gt;
&lt;li&gt;TempDB 磁碟 IO 明顯飆升。&lt;/li&gt;
&lt;li&gt;相同查詢的執行時間不穩定、變異大。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;偵測方式&lt;/strong&gt;：查看執行計畫節點屬性中的 Warning &amp;gt; SpillLevel；或查詢 &lt;code&gt;sys.dm_exec_query_stats&lt;/code&gt; 的 &lt;code&gt;total_spills&lt;/code&gt; 欄位找出高溢出查詢。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修正建議&lt;/strong&gt;：這個問題通常是統計資料過舊，建議定期執行 &lt;code&gt;UPDATE STATISTICS&lt;/code&gt;；此外，可以透過建立索引讓資料預先排序，減少 Sort 運算的需求，或將大量資料的操作拆分成批次處理。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;1&lt;/span&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;-- 更新統計資料
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;2&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;UPDATE&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;STATISTICS&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;Orders&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;WITH&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;FULLSCAN;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;3&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;4&lt;/span&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;-- 建立索引預先排序，減少 Sort 需求
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;5&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;CREATE&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;INDEX&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;IX_Orders_Date&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;6&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;ON&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;Orders&lt;span style="color:#6e7681"&gt; &lt;/span&gt;(OrderDate);&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h1 id="residual-predicate殘餘篩選條件"&gt;Residual Predicate（殘餘篩選條件）&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;成因&lt;/strong&gt;：Index Seek 只能用索引的前導鍵欄位（Seek Predicate）快速定位資料範圍，查詢條件中不屬於前導鍵的欄位，會在 Storage Engine 層逐列再次過濾（Predicate），稱為殘餘謂詞（Residual Predicate）。&lt;/li&gt;
&lt;li&gt;症狀：
&lt;ul&gt;
&lt;li&gt;執行計畫中有 Index Seek，表面上看起來正常。&lt;/li&gt;
&lt;li&gt;Seek 節點的 Rows Read 遠大於 Actual Rows，顯示大量資料列被讀取後又被篩掉。&lt;/li&gt;
&lt;li&gt;Logical reads 偏高但不易察覺。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;偵測方式&lt;/strong&gt;：點開執行計畫中 Index Seek 節點的屬性，區分 Seek Predicates（有效利用索引）與 Predicates（殘餘過濾）兩個欄位的內容。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修正建議&lt;/strong&gt;：將殘餘條件欄位加入複合索引的鍵欄位，或納入 &lt;code&gt;INCLUDE&lt;/code&gt; 清單，調整索引欄位順序以符合查詢的過濾邏輯。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;1&lt;/span&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;-- 原索引只有 OrderDate, Status 成為殘餘條件
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;2&lt;/span&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;-- 調整為複合索引以消除殘餘條件
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;3&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;CREATE&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;INDEX&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;IX_Orders_DateStatus&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;4&lt;/span&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;ON&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;Orders&lt;span style="color:#6e7681"&gt; &lt;/span&gt;(OrderDate,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;Status);&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h1 id="容易混淆的差異"&gt;容易混淆的差異&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Missing Index vs Residual Predicate&lt;/strong&gt;：前者是「完全沒有索引」，後者是「有索引但 Seek 只用了部分欄位」；看到執行計畫有 Index Seek 不代表沒有問題，需要進一步比對 Rows Read 與 Actual Rows 的差距。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Implicit Conversion&lt;/strong&gt;：它會靜默破壞現有索引，讓原本正常的 Seek 退化成 Scan，執行計畫只會出現一個小黃警告，極易被忽略；ORM 框架是最常見的來源，尤其是字串型別混用或 Collation 不一致的情境。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spill to TempDB&lt;/strong&gt;：與其他三者性質不同，它不是索引問題，而是「查詢工作記憶體不足」的問題；根本原因通常是統計資料過舊，光是建立索引並無法解決，必須從統計資料更新與查詢設計兩方面著手。&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>SQL Server 效能調校之三：三大 Join 運算子解密</title><link>https://kaiadv.com/posts/20260517/</link><pubDate>Sun, 17 May 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260517/</guid><description>&lt;p&gt;延續上篇，當我們確保了資料存取的效率（消滅了不必要的 Scan 與 Key Lookup）後，接下來要面對的就是關聯式資料庫最核心的動作：將多張資料表結合在一起（Join）。&lt;/p&gt;
&lt;p&gt;當你在 SQL 語法中寫下 &lt;code&gt;INNER JOIN&lt;/code&gt; 或 &lt;code&gt;LEFT JOIN&lt;/code&gt; 時，SQL Server 的 Query Optimizer（查詢最佳化程式）會根據資料表的大小、有沒有索引、以及資料是否已經排序，自動從武器庫中挑選最適合的實體運算子；在執行計畫中，一定會遇到以下這三位主角：&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="精緻的雙層迴圈nested-loops-巢狀迴圈"&gt;精緻的雙層迴圈：Nested Loops (巢狀迴圈)&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;運作原理&lt;/strong&gt;：概念上就像是寫程式時的兩層 for 迴圈；SQL Server 會將資料量較小的表作為外部表（Outer Table），針對外部表的「每一列」，逐一去掃描內部表（Inner Table）尋找匹配的資料。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;適合情境&lt;/strong&gt;：小表 Join 大表，且大表的 Join 欄位上有索引；當內部表有索引時，SQL Server 可以直接使用高效的 Index Seek 來尋找目標，效能極好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;效能警訊&lt;/strong&gt;：如果兩張表都很大，且內部表沒有索引（被迫變成 Table Scan），那麼時間複雜度 &lt;code&gt;O(N X M)&lt;/code&gt; 會讓效能急速惡化。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="暴力卻有效的碰撞hash-match-雜湊比對"&gt;暴力卻有效的碰撞：Hash Match (雜湊比對)&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;運作原理：&lt;/strong&gt; 處理過程分為兩個階段。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Build（建立）階段&lt;/strong&gt;：掃描較小的表，在記憶體中建立一個 Hash Table（雜湊表）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Probe（探測）階段&lt;/strong&gt;：逐一掃描較大的表，將每一列資料套用雜湊函數，去 Hash Table 中快速查詢並配對。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;適合情境&lt;/strong&gt;：兩張表都很大，且沒有合適索引的場合，這時 Query Optimizer 通常會自動選擇 Hash Match 作為最後防線；它的時間複雜度為 &lt;code&gt;O(N + M)&lt;/code&gt;，在處理無索引大數據時效能相對穩定。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;效能警訊&lt;/strong&gt;：最大的致命傷是「記憶體消耗」，如果記憶體不足以容納整個 Hash Table，就會發生 Spill to TempDB（溢出到硬碟）的現象（圖示上會出現黃色驚嘆號Warning），此時讀寫速度會大幅下降，是必須優先解決的瓶頸。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="完美排序的雙劍合璧merge-join-合併連接"&gt;完美排序的雙劍合璧：Merge Join (合併連接)&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;運作原理&lt;/strong&gt;：就像拉鍊一樣的結合方式，前提是兩個輸入資料集都必須已經依照 Join Key 排序完成；執行時，SQL Server 會同時推進兩個指標，依序往下比對，兩邊的資料都只需要掃描一遍即可完成配對。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;適合情境&lt;/strong&gt;：兩表已有排序順序（如有索引，或前置步驟已排序），且資料量大的場合；這是記憶體需求極低、效能非常優異的演算法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;效能警訊&lt;/strong&gt;：若資料本身未經排序，SQL Server 為了硬湊出 Merge Join，必須先在前方加入一個昂貴的 Sort（排序）運算子；這個額外付出的排序成本，往往會抵銷掉 Merge Join 帶來的所有優勢，得不償失。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="優化建議"&gt;優化建議&lt;/h1&gt;
&lt;p&gt;面對這三種 Join，我們該如何除錯與優化呢？&lt;/p&gt;</description></item><item><title>SQL Server 效能調校之二：看懂 Index Seek、Scan 與 Key Loop</title><link>https://kaiadv.com/posts/20260516/</link><pubDate>Sat, 16 May 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260516/</guid><description>&lt;p&gt;在上一篇中，建立了閱讀執行計畫的「方向感」，學會透過箭頭粗細找出塞車路段；順著箭頭一路往右追溯到最源頭，就會看到 SQL Server 是如何進入資料庫「拿資料」，這一步至關重要，因為資料庫系統最大的效能瓶頸往往在於磁碟 I/O（資料讀寫），是在「精準尋找」還是在「盲目翻找」，決定了這個查詢是只要 0.1 秒，還是要跑 10 分鐘。&lt;/p&gt;
&lt;p&gt;在執行計畫的最右側，最常看見的資料存取運算子有這幾種：&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="理想的境界index-seek"&gt;理想的境界：Index Seek&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;白話比喻&lt;/strong&gt;：就像查字典時，利用部首或注音索引，直接翻到你要的那一頁。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;背後意義&lt;/strong&gt;：這是效能最好的存取方式，代表 SQL Server 完美利用了 B-Tree 索引結構，精確定位到符合 &lt;code&gt;WHERE&lt;/code&gt; 條件的資料列；看到這個圖示通常代表你的索引設計順利發揮了作用。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="尷尬的中間地帶index-scan"&gt;尷尬的中間地帶：Index Scan&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;白話比喻&lt;/strong&gt;：不用翻整本字典，但把字典的「整個附錄/整個目錄」從頭到尾看了一遍。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;背後意義&lt;/strong&gt;：看到 &amp;ldquo;Index&amp;rdquo; 這個字或許會覺得令人安心，但請注意後面的 &amp;ldquo;Scan&amp;rdquo;；這代表查詢條件無法讓 SQL Server 縮小搜尋範圍，只好把整個索引從頭到尾掃了一遍。&lt;/li&gt;
&lt;li&gt;優化建議：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;改善查詢條件（避免索引失效）&lt;/strong&gt;：這是最常見的「索引殺手」，例如在欄位上套函數（如 &lt;code&gt;WHERE YEAR(created_at) = 2026&lt;/code&gt;），或發生型別隱含轉換（字串比對數字欄位），可以改成對索引友善的寫法，例如：&lt;code&gt;WHERE created_at &amp;gt;= '2024-01-01' AND created_at &amp;lt; '2025-01-01'&lt;/code&gt;，讓 SQL Server 能切換回 Index Seek。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;建立或調整 Composite Index（複合索引）&lt;/strong&gt;：把常常一起出現在 &lt;code&gt;WHERE&lt;/code&gt;、&lt;code&gt;ORDER BY&lt;/code&gt; 的欄位組合進同一個索引，且要注意欄位順序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;減少回傳欄位&lt;/strong&gt;：盡量避免 &lt;code&gt;SELECT *&lt;/code&gt;，只取真正需要的欄位，有助於讓 Optimizer 選擇更精準的索引。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="效能的紅燈table-scan--clustered-index-scan"&gt;效能的紅燈：Table Scan / Clustered Index Scan&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;白話比喻&lt;/strong&gt;：為了找書裡的一句話，從第一頁逐字逐句讀到最後一頁。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;背後意義&lt;/strong&gt;：這是最需要優先處理的狀況，代表完全沒有可用的索引，或查詢條件根本繞過了索引；SQL Server 必須把整張表掃描一遍，一筆一筆核對。&lt;/li&gt;
&lt;li&gt;優化建議：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;建立索引&lt;/strong&gt;：最直接的解法，可以先參考 Execution Plan 上方黃色字體的 Missing Index 提示來建立 Non-Clustered Index，這通常能立竿見影。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;檢查資料選擇性（Selectivity）&lt;/strong&gt;：如果你的 &lt;code&gt;WHERE&lt;/code&gt; 條件篩出來的資料佔了整張表的 20%–30% 以上，SQL Server 會認為「既然都要抓這麼多資料，不如直接掃整張表比較快」；此時加索引沒用，需要檢討的是查詢邏輯本身。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更新統計資料&lt;/strong&gt;：執行 &lt;code&gt;UPDATE STATISTICS&lt;/code&gt; 或 &lt;code&gt;sp_updatestats&lt;/code&gt;，有時候是資料庫的統計資訊太舊，導致 Optimizer 誤判，放著好好的索引不用跑去掃表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;檢查 &lt;code&gt;NOLOCK&lt;/code&gt; 或 Hint 干擾&lt;/strong&gt;：確認程式碼中是否有不必要的查詢提示（Hint）強制改變了 SQL Server 的預設行為。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="隱藏的效能殺手key-lookup"&gt;隱藏的效能殺手：Key Lookup&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;白話比喻&lt;/strong&gt;：用目錄找到了頁碼，但發現那頁只有大綱，還得跑去另一本詳細版的手冊裡翻出完整內容。&lt;/p&gt;</description></item><item><title>SQL Server 效能調校之一：不迷路的導航，Execution Plan 的閱讀方向與指標</title><link>https://kaiadv.com/posts/20260515/</link><pubDate>Fri, 15 May 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260515/</guid><description>&lt;p&gt;在開發與維護資料庫的日常中，會遇到這樣的場景，一段 SQL 查詢平常跑得順順的，卻突然卡住，或者明明加了 &lt;code&gt;WHERE&lt;/code&gt; 條件，資料庫卻慢到讓人想砸電腦。&lt;/p&gt;
&lt;p&gt;面對效能瓶頸，可以打開 SQL Server 提供的導航地圖「執行計畫」（Execution Plan），執行計畫是 SQL Server 查詢最佳化工具（Query Optimizer）的詳細報告；然而，初次見到這張充滿各種圖示與線條的圖表時，往往是讓人眼花撩亂，不知所措。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="閱讀方向從右到左從上到下"&gt;閱讀方向：從右到左、從上到下&lt;/h1&gt;
&lt;p&gt;在解讀 SQL Server 的圖形化執行計畫時，閱讀順序是：從右到左、從上到下。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;最右邊的節點（起點）&lt;/strong&gt;：代表「資料的來源」；這裡通常是實體資料表（Tables）或是索引（Indexes）。這是 SQL Server 第一步要去硬碟或記憶體裡把基礎資料撈出來的地方。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中間的節點（過程）&lt;/strong&gt;：每個圖示都代表一個「處理步驟」（Operator），例如：關聯（Join）、排序（Sort）、過濾（Filter）或群組化（Aggregate）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最左邊的節點（終點）&lt;/strong&gt;：代表「最終的輸出」；通常會是一個 &lt;code&gt;SELECT&lt;/code&gt;、&lt;code&gt;INSERT&lt;/code&gt;、&lt;code&gt;UPDATE&lt;/code&gt; 或 &lt;code&gt;DELETE&lt;/code&gt; 的圖示，表示這段查詢最後呈現給應用程式的結果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;從上到下&lt;/strong&gt;：當一個步驟需要結合多個資料來源（例如 &lt;code&gt;JOIN&lt;/code&gt; 兩張表）時，通常在圖形上方的分支會先被執行或作為外部輸入（Outer Input），下方的分支則作為內部輸入（Inner Input）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下次打開 Plan 時，將目光移到畫面的最右端，看看 SQL Server 是從哪些表開始動手的而非一開始就盯著最左邊的 &lt;code&gt;SELECT&lt;/code&gt; 看。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="解讀箭頭的粗細"&gt;解讀箭頭的「粗細」&lt;/h1&gt;
&lt;p&gt;在各個節點之間，會看到許多連接的箭頭；這些箭頭不僅僅是指出&lt;strong&gt;資料流動的方向&lt;/strong&gt;，它們還隱藏著極其重要的效能指標，箭頭的粗細，代表著傳遞的資料列數（Rows）多寡。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;極細的箭頭&lt;/strong&gt;：代表經過這個步驟後，只傳遞了非常少量的資料（甚至只有 1 筆）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;非常粗的箭頭&lt;/strong&gt;：代表這裡有龐大的資料量正在兩個節點之間搬運。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;掌握了箭頭粗細的含義，就能在幾秒鐘內靠「視覺」找出潛在的問題點：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;漏斗效應（粗進細出）&lt;/strong&gt;：如果看到一個節點，右邊進來的箭頭非常粗，但左邊出去的箭頭突然變得很細；這代表 SQL Server 在這個步驟（可能是一個 Filter 或 Hash Match）花費了巨大的力氣，過濾掉成千上萬筆不符合條件的資料，最後只留下幾筆，這通常是強烈的優化訊號，告訴我們「資料需要從源頭就開始進行篩選，而非把一堆資料搬進記憶體裡慢慢濾」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一路粗到底&lt;/strong&gt;：如果從最右邊到最左邊，箭頭始終粗得像水管一樣，這意味著你的查詢確實要回傳大量資料；這時你可以問應用程式端：「我們真的需要一次 &lt;code&gt;SELECT&lt;/code&gt; 幾百萬筆資料出來嗎？能不能做分頁處理？」&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;閱讀 Execution Plan 最重要，就是建立「方向感」，只要掌握了正確的閱讀方向，就不會在複雜的 Plan 裡迷失；從「由右至左找源頭」以及「看箭頭粗細找瓶頸」這兩個導航法則，就能在面對那張複雜的網路圖時，擁有清晰的解讀脈絡，而當我們知道資料是從哪裡來，又在哪裡塞車之後，下一步就是要檢視 SQL Server 到實體表中「拿資料」的手法是否夠聰明了。&lt;/p&gt;</description></item><item><title>從 WinForms 到 Web 之二：前後端分離架構</title><link>https://kaiadv.com/posts/20260510/</link><pubDate>Sun, 10 May 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260510/</guid><description>&lt;p&gt;如果說 MVC 是「後端包辦一切」，那前後端分離就是「後端只管資料，前端只管畫面」；兩者最根本的差異在於渲染發生的位置：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;── MVC ──────────────────────────────────────────────
瀏覽器 ──請求──▶ 後端（組好 HTML）──▶ 瀏覽器（顯示）
── 前後端分離 ────────────────────────────────────────
瀏覽器 ──請求──▶ 後端（回傳 JSON）──▶ 瀏覽器（自行渲染）
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;後端不再負責產生 HTML，它只提供結構化的資料（通常是 JSON）；畫面要長什麼樣子，完全是前端的責任。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="用-aspnet-core-web-api-來說明"&gt;用 ASP.NET Core Web API 來說明&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;後端&lt;/strong&gt;：只負責資料，不碰畫面&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 1&lt;/span&gt;&lt;span&gt;[ApiController]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 2&lt;/span&gt;&lt;span&gt;[Route(&amp;#34;api/[controller]&lt;span style="color:#a5d6ff"&gt;&amp;#34;)]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 3&lt;/span&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;class&lt;/span&gt; &lt;span style="color:#f0883e;font-weight:bold"&gt;ProductsController&lt;/span&gt; : ControllerBase
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 4&lt;/span&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 5&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;private&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;readonly&lt;/span&gt; AppDbContext _db;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 6&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; ProductsController(AppDbContext db) =&amp;gt; _db = db;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 7&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 8&lt;/span&gt;&lt;span&gt; [HttpGet]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt; 9&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;async&lt;/span&gt; Task&amp;lt;IActionResult&amp;gt; GetAll()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;10&lt;/span&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;11&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;var&lt;/span&gt; products = &lt;span style="color:#ff7b72"&gt;await&lt;/span&gt; _db.Products.ToListAsync();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;12&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;return&lt;/span&gt; Ok(products); &lt;span style="color:#8b949e;font-style:italic"&gt;// 純 JSON，沒有 HTML&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;13&lt;/span&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;14&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;15&lt;/span&gt;&lt;span&gt; [HttpPost]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;16&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;public&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;async&lt;/span&gt; Task&amp;lt;IActionResult&amp;gt; Create(ProductDto dto)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;17&lt;/span&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;18&lt;/span&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// 驗證邏輯、商業規則...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;19&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;var&lt;/span&gt; product = &lt;span style="color:#ff7b72"&gt;new&lt;/span&gt; Product { Name = dto.Name, Price = dto.Price };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;20&lt;/span&gt;&lt;span&gt; _db.Products.Add(product);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;21&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;await&lt;/span&gt; _db.SaveChangesAsync();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;22&lt;/span&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;return&lt;/span&gt; CreatedAtAction(nameof(GetAll), &lt;span style="color:#ff7b72"&gt;new&lt;/span&gt; { id = product.Id }, product);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;23&lt;/span&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;24&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;後端回傳的是這樣的 JSON：&lt;/p&gt;</description></item><item><title>從 WinForms 到 Web 之一：認識 MVC 架構</title><link>https://kaiadv.com/posts/20260509/</link><pubDate>Sat, 09 May 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260509/</guid><description>&lt;p&gt;公司有一套已運行多年的內部管理系統，原本是 Windows Forms 應用程式，現在決定整個搬到 Web 平台；新系統起初以 ASP.NET MVC + Razor Page 為主架構，同時也有部分功能採用 Web API 的形式，形成了一種混搭的局面；這讓我開始思考這兩種做法的本質差異是什麼？在什麼情況下應該選哪一種？在討論這個問題之前，得先把 MVC 架構弄清楚。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="mvc-是什麼"&gt;MVC 是什麼？&lt;/h1&gt;
&lt;p&gt;MVC 是一種將應用程式切分成三個層次的架構模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Model&lt;/strong&gt;：應用程式的核心，負責管理資料、業務規則與邏輯，獨立於使用者介面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;View&lt;/strong&gt;：資料的視覺呈現，在 ASP.NET 中就是 Razor Page（&lt;code&gt;.cshtml&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Controller&lt;/strong&gt;：中間協調者，接收使用者輸入、呼叫 Model 處理、再選擇適合的 View 回應。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;這三層緊密協作，最關鍵的特點是：整個渲染過程發生在伺服器端，瀏覽器只負責顯示已經組好的 HTML。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;瀏覽器 ──請求──▶ Controller ──呼叫──▶ Model（業務邏輯 + 資料存取）
│ │
│ ▼
│ DB / Repository
│ │
│◀────── 資料 ───────────┘
│
▼ 傳遞資料
View（Razor Page）
│
瀏覽器 ◀──完整 HTML──┘
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="用-aspnet-core-mvc-來說明"&gt;用 ASP.NET Core MVC 來說明&lt;/h2&gt;
&lt;p&gt;一個典型的 MVC 流程如下：&lt;/p&gt;</description></item><item><title>從模糊通訊到精確修復：如何運用 AI 協作、重構、除錯之開發工作流</title><link>https://kaiadv.com/posts/20260504/</link><pubDate>Mon, 04 May 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260504/</guid><description>&lt;p&gt;在真實的軟體維護與開發場景中，任務往往不是從一份清晰的規格書開始，而是一長串雜亂的商業通訊或 Email；這篇文章將探討如何將 AI 融入日常工作流，從「接收業務抱怨」到「提交安全的程式碼變更」，把 AI 當作技術協作的思考夥伴，而不僅僅是一台代碼生成機；以下是運用 AI 提升遺留系統（Legacy System）維護效率與安全性的協作方法：&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="從商業語言萃取技術問題--issue-intake--hypothesis"&gt;從商業語言萃取技術問題 ( Issue Intake &amp;amp; Hypothesis)&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;輸入來源&lt;/strong&gt;：雜亂的商業通訊記錄、Email 討論串或 PDF。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;協作模式&lt;/strong&gt;：讓 AI 扮演第一線的「需求翻譯官」。
&lt;ul&gt;
&lt;li&gt;可以請 AI 閱讀長篇大論的討論串，從中剝離情緒與表面症狀，找出真正的需求。&lt;/li&gt;
&lt;li&gt;接著，與 AI 討論根本原因的假設「這究竟是基礎設施層級的錯誤、系統整合失敗，還是單純的商業邏輯落差？」，這能有效避免我們在一開始就找錯系統層級，將「修復整合問題」的大方向，收斂為具體可行的技術行動。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="系統勘查與新舊行為比對-reconnaissance--comparison"&gt;系統勘查與新舊行為比對 (Reconnaissance &amp;amp; Comparison)&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;輸入來源&lt;/strong&gt;：舊版系統的執行腳本與現有程式碼架構。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;協作模式&lt;/strong&gt;：在改動任何遺留系統前，釐清「過去的基準是什麼」至關重要。
&lt;ul&gt;
&lt;li&gt;我們可以將舊有的腳本或程式碼作為來源，讓 AI 協助比對現行程式碼中的執行路徑與邏輯斷層，透過比對列出遺失的商業規則（例如：時間視窗的處理、過濾邏輯的差異），而非不斷的對著錯誤代碼做猜測。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="邊界談判與範圍收斂-scope-reduction--constraint-negotiation"&gt;邊界談判與範圍收斂 (Scope Reduction &amp;amp; Constraint Negotiation)&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;輸入來源&lt;/strong&gt;：比對結果與開發的限制及偏好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;協作模式&lt;/strong&gt;：這是實務上最關鍵，卻常被忽略的一步。
&lt;ul&gt;
&lt;li&gt;告訴 AI 你的底線，例如：「不引入新的抽象層」、「不改動共用的核心模組」、「避免不必要的全域設定（Global config）重構」，甚至限制修改的檔案數量。&lt;/li&gt;
&lt;li&gt;與 AI 進行條件談判，將原本龐大的系統修復目標，限縮為風險最低的最小程度變更，良好的維護不僅追求技術上的正確，更要契合團隊習慣、邊界與上線風險。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="迭代規劃與微創實作-iterative-implementation"&gt;迭代規劃與微創實作 (Iterative Implementation)&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;輸入來源&lt;/strong&gt;：收斂後的執行計畫與動態的環境反饋。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;協作模式&lt;/strong&gt;：在遺留系統中，修復計畫很少是一次到位的。
&lt;ul&gt;
&lt;li&gt;隨著開發進行或外部設定的調整，必須隨時與 AI 重新校準範圍；在撰寫程式碼時，嚴格要求 AI 僅針對痛點進行最小化修補，保留不想更動的既有邏輯，確保修改範圍集中，不引發大範圍的架構震盪。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="版本控制與技術敘事-git-hygiene--technical-pr-narrative"&gt;版本控制與技術敘事 (Git Hygiene &amp;amp; Technical PR Narrative)&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;輸入來源&lt;/strong&gt;：最終測試通過的程式碼差異 (Diff) 與工作紀錄。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;協作模式&lt;/strong&gt;：修補完成後，AI 也能協助提升版本控制的品質。
&lt;ul&gt;
&lt;li&gt;可以請 AI 協助檢視改動，並建議如何拆分為原子化提交（Atomic Commits，確保單一檔案或單一邏輯獨立提交）。&lt;/li&gt;
&lt;li&gt;此外，在未審核的前提下，讓 AI 根據你的習慣草擬 PR(Pull Request)，好的 PR 能清楚交代修改動機、改變了什麼、沒改變什麼，這是給審查者最好的技術文件，也能大大加速 Code Review 的流程。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;透過上述方法，AI 能夠成為引導診斷與控制範圍的最佳助手，讓我們重新思考軟體維護的工作流，可以是「萃取意圖、比對行為、收斂範圍、最小化實作，將過程沉澱為可重複的技術」，讓每次的修復都更安全、更精準且具備高度可追溯性。&lt;/p&gt;</description></item><item><title>搶救儲值金：OpenCode + OpenRouter 的背景扣費，配置 Small Model 心得分享</title><link>https://kaiadv.com/posts/20260503/</link><pubDate>Sun, 03 May 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260503/</guid><description>&lt;p&gt;承續上篇 &lt;a href="https://kaiadv.com/posts/20260502/"&gt;OpenCode 搭配 OpenRouter 的踩坑紀錄：意外的模型扣費之謎&lt;/a&gt;，在今日反覆測試之後，終於找到了初步的解決之道。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="今日行動馴服-opencode-的設定檔"&gt;今日行動：馴服 OpenCode 的設定檔&lt;/h1&gt;
&lt;p&gt;為了拿回控制權，嘗試透過設定檔明確指定 &lt;code&gt;small_model&lt;/code&gt;，在摸索過程中排除了幾個關鍵障礙：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;路徑迷蹤&lt;/strong&gt;：在 Windows 系統上，OpenCode 的設定檔位置並非在安裝目錄，而是位於 &lt;code&gt;%USERPROFILE%\.config\opencode\opencode.jsonc&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;**格式陷阱：**模型設定必須是頂層字串格式；我曾嘗試使用巢狀物件（nested objects）來描述模型屬性，但這會導致解析錯誤。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;聯動依賴（關鍵）&lt;/strong&gt;：這是過程中遇到的最大阻礙，如果只單獨設定 &lt;code&gt;small_model&lt;/code&gt; 而忽略了 &lt;code&gt;model&lt;/code&gt; 的宣告，OpenCode 會噴出 &lt;code&gt;ConfigInvalidError&lt;/code&gt; 並導致 Session 無法載入；兩者必須同時存在於設定檔中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Model ID 命名&lt;/strong&gt;：Model ID 格式不需要額外加上 &lt;code&gt;openrouter/&lt;/code&gt; 前綴，直接使用原廠定義的 ID 即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="最終有效的設定方案"&gt;最終有效的設定方案&lt;/h1&gt;
&lt;p&gt;經過多次失敗與修正，整理出這份能讓 OpenCode 穩定啟動的 &lt;code&gt;opencode.jsonc&lt;/code&gt; 範例：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;1&lt;/span&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;2&lt;/span&gt;&lt;span&gt; &lt;span style="color:#7ee787"&gt;&amp;#34;$schema&amp;#34;&lt;/span&gt;: &lt;span style="color:#a5d6ff"&gt;&amp;#34;https://opencode.ai/config.json&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;3&lt;/span&gt;&lt;span&gt; &lt;span style="color:#7ee787"&gt;&amp;#34;model&amp;#34;&lt;/span&gt;: &lt;span style="color:#a5d6ff"&gt;&amp;#34;google/gemini-3.1-pro-preview&amp;#34;&lt;/span&gt;, &lt;span style="color:#8b949e;font-style:italic"&gt;// 主模型設定
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;4&lt;/span&gt;&lt;span&gt; &lt;span style="color:#7ee787"&gt;&amp;#34;small_model&amp;#34;&lt;/span&gt;: &lt;span style="color:#a5d6ff"&gt;&amp;#34;google/gemini-3-flash-preview&amp;#34;&lt;/span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// 強制指定背景任務使用的模型，避免自動選取導致額外扣費
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;5&lt;/span&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;目前設定檔已能成功讓 OpenCode 運行，不再發生啟動失敗的問題。&lt;/p&gt;
&lt;p&gt;不過，這項修正是否能完美杜絕「意外扣費」，仍需觀察 OpenRouter 的 Generation Data；目前正在等待後台出現 Gemini 3 Flash Preview 的請求紀錄，以便驗證 OpenCode 是否確實將背景任務轉發到指定的 BYOK 模型上。&lt;/p&gt;</description></item><item><title>OpenCode 搭配 OpenRouter 的踩坑紀錄：意外的模型扣費之謎</title><link>https://kaiadv.com/posts/20260502/</link><pubDate>Sat, 02 May 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260502/</guid><description>&lt;p&gt;今天在開發時，遇到了一個有趣但也讓人有點困擾的狀況；使用的開發環境是 OpenCode 搭配 OpenRouter，主模型則是透過 OpenRouter BYOK (Bring Your Own Key) 綁定 Google AI Studio API Key 的 Google Gemini 3.1 Pro。&lt;/p&gt;
&lt;p&gt;本來預期所有的花費都會走自己的 Key，享受免費或可控的額度，沒想到卻在 OpenRouter 的後台發現了預期之外的扣費紀錄。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="案發現場被悄悄觸發的背景請求"&gt;案發現場：被悄悄觸發的背景請求&lt;/h1&gt;
&lt;p&gt;透過檢查 OpenRouter Log 的 Generation Data，我發現 OpenCode 在使用過程中，背景會自動觸發額外的 API 請求，而這些請求的行為跟我的全域設定完全不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;模型被切換&lt;/strong&gt;：請求並沒有發給 Gemini，而是被發送到了 &lt;code&gt;anthropic/claude-haiku-4.5&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Provider 改變&lt;/strong&gt;：走的是 Amazon Bedrock（OpenRouter 自己的後端）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BYOK 失效&lt;/strong&gt;：紀錄顯示 &lt;code&gt;is_byok: false&lt;/code&gt;，代表這筆請求沒有走我綁定的 Key，而是直接扣除了我 OpenRouter 帳戶裡的餘額。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用量特徵&lt;/strong&gt;：&lt;code&gt;tokens_prompt: 603, tokens_completion: 10&lt;/code&gt;，典型的輕量級背景任務用量，且 &lt;code&gt;origin&lt;/code&gt; 標示為 &lt;code&gt;https://opencode.ai/&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="推測原因獨立的-small-model-機制"&gt;推測原因：獨立的 Small Model 機制&lt;/h1&gt;
&lt;p&gt;仔細調查與推敲後，我認為這跟 OpenCode 底層的任務分發機制有關。&lt;/p&gt;
&lt;p&gt;OpenCode 內部應該有一個獨立的 &lt;code&gt;small_model&lt;/code&gt; 機制，專門用來處理像是「生成 Session 標題」、「壓縮摘要 Context」這類不需要強大推理能力的輕量級背景任務。&lt;/p&gt;</description></item><item><title>如何在 Claude Code 中透過 OpenRouter 串接第三方模型</title><link>https://kaiadv.com/posts/20260501/</link><pubDate>Fri, 01 May 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260501/</guid><description>&lt;p&gt;在 AI 輔助開發的過程中，為了能自由體驗更多不同模型的特性，無論是想測試各家模型在特定任務上的表現，還是單純想感受如 DeepSeek V4 Pro 這類強大開源模型的邏輯推演能力，需要有彈性的開發環境；這篇文章將分享如何透過簡單的配置，讓你在 Claude Code 中解鎖模型限制，串接 OpenRouter 的豐富的第三方模型。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="引導-cli-走向-openrouter"&gt;引導 CLI 走向 OpenRouter&lt;/h1&gt;
&lt;p&gt;要讓 Claude Code 放棄預設的官方伺服器並將請求轉發至 OpenRouter，我們需要設定特定的環境變數。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;關鍵技巧&lt;/strong&gt;：必須將官方的 &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; 留空；這樣可以完全避免 CLI 預設去向 Anthropic 伺服器進行驗證而產生錯誤，確保所有的請求都乾淨地透過 &lt;code&gt;ANTHROPIC_AUTH_TOKEN&lt;/code&gt; 轉發。&lt;/p&gt;
&lt;p&gt;在終端機（以 PowerShell 為例）輸入以下指令：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;1&lt;/span&gt;&lt;span&gt;&lt;span style="color:#79c0ff"&gt;$env:ANTHROPIC_BASE_URL&lt;/span&gt; = &lt;span style="color:#a5d6ff"&gt;&amp;#34;https://openrouter.ai/api&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;2&lt;/span&gt;&lt;span&gt;&lt;span style="color:#79c0ff"&gt;$env:ANTHROPIC_AUTH_TOKEN&lt;/span&gt; = &lt;span style="color:#a5d6ff"&gt;&amp;#34;Your_OpenRouter_API_Key&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;3&lt;/span&gt;&lt;span&gt;&lt;span style="color:#79c0ff"&gt;$env:ANTHROPIC_API_KEY&lt;/span&gt; = &lt;span style="color:#a5d6ff"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h1 id="抽換-claude-code-的邏輯大腦"&gt;抽換 Claude Code 的邏輯大腦&lt;/h1&gt;
&lt;p&gt;Claude Code 實際運作時，會根據任務的複雜度自動調度不同的模型，例如：預設會使用 Sonnet 來做快速的檔案結構掃描與意圖判斷，然後用 Opus 進行核心的程式碼生成與重構。&lt;/p&gt;
&lt;p&gt;與其單純設定單一的 &lt;code&gt;$env:ANTHROPIC_MODEL&lt;/code&gt;，更進階且優雅的做法是直接覆寫預設的任務模型；我們可以將 DeepSeek V4 Pro 部署在最關鍵的主力開發位置：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;1&lt;/span&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;# 將負責核心邏輯與重構的 Model 替換為 DeepSeek V4 Pro，也可以視需求將輕量級任務指派給其他反應更快的模型&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;2&lt;/span&gt;&lt;span&gt;&lt;span style="color:#79c0ff"&gt;$env:ANTHROPIC_DEFAULT_HAIKU_MODEL&lt;/span&gt; = &lt;span style="color:#a5d6ff"&gt;&amp;#34;deepseek/deepseek-v4-pro&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;3&lt;/span&gt;&lt;span&gt;&lt;span style="color:#79c0ff"&gt;$env:ANTHROPIC_DEFAULT_OPUS_MODEL&lt;/span&gt; = &lt;span style="color:#a5d6ff"&gt;&amp;#34;deepseek/deepseek-v4-pro&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681"&gt;4&lt;/span&gt;&lt;span&gt;&lt;span style="color:#79c0ff"&gt;$env:ANTHROPIC_DEFAULT_SONNET_MODEL&lt;/span&gt; = &lt;span style="color:#a5d6ff"&gt;&amp;#34;deepseek/deepseek-v4-pro&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;透過覆寫這些特定變數，等於讓不同的第三方模型各司其職，精準對應到不同的思考與執行階段。&lt;/p&gt;</description></item><item><title>AI 模型聚合平台 OpenRouter 靈活性與隱私的平衡</title><link>https://kaiadv.com/posts/20260430/</link><pubDate>Thu, 30 Apr 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260430/</guid><description>&lt;p&gt;在 AI 快速迭代的當前，開發者可能會面臨著兩難，是應該忠於單一模型供應商（如：Anthropic、Google、OpenAI），還是擁抱具備高 CP 值的開源模型（如：DeepSeek、GLM、Kimi、Qwen）？ &lt;a href="https://openrouter.ai/"&gt;OpenRouter&lt;/a&gt; 作為一個 AI 模型的聚合平台（Aggregator），為開發者提供了一種「抽象層」解決方案。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="核心價值api-的統一與抽象化"&gt;核心價值：API 的統一與抽象化&lt;/h1&gt;
&lt;p&gt;OpenRouter 的本質是將零散的模型市場標準化，透過單一的進入點（Single Entry Point），開發者可以獲得以下優勢：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;介面標準化&lt;/strong&gt;：無論底層模型是由哪家廠商提供，OpenRouter 均將其封裝為相容 OpenAI 的 API 格式；這意味著開發者在進行模型切換時，只需更改一個字串參數，而無需重新調整程式碼邏輯。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;供應商多樣性&lt;/strong&gt;：同一個模型（如： Llama 4）可能由多家算力供應商（Providers）代管；OpenRouter 會根據延遲、價格與可靠性進行動態路由，確保服務的穩定。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;支付統一化&lt;/strong&gt;：開發者無需在數十家供應商分別綁定信用卡，僅需在單一錢包儲值，即可跨模型、跨廠商消費。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="隱私防線理解零資料留存-zdr"&gt;隱私防線：理解「零資料留存 (ZDR)」&lt;/h1&gt;
&lt;p&gt;對於處理程式碼與商業邏輯的開發者而言，隱私是選擇平台時的首要考量；OpenRouter 在隱私保護上建立了一套透明的過濾機制，其核心在於 ZDR (Zero Data Retention)。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ZDR 的定義&lt;/strong&gt;：當模型標註為 ZDR 時，代表供應商承諾不會將資料存入磁碟，資料僅在記憶體中進行即時推理，且絕對不會被用於後續的模型訓練。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;過濾機制&lt;/strong&gt;：OpenRouter 允許用戶設定「強制執行 ZDR」；一旦開啟，系統將自動屏蔽所有不符合該隱私標準的供應商，確保資料路徑的純粹。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;訓練權限的權衡&lt;/strong&gt;：平台提供了細緻的開關，讓用戶決定是否接受「以數據換取折扣」的節點；對於測試性質的對話，用戶可選擇較便宜的非 ZDR 節點；而對於涉及敏感專利的開發工作，則可一鍵切換至高強度隱私模式。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="成本管理與透明化"&gt;成本管理與透明化&lt;/h1&gt;
&lt;p&gt;OpenRouter 的另一大特色在於其透明的定價模型；相較於原廠通常具備的固定分層計費（Tiers），聚合平台提供了更細粒度的選擇：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;動態降價&lt;/strong&gt;：由於聚合了多家供應商，平台競爭往往會推動價格低於原廠官方定價。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;免費額度與贊助&lt;/strong&gt;：許多新興模型為了推廣，會透過 OpenRouter 提供限時或限量的免費節點，成為開發者進行「模型選型」時的最佳實驗場。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="策略建議"&gt;策略建議&lt;/h1&gt;
&lt;p&gt;對於正處於嘗試期、尚未決定核心模型的開發者，建議採取以下策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;實驗階段（探索期）&lt;/strong&gt;：利用 OpenRouter 的統一 API 進行對照測試，觀察不同模型在特定領域（如：程式碼生成或長文本推理）的優劣。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隱私分級&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;通用技術詢問&lt;/strong&gt;：可選用具備成本優勢的非 ZDR 免費節點。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;專案程式碼實作&lt;/strong&gt;：建議開啟 Always enforce ZDR，並關閉所有涉及「資料用於訓練」的選項。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;架構解耦&lt;/strong&gt;：在專案初期即採用 OpenRouter 的抽象接口，為未來隨時切換至更高性能或更廉價的模型預留空間。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;OpenRouter 不僅是 API 轉發站，更是間接定義了「模型消費標準」的協議層，對於追求靈活性與技術前瞻性的人而言，可以從繁瑣的帳務管理與格式轉換中解放，並在保護數據隱私的前提下，以最低的摩擦成本享受 AI 技術的最新成果。&lt;/p&gt;</description></item><item><title>AI 應用的效率指標：Cache Hit Rate 快取命中率</title><link>https://kaiadv.com/posts/20260427/</link><pubDate>Mon, 27 Apr 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260427/</guid><description>&lt;p&gt;在當前的 AI 領域，Cache Hit Rate（快取命中率）已不再僅僅是後端開發的專有名詞，它已成為衡量大語言模型（LLM）應用效率、成本控制與反應速度的最核心指標之一。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="什麼是-cache-hit-rate"&gt;什麼是 Cache Hit Rate？&lt;/h1&gt;
&lt;p&gt;在 API 呼叫的脈絡下，Cache Hit Rate 指的是模型在處理請求時，能夠直接從快取中讀取「已計算過的前綴（Prefix）」的比例。其計算方式如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Cache Hit Rate = 快取讀取 Token 數 / (快取讀取 Token 數 + 快取寫入 Token 數 + 一般輸入 Token 數)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;當命中率越高，代表模型重複運算的次數越少，整體的執行效率也就越高。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="為什麼快取命中率如此重要"&gt;為什麼快取命中率如此重要？&lt;/h1&gt;
&lt;p&gt;Cache Hit Rate 對於生產環境中的 AI 應用有三大直接影響：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;成本大幅下降&lt;/strong&gt;：快取讀取的費用通常遠低於原始輸入；以某些供應商為例，快取讀取僅需原價的 10%；如果命中率從 90% 降至 70%，整體的運算成本幾乎會翻倍。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;回應速度提升&lt;/strong&gt;：跳過重複的預運算過程可以顯著減少首字反應時間（TTFT），尤其在 Context（上下文）越長的情況下，提速感越明顯。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配額（Quota）保護&lt;/strong&gt;：高命中率能減少 Token 的淨消耗，避免開發者或訂閱用戶在短時間內觸發服務上限。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在頂尖的 AI 開發團隊中，Cache Hit Rate 被視為與系統「可用性（Uptime）」同等重要的核心指標，一旦命中率異常過低，便會觸發告警機制進行排查。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="常見的快取破壞行為"&gt;常見的快取破壞行為&lt;/h1&gt;
&lt;p&gt;要維持高命中率，必須避免以下幾種會導致快取失效的行為：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;頻繁切換模型&lt;/strong&gt;：快取通常無法跨模型共用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;超過 TTL（生存時間）&lt;/strong&gt;：快取具有時效性，若閒置時間過長，快取將被自動清除。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;內容擾動&lt;/strong&gt;：修改 System Prompt、&lt;code&gt;AGENTS/CLAUDE.md&lt;/code&gt; 或變動 Tool 定義的順序，都會導致前綴無法匹配，必須重新建立快取。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作節奏不連貫&lt;/strong&gt;：長時間的閒置（Idle）會導致重建成本增加。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;對於一個穩定運行的 AI Session，90% 的快取命中率是理想的健康標準；如果長期低於 70%，開發者就應重新檢視提示詞（Prompt）的結構順序或快取生存時間的設定。&lt;/p&gt;</description></item><item><title>導入 AI 的決策：「預留算力」與「按量計費」</title><link>https://kaiadv.com/posts/20260426/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260426/</guid><description>&lt;p&gt;隨著生成式 AI 深入企業內部流程與產品，算力的「穩定性」與「成本控管」成為 IT 架構的重大考驗；在雲端 AI 模型（如 Azure OpenAI）的部署上，企業通常面臨兩種資源配置模式：「按量計費 (Pay-as-you-go)」與「預留算力 (Provisioned Throughput)」。&lt;/p&gt;
&lt;p&gt;這兩者不僅僅是計費方式的差異，更直接決定了企業級 AI 應用的可靠度與未來擴展性。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="核心概念美食街與專屬包廂"&gt;核心概念：美食街與專屬包廂&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;按量計費 (Pay-as-you-go)&lt;/strong&gt;：就像在熱門的美食街用餐；人少時很快就能買到餐點，但在尖峰時段必須跟所有人排隊競爭資源，甚至可能因為人潮爆滿而被拒絕服務（觸發限流）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;預留算力 (Provisioned)&lt;/strong&gt;：就像在高級餐廳長期預訂了一間專屬包廂；不論何時抵達、吃多快或吃多慢，這個空間永遠為你保留；即使今天沒去用餐，包廂費依然要付，但換來的是絕對的保障與尊榮體驗。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="預留算力能消除網路延遲嗎"&gt;預留算力能消除網路延遲嗎？&lt;/h1&gt;
&lt;p&gt;許多人誤以為購買預留算力後，跨國連線的延遲就會消失，這是一個迷思；在跨海調用 AI 模型時，我們需要區分兩種延遲：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;物理傳輸延遲（不變）&lt;/strong&gt;：資料在光纖中跨國傳輸的物理時間（如台灣到美國的 0.2 秒）；這是受物理法則限制的，無論買多少算力都不會改變。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;伺服器排隊延遲（完全消除）&lt;/strong&gt;：這是請求到達雲端機房後，等待 GPU 撥出空檔運算的時間。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;預留算力的威力，在於為企業開闢了一條專屬的高架快速道路；物理距離的里程數沒變，但永遠不會遇到「塞車」；這讓 AI 回應的「體感速度」大幅提升，且每次呼叫的等待時間都會變得高度一致。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="從財務與營運角度的綜合評估"&gt;從財務與營運角度的綜合評估&lt;/h1&gt;
&lt;p&gt;從帳面上看，預留算力是一筆不小的固定支出，但對於重度依賴 AI 的企業而言，往往是更划算的投資：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;變動成本轉化為固定成本&lt;/strong&gt;：企業能精確預估年度 AI 預算，不再擔心因為內部員工或外部客戶的突然爆量使用，導致下個月收到天價帳單。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;規模經濟的黃金交叉&lt;/strong&gt;：當企業的 AI 總體用量（Token 消耗量）達到一定規模時，預留算力所帶來的折扣，將使其平均單價遠低於按量計費。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;釋放開發潛能&lt;/strong&gt;：開發團隊不再需要為了遷就「限速（Rate Limit）」而撰寫複雜的重試邏輯或刻意降低處理速度；團隊可以放心開發高併發、大批次的自動化代理任務（Agentic Workflows），最大化 AI 的價值。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;「預留算力」不僅僅是一種付費方案，更是企業將 AI 視為關鍵數位基礎設施的宣示；它用固定的金錢成本，買到了極致的系統穩定度、可預測的效能，以及無後顧之憂的開發自由度；對於準備將 AI 投入大規模生產環境的企業來說，這是邁向成熟應用的必經之路。&lt;/p&gt;</description></item><item><title>如何透過 Microsoft Foundry 部署的模型設定 Codex</title><link>https://kaiadv.com/posts/20260425/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260425/</guid><description>&lt;p&gt;為什麼會想寫這篇文章呢？主要是因為我發現，現在還是有不少人在使用免費版的 AI 工具；同時，許多企業也正嘗試在雲端環境（例如透過 Microsoft Foundry）部署專屬的高階模型，卻往往卡在不知道如何設定，或部署完後不知道該如何串接到實際的工具中使用。&lt;/p&gt;
&lt;p&gt;因此，我決定把這篇心得分享出來，我會帶大家從雲端部署開始，一路到如何將模型成功串接到 Codex 上進行實戰；不管你是精打細算的「無課金御主」，還是正在為公司尋找解決方案的工程師，這篇文章都會是你的實作指南。&lt;/p&gt;
&lt;p&gt;在正式進入雲端部署之前，我們需要先準備好手邊的武器；首先，必須要先有個 Azure 帳號（如果還沒有可以去申請一個，微軟通常有提供免費額度可以試玩）；接下來，我們需要安裝對接的工具，可以選擇安裝有圖形介面的 &lt;a href="https://developers.openai.com/codex/cli"&gt;Codex App&lt;/a&gt;；如果習慣敲打鍵盤、熱愛終端機，也可以選擇安裝 &lt;a href="https://developers.openai.com/codex/cli"&gt;Codex CLI&lt;/a&gt;，兩者擇一即可。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="建立-azure-訂閱"&gt;建立 Azure 訂閱&lt;/h1&gt;
&lt;p&gt;準備好 Azure 帳號後，我們登入 &lt;a href="https://portal.azure.com/"&gt;Azure&lt;/a&gt;；要部署任何雲端資源之前，我們都必須先建立一個「訂閱 (Subscription)」，這就像是你在雲端上的專屬帳單與資源管理帳戶。&lt;/p&gt;
&lt;p&gt;請跟著以下步驟做：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 Azure 首頁上方的搜尋列，輸入並點選 &lt;strong&gt;「訂閱 (Subscriptions)」&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;進入頁面後，點擊左上角的 &lt;strong&gt;「+ 新增 (Add)」&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;選擇適合你的方案：
&lt;ul&gt;
&lt;li&gt;如果是初次體驗的無課金玩家，建議選擇「免費試用 (Free Trial)」，微軟會提供一筆免費額度讓你無痛跟著這篇教學實作。&lt;/li&gt;
&lt;li&gt;如果是為公司部署，則可以選擇「隨用隨付 (Pay-As-You-Go)」或是套用你們公司既有的企業合約。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="透過-ai-foundry-建立你的第一個-agent"&gt;透過 AI Foundry 建立你的第一個 Agent&lt;/h1&gt;
&lt;p&gt;當我們進入 Microsoft AI Foundry 的首頁後，會發現介面非常現代化，與其在後台設定一堆看不懂的雲端資源，微軟現在提供了一個更直覺的起手式！&lt;/p&gt;
&lt;p&gt;請跟著以下步驟做：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接點擊畫面上非常醒目的「Create an agent (建立代理程式)」按鈕。&lt;/li&gt;
&lt;li&gt;這個按鈕就像是一個智慧嚮導，點擊後系統會開始引導你建立「專案（Project）」。&lt;/li&gt;
&lt;li&gt;在建立專案的設定畫面中，系統同樣會讓你選擇前面準備好的「訂閱方案」，這時候就可以直接點選新建一個資源群組（Resource Group），把底層的煩人設定一次搞定！&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="挑選大腦部署你的-gpt-41-模型"&gt;挑選大腦：部署你的 GPT-4.1 模型&lt;/h1&gt;
&lt;p&gt;當 Agent 建立完成後，畫面會自動彈出「Deploy a model」的邀請，這就是我們要幫 AI 安裝「大腦」的時刻。&lt;/p&gt;
&lt;p&gt;請跟著以下步驟做：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;進入模型目錄&lt;/strong&gt;：點擊部署後會看到琳瑯滿目的模型清單；請直接搜尋並選擇你心儀的高階模型，本文會以「GPT-4.1」作為示範。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自訂部署名稱&lt;/strong&gt;：這裡建議取一個好記的名字，例如 &lt;code&gt;gpt-4.1&lt;/code&gt;；請記住這個名字，因為待會我們在 Codex 串接時會用到它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;調整性能指標 (TPM)&lt;/strong&gt;：部署時會有一個「Tokens Per Minute (TPM)」的拉桿；如果你是個人測試或初學者，設定一個適中的數值即可，這樣既能保證反應速度，也能有效控管預算。&lt;/li&gt;
&lt;li&gt;點擊「Deploy」後，稍微等待幾分鐘，你的專屬雲端模型就正式上線了！&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="取得連接憑證找到正確的通訊位址"&gt;取得連接憑證：找到正確的「通訊位址」&lt;/h1&gt;
&lt;p&gt;模型部署好了，接著我們要去撈取連接用的憑證；這裡有個非常重要的細節，請大家一定要看清楚，因為這是最容易出錯的地方！&lt;/p&gt;</description></item><item><title>Cursor Meetup：從 Vibe Coding 到 AI Agent 的自我進化</title><link>https://kaiadv.com/posts/20260419/</link><pubDate>Sun, 19 Apr 2026 00:00:00 +0800</pubDate><guid>https://kaiadv.com/posts/20260419/</guid><description>&lt;p&gt;前陣子參加了一場收穫滿滿的 Cursor Meetup，現場聚集了許多對使用 AI 工具開發充滿熱情的夥伴，從實戰 App 開發到複雜的 Agent 架構設計都有精彩討論；我整理了三位參與者的核心分享，這不僅是技術交流，更是對未來工作流的重新想像。&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="創意與開發的平衡讓-ai-更懂你自己"&gt;創意與開發的平衡，讓 AI 更懂你自己&lt;/h1&gt;
&lt;p&gt;參與者 A 分享了如何利用 AI 打造行程規劃 App，他提出了一個非常有人性的切入點：連點子都能由 AI 幫你量身打造。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用 Shared Memories 塑造人格&lt;/strong&gt;：可以透過 ChatGPT 的「長期記憶（Shared Memories）」功能，持續餵養你的興趣、偏好與生活習慣；這樣一來當你缺乏靈感時，AI 能根據它對你的了解，主動提議對你真正有幫助的功能；這不再只是寫程式，而是 AI 在幫你優化生活。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vibe Coding 視覺先行&lt;/strong&gt;：在開發初期，可以先請 AI 列出幾種建議的 UI 風格；因為在 Vibe Coding 的模式下，AI 產出的介面很容易趨同，先選定風格（Vibe）才能確保產品有獨特性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;開發流程的取捨&lt;/strong&gt;：對於追求速度的開發者，傳統的 SDD (設計文件) 或 TDD (測試驅動) 並非絕對必要；但在「放手讓 AI 自由生長」時，這些規範反而成了最好的約束工具，能確保 AI 清楚交代工作內容，不至於寫出沒人看得懂的黑盒子。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="分工合作藝術用-subagent-突破-token-上限"&gt;分工合作藝術，用 Subagent 突破 Token 上限&lt;/h1&gt;
&lt;p&gt;參與者 B 展示了如何打造一個專業的「翻譯 Agent」，重點在於不要讓一個 AI 做所有的事；將任務拆解成多個 Subagent（子代理），就像精密的流水線這種「專業分工」能完美解決 AI 記憶力（Token 上限）不足的問題，讓翻譯長文不再斷頭斷尾。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;結構化&lt;/strong&gt;：先把文件轉成標題分明的 Markdown（例如 &lt;code&gt;# 第一章&lt;/code&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;li&gt;&lt;strong&gt;組裝與導出&lt;/strong&gt;：最後再進行編排、彙整，並轉為 PDF。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心邏輯&lt;/strong&gt;：每個子代理都只讀取專屬的指令檔（&lt;code&gt;SKILL.md&lt;/code&gt;），透過一位「總指揮」調度，最後再加上人工 Review。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id="自我進化系統打造專屬ai-技能庫"&gt;自我進化系統，打造專屬AI 技能庫&lt;/h1&gt;
&lt;p&gt;參與者 C 則分享了讓 Agent 變聰明的流程（例如 Moltbot），這類 Agent 具備「學習」的能力：&lt;/p&gt;</description></item></channel></rss>