直接撈線上資料庫不行嗎?OLTP 與 OLAP 的殘酷 SQL 對決
老闆同一個提問,直接查線上交易資料庫要串五張表、現場算金額;星形架構三個 JOIN 收工。用一場 SQL 對決看懂 OLTP 與 OLAP 的差異
看完上一篇星形架構的優勢後,很多沒有開發過資料倉儲或數據中台的人可能會問:
「我直接從現有的線上交易資料庫撈資料不行嗎?為什麼要這麼麻煩,重新建一套事實表和維度表?」
我在只有 OLTP 線上系統的架構下,肩負建立數據中台的任務。這類問題不斷被提起:我該怎麼視覺化地呈現與解釋?這個起心動念,讓我寫下這篇文章——讓兩種架構同台對決:老闆的同一個提問,各寫一段 SQL,看差在哪裡。
比結構:蜘蛛網 vs. 星星狀
先看這兩種架構在資料庫裡長什麼樣子:
- OLTP(線上交易系統):為了確保每筆資料只存一次(正規化),資料表就像一張蜘蛛網,被拆得非常細碎。
- OLAP(數據中台/分析系統):為了讓報表好查詢,資料表被重組成星形架構,所有分析角度都圍繞著中間的「事實表」。

接著出題。這是一個每個月都會出現的真實商業提問:
🎯 老闆的提問:「2026 年 8 月,每個城市、每個商品類別的銷售金額是多少?」
我們來看看,兩位選手分別要怎麼寫這段 SQL。
選手一:星形架構的優雅解法(數據中台)
在數據中台中,事實表(Fact Table)與維度表(Dimension Table)已經預先整理好:
- 銷售事實表(fact_sales):已經算好 amount,並備妥關聯鑰匙。
- 日期維度(dim_date):含有年份(year)與月份(month)。
- 客戶維度(dim_customer):含有城市(city)。
- 商品維度(dim_product):含有類別(category)。
還記得上一篇「小明買咖啡豆」的那筆訂單嗎?它在數據中台長這樣:
銷售事實表 fact_sales(一筆訂單=一列數字)
| date_id | customer_id | product_id | amount |
|---|---|---|---|
| 20260815 | C001 | P001 | 1000 |
客戶維度 dim_customer
| customer_id | name | city |
|---|---|---|
| C001 | 小明 | 台北 |
商品維度 dim_product
| product_id | name | category |
|---|---|---|
| P001 | 咖啡豆 | 食品 |
日期維度 dim_date
| date_id | year | month |
|---|---|---|
| 20260815 | 2026 | 8 |
💻 星形架構的 SQL 語法:
SELECT
c.city,
p.category,
SUM(f.amount) AS total_sales_amount
FROM fact_sales f
JOIN dim_date d
ON f.date_id = d.date_id
JOIN dim_customer c
ON f.customer_id = c.customer_id
JOIN dim_product p
ON f.product_id = p.product_id
WHERE d.year = 2026
AND d.month = 8
GROUP BY
c.city,
p.category
ORDER BY
c.city,
p.category;
✅ 一句話理解:從 fact_sales 拿準備好的銷售金額,用三張維度表分別決定「時間、城市、商品類別」,然後直接加總。邏輯清晰,一目瞭然。
選手二:直接查線上系統的硬派解法(OLTP)
線上交易系統為了維持資料的一致性,通常採用高度「正規化」的設計。同一筆訂單,被拆進五張表:
- customers(客戶表)
- products(商品表)
- product_categories(商品類別表,與商品分開)
- orders(訂單主檔)
- order_items(訂單明細)
線上系統有些時候沒有直接存 amount,金額必須現場計算:數量(quantity)× 單價(unit_price)。
小明那筆訂單,在線上系統裡長這樣:
customers(客戶表)
| customer_id | name | city |
|---|---|---|
| C001 | 小明 | 台北 |
orders(訂單主檔)
| order_id | order_date | status | customer_id |
|---|---|---|---|
| O001 | 2026-08-15 | PAID | C001 |
order_items(訂單明細)
| order_id | product_id | quantity | unit_price |
|---|---|---|---|
| O001 | P001 | 2 | 500 |
products(商品表)
| product_id | name | category_id |
|---|---|---|
| P001 | 咖啡豆 | CAT1 |
product_categories(商品類別表)
| category_id | category_name |
|---|---|
| CAT1 | 食品 |
💻 正規化資料庫的 SQL 語法:
SELECT
c.city,
pc.category_name,
SUM(oi.quantity * oi.unit_price) AS total_sales_amount
FROM orders o
JOIN customers c
ON o.customer_id = c.customer_id
JOIN order_items oi
ON o.order_id = oi.order_id
JOIN products p
ON oi.product_id = p.product_id
JOIN product_categories pc
ON p.category_id = pc.category_id
WHERE o.order_date >= DATE '2026-08-01'
AND o.order_date < DATE '2026-09-01'
AND o.status = 'PAID'
GROUP BY
c.city,
pc.category_name
ORDER BY
c.city,
pc.category_name;
你發現了嗎?同一個問題,這個查詢要多做三件事:
- 把五張表串起來(JOIN 四次)
- 自己處理時間區間的過濾,還要記得排除未付款(status = 'PAID')
- 在查詢的當下現場計算營收(quantity × unit_price)
資深工程師看到這裡也許會說:「這段 SQL 我十分鐘就寫完了,哪裡複雜?」沒錯——複雜的從來不是寫一次,而是每張報表、每個 API 都重寫一次,而且各寫各的。

剛解題成功只是開端:老闆的下一個維度需求馬上就到
老闆看著報表說:「很好,那再加上『每個門市』。」
星形架構這邊,SQL 只改兩行——因為「門市」這個分析角度,早就是一張準備好的維度表(還記得上一篇星形圖裡的 dim_store 台北信義店嗎):
-- 原查詢只要加一個 JOIN、一個欄位
JOIN dim_store s
ON f.store_id = s.store_id
...
GROUP BY c.city, p.category, s.store_name
OLTP 這邊呢?先想清楚 store_id 存在 orders 還是 order_items、門市名稱要再 JOIN 哪張表,然後重寫查詢、重新測試。
如果你把查詢包成了 API,這就是再開發一個新 endpoint(API 底下的一條路徑,對應一種查法)——
下週老闆說「改成看每週趨勢」「只看金卡會員」,每一句話,都是一個新的 endpoint。

API 一題一答;維度建模一次建好,老闆之後的每一次追問,都只是換一個 GROUP BY。星形架構回答的不是「這一題」,是「這一類題」。
直接查線上資料庫的「五大災難」
小系統、臨時查詢直接查線上資料庫當然可以。但當企業規模變大、報表變多時,就會衍生以下痛點:
- JOIN 太多,查詢複雜易錯:報表開發者必須非常熟悉訂單主檔、明細、客戶、商品表的關聯。少 JOIN 一張或條件寫錯,數據就全錯了。
- 業務規則四處散落:「銷售金額」到底怎麼算?要扣折扣嗎?含稅嗎?排除退貨嗎?退貨要扣在下單那個月,還是退貨那個月?這一整組「到底怎麼算」的規則,如果每張報表、每個 API 都自己寫一遍 SUM(quantity * unit_price),最後全公司的數據一定對不起來——財務和業務儀表板報出兩個數字,兩邊 SQL 都沒寫錯,只是各自解讀了一套規則。
- 拖垮線上系統效能:線上資料庫(OLTP)的主要任務是處理客人下單、付款、更新庫存。讓報表在上面跑大量複雜的彙整與運算,非常容易導致線上交易系統卡頓甚至當機。
- 歷史分類規則難以追溯:咖啡豆今天屬於「食品」,明天改分類到「飲品沖泡」。報表到底要看交易當時的分類,還是現在的?如果線上資料庫沒有設計歷史對應、又沒有數據中台的歷史維度設計,這根本無解。
- AI 與 API 開發困難:當 AI 模型或 API 只是要調用一段簡單的銷售數據,背後卻得執行一大串複雜的 JOIN 和業務邏輯,既沒效率又難維護。
查 secondary、包 API、建 view:三個聰明方案為什麼都差一步?
有經驗的工程師還會提出一個聰明的替代方案:「我查唯讀副本(read replica)就好了啊——或者我們公司已經做了 primary/secondary 讀寫分離,報表都查 secondary,不就不會拖垮線上系統了嗎?」
沒錯,副本確實解掉了第三個災難——分析負載不再打到線上主庫。但也只有這一項。副本上的資料仍然是那張蜘蛛網:JOIN 還是一樣多、業務規則還是散落在每張報表裡、歷史狀態還是無法追溯。Replica 複製的是資料,不是模型。
第二個方案更進一步:「那我把常用查詢包成 API,統一打在 secondary 上,業務規則不就集中了嗎?」——API 集中化的是程式碼,不是資料模型。每一個新的分析角度、每一次老闆改需求,都是一個新 endpoint,資料 API 團隊很快就會變成全公司取數的排隊窗口。
這裡的差別,其實是「什麼時候算」:
| 差異點 | API(蓋在 OLTP 上) | 數據中台 |
|---|---|---|
| 規則什麼時候套 | 查詢當下,每查一次算一次 | 排程時算好一次(常見 T+1),所有人共用 |
| 查詢當下在做什麼 | 找表 → JOIN → 套規則 → 算金額,全包 | 挑維度 → 加總 |
| 規則放在哪 | 各個 endpoint 的程式碼裡,各寫各的 | 事實表欄位裡(金額已算好),寫死一次 |
API 把「算」這件事留在每一次查詢;數據中台把它提前到排程,一次算完給所有人用。

第三個方案最接近答案:「我用 view 來撰寫,把 JOIN 和計算規則寫在裡面呢?」——view 確實把邏輯集中了,但每次查詢仍然要在線上引擎現場算那一串 JOIN;如果你的資料庫為了效能開始建 materialized view(把查詢結果先算好、存成實體表)、加排程刷新,恭喜,你已經在手工打造一座小型的數據中台了。
Replica 解決「在哪裡查」,API 解決「誰來寫」,view 解決「寫在哪」——只有維度建模解決「還要再寫幾次」。
你會發現,這三個聰明的替代方案每多走一步,離數據中台就更近一步。差別只在,有沒有把它當成正式工程來蓋(版本控制、依賴管理、測試、歷史設計)。
問題從來不在 API 本身。對外交付數據,最後往往還是走 API——真正的差別在 API 蓋在哪裡。
蓋在 OLTP 蜘蛛網上,每個 endpoint 都要手刻一條 JOIN 路徑、規則各解讀各的;蓋在數據中台上,事實表與維度表已經備妥,endpoint 只剩下「組裝」——選維度、加總、輸出,又薄又快,全公司的算法還一致。
數據中台不是取代 API,是讓每一支 API 從手刻變組裝。
你們公司的報表現在是怎麼查的——直接查主庫、查讀寫分離的 secondary,還是已經有獨立的資料倉儲/數據中台?歡迎留言或回信告訴我,後續能寫文章回答大家的提問。
總結:兩者的核心差異比較
| 面向 | 直接查線上資料庫(OLTP,正規化) | 星形架構(OLAP,事實表+維度表) |
|---|---|---|
| 主要目的 | 支援交易系統,確保寫入與更新正確 | 支援分析與報表,確保查詢直覺快速 |
| 查詢方式 | 大量交易表與細節表互相 JOIN | 單一事實表 JOIN 幾張維度表 |
| 業務規則 | 寫在報表 SQL 裡,由開發者各自解讀 | 在數據中台/倉儲集中處理與定義 |
| 指標計算 | 查詢時「現場」計算(如:數量 × 單價) | 事實表中通常已先「標準化」算好 |
| 效能風險 | 跑大報表極易拖垮線上交易系統 | 分析負載與交易系統分離,互不干擾 |
| 資料即時性 | 即時,查到的就是當下狀態 | 依排程批次更新(常見 T+1),略有延遲 |
| 歷史管理 | 較難處理分類與主檔變更的問題 | 可利用「維度版本」輕鬆追溯歷史狀態 |
| 供 AI/API 使用 | 架構極度複雜,容易導致規則不一致 | 架構穩定、語意清晰,隨取即用 |
💡 一句話總結:事實表、維度表與星形架構的真正價值,就是把複雜的「清理、關聯、計算」工作在數據中台先做掉,讓後面的報表、API、AI 不用每次都從交易資料庫重新痛苦地拆解一次。
當然,數據中台也不是免費的午餐——要投入多少人力、什麼時候才值得建,是另一個完整的題目,之後再來談。
那咖啡豆改了分類、客戶搬家換了城市,報表到底該看「當時」還是「現在」?線上資料庫系統會怎麼設計?如果沒有這樣的設計,數據中台有沒有辦法解?下一篇,我們再來分析。
延伸閱讀
如果你也走在自己的路上,歡迎訂閱《小穗步電子報》——我會把每一次的覺察與練習,寫成信寄給你。→ 訂閱小穗步電子報