All writing2026.09.22 · CS 411 · Database Systems · 2 min

Aggregation, Views, and Updates: Choosing the Right Boundary

GROUP BY semantics, ordinary and materialized views, atomic updates, and when a stored routine is worth the coupling.

Aggregation, views, and stored routines look like separate SQL features. They share one design question: at what boundary should the database turn raw rows into a reusable result or operation?

GROUP BY changes the level of a row

Before grouping, one row may represent one order. After grouping by customer, one row represents one customer summary.

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 filters orders before grouping. HAVING filters customer groups afterward. Every selected expression must either identify the group or be computed from the group.

In plain terms: GROUP BY changes what one output row means. Many mistakes come from selecting a column that still belongs to the old row level.

Be precise with counts: COUNT(*) counts rows; COUNT(col) counts non-NULL values; COUNT(DISTINCT col) counts distinct non-NULL values and usually costs more.

A view stores a query, not its result

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;

An ordinary view is a named query. Each use expands into an underlying plan, which makes the view useful for a stable interface or shared definition—not as an automatic cache.

A materialized view stores the result and must be refreshed:

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;

The tradeoff is freshness versus read cost. If the dashboard tolerates data that is five minutes old, precomputation may be useful. If every payment must appear immediately, stale materialization may be unacceptable.

Make updates atomic

Avoid read-modify-write sequences that let two sessions overwrite each other:

-- Prefer one atomic statement.
UPDATE inventory
SET quantity = quantity - 1
WHERE product_id = 42
  AND quantity > 0
RETURNING quantity;

If no row is returned, the item was unavailable. The predicate and update happen as one database operation.

Stored routines are an interface decision

A stored procedure can keep several statements close to the data and expose one permission boundary. It can also hide logic from application tests, couple deployments to one database, and create a second codebase.

Use a routine when the operation must be atomic, is shared by several clients, or benefits materially from running beside the data. Keep orchestration and external service calls in application code.

Decision table

Need Tool
Reusable live query View
Expensive report with acceptable staleness Materialized view
Safe decrement or state transition Atomic UPDATE with a predicate
Multi-statement data operation shared by clients Stored routine, considered carefully
Workflow involving APIs and retries Application service

Review card

  • WHERE filters rows; HAVING filters groups.
  • Grouping changes the meaning of one result row.
  • A view stores a definition; a materialized view stores data.
  • Prefer atomic updates over application read-modify-write.
  • Stored routines are useful boundaries, not default homes for business logic.

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 是可選的邊界,不是商業邏輯的預設住處。