資料變多、人變多時,資料庫怎麼撐

寫給耀達 Lite 雲端進銷存的部署討論(2026-10-29)。前提:已選定 AWS 東京、Aurora PostgreSQL Serverless v2、所有店家共用資料表+Row-Level Security(第 5 題的方案 A)。
這份文件回答兩個問題:①資料累積很多時,新增與查詢會不會變慢、partitioning 與 sharding 是什麼;②幾萬人同時操作時,負載怎麼分流。

1. 先講結論

2. 資料庫怎麼找資料:索引

想像一本 1 億行的帳簿。要找「陳記早餐店 10 月 20 日那張銷貨單」:

根:店家 1~9999 店家 1~3000 店家 3001~6000 店家 6001~9999 店家 4128 9 月 店家 4128 10 月 橘色是實際走的路:根 → 中間層 → 葉子,3 次跳躍就到
B-tree 索引:每一層把範圍切細,資料量增加 100 倍,大約只多 1 層。

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 放進主鍵,將來兩條路都不用改表結構。

索引 (日期):先找出 10/20 全部店家的單 10/20 店家0001 … 店家0002 … 陳記 … 店家0004 … 店家9999(全部要看過,再丟掉別家) 索引 (store_id, 日期):直接跳到「陳記」那一段 陳記 10/19 陳記 10/20 ← 只讀這裡 陳記 10/21 欄位順序決定索引怎麼排;店家編號在前,每家的資料在索引裡是連在一起的。
同樣兩個欄位,順序不同,查詢要讀的量差很多。

4.2 Row-Level Security(RLS)把關

RLS 是 PostgreSQL 內建的功能:在資料表上設一條規則「只看得到 store_id 等於目前店家的列」。應用程式每次連線時先設定「現在是哪家店」,之後的每個查詢,資料庫都會自動加上這個條件。 AWS 的指引說,共用資料表(pool)時 RLS 是必須的,因為它把隔離的責任從「每個工程師每次都要記得寫」移到資料庫本身。

4.3 其他從第一天就要守的規矩

5. Partitioning(分割表)

5.1 是什麼

對應用程式來說還是同一張表(例如 sales_line),但資料庫在底下把它拆成好幾張實體的小表,每張叫一個「分割」(partition)。新增資料時,資料庫依照規則自動放進對的那一張。

比喻:原本一個大抽屜塞滿所有年份的發票,現在改成一年一個抽屜,外面貼著「2025」「2026」。找 2026 年的發票,只要開 2026 那個抽屜。

5.2 原理:分割修剪(partition pruning)

查詢的 WHERE 條件如果包含分割的欄位,PostgreSQL 在規劃查詢時會檢查每個分割的範圍,證明某些分割不可能有符合的資料,就直接跳過不讀。這就是 partition pruning,預設開啟。

SELECT … WHERE 單據日期 ≥ 2026-10-01 應用程式看到的:一張 sales_line 2024 分割 跳過 2025 分割 跳過 2026 分割 只讀這張 2027 分割 跳過 全部都在同一台資料庫主機裡;省的是「要翻的量」,不是多出一台主機的力氣。
Partition pruning:條件用到分割欄位時,無關的分割整張跳過。

5.3 三種切法

切法規則耀達 Lite 的例子
RANGE(範圍)依某欄的範圍分,範圍不重疊。銷貨明細依單據日期,一年一個分割。
LIST(清單)明列哪些值放哪個分割。依單據種類(銷貨、進貨、調整)分。
HASH(雜湊)把欄位算出雜湊值,除以固定數字取餘數,決定放哪個分割。依 store_id 雜湊成 16 份,每份大約放 1/16 的店家。

5.4 好處(PostgreSQL 官方文件列的)

5.5 代價與限制

跟「刪除欠費店家」的關係:依日期分割,丟掉的是「某一年所有店家」的資料,不是某一家。 想用分割秒刪某一家店,得「一家一個分割」(LIST 依店家),但幾萬家店就是幾萬個分割,超過官方建議的幾千個。所以分割表不能解決「刪單一店家」的問題,那一點還是靠半夜分批刪。

6. Sharding(分片)

6.1 是什麼

把店家分散到好幾台資料庫主機(每台叫一個 shard,分片),每台只放一部分店家的完整資料。陳記早餐店的所有資料都在第 2 台,好鄰居咖啡的都在第 1 台。

比喻:分割表是同一個倉庫裡多放幾個架子;分片是多開幾個倉庫,每個倉庫有自己的人手。

6.2 原理:先查路由,再連對的主機

應用程式 「陳記要開銷貨單」 路由表(哪家在哪台) 好鄰居咖啡 → 第 1 台 陳記早餐店 → 第 2 台 阿美麵包坊 → 第 3 台 ① 先問在哪台 第 1 台資料庫主機 店家 A 組的全部資料 第 2 台資料庫主機 店家 B 組(含陳記) 第 3 台資料庫主機 店家 C 組的全部資料 每台有自己的 CPU 與記憶體:三台就有三倍的力氣。陳記的請求只會碰第 2 台。
Sharding:先查「這家店在哪台」,再把查詢送過去。

6.3 兩種做法

做法一:自己做(多套共用資料表+路由表)

做法二:Aurora PostgreSQL Limitless Database(AWS 代管的分片)

6.4 分片的通用代價

7. 分割表與分片的比較

Partitioning 分割表Sharding 分片
拆在哪裡同一台主機內,一張表拆成多張多台主機,每台放一部分店家
多出運算能力?沒有有,台數越多力氣越大
解決什麼超大表的查詢範圍、整批刪除舊資料、維護(清理、索引重建)單台主機的 CPU、記憶體、寫入量到頂
應用程式要改嗎幾乎不用(還是同一張表),但主鍵要含分割欄位自己做:要加路由;Limitless:大致不用,但建表方式不同
複雜度中高
什麼時候做某張表大到超過主機記憶體,或要定期整批丟掉舊資料主機已經開到最大、加了唯讀副本與快取,寫入還是撐不住

8. 幾萬人同時操作:負載怎麼分流

8.1 先算清楚「同時」是多少

「幾萬人在線」跟「資料庫同時在跑幾萬個查詢」差非常多。人在畫面上看、想、打字的時間,資料庫是閒著的。有個排隊理論的公式叫利特爾法則(Little's Law):

同時在處理的數量 = 每秒進來的數量 × 每個要花的時間

假設(只是示範數字):3 萬人在線,平均每人 10 秒按一次會打到伺服器的動作,每個動作在資料庫平均花 5 毫秒:

重點:決定負載的是「每秒幾個請求」和「每個請求多重」,不是在線人數。所以分流的方向有兩個:讓每個請求變輕、讓請求分散到不同地方處理。

8.2 整體架構:一層一層分攤

店家的瀏覽器(台灣各地) ① CloudFront(CDN):畫面的 HTML、JS、CSS、圖片 這些檔案在離店家最近的節點就回應,完全不到伺服器 ② 負載平衡器(Application Load Balancer) 把 API 請求平均分給後面的容器 ③ 容器 1(ASP.NET Core) 容器 2 容器 N(自動增減) ④ 快取(ElastiCache) 商品清單、設定這類常讀少改的資料 ⑤ 佇列(SQS)+背景工作容器 成本計價、Excel 匯出、寄信 ⑥ RDS Proxy(連線池) 所有容器共用一池資料庫連線,不讓連線數爆掉 ⑦ Aurora 主機(writer) 開單、存檔等所有「寫」,自動加大 唯讀副本(reader)× 1~15 報表、清單查詢等「讀」 ⑧ 以上都撐不住時,才把 ⑦ 變成好幾套(分片,第 6 節)
由上往下,每一層都先擋掉一部分工作,真正到資料庫主機的只剩必要的讀寫。

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 文件)。

另外,Aurora Serverless v2 的主機本身會依負載自動變大變小,最大到 384 ACU(約 768 GiB 記憶體,PostgreSQL 版本要夠新)。這叫垂直擴充,是最省事的一招,在加副本之前通常先靠它撐。

⑧分片:寫入量大到主機開到最大還是不夠,才分片(第 6 節)。ERP 的特性是「讀多寫少」,大部分壓力在前面幾層就能分掉。

沒查證、要提醒的:上面的數字(每人 10 秒一個動作、每個動作 5 毫秒、一張單 5 行)都是示範用的假設。耀達 Lite 實際的請求量、每個查詢多重,要等程式寫出來之後用壓力測試量。 各層的月費也還沒試算。

9. 耀達 Lite 的分階段路線

店家數只是抓個方向,實際以監控數字(CPU、記憶體、查詢時間)決定什麼時候進下一階段。

階段做什麼什麼訊號出現時進下一階段
第一版上線
(試用到幾百家)
  • 一套 Aurora Serverless v2,最低容量留一點(不縮到 0,避免早上第一個人等 15 秒)
  • 第 4 節全部做到:store_id 在主鍵與索引最前面、RLS、不跨店查詢、容器不存狀態、讀寫兩個入口
  • CDN、負載平衡器、自動擴充的容器(Express Mode 已包含後兩者)
  • RDS Proxy
主機常常自動加大到高檔;報表跑的時候開單變慢
第二階段
(幾千家)
  • 加 1~2 台唯讀副本,報表與清單改讀副本
  • 快取常讀少改的資料
  • 成本計價、匯出改成背景工作
  • 最大的幾張表(單據明細)超過主機記憶體時,依日期分割;舊年份封存
主機開到很大,寫入仍是瓶頸;或單一套的資料量、備份還原時間大到不舒服
第三階段
(上萬家)
  • 分片:新店家放第二套 Aurora(自己做路由),或評估 Aurora Limitless
  • 跨店統計另外複製到報表用的資料庫
  • 特別大的店可以單獨一套
—

一句話:第一天只要把「店家編號」放對位置、程式寫成可以多開,後面每一階段都是加東西,不用拆掉重來。

10. 名詞對照

名詞白話
索引(index)、B-tree資料表的目錄,一層層往下指,幾次就找到
全表掃描(sequential scan)從頭翻到尾
VACUUMPostgreSQL 回收刪除、修改後留下的舊列的清理工作
Partitioning 分割表同一台主機內,一張表在底下拆成多張
Partition pruning 分割修剪查詢時跳過不可能有資料的分割
Sharding 分片、shard資料分散到多台主機,每台是一個分片
Shard key 分片鍵決定資料放哪一台的欄位,耀達 Lite 是店家編號
Rebalance把一部分店家從擠的主機搬到空的主機
垂直擴充把同一台變大
水平擴充多開幾台一樣的
讀寫分離、writer/reader寫入給主機,讀取給唯讀副本
連線池(connection pool)一群共用、輪流使用的資料庫連線
快取(cache)把常讀的資料先放在更快的地方
佇列(queue)排隊等背景處理的工作單
CDN分散在各地、就近回應靜態檔案的節點網路
ACUAurora Serverless 的容量單位;1 ACU 約 2 GiB 記憶體加對應的 CPU

11. 來源

B-tree 層數、規模感與利特爾法則的數字是估算與示範假設,不是量測結果。