SQL操作:WITH表達式及其應用

摘要:本文將圍繞WITH,以及更高階的WITH RECURSIVE表達式介紹其語法特徵和具體使用規範,以及在GaussDB(DWS)中如何進行WITH表達式的調優

本文分享自華爲雲社區《GaussDB(DWS) SQL進階之SQL操作之WITH表達式》,作者: 兩杯咖啡 。

SQL標準1999中,在傳統SQL語法的基礎上增加了with表達式的使用,使得SQL語句的編程可以更加靈活和具備可擴展性。本文將圍繞with,以及更高階的with recursive表達式介紹其語法特徵和具體使用規範,以及在GaussDB(DWS)中如何進行with表達式的調優。同時,對Oracle的connect by語法進行探討,研究其使用with recursive進行遷移改寫的方法。

一. WITH表達式及其應用

WITH表達式用於定義查詢中公用語句塊,每個語句塊稱爲CTE,即common table expr,可以理解爲一個帶名稱的子查詢,之後該查詢可以以其名稱在查詢中被多次引用,類似於高級編程語言中的函數。TPC-DS benchmark測試集中有很多包含WITH表達式的SQL語句,99個查詢中有24個相關語句。對於查詢複雜的AP場景,WITH表達式的應用場景非常廣泛,很多客戶現場都在使用WITH表達式,尤其對於多年維護的應用程序,使用WITH表達式是進行SQL編寫演進的一個優秀實踐。

以TPC-DS Q1爲例:

with customer_total_return as
(select sr_customer_sk as ctr_customer_sk
,sr_store_sk as ctr_store_sk
,sum(SR_FEE) as ctr_total_return
from store_returns
,date_dim
where sr_returned_date_sk = d_date_sk
and d_year =2000
group by sr_customer_sk
,sr_store_sk)
 select  c_customer_id
from customer_total_return ctr1
,store
,customer
where ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2
from customer_total_return ctr2
where ctr1.ctr_store_sk = ctr2.ctr_store_sk)
and s_store_sk = ctr1.ctr_store_sk
and s_state = 'TN'
and ctr1.ctr_customer_sk = c_customer_sk
order by c_customer_id
limit 100;

該查詢中定義了一個名稱爲customer_total_return的CTE,該CTE查詢2000年退貨的相關信息。在主查詢中該CTE被調用了兩遍,如果不使用CTE,則customer_total_return定義的SQL需要在該查詢中寫兩遍,使得查詢更長更難以維護。

WITH表達式的語法如下:

[WITH [RECURSIVE] with_query [,…] ] SELECT …

其中,with_query的語法爲:

with_query_name [ ( column_name [, ...] ) ]
AS ( {select | values | insert | update | delete} )

關鍵要點如下:

  • 每個CTE的AS語句指定的SQL語句,必須是可以返回查詢結果的語句,可以是普通的SELECT語句,也可以是INSERT、UPDATE、DELETE、VALUES等其它語句,需要通過RETURNING子句返回元組。例如:
WITH s AS (INSERT INTO t VALUES(1) RETURNING a) SELECT * FROM s;
  • 單個WITH表達式表示一個SQL語句塊中的CTE定義,可以同時定義多個CTE,每個CTE可以指定列名,也可以默認使用查詢輸出列的別名。例如:
WITH s1(a, b) AS (SELECT x, y FROM t1), s2 AS (SELECT x, y FROM t2) SELECT * FROM s1 JOIN s2 ON s1.a=s2.x;

該語句中定義了兩個CTE,s1和s2,其中s1指定了列名爲a, b,s2未指定列名,則列名爲輸出列名x, y。

  • 每個CTE可以在主查詢中引用0次、1次或多次。
  • 同一個語句塊中不能出現同名的CTE,即不支持高級語言的重載。但不同語句塊中可以出現同名的CTE。此時,語句中引用的CTE則是距離引用位置最近的語句塊中的CTE。
  • 除非使用WITH RECURSIVE,否則CTE不允許自引用,即CTE的定義中引用當前CTE。
  • 由於SQL語句中可能包含多個SQL語句塊,每個語句塊都可以包含一個WITH表達式,每個WITH表達式中的CTE可以在當前語句塊、當前語句塊的後續CTE中,以及子層語句塊中引用,但不能在父層語句塊中引用。由於每個CTE的定義也是個語句塊,因此也支持在該語句塊中定義WITH表達式。例如:
WITH tmp AS (SELECT a FROM t) -- 1st tmp
SELECT SUM(a) FROM
(WITH tmp AS (SELECT a * 2 AS a FROM tmp) -- 2nd tmp
SELECT a FROM tmp t1 -- 3rd tmp
WHERE EXISTS(SELECT a FROM tmp t2 WHERE t2.a=t1.a)); -- 4th tmp

注:

<1> 該語句中定義了兩個同名CTE-tmp,一個定義在最外層主語句中,另一個定義在內層子查詢中。

<2> 語句中一共引用了三次tmp,其中第三次和第四次的引用都是引用子查詢中的tmp,而子查詢tmp中使用的tmp(第二次的引用)則引用最外層的tmp。(想想看,爲什麼?)

特殊地,如果CTE出現在相關子查詢中,也可以使用父層的列或表達式,此時引用CTE的地方都視爲使用父層的列或表達式。例如:

update relate_table_010
   set c_birth_month =
       (with tmp1 as (select s_store_sk, s_company_id, s_market_id
                        from store
                       where s_market_id = c_birth_day)
         select cc_mkt_id
           from call_center
          where cc_mkt_id + 1 in
                (select web_mkt_id
                   from web_site
                  inner join tmp1
                     on web_site_sk = s_store_sk
                  where s_market_id = cc_mkt_id))
          where c_birth_day = 9;

該語句中,CTE tmp1中使用了外層relate_table_010的列c_birth_day。

二. With recursive

WITH表達式極大的方便了語句內相同SQL實現的複用,向高級編程語言邁進了一步,但相比高級編程語言而言,仍然缺少一個重要的語法支持,即循環。SQL仍然無法像高級編程語言使用for, while一樣,支持不確定循環次數的執行。爲此,SQL支持了with recursive語法,來解決這一問題,可以用在樹和圖的拓撲搜索上。以下圖的樹爲例:

在GaussDB(DWS)中,可以使用表tree來存儲所有節點及父子信息,表定義語句如下:

CREATE TABLE tree(id INT, parentid INT);

表中數據如下:

通過以下WITH RECURSIVE語句,我們可以返回從頂層1號節點開始,整個樹的節點,以及層次信息:

WITH RECURSIVE nodeset AS
(
-- recursive initializing query
SELECT id, parentid, 1 AS level FROM tree
WHERE id = 1
UNION ALL
-- recursive join query
SELECT tree.id, tree.parentid, level + 1 FROM tree, nodeset
WHERE tree.parentid = nodeset.id
)
SELECT * FROM nodeset ORDER BY id;

上述查詢中,我們可以看出,一個典型的WITH RECURSIVE表達式包含至少一個遞歸查詢的CTE,該CTE中的定義爲一個UNION ALL集合操作,第一個分支爲遞歸起始查詢,第二個分支爲遞歸關聯查詢,需要自引用第一部分進行不斷遞歸關聯。該語句執行時,遞歸起始查詢執行一次,關聯查詢執行若干次並將結果疊加到起始查詢結果集中,直到某一些關聯查詢結果爲空,則返回。

上述查詢的執行結果如下:

起始查詢結果包含level=1的結果集,關聯查詢執行了五次,前四次分別輸出level=2,3,4,5的結果集,在第五次執行時,由於沒有parentid和輸出結果集id相等的記錄,也就是再沒有多餘的孩子節點,因此查詢結束。

從WITH RECURSIVE的執行過程來看,是典型的層次遍歷(廣度優先)的執行方式,因此WITH RECURSIVE也可以稱爲層次查詢。除了典型的樹、圖的拓撲查找應用,WITH RECURSIVE還可以用於模擬多數的複雜循環操作,只要我們正確定義起始條件、循環條件和終止條件。

例如:下例將整數1000-1001轉化成二進制串。

WITH RECURSIVE integer AS
(
SELECT x AS orig, x, '' AS binary_text FROM GENERATE_SERIES(1000, 1010) AS set(x)
UNION ALL
SELECT orig, FLOOR(x/2)::int, CASE WHEN x % 2 = 1 THEN '1' ELSE '0' END || binary_text FROM INTEGER WHERE x > 0
)
SELECT orig, binary_text FROM integer WHERE x = 0 ORDER BY orig;

執行結果如下:

三. GaussDB(DWS)的實現

在PG中,CTE的掃描使用了專門的執行算子WorkTableScan,用於將數據集中緩存起來,供其它引用使用,做到了一次掃描,多次使用的效果。對於GaussDB(DWS),不下推的計劃繼承了PG的計劃。TPC-DS Q1的計劃,如下圖所示:

第15號算子即CTE Scan,對CTE customer_total_return的結果進行緩存,供第8號和第14號CTE scan算子使用。

對於GaussDB(DWS)分佈式系統,數據是分佈存儲在各個DN的,因此這樣的做法是不適合的。在GaussDB(DWS)中,目前將CTE的實現inline到各個調用的地方進行,保證計劃的分佈式下推執行。TPC-DS Q1的計劃,如下圖所示:

紅框中的兩個計劃即是兩個CTE的執行部分。

GaussDB(DWS)嵌入的執行方式,對於CTE多次執行,根據不同的過濾條件可以生成不同的計劃,某些場景是適合的。後續需要結合PG的共享執行機制,對過濾條件相同的執行語句塊進行一次執行,結果共享的改進,減少數據處理和運算量。

對於WITH RECURSIVE表達式,GaussDB(DWS)也支持其分佈式執行,計劃如下所示:

同時,由於WITH RECURSIVE涉及到循環運算,在語句寫得不好的時候,可能出現循環次數過多導致數據庫執行異常,因此GaussDB(DWS)引入了參數max_recursive_times,用於控制WITH RECURSIVE的最大循環次數,默認值爲200,超過該次數則報錯。

四. Oracle CONNECT BY的遷移

讀到這裏,可能細心的讀者已經發現了,WITH RECURSIVE和Oracle支持的CONNECT BY特性功能很相似,都是用於進行不定次數的循環運算,但語法不同。

Oracle CONNECT BY功能的基本語法如下:

SELECT * FROM tablename [START WITH <condition1>] CONNECT BY <condition2>;

其中START WITH子句用於指定起始條件,即<condition1>,循環關聯條件爲<condition2>,其中可以使用PRIOR關鍵字來表示來自於上一循環的列。例如上節中所述的樹遍歷的例子,使用Oracle的Connect By語法,語句如下:

SELECT * FROM tree START WITH id = 1 CONNECT BY PRIOR id = parentid;

可以看出,Oracle的CONNECT BY實現了基本的樹和圖拓撲關係查找的功能,用法較簡單,但相較於WITH RECURSIVE,不如其靈活,對於一些複雜的循環語句,尤其是起始語句和循環關聯語句的輸出列不相同的場景,無法支持。

但由於GaussDB(DWS)目前很多客戶都是從Oracle系統遷移而來,因此面臨着將Oracle的CONNECT BY語法改寫爲WITH RECURSIVE的需求。對於基本語法,我們可以進行如下基本的改寫以滿足其功能:

WITH RECURSIVE tmp_cte AS
(
SELECT * FROM table WHERE <condition1>
UNION ALL
SELECT table.* FROM table JOIN tmp_cte ON <condition2>
)
SELECT * FROM tmp_cte;

其中<condition2>需要對Oracle的PRIOR表達式進行改寫,明確PRIOR修飾的列爲table表的列,非PRIOR修飾的列爲tmp_cte對應的列。

爲了更準確地表示遍歷的層次關係,Oracle的CONNECT BY功能還支持一些僞列和其它表達式,其基本語義和改寫方式如下表所示,請讀者下來思考具體的改寫方法。

  • 終止循環嵌套選項

【語法】CONNECT BY NO CYCLE <condition>

【語義】通過在循環關聯條件前指定NO CYCLE,在遇到循環嵌套重複行時,主動終止重複行的重複循環。

【示例】SELECT * FROM tree START WITH id = 1 CONNECT BY NOCYCLE PRIOR id = parentid;

【改寫方式】GaussDB(DWS)中支持在WITH RECURSIVE表達式定義的語句塊中使用UNION,而非UNION ALL,此時會對輸出行去重,自動終止循環,但要求輸出行完全來自初始行,不能增加其它表達式,否則一併參與去重。例如:

WITH RECURSIVE nodeset AS
(
SELECT id, parentid, 1 AS level FROM tree
WHERE id = 1
UNION
SELECT tree.id, tree.parentid, level + 1 FROM tree, nodeset
WHERE tree.parentid = nodeset.id
)
SELECT * FROM nodeset ORDER BY id;

注:此改寫仍與Oracle有區別,即Oracle可以重複輸出重複行一次,而本改寫自動跳過;另外本改寫不能增加其它僞列及表達式,例如:level等。

  • 層次排序選項

【語法】ORDER SIBLINGS BY <column>[, …]

【語義】CONNECT BY默認深度遞歸遍歷並輸出,此選項修改排序順序爲層次,<column>。

【示例】SELECT * FROM tree START WITH id = 1 CONNECT BY PRIOR id = parentid ORDER SIBLINGS BY id;

【改寫方式】可以在WITH RECUSIVE的語句塊輸出列增加僞列LEVEL(見下方說明), path_array(),然後按照該兩列排序。其中path_array()的入參爲排序列,含義爲從根到當前節點的值。

  • 僞列LEVEL/ CONNECT_BY_ISLEAF/CONNECT_BY_ISCYCLE

【語義】LEVEL表示當前行的遍歷層次/CONNECT_BY_ISLEAF表示當前行是否爲遍歷終止節點(葉子節點)/ CONNECT_BY_ISCYCLE表名當前行是否爲循環重複行,與NO CYCLE搭配使用纔有意義

【示例】SELECT id, parentid, LEVEL, CONNECT_BY_IS_LEAF, CONNECT_BY_IS_CYCLE FROM tree START WITH id = 1 CONNECT BY NO CYCLE PRIOR id = parentid;

【改寫方式】LEVEL可以通過增加僞列實現,例如上文示例。CONNECT_BY_ISLEAF則需要與輸出結果集的遞歸join列關聯,根據關聯結果判斷。由於不支持NO CYCLE,CONNECT_BY_ISCYCLE不支持改寫。

  • 操作符CONNECT_BY_ROOT(column)

【語義】返回遍歷開始行對應的column值

【示例】SELECT id, parentid, CONNECT_BY_ROOT(id) FROM tree START WITH id = 1 CONNECT BY PRIOR id = parentid;

【改寫方式】可以在WITH RECUSIVE的語句塊輸出列增加標識起始行的列,在嵌套過程中該列值始終繼承第一行的值。

  • 函數SYS_CONNECT_BY_PATH(column, char)

【語義】返回從起始行到當前行嵌套的所有column的值,以char分隔。

【示例】SELECT id, parentid, SYS_CONNECT_BY_PATH(id, ‘/’) FROM tree START WITH id = 1 CONNECT BY PRIOR id = parentid;

【改寫方式】可以在WITH RECUSIVE的語句塊輸出列增加標識起始行到當前行的相應列的字符串,在嵌套過程中通過字符串連接增加當前行的值。

五. 總結

本文中所講到的WITH表達式及WITH RECURSIVE表達式的用法,涉及很多SQL中複雜的操作,當然掌握其語法也在熟練掌握SQL的過程中更進了一步。

想了解GuassDB(DWS)更多信息,歡迎微信搜索“GaussDB DWS”關注微信公衆號,和您分享最新最全的PB級數倉黑科技,後臺還可獲取衆多學習資料哦~

 

點擊關注,第一時間瞭解華爲雲新鮮技術~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章