---
title: 研發度量會說謊 — 七個讓指標默默出錯的語意陷阱
description: 研發效能指標算錯不會 crash、只會默默產出錯數字誤導管理。這篇用真實案例拆解七個語意陷阱：工時起點算錯把 30 天灌成 94 天、狀態語意沒對齊、reject 全 0 卻不是資料壞、移票灌高完成率，附上線前的語意審查清單。
category: career
tags: [dev-metrics, devex, dora, qa-metrics, engineering-productivity]
date: 2026-07-27
faq:
  - q: 為什麼研發指標「看起來對」卻不能信？
    a: 因為指標算錯不會 crash、不會報錯，只會默默產出一個「合理但錯」的數字。程式 bug 你會看到紅字，度量 bug 你只會看到一個漂亮的圖表——然後拿它去做人事與排程判斷。錯的方向通常不是隨機噪音，而是系統性偏移（例如工時全體高估、某類人整批消失），最傷。
  - q: dev cycle（開發週期）該從哪個時間點起算？
    a: 用「首次進入 in-progress / dev 狀態」，不要用「票建立日」。票從建立到真正開工，中間可能排隊等規格、等排期 10 天以上。曾有一張實際 30 個工作天的票，用建立日起算變成 94 個日曆天——三倍灌水。而且停留天數要扣掉週末假日（工作天 ≠ 日曆天）。
  - q: 一個指標整排都是 0，是不是資料壞了？
    a: 不一定。先確認是不是「指標定義」把資料濾光了。曾遇到 reject 數全 0，查下去是「只算已完成的票 × 票完成後會漂到別的分類」兩層濾網結構性殺光，底層資料其實很健康。同理，欄位是 NULL 也不代表被洗掉——要有「曾經有值」的正面證據，才能下「資料受損」的結論。
  - q: 度量上線前要怎麼驗證它沒說謊？
    a: 三招。一、改指標邏輯前先寫 parametric test 把現有行為固定住，避免靜默漂移。二、每個彙總數字都要能 drill 回原始清單，用同一段 SQL，否則彈窗總數會跟卡片對不上。三、拿極端值（全 0、爆高、突然歸零）當訊號主動查，而不是等人回報「這數字怪怪的」。
---

# 研發度量會說謊

程式有 bug 會 crash、會報紅字，你馬上知道。**度量有 bug 不會 crash——它只會默默產出一個「看起來很合理」的錯數字**，然後這個數字被畫成漂亮的圖表、被拿去排 sprint、被拿去評績效。等有人發現不對，錯誤判斷已經做完了。

這篇是踩過一整輪 DORA / DevEx 度量坑之後沉澱的七個**語意陷阱**。它們的共同點是：不會噴錯、只會騙人，而且騙的方向通常是**系統性偏移**（全體高估、某類人整批消失），不是隨機噪音——所以特別難察覺、也特別傷。

## 核心：度量的錯不在算術，在語意

```mermaid
flowchart LR
    A[原始事件<br>狀態變更 / 時間戳] --> B[指標定義<br>起點? 終點? 算誰?]
    B --> C[彙總數字]
    C --> D[圖表 / 報告]
    D --> E[人事 / 排程判斷]

    B -.語意錯在這.-> X["'合理但錯'的數字<br>不會 crash"]
    X --> C

    style B fill:#f59e0b,color:#fff
    style X fill:#ef4444,color:#fff
    style E fill:#7c3aed,color:#fff
```

算術很少出錯，出錯的是**「這個數字到底代表什麼」**。下面七個都是在這一層翻車的。

---

## 陷阱 1：別用「票建立日」當工時起點

最常見、也最貴的一個。

一張票從**建立**到**真正開工**，中間可能排隊等規格、等排期、等人力——這段等待可以長達 10 天以上。如果你把「票建立日」當成工時起點，就等於把排隊時間算進了開發成本。

> **真實案例**：一張實際只花了 **30 個工作天**的票，用「建立日 → 完成日」的日曆天算法，變成了 **94 天**。三倍灌水，而且是系統性地灌——每一張排過隊的票都被高估。

**正解**：起點用「首次進入 in-progress / dev 狀態」的時間戳。要拿的是「開工到完成」，不是「進系統到完成」。

```mermaid
flowchart LR
    C[建立票] -->|排隊 10+ 天<br>等規格/等排期| P[首次 in-progress]
    P -->|真正開發| D[完成]

    C -.❌ 錯起點.-> D
    P -.✅ 對起點.-> D

    style C fill:#94a3b8,color:#fff
    style P fill:#10b981,color:#fff
```

---

## 陷阱 2：狀態語意必須逐個對齊

工作流裡兩個看起來很像的狀態，語意可能天差地遠。

- 「**分配給 QA**」= 票被指派到 QA 名下，但**還沒開始測**
- 「**QA 測試中**」= QA **真的在跑測試**

如果你做「開測後 RD 又改 code」的風險偵測，把「分配給 QA」也算成「已開測」，就會**大量誤報**——一堆根本還沒進測試的票被標成「危險」。

**正解**：每個狀態的語意要逐個確認，不能靠名字猜。畫一張狀態語意對照表，把「這個狀態代表工作真的發生了嗎」逐格填清楚。

---

## 陷阱 3：工作流有例外路徑，別讓人「從報告消失」

你以為的標準流程：`todo → in-progress → qa → done`。

現實：**某些團隊會跳關**。例如 PR 一開就自動把狀態從 `todo` 直接切到 `qaing`，完全跳過 `in-progress`。如果你的指標只認「首次進 in-progress」當起點、又沒設 fallback，這些人的票會**查不到起點 → 直接從報告上消失**。

> 結果就是：那個團隊的產出「憑空蒸發」，管理層看到的是一張少了一整組人的報告，卻不知道是度量把他們吃掉了。

**正解**：起點/終點都要有**降級 fallback**。例如「首次 in-progress，查不到就退而取『首個非 todo 狀態』」；終點「首個 release-candidate，查不到就退 done」。每條路徑都要能落到一個合理值。

---

## 陷阱 4：工作天 ≠ 日曆天

卡關、停留、週期這類「天數」指標，**一定要扣掉週末與假日**。

一張票週五進 QA、週一有人看，用日曆天算是 3 天，實際只過了 1 個工作天。跨越連假時誤差更誇張。所有拿去跟「SLA / 目標天數」比較的停留指標，都要用工作天口徑，否則週末多的月份會無端「變慢」。

**正解**：內建工作天計算（扣週末 + 國定假日）。這是所有時間類指標的預設，不是加值功能。

---

## 陷阱 5：「全是 0」不一定是資料壞了

看到一整排 0，第一反應是「資料管線壞了」。先別急著這樣報。

> **真實案例**：reject（打回）數整排都是 0。查下去發現資料完全健康，是**指標定義**把它濾光的——這個指標「只算已完成的票」，但票一旦完成就會漂移到另一個分類，於是「已完成 ∩ 曾被 reject」這個集合被兩層濾網**結構性地殺成空集合**。

底層每一筆 reject 事件都好好在資料庫裡，是定義讓它顯示為 0。

**正解**：報「資料壞掉」之前，先問「是不是定義把它濾光了」。把濾網條件一層層攤開，看是不是某兩個條件的交集天生為空。

---

## 陷阱 6：「欄位是 NULL」不代表它「被洗掉」

這是陷阱 5 的鏡像。看到欄位一片 NULL，別急著報「資料受損」。

NULL 可能是「從來就沒填過」，也可能是「本來有值後來被覆蓋掉」——這兩件事的嚴重程度完全不同。前者是正常，後者才是事故。

> **真實案例**：一個外部登入整合，某些欄位（部門 / 職級）只在 userinfo 端點回傳、不在 token 裡。當 userinfo 端點 5xx 時，如果無腦「整包覆蓋」就會把既有的部門職級**洗成 NULL**。正確做法是偵測到「這次只拿到 token、沒拿到 userinfo」就**跳過覆蓋**，區分「對方刪了欄位」與「我這次抓失敗」。

**正解**：要下「資料受損」的結論，需要**「曾經有值」的正面證據**（歷史快照、稽核紀錄），不能光憑現在是 NULL 就斷言。訓練自己不做無證據的斷言。

---

## 陷阱 7：狀態瞬間連跳 ≠ 沒投入

有時你會看到一張票的狀態在**同一秒內連跳好幾格**：`todo → in-progress → qa → done`，時間戳幾乎一樣。

直覺會判成「造假」或「沒真的做」。通常都不是——這是**事後補登**：人做完了才回來一次把狀態點過去。工作是真的發生了，只是狀態機被一次補完。

**正解**：連跳當成「補登訊號」處理，不要當「造假」或「漏資料」報。真要抓造假，得看別的證據（有沒有對應的 commit / MR / 測試產出），不能只看狀態時間戳。

---

## 加碼：兩個「join 錯資料」的真實戰記

上面七個是語意層，這兩個是**實作層**踩的坑，同樣是「數字看起來對、其實錯」：

**「卡了 38 天」其實只卡了 22 小時。** 一張跨組的票有 mirror（鏡像）row，狀態歷史表只存 base id。用了帶後綴的 task_id 去查 → 查不到 → fallback 回「票建立日」→ 於是把「從開票到現在」整段算成「卡在現狀」。修法：切掉後綴對到 base id，並 fallback 到真實的 time-in-status，而不是建立日。

**移票灌出的 100% 完成率。** Sprint 完成率如果用 live count（現在還在這個 sprint 的票），那些**做到一半被移出 sprint** 的票會消失，分母縮小、比率被灌到接近 100%。正解：用「最後一張每日 burndown 快照」的 done/total，而不是即時數字；live count 另存供對照就好。

---

## 上線前的語意審查清單

把度量當成「會默默出錯的生產程式」來對待：

```mermaid
mindmap
  root((度量上線前<br>語意審查))
    時間口徑
      起點用開工不用建立日
      工作天扣週末假日
      終點有 fallback
    狀態語意
      逐格確認代表什麼
      例外路徑不讓人消失
      連跳當補登不當造假
    極端值
      全 0 先查是不是被濾光
      NULL 要正面證據才報受損
      爆高/歸零主動查
    可驗證
      改邏輯前先寫 parametric test
      每個數字能 drill 回原始清單
      drill 用同一段 SQL 對得上
```

三條鐵則收尾：

1. **改指標邏輯前，先寫 parametric test 把現有行為釘死。** 度量漂移是靜默的，沒有測試守著，下次重構就悄悄變了數字沒人知道。
2. **每個彙總數字都要能 drill 回原始清單，而且用同一段 SQL。** 否則彈窗總數會跟卡片對不上——那種「加起來對不上」的信任崩壞，一次就毀掉整個儀表板的公信力。
3. **報告不要誇大。** 「AI 輔助度量」= 加速人工整理 + 人把關，不能寫成「全自動、不耗人力」。管理層最在意的是失真，一次浮誇會賠掉所有數字的可信度。

---

## 一句話

> 度量不會 crash，只會騙你。而它騙的方向，通常剛好是你最想聽的那個方向——所以每個「漂亮的數字」，上線前都值得被當成嫌疑犯審一遍。
