Postgres 中大型 Jsonb 資料的效能問題

前言
最近團隊的幾支 API 出現了效能問題,這種狀況通常都是查詢寫得不夠好造成的,不過這次遇到了一個蠻有趣的案例,出問題的這支 API 其實只做更新而已。這篇就來分享一下這次的經驗,順便看看 PostgreSQL 在更新 JSONB 的時候效能到底發生了什麼事。
背景
Technology Stack
- .NET 6 Web API
- 用 Entity Framework Core 跟資料庫溝通
- PostgreSQL
API 做的事
- 更新 domain model 裡的資料
- 更新成功後,要保留 domain model 原本的狀態
- 把原本的 model 整個序列化成 JSON
- 序列化後的 JSON 用 JSONB 型別存進資料庫
保留原始的 model 是商業需求,用 JSONB 存是最直覺的做法,不過這也連帶影響到效能,因為某些情況下原始的 model 會非常大(大概 5k)。
PostgreSQL 的 Toast 問題
PostgreSQL 儲存資料的機制是把資料切成固定大小的區塊,也就是所謂的「page」,一般是 8KB,每個 page 至少要能放進 4 個 tuple(row)。所以當資料表中的某一個 row 超過 2KB 的時候,PostgreSQL 就會啟動「TOAST」(The Oversized-Attribute Storage Technique)機制,用比較有效率的方式處理過大的資料,把這些超大的值從主要的 data page 拉出來另外存,免得單一 row 整個撐大。但 TOAST 本身也帶來額外的複雜度與 overhead,對效能的影響其實蠻大的,尤其是在更新資料的時候。想更深入了解 PostgreSQL 的 JSONB 跟 TOAST 機制、以及對效能的影響,可以看這篇 Postgres performance cliffs with large JSONB values and TOAST。
結論
雖然把整個 domain model 存下來確實會有效能上的代價,但這畢竟是商業需求,而且目前的使用頻率還在可以接受的範圍,所以我們短期內也不打算動這個設計。不過之後如果真的需要優化,比起現在這樣馬上寫進資料庫,我們比較想把封存這個步驟丟到 message queue 裡面延後處理。
JSONB 用起來確實方便,但方便歸方便,效能這塊還是得放在心上。這次的經驗也讓我們更實際地體會到,拿 JSONB 存大型資料到底要付出哪些 trade-off。