Aggregation、view 與 stored routine 看似是不同的 SQL 功能,實際上都在回答同一個設計問題:資料庫應該在哪一個邊界,把原始 rows 轉成可重用的結果或操作?
GROUP BY 會改變一列代表的東西
Grouping 前,一列可能代表一張訂單;依 customer grouping 後,一列改成代表一位 customer 的摘要。
SELECT customer_id,
COUNT(*) AS orders,
SUM(total) AS revenue
FROM orders
WHERE created_at >= DATE '2026-01-01'
GROUP BY customer_id
HAVING SUM(total) >= 1000;
WHERE 在 grouping 前過濾訂單;HAVING 在 grouping 後過濾 customer group。被選出的 expression 必須用來識別 group,或能由 group 計算出來。
白話來說:
GROUP BY會改變「輸出一列代表什麼」。很多錯誤都是把 grouping 前的單筆欄位,偷渡進 grouping 後的摘要列。
Count 也要說清楚:COUNT(*) 計 rows,COUNT(col) 只計非 NULL,COUNT(DISTINCT col) 再加上去重成本。
View 保存查詢,不保存結果
CREATE VIEW paid_order_summary AS
SELECT customer_id, COUNT(*) AS orders, SUM(total) AS revenue
FROM orders
WHERE status = 'paid'
GROUP BY customer_id;
一般 view 是有名字的 query。使用時資料庫仍會展開底層計畫,因此它適合提供穩定介面或統一定義,不是自動 cache。
Materialized view 才會保存結果,但需要 refresh:
CREATE MATERIALIZED VIEW monthly_revenue AS
SELECT date_trunc('month', created_at) AS month, SUM(total) AS revenue
FROM orders
WHERE status = 'paid'
GROUP BY 1;
取捨是 freshness 對 read cost。Dashboard 能接受五分鐘前的資料,就有機會預先計算;每筆付款都必須立刻出現,就不能隨便接受 stale result。
讓更新保持 atomic
避免先讀、在 application 計算、再寫回的流程,兩個 session 很容易互相覆蓋:
UPDATE inventory
SET quantity = quantity - 1
WHERE product_id = 42
AND quantity > 0
RETURNING quantity;
沒有回傳 row 就代表庫存不足。檢查條件與扣庫存在同一個 database operation 內完成。
Stored routine 是介面決策
Stored procedure 可以讓多個 statement 靠近資料執行,並提供單一權限邊界;也可能讓 application test 看不到邏輯、部署綁死特定資料庫,甚至形成第二套程式碼。
當操作必須 atomic、由多個 client 共用,或靠近資料執行有明確收益時,routine 才合理。需要呼叫外部 API、retry 與 workflow orchestration 的流程,留在 application service。
決策表
| 需求 | 工具 |
|---|---|
| 重複使用、每次讀最新資料的 query | View |
| 昂貴報表,可接受短暫 stale | Materialized view |
| 安全扣庫存或 state transition | 有 predicate 的 atomic UPDATE |
| 多 client 共用的多 statement 資料操作 | 謹慎使用 stored routine |
| 包含 API 與 retry 的 workflow | Application service |
複習卡
WHERE過濾 rows;HAVING過濾 groups。- Grouping 會改變結果列的意義。
- View 保存定義;materialized view 保存資料。
- Atomic update 優於 application read-modify-write。
- Stored routine 是可選的邊界,不是商業邏輯的預設住處。