iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0

「用什麼資料庫」這個問題,預設答案通常是 PostgreSQL。但預設答案沒有回答我真正的問題:我的瓶頸在哪裡?

選工具之前先量瓶頸,順序反過來的話,你只是在抄別人的答案。

先量瓶頸,再選工具

採集一篇財報的流程是四步:查公告清單 → 下載 PDF → 解析成 markdown → 寫一列索引。

把時間畫成一條線,前三步吃掉的時間是壓倒性的多數:下載要等來源回應,解析是 CPU 密集的工作,兩者都是「秒」級的。而「寫一列」是十幾個短欄位,內容還不含正文(正文寫成檔案,資料庫只存路徑)。

我的結論是:採集系統的瓶頸在網路 IO 和來源的流量限制,不在資料庫寫入。

既然瓶頸不在資料庫,選一個要另外維運的資料庫,就是自己找事做。

多個市場同時採集,SQLite 撐得住嗎?

這是我最常被問的問題。我的做法是「一個市場一支獨立的採集程式、各自一個行程,同時寫同一個 SQLite 檔」。

SQLite 的寫入確實是單寫鎖——同時間只有一個寫者。聽起來很糟,但配上 WAL 模式之後就不是問題:

  • 預設的 rollback journal 模式:寫入時要鎖住整個資料庫,讀取也要排隊。
  • WAL(Write-Ahead Logging)模式:寫入是 append 到一個 sidecar 檔,讀取和寫入可以並行,不會整庫鎖住。

db.py 裡的 get_conn() 就是這幾行:

conn = sqlite3.connect(DB_PATH, timeout=30)  # 30s 鎖等待, 支援多市場並行寫
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")

兩個細節值得講:

一、timeout=30 這是「搶不到鎖時願意等多久」。並行寫入的場景下,等 30 秒比直接拋錯合理得多——因為我知道對面那個寫者大概幾毫秒後就會放手。

二、WAL 是持久化設定,但程式還是每次連線檢查一次。 PRAGMA journal_mode=WAL 一旦設定就會寫進資料庫檔本身,之後每次連線都是 WAL 模式。那為什麼還要在程式裡跑一次?因為防的是「第一次」——資料庫剛建立、或從別的機器同步過來的時候。而且如果兩個行程同時在切換模式,其中一個會拿到 database is locked;所以那段包了 try/except,遇到就忽略,等下次連線再說。這是真的會發生的情況,不是防禦性寫好看的。

那把鎖真的不會成為瓶頸嗎?

粗估一下量級就好:一篇財報從下載到解析完是級,寫一列索引是毫秒級。就算三個市場同時在跑,寫入通道的利用率也還不到 1%。

這就是為什麼「單寫鎖」在我的場景裡不是問題:不是鎖不存在,是我的工作負載根本排不到那條隊伍。

反過來說,如果我做的是每秒幾百上千次寫入的東西,SQLite 會立刻爆掉,這沒什麼好辯的。

什麼時候我會換掉它

我給自己畫了三條線,任一條成立就換 PostgreSQL:

  1. 寫入頻率到每秒幾百上千次。 財報採集到不了——一家公司一年就那幾份文件。
  2. 需要跨機器同時寫入。 現在是「一台機器寫、很多地方讀」,存取模式很單純。
  3. 需要真正的多使用者交易與權限控制。 現在寫入端只有我自己一個人。

還有一個隱藏的理由:D1 本身就是 SQLite。

本地用 SQLite、雲端用 D1,表結構可以原樣搬過去。如果本地是 PostgreSQL、雲端是 D1,那份 schema 就得手寫兩次,而且兩邊的型別、日期處理、函式都不一樣,遲早會漂移。選 SQLite 有一半是為了「本地和雲端講同一種語言」。

誠實的缺點

SQLite 沒有網路層。這代表雲端那份資料不會自己出現——我得靠一支同步程式把它推上去。

這個「一份資料、兩個地方」的代價,平常看不出來,但它在刪除這件事上咬了我一口:本地清掉的東西,雲端不會跟著清。

明天(Day 5)講資料模型的時候,我會把這個洞跟補法一起寫出來——它也是那張表上唯一一個「為了補洞而存在」的設計。


上一篇
免費檔能撐多少?把雲端成本算給你看
下一篇
資料模型:一張 documents 表背後的取捨
系列文
30 天打造東亞財報全文 API:PDF 解析、亂碼修復、章節切分到 MCP 上線實錄5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言