sqlite3-樹形遞歸查詢-with
在一個遞歸公共表表達式裏,遞歸表用CTE表名命名。在上面的遞歸公共表表達式示意圖中,遞歸表必須且只能出現在遞歸查詢的FROM子句裏,不能
出現在initial-select或者the recursive-select,包括subqueries的任何地方。
初始查詢可以是一個聯合查詢,但是它不能包含ORDER BY, LIMIT, OFFSET.
遞歸查詢必須是一個簡單查詢,而不能是一個聯合查詢語句。遞歸查詢允許包含ORDER BY, LIMIT, OFFSET.
執行遞歸查詢表內容的基本算法如下:
1.執行初始查詢並將查詢結果放入一個隊列;
2.如果隊列不為空
1.從隊列中提取一條記錄;
2.將這條記錄插入遞歸表;
3.假設剛剛被提取的記錄是遞歸表中唯一一條記錄,然後,運行遞歸查詢,把所有結果放入隊列
以上,基本過程可能被以下附加規則修改:
如果,用一個UNION操作符把初始查詢和遞歸查詢銜接起來,那麽,僅僅當這條記錄不在隊列時,才將這條記錄添加到隊列中。重復的記錄被丟棄,在加入隊列之前。
即使在遞歸查詢時,重復的隊列已經從隊列中提取出來,如果,操作符是UNION ALL,那麽在初始查詢和遞歸查詢階段產生的記錄總是被添加到隊列當中,即使他們存在重復。
判斷記錄是否重復,NULL值與另一個NULL值比較是相等的,和其他值比較是不相等的。
LIMIT子句,如果出現,判斷遞歸表中記錄的個數,一旦達到指定記錄個數,遞歸停止。一個以0作為限制數值意味闃沒有記錄被添加到遞歸表。
一個負值意味著添加到遞歸表的記錄個數是沒有限制的。
OFFSET子句,如果出現,有一個正值N, 會阻止前N條記錄添加到遞歸表中。前N條記錄仍然被遞歸查詢處理。只是他們不添加到遞歸表中。當所有OFFSET記錄被跳過,才開始記錄
記錄個數,直到達到限制數值。
如果有ORDER BY子句出現,它會在步驟2a中,決定隊列中記錄被提取的順序,如果沒有ORDER BY, 那麽在被提取的記錄順序是未定義的。在當前實現中,如果省略ORDER BY子句,隊列是一個先進先出隊列。
但是應用程序不應該依賴這一個事實,因為它可能會改變。
遞歸查詢示例:
以下查詢返回1到1000000之間的所有整數。
with recursive
cnt(x) as (values(1) union all select x+1 from cnt where x < 1000000 br style=‘font-size:16px;font-style:normal;font-weight:normal;color:rgb(0, 0, 0);‘ /> select * from cnt;
思考這個查詢是如何工作的,
初始查詢首先運行和返回一個單一字段的單一記錄1,這條記錄被添加到隊列中。
在步驟2a中,這條記錄從隊列中移出並添加到遞歸表中,
接著,執行遞歸查詢。
按照步驟2c,將產生的單一記錄2添加到隊列中。這裏隊列仍然還有一條記錄,因此步驟2繼續重復。
記錄2按照步驟2a和2c被提取並添加到遞歸表中,接著記錄2被使用,就好像它是遞歸表中的全部內容,遞歸查詢繼續。結果產生一條記錄3,並被添加到隊列
這樣重復999999次,直到最後隊列中包含的記錄為1000000,這條記錄被提取和添加到遞歸表,但是,這次,where子句會引起遞歸查詢返回結果無記錄,因此隊列
剩下為空和遞歸結束。
優化註意事項:
在上面的討論中,如“插入行插入遞歸表”報表應在概念上理解,不能從字面上。
這聽起來好像是SQLite的積累含有一百萬行一個巨大的表,然後回去和掃描該表由上到下產生的結果。
實際的情況是,查詢優化器認為,在“CNT”遞歸表值只使用一次。
從而各行被添加到遞歸表,該行被立即返回作為主SELECT語句的結果,然後丟棄。
SQLite不累積一百萬行的臨時表。很少的內存是需要運行上面的例子。
但是,如果例如曾使用UNION代替UNION ALL,那麽SQLite的將不得不保持所有以前生成的內容,以檢查重復。
出於這個原因,程序員應該努力利用一切,而不是UNION UNION,可行的情況下。
在以上討論中,像插入記錄到遞歸表中的語句應該在概念上理解,不能從字面上,它聽起來就像是sqlite正在積累一張含有一百萬行的巨大的表,
然後回去和從上到下掃描該表並產生結果,實際的情況是,查詢優化器認為,在"cnt"遞歸表中的值僅僅被使用一次,因此,當一條記錄被添加到遞歸表時,
記錄直接作為主查詢語句的結果返回,然後丟棄。sqlite不會累積一張一百萬行的臨時表。運行以上示例,只需很少的內存空間。無論怎麽樣,如果示例
使用UNION代替UNION ALL,然後sqlite不得不保留所有先前產生的記錄內容,以檢查是否重復。
因為這個原因,在可行情況下,程序員應該努力使用UNION ALL代替UNION.
對上面的例子做一些改變,如下:
with recursive
cnt(x) as (select 1 union all select x+1 from cnt limit 1000000)
select * from cnt;
這裏有兩個地方不同,初始查詢用"SELECT 1"代替"VALUES(1)".但是這些只不過是利用不同的語句做相同的事。另一個不同的地方是遞歸結束通過一個LIMIT而不是
一個WHERE子句,使用LIMIT意味著當一百萬行記錄添加到遞歸表時(主查詢執行返回,由於查詢優化器),接著遞歸直接結束而不管在隊列中還有多少條記錄。
在一個更復雜的查詢中,它有時是很難的,要保證where子句最終引起隊列為空和遞歸中止,但是,LIMIT子句總是能會停止遞歸。如果遞歸記錄的上界大小是已知的,
為安全起見,總是包含一個LIMIT子句是一個好的方式。
分層查詢示例:
創建一張表,描述一個組織的成員以及組織內部的關系鏈
CREATE TABLE org(
name TEXT PRIMARY KEY,
boss TEXT REFERENCES org,
height INT,
-- other content omitted
);
在組織裏的每個成員都有一個名字,所有的成員只有一個老板,也就是整個組織的最頂端,這個表的所有記錄關系形成一個棵樹。
這裏有一個查詢,計算ALICE組織部門中每個人的平均體重,包括ALICE
WITH RECURSIVE
works_for_alice(n) AS (
VALUES(‘Alice‘)
UNION
SELECT name FROM org, works_for_alice
WHERE org.boss=works_for_alice.n
)
SELECT avg(height) FROM org
WHERE org.name IN works_for_alice;
下面一個例子在一個WITH子句中,使用兩個公共表表達式,以下表表示一個家庭樹
CREATE TABLE family(
name TEXT PRIMARY KEY,
mom TEXT REFERENCES family,
dad TEXT REFERENCES family,
born DATETIME,
died DATETIME, -- NULL if still alive
-- other content
);
這個家庭表跟之前的組織表是相似的,除了每個成員都有兩個父結點。我們想要知道ALICE所有健在的祖輩,從老到小。一個普通的公共表表達式,"parent_of",首先被定義
這個普通公共表表達式是一個視圖,被用來查找每個人的所有父輩。普通公共表表達式在遞歸公共表表達式ancestor_of_alice中使用.
接著,遞歸公共表表達式在後面查詢被使用:
WITH RECURSIVE
parent_of(name, parent) AS
(SELECT name, mom FROM family UNION SELECT name, dad FROM family),
ancestor_of_alice(name) AS
(SELECT parent FROM parent_of WHERE name=‘Alice‘
UNION ALL
SELECT parent FROM parent_of JOIN ancestor_of_alice USING(name))
SELECT family.name FROM ancestor_of_alice, family
WHERE ancestor_of_alice.name=family.name
AND died IS NULL
ORDER BY born;
查詢圖表:
版本控制系統通常存儲每個工程的變化版本,作為一個有向無環圖,調用項目的每個版本簽入,一次簽入可能是0或者有很多的父節點。
大部分簽入,除了第一次,有一個父節點,但是,在合並情況下時,一個簽入可能有兩,三個或者更多的父節點。跟蹤簽入的和它們發生的順序的模式,
就像如下所示:
CREATE TABLE checkin(
id INTEGER PRIMARY KEY,
mtime INTEGER -- timestamp when this checkin occurred
);
CREATE TABLE derivedfrom(
xfrom INTEGER NOT NULL REFERENCES checkin, -- parent checkin
xto INTEGER NOT NULL REFERENCES checkin, -- derived checkin
PRIMARY KEY(xfrom,xto)
);
CREATE INDEX derivedfrom_back ON derivedfrom(xto,xfrom);
此圖是無環圖,我們假定每個孩子簽入不超過其所有父節點的修改時間,但是,與前面的例子不同的是,這個圖在任何兩次簽入之間的可能有多條不同長度的路徑。
我們想要知道在時間線上最近的20次簽入,對於簽入"@BASELINE,(在整個DAG有成千上萬個祖先)這個查詢類似於使用Fossil版本控制系統,顯示最近的N個簽入。
示例: http://www.sqlite.org/src/timeline?p=trunk&n=30
WITH RECURSIVE
ancestor(id,mtime) AS (
SELECT id, mtime FROM checkin WHERE id=@BASELINE
UNION
SELECT derivedfrom.xfrom, checkin.mtime
FROM ancestor, derivedfrom, checkin
WHERE ancestor.id=derivedfrom.xto
AND checkin.id=derivedfrom.xfrom
ORDER BY checkin.mtime DESC
LIMIT 20
)
SELECT * FROM checkin JOIN ancestor USING(id);
在遞歸查詢裏按時間降序會使得查詢執行得更快,通過防止它從很早之前合並簽入的分支遍歷。order by 使得遞歸查詢把重點放在最近簽入的記錄上,剛好也是我們想要得到的。
如果在遞歸查詢中沒有使用order by, 一個可能是遍歷所有成千上萬的提交記錄,按時間線重對它們進行排序。接著,返回前20條記錄。在order by基礎上建立一個優先級隊列
使得遞歸查詢首先查找最近的提交記錄。允許使用LIMIT子句,使得查詢範圍限制在感興趣的簽入記錄上。
通過使用ORDER BY,深度優先對比廣度優先搜索遍歷樹,
ORDER BY子句的遞歸查詢可以用來控制搜索樹是否是深度優先或廣度優先。為了說明這一點,我們將對上面示例中的ORG表進行一處修改,沒有了Height列,
並且插入以下這些數據。
CREATE TABLE org(
name TEXT PRIMARY KEY,
boss TEXT REFERENCES org
) WITHOUT ROWID;
INSERT INTO org VALUES(‘Alice‘,NULL);
INSERT INTO org VALUES(‘Bob‘,‘Alice‘);
INSERT INTO org VALUES(‘Cindy‘,‘Alice‘);
INSERT INTO org VALUES(‘Dave‘,‘Bob‘);
INSERT INTO org VALUES(‘Emma‘,‘Bob‘);
INSERT INTO org VALUES(‘Fred‘,‘Cindy‘);
INSERT INTO org VALUES(‘Gail‘,‘Cindy‘);
這裏有一個查詢對樹結構,采用深度優先策略
WITH RECURSIVE
under_alice(name,level) AS (
VALUES(‘Alice‘,0)
UNION ALL
SELECT org.name, under_alice.level+1
FROM org JOIN under_alice ON org.boss=under_alice.name
ORDER BY 2
)
SELECT substr(‘..........‘,1,level*3) || name FROM under_alice;
在“ORDER BY2”(即等同於“ORDER BY under_alice.level+1”)會導致組織結構圖中更高層次的成員(用較小的“級別”的值)能夠得到優先處理,造成了廣度優先搜索。
輸出結果為:
Alice
...Bob
...Cindy
......Dave
......Emma
......Fred
......Gail
但是,如果我們改變了ORDER BY子句添加“DESC”修飾符,這將導致在組織中較低層次(與較大的“級別”的值)優先處理,造成了深度優先搜索:
WITH RECURSIVE
under_alice(name,level) AS (
VALUES(‘Alice‘,0)
UNION ALL
SELECT org.name, under_alice.level+1
FROM org JOIN under_alice ON org.boss=under_alice.name
ORDER BY 2 DESC
)
SELECT substr(‘..........‘,1,level*3) || name FROM under_alice;
修改後的查詢結果為:
Alice
...Bob
......Dave
......Emma
...Cindy
......Fred
......Gail
當在遞歸查詢中省略OREDER BY時,隊列就像是一個FIFO, 這造成了廣度優先搜索
sqlite3-樹形遞歸查詢-with