# 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