# SQL優化查詢的效能與時間
此為翻譯文章,能夠了解到SQL如果用了許多SUM,COUNT,可能會讓查詢消耗許多效能,如何使用 ==CTE==(Common Table Expressions)讓查詢時間縮短了 80%以上 。
系統在高負載下逐漸吃不消,頁面載入變慢,儀表板頻頻超時,問題的根源是一個跨多個服務使用的關鍵查詢,隨著時間推移,這個查詢不斷增加連接、篩選條件和計算,最終成為整個系統中最慢的瓶頸。
我們嘗試了各種方法:建立索引、反正規化、使用查詢提示,但都無法解決問題。直到有位開發者運用了一個鮮為人知的 SQL 小技巧,將查詢重新改寫,才迎刃而解:
## 問題:擁腫的分析查詢
```typescript=
SELECT
users.id,
users.name,
COUNT(DISTINCT orders.id) AS order_count,
SUM(orders.amount) AS total_spent,
MAX(logins.timestamp) AS last_login
FROM users
LEFT JOIN orders ON users.id = orders.user_id
LEFT JOIN logins ON users.id = logins.user_id
WHERE users.created_at >= '2023-01-01'
GROUP BY users.id;
```
包含以下內容的表格上:
* 500 萬使用者
* 5000 萬訂單
* 8000 萬個登錄事件
平均耗時超過 9.3 秒
## 解決方法:可重用的內聯 CTE
問題不只是資料量龐大,而是查詢中出現了過多的重複連接(joins)與掃描。為了計算彙總資料,我們對每個使用者都需連接像是 ==orders== 和 ==logins== 這類的大型資料表,導致效能大幅下降。
因此,我們重構了查詢邏輯,導入了 CTE(Common Table Expressions)。這不僅讓邏輯更清晰,也讓我們能夠預先彙總資料並重複使用中間結果,大幅提升查詢效率。
```typescript=
WITH recent_users AS (
SELECT id, name
FROM users
WHERE created_at >= '2023-01-01'
),
order_stats AS (
SELECT user_id, COUNT(*) AS order_count, SUM(amount) AS total_spent
FROM orders
GROUP BY user_id
),
last_logins AS (
SELECT user_id, MAX(timestamp) AS last_login
FROM logins
GROUP BY user_id
)
SELECT
u.id,
u.name,
o.order_count,
o.total_spent,
l.last_login
FROM recent_users u
LEFT JOIN order_stats o ON u.id = o.user_id
LEFT JOIN last_logins l ON u.id = l.user_id;
```
現在,每個聯接都會命中預先計算的、 分組的和較小的數據集,而不是原始Table。
## 基準測試結果
我們針對運行兩個版本。以下是所看到的數據:
```tex=
| Query Version | Avg Time | % Improvement |
| ------------- | -------- | ---------------- |
| Original | 9.3s | - |
| CTE Refactor | 1.8s | **80.6% faster** |
```
指數化之前有所説明,但這次 CTE 重組帶來了最大的收益。
## 這樣做的好處
這種加速背後有兩個關鍵原則:
### 1. 避免重複工作
原本查詢中的每個 ==LEFT JOIN== 都會掃描整個資料表,並在每一列上重新計算彙總資料,造成大量重複運算與資源浪費。
透過預先彙總的 CTE(Common Table Expressions),查詢規劃器(query planner)只需處理經過簡化的中間結果,大幅減少資料量與運算成本,整體效能因此顯著提升。
### 2. 打破複雜性
複雜的大型查詢容易讓查詢最佳化器(optimizer)難以做出有效判斷。
將邏輯拆分為多個 CTE(Common Table Expressions),可以讓資料庫更清楚地理解每個子查詢的結構,進而更有效地進行子計劃(subplans)的平行處理與最佳化,提升整體執行效能與可維護性。
### 值得關注的問題
在 PostgreSQL 中,CTE(Common Table Expressions)過去會被視為最佳化的障礙,意思是它們不會被內嵌(inlined)進主查詢中。從 PostgreSQL 12 開始,非具體化(non-materialized)的 CTE 預設會被內嵌,除非你明確使用 ==MATERIALIZED== 關鍵字。
CTE 並不適合用在單純的篩選條件上;它真正發揮效用的情境是當你需要重複使用查詢結果,或希望減少資料掃描的範圍與次數。
使用 ==EXPLAIN ANALYZE== 來觀察實際的執行計畫,以確認 CTE 是否帶來效能提升。有時候,使用子查詢(subquery)反而會比 CTE 表現更好,這取決於資料庫的最佳化策略與實際資料分布。
### 學到了什麼
* CTE(Common Table Expressions)不只是為了讓查詢更易讀,在用來做預先彙總時,它也能大幅提升效能。
* 查詢的複雜度常常是悄悄累積的 —— 原本執行快速的查詢,隨著需求成長,逐漸演變成難以擴充的效能瓶頸,而我們往往沒有即時察覺。
* 將查詢邏輯拆解,不只讓開發者更容易理解,也有助於查詢規劃器(query planner)更有效地做出最佳化決策,提升整體查詢效率。
### 優化之前...
* 從 ==pg_stat_statements== 中找出執行最慢的查詢,這是優化的起點。
* 接著,檢查是否有多餘的資料表連接(joins)或重複的查詢邏輯,這些常是效能瓶頸的根源。
* 每次修改後都要進行效能測試(benchmark)—— 在一個資料結構中有效的方法,可能在另一個架構下反而造成反效果。
## 最後的思考
* CTE(Common Table Expressions)常被當作提升查詢可讀性的工具來教學,但當它用於資料預先彙總與邏輯切分時,往往能帶來顯著的效能改善 —— 即使不動用索引或調整資料結構也能達成。
* 這個方法幫助我們避免了一次大規模的系統重構。如果你也遇到長時間執行的 JOIN 或緩慢的分析查詢,不妨試著用 CTE 重構查詢邏輯。
* 它可能不只替你省下數天的排查時間,還能節省運算成本。
>資料翻譯來源:https://medium.com/@kanishks772/we-cut-80-of-our-query-time-by-using-this-little-known-sql-pattern-fadec2bdb592