SQL優化(五) PostgreSQL (遞歸)CTE 通用表表達式

  原創文章,轉載請務必將下面這段話置於文章開頭處。
  本文轉發自Jason’s Blog原文連接 http://www.jasongj.com/sql/cte/ sql

CTE or WITH

WITH語句一般被稱爲通用表表達式(Common Table Expressions)或者CTEs。 數組

WITH語句做爲一個輔助語句依附於主語句,WITH語句和主語句均可以是SELECT,INSERT,UPDATE,DELETE中的任何一種語句。 oop

例講CTE

WITH語句最基本的功能是把複雜查詢語句拆分紅多個簡單的部分,以下例所示 優化

WITH regional_sales AS ( SELECT region, SUM(amount) AS total_sales FROM orders GROUP BY region ), top_regions AS ( SELECT region FROM regional_sales WHERE total_sales > (SELECT SUM(total_sales)/10 FROM regional_sales ) SELECT region, product, SUM(quantity) AS product_units, SUM(amount) AS product_sales FROM orders WHERE region IN (SELECT region FROM top_regions) GROUP BY region, product;

該例中,定義了兩個WITH輔助語句,regional_sales和top_regions。前者算出每一個區域的總銷售量,後者了查出全部銷售量佔全部地區總銷售裏10%以上的區域。主語句經過將這個CTEs及訂單表關聯,算出了頂級區域每件商品的銷售量和銷售額。 spa

固然,本例也能夠不使用CTEs而使用兩層嵌套子查詢來實現,但使用CTEs更簡單,更清晰,可讀性更強。 code

在WITH中使用數據修改語句

文章開頭處提到,WITH中能夠不只可使用SELECT語句,同時還能使用DELETE,UPDATE,INSERT語句。所以,可使用WITH,在一條SQL語句中進行不一樣的操做,以下例所示。 遞歸

WITH moved_rows AS ( DELETE FROM products WHERE "date" >= '2010-10-01' AND "date" < '2010-11-01' RETURNING * ) INSERT INTO products_log SELECT * FROM moved_rows;

本例經過WITH中的DELETE語句從products表中刪除了一個月的數據,並經過RETURNING子句將刪除的數據集賦給moved_rows這一CTE,最後在主語句中經過INSERT將刪除的商品插入products_log中。 事務

若是WITH裏面使用的不是SELECT語句,而且沒有經過RETURNING子句返回結果集,則主查詢中不能夠引用該CTE,但主查詢和WITH語句仍然能夠繼續執行。這種狀況能夠實現將多個不相關的語句放在一個SQL語句裏,實現了在不顯式使用事務的狀況下保證WITH語句和主語句的事務性,以下例所示。 get

WITH d AS ( DELETE FROM foo ), u as ( UPDATE foo SET a = 1 WHERE b = 2 ) DELETE FROM bar;

WITH使用注意事項

  1. WITH中的數據修改語句會被執行一次,而且確定會徹底執行,不管主語句是否讀取或者是否讀取全部其輸出。而WITH中的SELECT語句則只輸出主語句中所須要記錄數。
  2. WITH中使用多個子句時,這些子句和主語句會並行執行,因此當存在多個修改子語句修改相同的記錄時,它們的結果不可預測。
  3. 全部的子句所能「看」到的數據集是同樣的,因此它們看不到其它語句對目標數據集的影響。這也緩解了多子句執行順序的不可預測性形成的影響。
  4. 若是在一條SQL語句中,更新同一記錄屢次,只有其中一條會生效,而且很難預測哪個會生效。
  5. 若是在一條SQL語句中,同時更新和刪除某條記錄,則只有更新會生效。
  6. 目前,任何一個被數據修改CTE的表,不容許使用條件規則,和ALSO規則以及INSTEAD規則。

WITH RECURSIVE

WITH語句還能夠經過增長RECURSIVE修飾符來引入它本身,從而實現遞歸 it

WITH RECURSIVE實例

WITH RECURSIVE通常用於處理邏輯上層次化或樹狀結構的數據,典型的使用場景是尋找直接及間接子結點。

定義下面這樣的表,存儲每一個區域(省、市、區)的id,名字及上級區域的id

CREATE TABLE chinamap ( id INTEGER, pid INTEGER, name TEXT );

須要查出某個省,好比湖北省,管轄的全部市及市轄地區,能夠經過WITH RECURSIVE來實現,以下

WITH RECURSIVE result AS
(
  SELECCT
    id,
    name
  FROM  chinamap
  WHERE id = 11
  UNION ALL SELECT origin.id, result.name || ' > ' || origin.name FROM result JOIN chinamap origin ON origin.pid = result.id ) SELECT id, name FROM result;

結果以下

id | name  -----+-------------------------- 11 | 湖北省 110 | 湖北省 > 武漢市 120 | 湖北省 > 孝感市 130 | 湖北省 > 宜昌市 140 | 湖北省 > 隨州市 150 | 湖北省 > 仙桃市 160 | 湖北省 > 荊門市 170 | 湖北省 > 枝江市 180 | 湖北省 > 神農架市 111 | 湖北省 > 武漢市 > 武昌區 112 | 湖北省 > 武漢市 > 下城區 113 | 湖北省 > 武漢市 > 江岸區 114 | 湖北省 > 武漢市 > 江漢區 115 | 湖北省 > 武漢市 > 漢陽區 116 | 湖北省 > 武漢市 > 洪山區 117 | 湖北省 > 武漢市 > 青山區 (16 rows)

WITH RECURSIVE 執行過程

從上面的例子能夠看出,WITH RECURSIVE語句包含了兩個部分
- non-recursive term(非遞歸部分),即上例中的union all前面部分
- recursive term(遞歸部分),即上例中union all後面部分

執行步驟以下
1. 執行non-recursive term。(若是使用的是union而非union all,則需對結果去重)其結果做爲recursive term中對result的引用,同時將這部分結果放入臨時的working table中
2. 重複執行以下步驟,直到working table爲空:用working table的內容替換遞歸的自引用,執行recursive term,(若是使用union而非union all,去除重複數據),並用該結果(若是使用union而非union all,則是去重後的結果)替換working table

以上面的query爲例,來看看具體過程
1.執行

SELECT id, name FROM chinamap WHERE id = 11

結果集和working table爲

11 | 湖北

2.執行

SELECT origin.id, result.name || ' > ' || origin.name FROM result JOIN chinamap origin ON origin.pid = result.id

結果集和working table爲

110 | 湖北省 > 武漢市 120 | 湖北省 > 孝感市 130 | 湖北省 > 宜昌市 140 | 湖北省 > 隨州市 150 | 湖北省 > 仙桃市 160 | 湖北省 > 荊門市 170 | 湖北省 > 枝江市 180 | 湖北省 > 神農架市

3.再次執行recursive query,結果集和working table爲

111 | 湖北省 > 武漢市 > 武昌區 112 | 湖北省 > 武漢市 > 下城區 113 | 湖北省 > 武漢市 > 江岸區 114 | 湖北省 > 武漢市 > 江漢區 115 | 湖北省 > 武漢市 > 漢陽區 116 | 湖北省 > 武漢市 > 洪山區 117 | 湖北省 > 武漢市 > 青山區

4.繼續執行recursive query,結果集和working table爲空
5.結束遞歸,將前三個步驟的結果集合並,即獲得最終的WITH RECURSIVE的結果集

嚴格來說,這個過程實現上是一個迭代的過程而非遞歸,不過RECURSIVE這個關鍵詞是SQL標準委員會定立的,因此PostgreSQL也延用了RECURSIVE這一關鍵詞。

WITH RECURSIVE 防止死循環

從上一節中能夠看到,決定是否繼續迭代的working table是否爲空,若是它永不爲空,則該CTE將陷入無限循環中。
對於自己並不會造成循環引用的數據集,無段做特別處理。而對於自己可能造成循環引用的數據集,則須經過SQL處理。

一種方式是使用UNION而非UNION ALL,從而每次recursive term的計算結果都會將已經存在的數據清除後再存入working table,使得working table最終會爲空,從而結束迭代。

然而,這種方法並不老是有效的,由於有時可能須要這些重複數據。同時UNION只能去除那些全部字段都徹底同樣的記錄,而頗有可能特定字段集相同的記錄即應該被刪除。此時能夠經過數組(單字段)或者ROW(多字段)記錄已經訪問過的記錄,從而實現去重的目的。

WITH RECURSIVE 求最短路徑

定義無向有環圖以下圖所示
Non-directional cycle graph

定義以下表並存入每條邊的權重

CREATE TABLE graph ( id char, neighbor char, value integer ); INSERT INTO graph VALUES('A', 'B', 3), ('A', 'C', 5), ('A', 'D', 4), ('B', 'E', 8), ('B', 'C', 4), ('E', 'C', 7), ('E','F', 10), ('C', 'D', 3), ('C', 'F', 6), ('F','D', 5);

計算思路以下:
- 由於是無向圖,因此首先要將各條邊的id和neighbor交換一次以方便後續計算。
- 利用WITH RECURSIVE算出全部可能的路徑並計算其總權重。
- 由於該圖有環,爲避免無限循環,同時爲了計算路徑,將通過的結點存於數據中,當下一個結點已經在數據中時,說明該結點已被計算。
- 最終可算出全部可能的路徑及其總權重

實現以下

WITH RECURSIVE edges AS ( SELECT id, neighbor, value FROM graph UNION ALL SELECT neighbor, id, value FROM graph ), all_path (id, neighbor, value, path, depth, cycle) AS ( SELECT id, neighbor, value, ARRAY[id], 1, 'f'::BOOLEAN FROM edges WHERE id = 'A' UNION ALL SELECT all_path.id, edges.neighbor, edges.value + all_path.value, all_path.path || ARRAY[edges.id], depth + 1, edges.id = ANY(all_path.path) FROM edges JOIN all_path ON all_path.neighbor = edges.id AND NOT cycle ), a_f AS ( SELECT rank() over(order by value) AS rank, path || neighbor AS path, value, depth FROM all_path WHERE neighbor = 'F' ) SELECT path, value, depth FROM a_f WHERE rank = 1;

WITH RECURSIVE 使用限制

  • 若是在recursive term中使用LEFT JOIN,自引用必須在「左」邊
  • 若是在recursive term中使用RIGHT JOIN,自引用必須在「右」邊
  • recursive term中不容許使用FULL JOIN
  • recursive term中不容許使用GROUP BY和HAVING
  • 不容許在recursive term的WHERE語句的子查詢中使用CTE的名字
  • 不支持在recursive term中對CTE做aggregation
  • recursive term中不容許使用ORDER BY
  • LIMIT / OFFSET不容許在recursive term中使用
  • FOR UPDATE不可在recursive term中使用
  • recursive term中SELECT後面不容許出現引用CTE名字的子查詢
  • 同時使用多個CTE表達式時,不容許多表達式之間互相訪問(支持單向訪問)
  • 在recursive term中不容許使用FOR UPDATE

CTE 優缺點

  • 可使用遞歸 WITH RECURSIVE,從而實現其它方式沒法實現或者不容易實現的查詢
  • 當不須要將查詢結果被其它獨立查詢共享時,它比視圖更靈活也更輕量
  • CTE只會被計算一次,且可在主查詢中屢次使用
  • CTE可極大提升代碼可讀性及可維護性
  • CTE不支持將主查詢中where後的限制條件push down到CTE中,而普通的子查詢支持

SQL優化系列

相關文章
相關標籤/搜索