31 篇文章憑空消失:一個寫死的 .limit(80),沒有任何 error
我沒刪過任何一篇文章,但有 31 篇從網站上徹底不見了——系列卡找不到、分類按鈕點不到、站內搜尋也搜不到。沒有 error、沒有 log、不留痕跡。查了半天,兇手是一行
.limit(80)。
症狀:資料還在,人卻不見了
我的咖啡品牌網站有一個文章區,累積了好幾年的內容,從 2020 年最早期的咖啡筆記到最近的開發雜記都有。
某天我在整理內容時發現不對勁:最早期那批咖啡文章,好像從前台消失了。系列卡的計數少了、分類按鈕點進去看不到、站內搜尋打關鍵字也搜不出來。
但我很確定我沒刪過它們。我去資料庫查,文章好端端躺在那裡,status 是 published,什麼都正常。
資料還在,前台卻看不到——這種 bug 比「資料真的被刪了」更讓人發毛,因為你完全不知道從哪查起。沒有錯誤訊息可以搜,沒有堆疊可以追。它不是壞掉,是「安靜地少了一塊」。



根因:.limit(80) 的靜默截斷
我去翻文章列表頁 ContentList.vue 的資料查詢,看到這行:
.limit(80)
這是很久以前寫的,當時文章大概只有五六十篇,80 綽綽有餘。我實測了一下當下真實的已發布文章數:111 篇。
問題就在這。前端一次只撈 80 篇,超過的 31 篇——多數正好是 2020 到 2023 年那批最早期的咖啡文章——根本沒被撈進來。而它們不是「撈進來但沒顯示」,是從一開始就不在前端的資料裡。所以系列卡的計數、分類的分桶、站內的搜尋,全部都是在這 80 篇的範圍內運作,那 31 篇對整個前台來說等於不存在。
.limit(80) 最坑的地方在於它不會報錯。它不是「資料太多、放棄、拋例外」,而是「安靜地給你前 80 筆,剩下的當作沒有」。這就是靜默截斷(silent truncation)——最危險的那種 bug,因為系統的每個部分看起來都運作正常,只是餵給它們的資料本身就少了一塊。
止血:先花 30 秒把 80 改 200
發現根因之後,我沒有馬上做「正確」的大改。我先做了止血:
.limit(200)
一行、30 秒、31 篇文章立刻回來。
我知道 200 不是根治——今天是 111 篇,哪天寫到 201 篇,同樣的 bug 會一模一樣地再發生。但止血和根治是兩件事:31 篇文章正在對真實訪客消失,這個當下最重要的是先讓它們回來,而不是花一小時做完美的架構再上線。
權宜擴容不是罪。罪是「改完 200 就忘了」,讓它變成下一個定時炸彈。所以我在改的同時,就把根治排進了待辦——而且當天就做了。



根治:把上限這件事從前端拿掉
真正的解法是讓「一次全載」這個前提消失。我做了三件事:
1. 後端分批 RPC。 新增一個 fn_get_content_list_page,接受 limit / offset,前端改成「先載第一批 30 篇,使用者往下滑再載更多」。這樣不管總數是 111 還是 1111,前端永遠不會一次撈爆。這個 RPC 順便把全站關鍵字搜尋也做進去(包含比對舊文章的英文 slug 命名),還把「閱讀分鐘數」在資料庫端算好回傳,前端不用自己算。
2. 計數跟列表分開。 原本的計數是 pages.value.filter(...).length——用「已經載進前端的文章」去算「這個分類有幾篇」。這是雙重錯誤:它同時把「載入量」和「計數正確性」綁死在同一個上限上。只要載入被截斷,計數就跟著錯。我改成獨立的 countRows 查詢,直接問資料庫「這個 category、這個 series_slug 有幾篇」,只查兩個窄欄位,不搬整包內容。「有幾篇」和「載哪幾篇」本來就是兩個問題,不該共用一個資料來源。
3. 列表不搬整包內容。 原本列表查詢會把每篇的 content jsonb(完整文章內容)整包搬過來,光是列表頁就扛著所有文章的全文。我讓列表只回摘要、封面圖、閱讀分鐘這些扁平欄位。單批 payload 從 348954 bytes 降到 22114 bytes,大概少了 94%。



驗證:「不見的回來了」要用數字證明
修「消失的資料」有個陷阱:你很容易改完看一眼「喔文章回來了」就收工。但我要的是證明數字精確吻合,不是「看起來變多了」。
我用兩條線交叉驗證:
- SQL 計數:直接問資料庫每個分類、每個系列各有幾篇。
- Playwright 實跑:起 dev server,真的去點分類按鈕、真的往下滑載入更多,數前台顯示的數字。
修之前,分類按鈕加起來是 8+51+6 = 65,跟資料庫對不上(缺 15)。修之後變成 23+70+13 = 106,加上 5 篇已知的例外情況(跨分類的特殊文章),正好 111,跟資料庫的總數吻合到個位數。Playwright 也確認了「先載 30、載入更多再疊 60」沒有重複、沒有漏。
數字對上,我才敢說修好了。
給你帶走的三件事
- 任何前端
.limit(N)撈列表,都要先問「N 之後會怎樣」。 多數情況是靜默截斷——不報錯,只是安靜地少給你資料。這是最難查的一類 bug,因為系統每個部分看起來都正常。
- 「有幾篇」跟「載哪幾篇」是兩個問題。 別用「載進來的陣列長度」當計數,它會隨著載入上限一起錯。計數就去問資料庫要 COUNT,列表就分批載,兩者分開。
- 修完消失的資料,用資料庫層的 COUNT 逐桶比對前台顯示。 數字精確吻合才算修好,「看起來變多了」不算。可以的話用 Playwright 真的去點、去滑,模擬真實使用者的載入路徑。
那行 .limit(80) 在我的 code 裡躺了好幾年,一直到文章數悄悄越過 80 才發作。它提醒我一件事:寫死的數字不會通知你它過期了,它只會在你沒注意的時候,安靜地開始說謊。

