MariaDB表表達式之公用表表達式(CTE)
前言
公用表表達式(Common Table Expression,CTE)和派生表類似,都是虛擬的表,但是相比于派生表,CTE具有一些優(yōu)勢和方便之處。
CTE有兩種類型:非遞歸的CTE和遞歸CTE。
CTE是標準SQL的特性,屬于表表達式的一種,MariaDB支持CTE,MySQL 8才開始支持CTE。
1.非遞歸CTE
CTE是使用WITH子句定義的,包括三個部分:CTE名稱cte_name、定義CTE的查詢語句inner_query_definition和引用CTE的外部查詢語句outer_query_definition。
它的格式如下:
WITH cte_name1[(column_name_list)] AS (inner_query_definition_1) [,cte_name2[(column_name_list)] AS (inner_query_definition_2)] [,...] outer_query_definition
其中column_name_list指定inner_query_definition中的列列表名,如果不寫該選項,則需要保證在inner_query_definition中的列都有名稱且唯一,即對列名有兩種命名方式:內(nèi)部命名和外部命名。
注意,outer_quer_definition必須和CTE定義語句同時執(zhí)行,因為CTE是臨時虛擬表,只有立即引用它,它的定義才是有意義的。

下面語句是一個簡單的CTE的用法。首先定義一張?zhí)摂M表,也就是CTE,然后在外部查詢中引用它。
CREATE OR REPLACE TABLE t(id INT NOT NULL PRIMARY KEY,sex CHAR(3),NAME CHAR(20));
INSERT INTO t VALUES (1,'nan','David'),(2,'nv','Mariah'),(3,'nv','gaoxiaofang'),(4,'nan','Jim'),
(5,'nv','Selina'),(6,'nan','John'),(7,'nan','Monty'),(8,'nv','xiaofang');
# 定義CTE,順便為每列重新命名,且使用ORDER BY子句
WITH nv_t(myid,mysex,myname) AS (
SELECT * FROM t WHERE sex='nv' ORDER BY id DESC
)
# 使用CTE
SELECT * FROM nv_t;
+------+-------+-------------+
| myid | mysex | myname |
+------+-------+-------------+
| 2 | nv | Mariah |
| 3 | nv | gaoxiaofang |
| 5 | nv | Selina |
| 8 | nv | xiaofang |
+------+-------+-------------+從結(jié)果中可以看到,在CTE的定義語句中使用ORDER BY子句是沒有任何作用的。
在這里可以發(fā)現(xiàn),CTE和派生表需要滿足的幾個共同點:每一列要求有列名,包括計算列;列名必須唯一;不能使用ORDER BY子句,除非使用了TOP關(guān)鍵字(標準SQL嚴格遵守不能使用ORDER BY的規(guī)則,但MySQL/MariaDB中允許)。不僅僅是CTE和派生表,其他表表達式(內(nèi)聯(lián)表值函數(shù)(sql server才支持)、視圖)也都要滿足這些條件。究其原因,表表達式的本質(zhì)是表,盡管它們是虛擬表,也應(yīng)該滿足形成表的條件。
一方面,在關(guān)系模型中,表對應(yīng)的是關(guān)系,表中的行對應(yīng)的是關(guān)系模型中的元組,表中的字段(或列)對應(yīng)的是關(guān)系中的屬性。屬性由三部分組成:屬性的名稱、屬性的類型和屬性值。因此要形成表,必須要保證屬性的名稱,即每一列都有名稱,且唯一。
另一方面,關(guān)系模型是基于集合的,在集合中是不要求有序的,因此不能在形成表的時候讓數(shù)據(jù)按序排列,即不能使用ORDER BY子句。之所以在使用了TOP后可以使用ORDER BY子句,是因為這個時候的ORDER BY只為TOP提供數(shù)據(jù)的邏輯提取服務(wù),并不提供排序服務(wù)。例如使用ORDER BY幫助TOP選擇出前10行,但是這10行數(shù)據(jù)在形成表的時候不保證是順序的。
相比派生表,CTE有幾個優(yōu)點:
1.多次引用:避免重復書寫。
2.多次定義:避免派生表的嵌套問題。
3.可以使用遞歸CTE,實現(xiàn)遞歸查詢。
例如:
# 多次引用,避免重復書寫
WITH nv_t(myid,mysex,myname) AS (
SELECT * FROM t WHERE sex='nv'
)
SELECT t1.*,t2.*
FROM nv_t t1 JOIN nv_t t2
WHERE t1.myid = t2.myid+1;
# 多次定義,避免派生表嵌套
WITH
nv_t1 AS ( /* 第一個CTE */
SELECT * FROM t WHERE sex='nv'
),
nv_t2 AS ( /* 第二個CTE */
SELECT * FROM nv_t1 WHERE id>3
)
SELECT * FROM nv_t2;如果上面的語句不使用CTE而使用派生表的方式,則它等價于:
SELECT * FROM (SELECT * FROM (SELECT * FROM t WHERE sex='nv') AS nv_t1) AS nv_t2;
2.遞歸CTE
SQL語言是結(jié)構(gòu)化查詢語言,它的遞歸特性非常差。使用遞歸CTE可稍微改善這一缺陷。
公用表表達式(CTE)具有一個重要的優(yōu)點,那就是能夠引用其自身,從而創(chuàng)建遞歸CTE。遞歸CTE是一個重復執(zhí)行初始CTE以返回數(shù)據(jù)子集直到獲取完整結(jié)果集的公用表表達式。
當某個查詢引用遞歸CTE時,它即被稱為遞歸查詢。遞歸查詢通常用于返回分層數(shù)據(jù),例如:顯示某個組織圖中的雇員或物料清單方案(其中父級產(chǎn)品有一個或多個組件,而那些組件可能還有子組件,或者是其他父級產(chǎn)品的組件)中的數(shù)據(jù)。
遞歸CTE可以極大地簡化在SELECT、INSERT、UPDATE、DELETE或CREATE VIEW語句中運行遞歸查詢所需的代碼。
也就是說,遞歸CTE通過引用自身來實現(xiàn)。它會不斷地重復查詢每一次遞歸得到的子集,直到得到最后的結(jié)果。這使得它非常適合處理"樹狀結(jié)構(gòu)"的數(shù)據(jù)或者有"層次關(guān)系"的數(shù)據(jù)。
2.1 語法
遞歸cte中包含一個或多個定位點成員,一個或多個遞歸成員,最后一個定位點成員必須使用"union [all]"(mariadb中的遞歸CTE只支持union [all]集合算法)聯(lián)合第一個遞歸成員。
以下是單個定位點成員、單個遞歸成員的遞歸CTE語法:
with recursive cte_name as (
select_statement_1 /* 該cte_body稱為定位點成員 */
union [all]
cte_usage_statement /* 此處引用cte自身,稱為遞歸成員 */
)
outer_definition_statement /* 對遞歸CTE的查詢,稱為遞歸查詢 */其中:
select_statement_1:稱為"定位點成員",這是遞歸cte中最先執(zhí)行的部分,也是遞歸成員開始遞歸時的數(shù)據(jù)來源。
cte_usage_statement:稱為"遞歸成員",該語句中必須引用cte自身。它是遞歸cte中真正開始遞歸的地方,它首先從定位點成員處獲取遞歸數(shù)據(jù)來源,然后和其他數(shù)據(jù)集結(jié)合開始遞歸,每遞歸一次都將遞歸結(jié)果傳遞給下一個遞歸動作,不斷重復地查詢后,當最終查不出數(shù)據(jù)時才結(jié)束遞歸。
outer_definition_statement:是對遞歸cte的查詢,這個查詢稱為"遞歸查詢"。
2.2 遞歸CTE示例(1)
舉個最經(jīng)典的例子:族譜。
例如,下面是一張族譜表
CREATE OR REPLACE TABLE fork(id INT NOT NULL UNIQUE,NAME CHAR(20),father INT,mother INT);
INSERT INTO fork VALUES
(1,'chenyi',2,3),(2,'huagner',4,5),(3,'zhangsan',NULL,NULL),
(4,'lisi',6,7),(5,'wangwu',8,9),(6,'zhaoliu',NULL,NULL),(7,'sunqi',NULL,NULL),
(8,'songba',NULL,NULL),(9,'yangjiu',NULL,NULL);
MariaDB [test]> select * from fork;
+----+----------+--------+--------+
| id | name | father | mother |
+----+----------+--------+--------+
| 1 | chenyi | 2 | 3 |
| 2 | huagner | 4 | 5 |
| 3 | zhangsan | NULL | NULL |
| 4 | lisi | 6 | 7 |
| 5 | wangwu | 8 | 9 |
| 6 | zhaoliu | NULL | NULL |
| 7 | sunqi | NULL | NULL |
| 8 | songba | NULL | NULL |
| 9 | yangjiu | NULL | NULL |
+----+----------+--------+--------+該族譜表對應(yīng)的結(jié)構(gòu)圖:

如果要找族譜中某人的父系,首先在定位點成員中獲取要從誰開始找,例如上圖中從"陳一"開始找。那么陳一這個記錄就是第一個遞歸成員的數(shù)據(jù)源,將這個數(shù)據(jù)源聯(lián)接族譜表,找到陳一的父親黃二,該結(jié)果將通過union子句結(jié)合到上一個"陳一"中。再次對黃二遞歸,找到李四,再對李四遞歸找到趙六,對趙六遞歸后找不到下一個數(shù)據(jù),所以這一分支的遞歸結(jié)束。
遞歸cte的語句如下:
WITH recursive fuxi AS (
SELECT * FROM fork WHERE `name`='chenyi'
UNION
SELECT f.* FROM fork f JOIN fuxi a WHERE f.id=a.father
)
SELECT * FROM fuxi;演變結(jié)果如下:
首先執(zhí)行定位點部分的語句,得到定位點成員,即結(jié)果中的第一行結(jié)果集:

根據(jù)該定位點成員,開始執(zhí)行遞歸語句:

遞歸時,按照f.id=a.father的條件進行篩選,得到id=2的結(jié)果,該結(jié)果通過union和之前的數(shù)據(jù)結(jié)合起來,作為下一次遞歸的數(shù)據(jù)源fuxi。
再進行第二次遞歸:

第三次遞歸:

由于第三次遞歸后,id=6的father值為null,因此第四次遞歸的結(jié)果為空,于是遞歸在第四次之后結(jié)束。
2.2 遞歸CTE示例(2)
該CTE示例主要目的是演示切換遞歸時的字段名稱。
例如,有幾個公交站點,它們之間的互通性如下圖:

對應(yīng)的表為:
CREATE OR REPLACE TABLE bus_routes (src char(50), dst char(50));
INSERT INTO bus_routes VALUES
('stopA','stopB'),('stopB','stopA'),('stopA','stopC'),('stopC','stopB'),('stopC','stopD');
MariaDB [test]> select * from bus_routes;
+-------+-------+
| src | dst |
+-------+-------+
| stopA | stopB |
| stopB | stopA |
| stopA | stopC |
| stopC | stopB |
| stopC | stopD |
+-------+-------+要計算以stopA作為起點,能到達哪些站點的遞歸CTE如下:
WITH recursive dst_stop AS (
SELECT src AS dst FROM bus_routes WHERE src='stopA' /* note: src as dst */
UNION
SELECT b.dst FROM bus_routes b
JOIN dst_stop d
WHERE d.dst=b.src
)
SELECT * FROM dst_stop;結(jié)果如下:
+-------+ | dst | +-------+ | stopA | | stopB | | stopC | | stopD | +-------+
首先執(zhí)行定位點語句,得到定位點成員stopA,字段名為dst。
再將定位點成員結(jié)果和bus_routes表聯(lián)接進行第一次遞歸,如下圖:

再進行第二次遞歸:

再進行第三次遞歸,但第三次遞歸過程中,stopD找不到對應(yīng)的記錄,因此遞歸結(jié)束。
2.2 遞歸CTE示例(3)
仍然是公交路線圖:

計算以stopA為起點,可以到達哪些站點,并給出路線圖。例如:stopA-->stopC-->stopD。
以下是遞歸CTE語句:
WITH recursive bus_path(bus_path,bus_dst) AS (
SELECT src,src FROM bus_routes WHERE src='stopA'
UNION
SELECT CONCAT(b2.bus_path,'-->',b1.dst),b1.dst
FROM bus_routes b1
JOIN bus_path b2
WHERE b2.bus_dst = b1.src AND LOCATE(b1.dst,b2.bus_path)=0
)
SELECT * FROM bus_path;首先獲取起點stopA,再獲取它的目標stopB和stopC,并將起點到目標使用"-->"連接,即concat(src,"-->","dst")。再根據(jù)stopB和stopC,獲取它們的目標。stopC的目標為stopD和stopB,stopB的目標為stopA。如果連接成功,那么路線為:
stopA-->stopB-->stopA 目標:stopA stopA-->stopC-->stopD 目標:stopD stopA-->stopC-->stopB 目標:stopB
這樣會無限遞歸下去,因此我們要判斷何時結(jié)束遞歸。判斷的方法是目標不允許出現(xiàn)在路線中,只要出現(xiàn),說明路線會重復計算。
總結(jié)
到此這篇關(guān)于MariaDB表表達式之公用表表達式(CTE)的文章就介紹到這了,更多相關(guān)MariaDB公用表表達式CTE內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Mysql/MariaDB啟動時處于進度條狀態(tài)導致啟動失敗的原因及解決辦法
本文給大家介紹Mysql/MariaDB啟動時一直處于進度條狀態(tài),進度條結(jié)束后提示error。究竟是什么原因呢?該怎么解決呢?跟著小編一起看看該如何解決此問題呢。2015-09-09
MariaDB性能調(diào)優(yōu)工具mytop的使用詳解
這篇文章主要給大家介紹了關(guān)于MariaDB性能調(diào)優(yōu)工具mytop的使用,文中介紹的非常詳細,對大家具有一定的參考價值,需要的朋友們下面來一起看看吧。2017-03-03
Windows Server 2016 服務(wù)器配置指南 之 MariaDB數(shù)據(jù)庫環(huán)境搭建方法
這篇文章主要介紹了Windows Server 2016 服務(wù)器配置指南 之 MariaDB數(shù)據(jù)庫環(huán)境搭建方法,需要的朋友可以參考下2017-08-08
在Ubuntu系統(tǒng)中安裝MariaDB數(shù)據(jù)庫的教程
這篇文章主要介紹了在Ubuntu系統(tǒng)中安裝MariaDB數(shù)據(jù)庫的教程,同時也適用于其他Debian系的Linux系統(tǒng),需要的朋友可以參考下2015-06-06
MariaDB中1045權(quán)限錯誤導致拒絕用戶訪問的錯誤解決方法
這篇文章主要介紹了MariaDB中1045權(quán)限錯誤導致拒絕用戶訪問的錯誤解決方法,需要的朋友可以參考下2016-01-01

