資料變多、人變多時,資料庫怎麼撐
寫給耀達 Lite 雲端進銷存的部署討論(2026-10-29)。前提:已選定 AWS 東京、Aurora PostgreSQL Serverless v2、所有店家共用資料表+Row-Level Security(第 5 題的方案 A)。
這份文件回答兩個問題:①資料累積很多時,新增與查詢會不會變慢、partitioning 與 sharding 是什麼;②幾萬人同時操作時,負載怎麼分流。
1. 先講結論
- 表變大本身不太會讓「查一家店的資料」變慢,前提是索引設計對:每個索引的第一欄放「店家編號」。資料庫找資料靠索引,索引的層數隨資料量增加得非常慢。
- Partitioning(分割表):同一台資料庫主機裡,把一張大表拆成好幾張小表,查詢時只翻相關的那幾張。它不會多出運算能力,主要幫助「大量刪除舊資料」與「超大表的維護」。
- Sharding(分片):把店家分到好幾台資料庫主機上。這才是真正多出運算能力的做法,但複雜度最高,應該是最後一步。
- 幾萬人在線,不等於資料庫同時跑幾萬個查詢。分流主要靠:應用程式多開幾個容器、連線池、快取、把報表交給唯讀副本、把耗時工作丟到背景。資料庫主機本身也能自動變大。
- 耀達 Lite 照順序走:先把索引與店家編號做對 → 加唯讀副本、快取、背景工作 → 大表分割 → 真的撐不住才分片。
2. 資料庫怎麼找資料:索引
想像一本 1 億行的帳簿。要找「陳記早餐店 10 月 20 日那張銷貨單」:
- 沒有索引:從第一行翻到最後一行,資料越多越慢,這叫「全表掃描」(sequential scan)。
- 有索引:像字典的部首目錄。PostgreSQL 預設的索引是 B-tree,一層一層往下指,幾次跳躍就找到。
B-tree 每一個節點能放幾百個指標,所以層數成長得很慢。粗估:1 億列大約 3~4 層、100 億列大約 4~5 層(實際層數看欄位大小,這是估算)。 資料從 1 千萬變成 1 億,查一筆的成本只多一點點,不是多 10 倍。
3. 資料變多,到底什麼時候才會變慢
「資料累積太多會拖慢新增與查詢」,在下面這幾種狀況才會真的發生:
| 狀況 | 為什麼會慢 | 怎麼解 |
|---|---|---|
| 索引沒有把「店家編號」放第一欄 | 例如索引只有「單據日期」,查陳記 10 月的單,資料庫會先撈出所有店家 10 月的單,再一筆筆篩掉別家。店家越多越慢。 | 複合索引 (store_id, 單據日期),見第 4 節。 |
| 常用的資料放不進記憶體 | 資料庫把常用的索引與資料放在記憶體裡。放不下時要讀磁碟,慢上很多倍。 | Aurora Serverless v2 會自動加大(記憶體跟著 ACU 變多);舊資料封存;分割表讓「最近的資料」集中。 |
| 大量刪除、大量修改 | PostgreSQL 刪除或修改時,舊的那一列不會馬上消失,要等 VACUUM(自動清理)回收。一次刪幾百萬列,清理工作會吃掉很多資源,別家店也會被拖慢。 |
分批、半夜刪;或用分割表,整個分割丟掉(第 5 節)。 |
| 跨很多資料的報表 | 「這家店三年的銷貨利潤分析」要讀很多列,本來就重;跑報表的同時,開單的人會跟它搶資源。 | 報表交給唯讀副本(第 8 節);預先彙總(每天晚上算好每月合計)。 |
| 新增越來越慢 | 每新增一列,所有索引都要跟著更新。索引越多、越大,新增越重。 | 只建真正會用到的索引;B-tree 的層數成長很慢,單純「資料多」對新增的影響有限。 |
算一下規模感(假設值,只為了抓數量級):1,000 家店、每家每天 30 張銷貨單、每張 5 行明細,一年大約是 1,000 × 30 × 5 × 365 ≈ 5,500 萬列明細。 對 PostgreSQL 來說,幾千萬到幾億列、索引設計正確的表,是常見的規模,不需要一開始就分割或分片。
4. 第一道防線:從第一天就做對的事
下面這些不花錢、不增加維運,但上線之後才補會非常痛,所以第一版就要做。
4.1 每張表都有店家編號,而且放在索引與主鍵的最前面
-- 銷貨單明細:主鍵與索引都以 store_id 開頭
CREATE TABLE sales_line (
store_id bigint NOT NULL,
id bigint NOT NULL,
sale_id bigint NOT NULL,
product_id bigint NOT NULL,
qty numeric(18,4) NOT NULL,
price numeric(18,2) NOT NULL,
PRIMARY KEY (store_id, id)
);
CREATE INDEX ON sales_line (store_id, sale_id);
CREATE INDEX ON sales_line (store_id, product_id);
為什麼主鍵也要帶 store_id:以後要做分割表或分片時,PostgreSQL 規定主鍵與唯一鍵必須包含分割的欄位(官方文件原話:constraint's columns must include all of the partition key columns)。
第一天就把 store_id 放進主鍵,將來兩條路都不用改表結構。
4.2 Row-Level Security(RLS)把關
RLS 是 PostgreSQL 內建的功能:在資料表上設一條規則「只看得到 store_id 等於目前店家的列」。應用程式每次連線時先設定「現在是哪家店」,之後的每個查詢,資料庫都會自動加上這個條件。
AWS 的指引說,共用資料表(pool)時 RLS 是必須的,因為它把隔離的責任從「每個工程師每次都要記得寫」移到資料庫本身。
4.3 其他從第一天就要守的規矩
- 不寫跨店家的 join:每個查詢只碰一家店。這樣以後分片時,每個查詢都只會落在一台主機上。
- 應用程式不在記憶體裡存使用者狀態(登入狀態放在 cookie/token 或共用快取):這樣才能隨時多開、少開容器(第 8 節)。
- 資料存取分成「寫」與「讀」兩個入口:一開始兩個入口連到同一台,以後加唯讀副本時只改設定。
- 金額、數量用
numeric,不用浮點數。
5. Partitioning(分割表)
5.1 是什麼
對應用程式來說還是同一張表(例如 sales_line),但資料庫在底下把它拆成好幾張實體的小表,每張叫一個「分割」(partition)。新增資料時,資料庫依照規則自動放進對的那一張。
比喻:原本一個大抽屜塞滿所有年份的發票,現在改成一年一個抽屜,外面貼著「2025」「2026」。找 2026 年的發票,只要開 2026 那個抽屜。
5.2 原理:分割修剪(partition pruning)
查詢的 WHERE 條件如果包含分割的欄位,PostgreSQL 在規劃查詢時會檢查每個分割的範圍,證明某些分割不可能有符合的資料,就直接跳過不讀。這就是 partition pruning,預設開啟。
5.3 三種切法
| 切法 | 規則 | 耀達 Lite 的例子 |
|---|---|---|
| RANGE(範圍) | 依某欄的範圍分,範圍不重疊。 | 銷貨明細依單據日期,一年一個分割。 |
| LIST(清單) | 明列哪些值放哪個分割。 | 依單據種類(銷貨、進貨、調整)分。 |
| HASH(雜湊) | 把欄位算出雜湊值,除以固定數字取餘數,決定放哪個分割。 | 依 store_id 雜湊成 16 份,每份大約放 1/16 的店家。 |
5.4 好處(PostgreSQL 官方文件列的)
- 常用的資料集中在少數分割時,查詢可以快很多。
- 大量刪除可以整個分割丟掉:
DROP TABLE或DETACH PARTITION一個分割,比一列一列DELETE快非常多,而且完全不產生VACUUM的清理負擔。例如 10 年前的明細封存後,整年丟掉。 - 很少用的舊資料可以搬到比較便宜的儲存。
5.5 代價與限制
- 主鍵、唯一鍵一定要包含分割欄位。依日期分割時,明細的主鍵要變成
(store_id, 單據日期, id)這種樣子;依店家雜湊時,第 4 節的(store_id, id)已經符合。 - 查詢條件沒有用到分割欄位時,修剪不了,反而要翻每一個分割。
- 分割數量不是越多越好。官方文件:查詢規劃器處理「最多幾千個分割」還算可以,前提是修剪後只剩少數幾個;剩下越多,規劃越久、越吃記憶體。
- 官方的經驗法則:表的大小超過資料庫主機的記憶體,分割才通常划算。小表分割只是增加複雜度。
- 它不會讓主機變強:所有分割都在同一台主機上,CPU、記憶體還是那一份。
跟「刪除欠費店家」的關係:依日期分割,丟掉的是「某一年所有店家」的資料,不是某一家。 想用分割秒刪某一家店,得「一家一個分割」(LIST 依店家),但幾萬家店就是幾萬個分割,超過官方建議的幾千個。所以分割表不能解決「刪單一店家」的問題,那一點還是靠半夜分批刪。
6. Sharding(分片)
6.1 是什麼
把店家分散到好幾台資料庫主機(每台叫一個 shard,分片),每台只放一部分店家的完整資料。陳記早餐店的所有資料都在第 2 台,好鄰居咖啡的都在第 1 台。
比喻:分割表是同一個倉庫裡多放幾個架子;分片是多開幾個倉庫,每個倉庫有自己的人手。
6.2 原理:先查路由,再連對的主機
6.3 兩種做法
做法一:自己做(多套共用資料表+路由表)
- 開第 2 套、第 3 套 Aurora,每套都是第 5 題方案 A 的「共用資料表+RLS」。
- 另外放一張小小的路由表:哪家店在哪一套。新註冊的店放到比較空的那套。
- 應用程式拿到請求後,先查路由表(通常會快取起來),再用對應的連線。
- 好處:每一套都是普通的 Aurora,好懂、好查。代價:要自己寫路由;要把已經在用的大店搬到另一套時,得自己搬資料、切換、清掉舊的(AWS 的決策表把 pool 的搬遷評為「費工」)。
做法二:Aurora PostgreSQL Limitless Database(AWS 代管的分片)
- AWS 把分片包起來:前面一層叫 router(路由節點),接收 SQL、記得資料在哪、把命令送到後面的 shard(分片節點),再把結果合起來回傳;對應用程式來說還是一個資料庫。
- 建表時指定 shard key(分片鍵),耀達 Lite 就是
store_id。同一個分片鍵的值一定放在同一個分片(雜湊分法)。 - 幾種表:sharded table(分散到各分片,例如單據)、collocated(兩張分片表用同一個分片鍵、同一家店的資料放一起,join 只在一個分片內做,例如單據頭與明細)、reference table(每個分片都有一份完整複本,適合很少改的共用資料,例如郵遞區號、銀行清單)。
- 東京有,台北沒有(官方:除了台北以外的所有地區)。
- 代價:它是給非常大規模用的,起步門檻與費用比一般 Aurora 高(沒試算);有自己的限制(例如只支援特定版本與 I/O-Optimized 儲存),導入前要逐條讀限制。耀達 Lite 在初期用不到。
6.4 分片的通用代價
- 跨分片的查詢很難:例如營運方(耀達)想看「全部店家本月營業額排名」,要到每一台各問一次再合起來。所以跨店統計通常另外複製一份到報表用的資料庫。
- 搬家(rebalance):某台太滿,要把一部分店家搬走,過程要停寫或做同步,是分片最麻煩的地方。
- 跨分片的交易:一筆交易同時改兩台的資料,要特別處理。耀達 Lite 只要遵守「一個查詢只碰一家店」,就不會遇到。
7. 分割表與分片的比較
| Partitioning 分割表 | Sharding 分片 | |
|---|---|---|
| 拆在哪裡 | 同一台主機內,一張表拆成多張 | 多台主機,每台放一部分店家 |
| 多出運算能力? | 沒有 | 有,台數越多力氣越大 |
| 解決什麼 | 超大表的查詢範圍、整批刪除舊資料、維護(清理、索引重建) | 單台主機的 CPU、記憶體、寫入量到頂 |
| 應用程式要改嗎 | 幾乎不用(還是同一張表),但主鍵要含分割欄位 | 自己做:要加路由;Limitless:大致不用,但建表方式不同 |
| 複雜度 | 中 | 高 |
| 什麼時候做 | 某張表大到超過主機記憶體,或要定期整批丟掉舊資料 | 主機已經開到最大、加了唯讀副本與快取,寫入還是撐不住 |
8. 幾萬人同時操作:負載怎麼分流
8.1 先算清楚「同時」是多少
「幾萬人在線」跟「資料庫同時在跑幾萬個查詢」差非常多。人在畫面上看、想、打字的時間,資料庫是閒著的。有個排隊理論的公式叫利特爾法則(Little's Law):
同時在處理的數量 = 每秒進來的數量 × 每個要花的時間
假設(只是示範數字):3 萬人在線,平均每人 10 秒按一次會打到伺服器的動作,每個動作在資料庫平均花 5 毫秒:
- 每秒進來:30,000 ÷ 10 = 3,000 個請求
- 資料庫同時在處理:3,000 × 0.005 秒 = 約 15 個查詢
重點:決定負載的是「每秒幾個請求」和「每個請求多重」,不是在線人數。所以分流的方向有兩個:讓每個請求變輕、讓請求分散到不同地方處理。
8.2 整體架構:一層一層分攤
8.3 每一層在做什麼
①CDN(CloudFront):耀達 Lite 的畫面檔案(HTML、JavaScript、CSS)每個人都一樣,放在 CDN,店家打開網頁時從最近的節點拿,伺服器完全不用處理。這一層就擋掉大部分的流量。
②③負載平衡器+多個容器:程式跑在好幾個一模一樣的容器裡,負載平衡器把請求輪流分給它們。人多時 ECS 自動多開容器,人少時關掉。 這叫水平擴充:不是把一台變大,而是多開幾台一樣的。前提是第 4.3 節說的「容器不存使用者狀態」,否則請求被分到別的容器就會掉登入。
④快取:商品清單、商品類別、預設項目這類「開單時一直讀、很少改」的資料,第一次從資料庫讀出來後放進快取(記憶體資料庫,例如 Redis),下次直接從快取拿。 快取的難處是「資料改了要記得清掉舊的」,所以只放常讀少改、而且晚幾秒更新也沒關係的東西。庫存數量、金額不放快取,一律以資料庫為準。
⑤背景工作:成本計價、匯出 Excel、寄通知信這些花時間的事,不在店家按下按鈕的當下做完,而是丟一張「工作單」到佇列,由背景容器慢慢做,做完再通知。 這樣店家不用等,資料庫的尖峰也被攤平。
⑥連線池(RDS Proxy):每一條 PostgreSQL 連線都會佔用資料庫主機的記憶體。容器一多,如果每個容器各自開幾十條連線,資料庫可能還沒忙,就先被連線數拖垮。 RDS Proxy 擋在中間,讓所有容器共用一池數量固定的連線,用完就還,給下一個請求用。東京有,台北沒有(這也是第 3 題選東京的理由之一)。
⑦讀寫分離:Aurora 一套叢集裡有一台主機負責寫,另外可以加最多 15 台唯讀副本。所有機器讀同一份儲存,副本跟主機的延遲通常遠低於 0.1 秒(AWS 文件)。
- 寫:開單、存檔、改資料,一律到主機(writer endpoint)。
- 讀:報表、清單、搜尋,交給副本(reader endpoint,Aurora 會把連線分到各台副本)。
- 注意:剛存完馬上要顯示的畫面(例如存檔後打開那張單),要讀主機,不然可能讀到幾十毫秒前的舊資料。
另外,Aurora Serverless v2 的主機本身會依負載自動變大變小,最大到 384 ACU(約 768 GiB 記憶體,PostgreSQL 版本要夠新)。這叫垂直擴充,是最省事的一招,在加副本之前通常先靠它撐。
⑧分片:寫入量大到主機開到最大還是不夠,才分片(第 6 節)。ERP 的特性是「讀多寫少」,大部分壓力在前面幾層就能分掉。
沒查證、要提醒的:上面的數字(每人 10 秒一個動作、每個動作 5 毫秒、一張單 5 行)都是示範用的假設。耀達 Lite 實際的請求量、每個查詢多重,要等程式寫出來之後用壓力測試量。 各層的月費也還沒試算。
9. 耀達 Lite 的分階段路線
店家數只是抓個方向,實際以監控數字(CPU、記憶體、查詢時間)決定什麼時候進下一階段。
| 階段 | 做什麼 | 什麼訊號出現時進下一階段 |
|---|---|---|
| 第一版上線 (試用到幾百家) |
|
主機常常自動加大到高檔;報表跑的時候開單變慢 |
| 第二階段 (幾千家) |
|
主機開到很大,寫入仍是瓶頸;或單一套的資料量、備份還原時間大到不舒服 |
| 第三階段 (上萬家) |
|
— |
一句話:第一天只要把「店家編號」放對位置、程式寫成可以多開,後面每一階段都是加東西,不用拆掉重來。
10. 名詞對照
| 名詞 | 白話 |
|---|---|
| 索引(index)、B-tree | 資料表的目錄,一層層往下指,幾次就找到 |
| 全表掃描(sequential scan) | 從頭翻到尾 |
| VACUUM | PostgreSQL 回收刪除、修改後留下的舊列的清理工作 |
| Partitioning 分割表 | 同一台主機內,一張表在底下拆成多張 |
| Partition pruning 分割修剪 | 查詢時跳過不可能有資料的分割 |
| Sharding 分片、shard | 資料分散到多台主機,每台是一個分片 |
| Shard key 分片鍵 | 決定資料放哪一台的欄位,耀達 Lite 是店家編號 |
| Rebalance | 把一部分店家從擠的主機搬到空的主機 |
| 垂直擴充 | 把同一台變大 |
| 水平擴充 | 多開幾台一樣的 |
| 讀寫分離、writer/reader | 寫入給主機,讀取給唯讀副本 |
| 連線池(connection pool) | 一群共用、輪流使用的資料庫連線 |
| 快取(cache) | 把常讀的資料先放在更快的地方 |
| 佇列(queue) | 排隊等背景處理的工作單 |
| CDN | 分散在各地、就近回應靜態檔案的節點網路 |
| ACU | Aurora Serverless 的容量單位;1 ACU 約 2 GiB 記憶體加對應的 CPU |
11. 來源
- PostgreSQL 官方文件:Table Partitioning(三種切法、partition pruning、主鍵必須含分割欄位、刪除分割免 VACUUM、分割數量建議、表大於記憶體才划算)
- AWS:多租戶 PostgreSQL 決策表(pool 的查詢效能、搬遷難度)
- AWS:多租戶 PostgreSQL 最佳做法(pool 必須用 RLS)
- AWS:Aurora Replicas(最多 15 台唯讀副本、延遲通常遠低於 100 毫秒)
- AWS:How Aurora serverless works(PostgreSQL 最大 384 ACU、版本與平台版本要求)
- AWS:Aurora Serverless v2 可以縮到 0、約 15 秒恢復
- AWS:Aurora PostgreSQL Limitless Database 架構(router、shard、shard key、collocated、reference table)
- AWS:Limitless 支援地區(除了台北以外都有)
- AWS:RDS Proxy 支援地區(東京有、台北沒有)
- AWS:ECS Express Mode(Fargate+負載平衡+自動擴充)
B-tree 層數、規模感與利特爾法則的數字是估算與示範假設,不是量測結果。