先說清楚我的角色
程式碼是團隊的工程師寫的,不是我。
我做的是另外三件事:把根因查出來、決定先修哪一條、以及在上線後用實測數字證明真的修好了。這個案子值得寫下來的原因也在這裡——它是一個沒有工程背景的 PM,在一個沒有資深 DBA 的公司裡,怎麼用證據推動技術決策。
問題背景
系統升級到 .NET 9 之後,資料庫 CPU 開始間歇性飆高,結帳偶發逾時。這種問題最麻煩的地方是每個人都有一套說法:可能是新框架、可能是流量成長、可能是結帳在互相鎖住。
而且其中兩條線已經被優化過了——問卷查詢在 2024 年 12 月和 2025 年 10 月各做過一次,都有改善、都上線了、也都沒有真正解決。
我要回答的其實是一個很簡單的問題:到底是哪幾支查詢在吃 CPU? 不是猜,是查。
具體實作
一、先證明那個大家都相信的原因是錯的
當時最主流的假設是「結帳互卡」——兩張訂單同時進來互相鎖住。這個假設很合理,也很難反駁,因為「它偶爾才發生」永遠可以解釋任何反證。
我用三層證據把它排除掉:
- 程式碼:grep 整條結帳路徑,
BeginTransaction出現 0 次。沒有交易就沒有互卡的條件。 - 資料庫即時狀態:查當下的阻塞情形,0 個 blocking、0 個鎖等待。
- 歷史紀錄:Query Store 拉七天,結帳相關的鎖等待累計為 0。
三層都指向同一件事:架構上不會發生。證明一個東西不存在,比找出一個東西存在還難,所以我不用「我測不到」當結論,而是用三種互相獨立的方法各查一次。
排除掉這個之後,真正的兇手才浮出來。
二、影響程度的證據,用客服的痛、不用我的碼表
「結帳很慢」要慢到什麼程度才值得動手術?我一開始想用自己實測的秒數當證據,後來丟掉了——我在辦公室按碼表量到的數字,只代表那一刻、那條網路、那台機器,說服不了任何人,也承擔不起「動付款路徑」這種決策的重量。
改用的做法:把一段期間內客服實際收到的「結帳有問題」事件逐筆撈出來,對到資料庫 CPU 的時間軸上。六起客訴,起起都落在 CPU 飆高的時段裡。這種證據有兩個好處:它是客人真實的痛,不是我的體感;而且事件時間點與資源指標對得上,因果的方向就立得住。
挑證據跟挑查法一樣重要——你要的不是「我覺得慢」,是「客人在痛、而且痛的時間跟系統指標吻合」。
三、逐條定位根因
問卷查詢(153 秒):舊做法是 WHERE Reply LIKE '%分數%',每查一次就把整篇評論文字掃過一遍。分數本來是埋在 JSON 裡的,讀取時才現算。這不是查詢寫法的問題,是資料結構的問題——所以前兩次「加 view 欄位」「加快取」都只能治標。
修法是把分數在寫入時就算好、拆成獨立欄位與資料表,用 dual-write 加 backfill 分階段切換。這是唯一真正動到底層結構的一次,也是唯一根治的一次。
結帳 payload SQL(3,000ms):LINQ 寫成 Where(w => w.OrdersId == X || w.AddOrderId == Y)。這個 OR 讓資料庫走不到任何一個索引,只能全表掃描。修法很小——拆成兩個各自走索引的查詢再合併——效果是 3,000ms 變成 0.13ms。
一行寫法的差別,兩萬倍。
批量列印物流單(9.3 分):驗收方式是直接比對 Mongo 的列印紀錄,改版前 721 次、改版後 486 次真實列印,中位數從 4.8 秒降到 1.1 秒。
四、拆成分波清單,不要一次全上
找出來的問題不只三條——完整清單有 21 項。但一次全改等於沒有辦法歸因:改了二十件事之後系統變快了,你永遠不知道是哪一件起了作用、哪一件其實是白做的。我把 21 項按風險與預期效益排成三波,每一波上線後量一次,確認因果關係成立再推下一波。
結帳這條動到付款路徑,另外走了完整的測試矩陣:五條付款路徑(網頁綁卡、網頁 LinePay、網頁重新付款、App LinePay、App 綠界)逐一驗過才上正式站。
結果
| 項目 | 改善前 | 改善後 |
|---|---|---|
| 問卷查詢最慢單次 | 153 秒 | 7 秒 |
| 問卷查詢月總 CPU | 推估約 44,000 CPU 秒 | 39.8 CPU 秒(實測) |
| 結帳 payload SQL | 約 3,000ms | 0.13ms |
| 批量列印(最慢一次) | 9.3 分 | 1.3 分 |
| 單筆列印中位數 | 4.8 秒 | 1.1 秒 |
| 結帳逾時 | 偶發 | 0 |
上線後實測 8 筆綁卡結帳,平均 5.06 秒、零逾時。修法前同類測試會直接撞上 30 秒逾時。
數據源全部標明:效能數字來自 Azure Query Store,列印數字來自 Mongo 的列印 log。唯一的推估值是「改善前月總 CPU」,因為那筆原始紀錄已經超過 30 天保留期被清掉了——我在報告裡也是這樣寫的,標成推估、只供量級參考。
把判斷寫進自動化
專案收尾之後我做了一件事:把這半年學會的「怎麼判斷誰是兇手」寫成規則,交給一支常駐的監控機器人——資料庫 CPU 告警一進來,它自動抓出當下最吃 CPU 的查詢、跑完初步診斷、寄一封分析報告到我信箱。
其中最重要的一條判斷規則,是我自己踩過的坑:看 CPU 消耗,不要看執行時間。 一支查詢跑得久,可能只是在等鎖、等 IO,它是受害者不是兇手;真正的兇手是 CPU 時間高的那支。這條規則寫進機器人之後,等於把「我的判斷力」變成一個不用我在場也會執行的東西——這是我對「效能治理」的定義:不是修好幾支查詢,是讓下一次問題出現時,診斷自動開始。
同樣的症狀,不一樣的兇手
兩個月後「結帳慢」又被回報了一次。如果我直接拿上次的答案抄——「應該又是 SQL 寫法」——就錯了。這次逐步拆解後發現是完全不同的兩件事疊在一起:結帳的中繼頁面裡寫死了固定秒數的空等,加上資料庫統計值過期放大了 IO 的尾巴。
症狀會重複,根因不會忠誠。 每一次都要重新查,上一次的結論只是這一次的假設之一。
學到的東西
- 「已經優化過了」不等於「已經解決了」。 前兩次優化都是真的有效、也都是當時可行的選擇;只是真正的瓶頸在資料結構,而動資料結構風險高、估要一個月以上,所以一直被留著。把「為什麼這次才根治」寫清楚,比宣告成果重要。
- 報告要標可信度。 我在給老闆的報告裡把每個數字分成「實測」和「推估」,並附上口徑說明。這件事沒有讓報告變弱,反而讓實測的那些數字更有份量。
- 不要獨佔功勞,但也不要低估自己的貢獻。 程式是工程師寫的。但這三條線在被診斷出來之前已經躺了一年以上,沒有人知道該從哪裡開始。定位問題本身就是產出。
技術架構
Azure Query Store、T-SQL、SQL Statistics、MongoDB、Azure CLI、Application Insights。
診斷方法與判斷規則已沉澱為常駐監控機器人與內部 playbook。