你的 SQL 可能很慢,但你不知道
大多數工程師寫 SQL 的標準是「功能正確」,但在資料量小的時候,幾乎所有寫法都夠快。 問題出在資料量增長後 — 同樣的邏輯,優化的寫法可能比未優化的快上百倍。 以下是一個真實案例:兩個查詢,功能完全相同,但執行時間差了 100 倍。
為什麼差這麼多?慢查詢中的子查詢是「相關子查詢(Correlated Subquery)」,對每一行外部資料都重新執行一次。 若 orders 表有 100 萬行,子查詢就跑 100 萬次。而 Window Function 只需掃描一次資料集即可完成計算。
本文將涵蓋三大主題,幫你從「SQL 能跑就好」升級到「SQL 要跑得好」:
Window Functions
不用 GROUP BY 也能做聚合計算,保留所有行的同時進行統計
CTE(Common Table Expression)
讓複雜查詢拆解成可讀的邏輯單元,支援遞迴處理樹狀結構
EXPLAIN ANALYZE
看懂執行計畫,精準定位效能瓶頸,用 Index 把慢查詢變快查詢
Window Functions — 聚合不折疊行
Window Functions 是 SQL 中最被低估的功能之一。它和 GROUP BY 最根本的區別在於: GROUP BY 會把多行「折疊」成一行,而 Window Functions 保留所有原始行,同時在每行上附加計算結果。
PARTITION BY vs ORDER BY in OVER():PARTITION BY 定義計算的分組範圍(類似 GROUP BY),ORDER BY 定義視窗內的排序順序(影響排名、累計等計算)。 兩者可以同時使用,也可以只用其中一個。
四大排名函數:ROW_NUMBER、RANK、DENSE_RANK、PERCENT_RANK
排名函數是 Window Functions 最常見的應用場景,但四種函數處理「同分」的方式完全不同, 選錯會導致業務邏輯錯誤。下面用一個具體例子說明差異:
ROW_NUMBER()
適用:需要每行唯一編號時(例如分頁實作)
注意:同分時順序不確定,不適合排行榜
RANK()
適用:標準競賽排名(奧運模式:沒有第3,直接第4)
注意:名次不連續,某些業務場景可能造成混淆
DENSE_RANK()
適用:需要名次連續的排行榜(最常用的業務排名)
注意:名次連續但不唯一,需確認業務對同名次的處理
PERCENT_RANK()
適用:統計分析,了解數據在整體分布中的位置
注意:第一名固定是 0.0,最後一名固定是 1.0
LAG/LEAD — 讀取前後行的資料
LAG() 和LEAD()是時序分析的神器。LAG 讀取「前幾行」的值,LEAD 讀取「後幾行」的值。 最典型的應用是計算環比成長率:今天和昨天比,這個月和上個月比。 不用自己 JOIN,也不用子查詢,一行就能搞定。
搭配 PARTITION BY:每個分組獨立計算
LAG/LEAD 和其他 Window Functions 一樣,可以搭配 PARTITION BY 讓每個分組獨立計算, 不同分組之間的邊界不會互相干擾。下面的例子計算每個用戶相鄰訂單的時間差:
Sliding Window — 移動視窗計算
Sliding Window(移動視窗)是 Window Functions 最強大的特性之一,讓你可以定義「計算範圍」: 不是計算整個 PARTITION,而是計算「當前行前後 N 行」的範圍。 常見應用包含 7 天移動平均、累計銷售額、滾動總和等。
ROWS vs RANGE 的差異(容易踩坑!)
ROWS(按物理行數)
精確計算「前 N 行」。推薦使用。
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
RANGE(按值的範圍)
相同排序值的行算同一組,可能包含意外的行數。
RANGE BETWEEN 6 PRECEDING AND CURRENT ROW
CTE — 讓複雜查詢可讀
CTE(Common Table Expression,公共表表達式)用 WITH 關鍵字開頭, 讓你把一個複雜查詢拆成多個具名的邏輯單元,每個單元可以被後面的查詢引用。 最大的優點是可讀性:複雜的 Nested Subquery 可以變成清晰的線性邏輯。
遞迴 CTE — 處理樹狀結構的利器
CTE 支援遞迴(WITH RECURSIVE), 可以處理自引用的樹狀結構,例如組織架構、商品分類樹、評論回覆鏈。 遞迴 CTE 由兩部分組成:基礎查詢(錨點)和遞迴查詢,用 UNION ALL 連接。
遞迴 CTE 的執行邏輯:先執行基礎查詢得到初始結果集,然後把結果集作為輸入執行遞迴查詢, 得到新結果再作為輸入繼續遞迴,直到遞迴查詢回傳空集合為止。 務必加上 WHERE level < N 等防護條件,避免資料有循環時造成無限遞迴。
EXPLAIN ANALYZE — 看懂執行計畫
優化 SQL 最重要的工具是 EXPLAIN ANALYZE。 它告訴你 PostgreSQL 實際上怎麼執行你的查詢:掃描了哪些表、用了哪些 Index、每個步驟花了多少時間。 沒有這個工具,所謂的「優化」只是猜測。有了它,才能做到有根據的效能改善。
關鍵指標解讀
從頭到尾掃描整張表,資料量大時是效能殺手。解法:在 WHERE 條件欄位建立 Index。
透過 B-Tree Index 快速定位資料,是理想狀態。確認 EXPLAIN 中出現了預期的 Index 名稱。
適合小資料集,但當外層資料量大時複雜度是 O(n²)。大資料量應改用 Hash Join。
估計成本,非毫秒數。X 是回傳第一行的成本,Y 是回傳所有行的成本。用來比較不同計畫的相對代價。
估計行數 vs 實際行數。差距太大代表統計資料過時,需執行 ANALYZE tablename 更新。
這個節點被執行了 N 次。若 loops 很大,actual time 要乘以 N 才是真正花費的時間。
加 Index 後的效果對比
常用 PostgreSQL 進階語法彙整
除了 Window Functions 和 CTE,PostgreSQL 還有幾個在實際業務場景非常實用的進階語法, 了解這些可以讓你少寫很多應用層程式碼,把邏輯下推到資料庫層處理。
1. UPSERT — 不存在就插入,存在就更新
2. JSONB — 比 MongoDB 更強的 JSON 支援
3. 全文搜尋 — 不需要 Elasticsearch 的基本全文搜尋
4. Keyset Pagination — 比 OFFSET 快 100 倍的分頁
Keyset Pagination 的限制:不支援「跳頁」(例如直接跳到第 50 頁),只能「上一頁/下一頁」。 但對大多數業務場景(Feed、列表滾動載入)這不是問題,而且換來的效能提升非常顯著。 如果業務需要跳頁,才考慮用 OFFSET 或其他方案。
上一篇
EP.06 Redis 進階:Pub/Sub、Lua Script
下一篇
EP.08(Coming Soon)