PostgreSQL中RECURSIVE遞歸查詢使用總結


RECURSIVE

前言

WITH提供了一種方式來書寫在一個大型查詢中使用的輔助語句。這些語句通常被稱為公共表表達式或CTE,它們可以被看成是定義只在一個查詢中存在的臨時表。在WITH子句中的每一個輔助語句可以是一個SELECT、INSERT、UPDATE或DELETE,並且WITH子句本身也可以被附加到一個主語句,主語句也可以是SELECT、INSERT、UPDATE或DELETE。

CTE or WITH

WITH語句通常被稱為通用表表達式(Common Table Expressions)或者CTEs。

WITH語句作為一個輔助語句依附於主語句,WITH語句和主語句都可以是SELECT,INSERT,UPDATE,DELETE中的任何一種語句。

舉個栗子

WITH result AS (
    SELECT d.user_id
    FROM documents d
    GROUP BY d.user_id
),info as(
    SELECT t.*,json_build_object('id', ur.id, 'name', ur.name) AS user_info
    FROM result t
    LEFT JOIN users ur on ur.id = t.user_id
    WHERE ur.id IS NOT NULL
)select * from info

定義了兩個WITH輔助語句,result和info。result查詢出符合要求的user信息,然后info對這個信息進行組裝,組裝出我們需要的數據信息。

當然不用這個也是可以的,不過CTE主要的還是做數據的過濾。什么意思呢,我們可以定義多層級的CTE,然后一層層的查詢過濾組裝。最終篩選出我們需要的數據,當然你可能會問為什么不一次性拿出所有的數據呢,當然如果數據很大,我們通過多層次的數據過濾組裝,在效率上也更好。

在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語句和主語句的事務性,如下例所示。

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

The sub-statements in WITH中的子語句被和每一個其他子語句以及主查詢並發執行。因此在使用WITH中的數據修改語句時,指定更新的順序實際是以不可預測的方式發生的。RETURNING數據是在不同WITH子語句和主查詢之間傳達改變的唯一方法。

WITH t AS (
    UPDATE products SET price = price * 1.05
    RETURNING *
)
SELECT * FROM products;

外層SELECT可以返回在UPDATE動作之前的原始價格,而在

WITH t AS (
    UPDATE products SET price = price * 1.05
    RETURNING *
)
SELECT * FROM t;

外部SELECT將返回更新過的數據。

WITH使用注意事項

1、WITH中的數據修改語句會被執行一次,並且肯定會完全執行,無論主語句是否讀取或者是否讀取所有其輸出。而WITH中的SELECT語句則只輸出主語句中所需要記錄數。

2、WITH中使用多個子句時,這些子句和主語句會並行執行,所以當存在多個修改子語句修改相同的記錄時,它們的結果不可預測。

3、所有的子句所能“看”到的數據集是一樣的,所以它們看不到其它語句對目標數據集的影響。這也緩解了多子句執行順序的不可預測性造成的影響。

4、如果在一條SQL語句中,更新同一記錄多次,只有其中一條會生效,並且很難預測哪一個會生效。

5、如果在一條SQL語句中,同時更新和刪除某條記錄,則只有更新會生效。

6、目前,任何一個被數據修改CTE的表,不允許使用條件規則,和ALSO規則以及INSTEAD規則。

RECURSIVE

可選的RECURSIVE修飾符將WITH從單純的句法便利變成了一種在標准SQL中不能完成的特性。通過使用RECURSIVE,一個WITH查詢可以引用它自己的輸出。

比如下面的這個表:

create table document_directories
(
    id         bigserial                                          not null
        constraint document_directories_pk
            primary key,
    name       text                                               not null,
    created_at timestamp with time zone default CURRENT_TIMESTAMP not null,
    updated_at timestamp with time zone default CURRENT_TIMESTAMP not null,
    parent_id  bigint                   default 0                 not null
);

comment on table document_directories is '文檔目錄';

comment on column document_directories.name is '名稱';

comment on column document_directories.parent_id is '父級id';

INSERT INTO public.document_directories (id, name, created_at, updated_at, parent_id) VALUES (1, '中國', '2020-03-28 15:55:27.137439', '2020-03-28 15:55:27.137439', 0);
INSERT INTO public.document_directories (id, name, created_at, updated_at, parent_id) VALUES (2, '上海', '2020-03-28 15:55:40.894773', '2020-03-28 15:55:40.894773', 1);
INSERT INTO public.document_directories (id, name, created_at, updated_at, parent_id) VALUES (3, '北京', '2020-03-28 15:55:53.631493', '2020-03-28 15:55:53.631493', 1);
INSERT INTO public.document_directories (id, name, created_at, updated_at, parent_id) VALUES (4, '南京', '2020-03-28 15:56:05.496985', '2020-03-28 15:56:05.496985', 1);
INSERT INTO public.document_directories (id, name, created_at, updated_at, parent_id) VALUES (5, '浦東新區', '2020-03-28 15:56:24.824672', '2020-03-28 15:56:24.824672', 2);
INSERT INTO public.document_directories (id, name, created_at, updated_at, parent_id) VALUES (6, '徐匯區', '2020-03-28 15:56:39.664924', '2020-03-28 15:56:39.664924', 2);
INSERT INTO public.document_directories (id, name, created_at, updated_at, parent_id) VALUES (7, '漕寶路', '2020-03-28 15:57:14.320631', '2020-03-28 15:57:14.320631', 6);

這是一個無限級分類的列表,我們制造幾條數據,來分析下RECURSIVE的使用。

WITH RECURSIVE res AS (
    SELECT id, name, parent_id
    FROM document_directories
    WHERE id = 5
    UNION
    SELECT dd.id,
           dd.name || ' > ' || d.name,
           dd.parent_id
    FROM res d
             INNER JOIN document_directories dd ON dd.id = d.parent_id
)
select *
from res

當然這個sql也可以這樣寫
WITH RECURSIVE res(id, name, parent_id) AS (
    SELECT id, name, parent_id
    FROM document_directories
    WHERE id = 5
    UNION
    SELECT dd.id,
           dd.name || ' > ' || d.name,
           dd.parent_id
    FROM res d
             INNER JOIN document_directories dd ON dd.id = d.parent_id
)
select *
from res

遞歸查詢的過程

這是pgsql操作文檔中的描述:

1、計算非遞歸項。對UNION(但不對UNION ALL),拋棄重復行。把所有剩余的行包括在遞歸查詢的結果中,並且也把它們放在一個臨時的工作表中。

2、只要工作表不為空,重復下列步驟:

a計算遞歸項,用當前工作表的內容替換遞歸自引用。對UNION(不是UNION ALL),拋棄重復行以及那些與之前結果行重復的行。將剩下的所有行包括在遞歸查詢的結果中,並且也把它們放在一個臨時的中間表中。

b用中間表的內容替換工作表的內容,然后清空中間表。

拆解下執行的過程

其實執行就分成了兩部分:

1、non-recursive term(非遞歸部分),即上例中的union前面部分

2、recursive term(遞歸部分),即上例中union后面部分

拆解下我們上面的sql

1、執行非遞歸部分
  SELECT id, name, parent_id
    FROM document_directories
    WHERE id = 5
結果集和working table為
5	浦東新區	2
2、執行遞歸部分,如果是UNION,要用當前查詢的結果和上一個working table的結果進行去重,然后放到到臨時表中。然后把working table的數據替換成臨時表里面的數據。
 SELECT dd.id,
           dd.name || ' > ' || d.name,
           dd.parent_id
    FROM res d
             INNER JOIN document_directories dd ON dd.id = d.parent_id
結果集和working table為
2	上海 > 浦東新區	1
3、同2,直到數據表中沒有數據。
 SELECT dd.id,
           dd.name || ' > ' || d.name,
           dd.parent_id
    FROM res d
             INNER JOIN document_directories dd ON dd.id = d.parent_id
結果集和working table為
1	中國 > 上海 > 浦東新區	0
4、結束遞歸,將前幾個步驟的結果集合並,即得到最終的WITH RECURSIVE的結果集

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

WITH RECURSIVE 使用限制

1、 如果在recursive term中使用LEFT JOIN,自引用必須在“左”邊
2、 如果在recursive term中使用RIGHT JOIN,自引用必須在“右”邊
3、 recursive term中不允許使用FULL JOIN
4、 recursive term中不允許使用GROUP BY和HAVING
5、 不允許在recursive term的WHERE語句的子查詢中使用CTE的名字
6、 不支持在recursive term中對CTE作aggregation
7、 recursive term中不允許使用ORDER BY
8、 LIMIT / OFFSET不允許在recursive term中使用
9、 FOR UPDATE不可在recursive term中使用
10、 recursive term中SELECT后面不允許出現引用CTE名字的子查詢
11、 同時使用多個CTE表達式時,不允許多表達式之間互相訪問(支持單向訪問)
12、 在recursive term中不允許使用FOR UPDATE

CTE 優缺點

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

UNION與UNION ALL的區別

UNION用的比較多union all是直接連接,取到得是所有值,記錄可能有重復 union 是取唯一值,記錄沒有重復

1、UNION 的語法如下:

    [SQL 語句 1]
      UNION
     [SQL 語句 2]

2、UNION ALL 的語法如下:

  [SQL 語句 1]
      UNION ALL
     [SQL 語句 2]

UNION和UNION ALL關鍵字都是將兩個結果集合並為一個,但這兩者從使用和效率上來說都有所不同。

1、對重復結果的處理:UNION在進行表鏈接后會篩選掉重復的記錄,Union All不會去除重復記錄。
2、對排序的處理:Union將會按照字段的順序進行排序;UNION ALL只是簡單的將兩個結果合並后就返回。

從效率上說,UNION ALL 要比UNION快很多,所以,如果可以確認合並的兩個結果集中不包含重復數據且不需要排序時的話,那么就使用UNION ALL。

總結

  • UNION去重且排序

  • UNION ALL不去重不排序(效率高)

總結

recursive是pgsql中提供的一種遞歸的機制,比如當我們查詢一個完整的樹形結構使用這個就很完美,但是我們應該避免發生遞歸的死循環,也就是數據的環狀。當然他只是cte中的一個查詢的屬性,對於cte的使用,我們也不能忽略它需要注意的地方,使用多個子句時,這些子句和主語句會並行執行。我們是不能判斷那個將會被執行的,在一條SQL語句中,更新同一記錄多次,只有其中一條會生效,並且很難預測哪一個會生效。當然功能還是很強大的,WITH語句和主語句都可以是SELECT,INSERT,UPDATE,DELETE中的任何一種語句,我們可以組裝出我們需要的任何操作的場景。

參考

【SQL優化(五) PostgreSQL (遞歸)CTE 通用表表達式】http://www.jasongj.com/sql/cte/
【WITH查詢(公共表表達式)】http://postgres.cn/docs/11/queries-with.html
【UNION與UNION ALL的區別】https://juejin.im/post/5c131ee4e51d45404123d572
【PostgreSQL的遞歸查詢(with recursive)】https://my.oschina.net/Kenyon/blog/55137


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM