zoukankan      html  css  js  c++  java
  • 【原文】PostgreSQL中RECURSIVE递归查询使用总结

    原文:https://www.cnblogs.com/ricklz/p/12590618.html

    https://www.runoob.com/postgresql/postgresql-with.html

    __________________________________________________________________________________________

    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

    ————————————————————————————————————————————————————————————

    PostgreSQL WITH 子句

    在 PostgreSQL 中,WITH 子句提供了一种编写辅助语句的方法,以便在更大的查询中使用。

    WITH 子句有助于将复杂的大型查询分解为更简单的表单,便于阅读。这些语句通常称为通用表表达式(Common Table Express, CTE),也可以当做一个为查询而存在的临时表。

    WITH 子句是在多次执行子查询时特别有用,允许我们在查询中通过它的名称(可能是多次)引用它。

    WITH 子句在使用前必须先定义。

    语法

    WITH 查询的基础语法如下:

    WITH
       name_for_summary_data AS (
          SELECT Statement)
       SELECT columns
       FROM name_for_summary_data
       WHERE conditions <=> (
          SELECT column
          FROM name_for_summary_data)
       [ORDER BY columns]

    name_for_summary_data 是 WITH 子句的名称,name_for_summary_data 可以与现有的表名相同,并且具有优先级。

    可以在 WITH 中使用数据 INSERT, UPDATE 或 DELETE 语句,允许您在同一个查询中执行多个不同的操作。

    WITH 递归

    在 WITH 子句中可以使用自身输出的数据。

    公用表表达式 (CTE) 具有一个重要的优点,那就是能够引用其自身,从而创建递归 CTE。递归 CTE 是一个重复执行初始 CTE 以返回数据子集直到获取完整结果集的公用表表达式。

    实例

    创建 COMPANY 表(下载 COMPANY SQL 文件 ),数据内容如下:

    runoobdb# select * from COMPANY;
     id | name  | age | address   | salary
    ----+-------+-----+-----------+--------
      1 | Paul  |  32 | California|  20000
      2 | Allen |  25 | Texas     |  15000
      3 | Teddy |  23 | Norway    |  20000
      4 | Mark  |  25 | Rich-Mond |  65000
      5 | David |  27 | Texas     |  85000
      6 | Kim   |  22 | South-Hall|  45000
      7 | James |  24 | Houston   |  10000
    (7 rows)

    下面将使用 WITH 子句在上表中查询数据:

    With CTE AS
    (Select
     ID
    , NAME
    , AGE
    , ADDRESS
    , SALARY
    FROM COMPANY )
    Select * From CTE;

    得到结果如下:

    id | name  | age | address   | salary
    ----+-------+-----+-----------+--------
      1 | Paul  |  32 | California|  20000
      2 | Allen |  25 | Texas     |  15000
      3 | Teddy |  23 | Norway    |  20000
      4 | Mark  |  25 | Rich-Mond |  65000
      5 | David |  27 | Texas     |  85000
      6 | Kim   |  22 | South-Hall|  45000
      7 | James |  24 | Houston   |  10000
    (7 rows)

    接下来让我们使用 RECURSIVE 关键字和 WITH 子句编写一个查询,查找 SALARY(工资) 字段小于 20000 的数据并计算它们的和:

    WITH RECURSIVE t(n) AS (
       VALUES (0)
       UNION ALL
       SELECT SALARY FROM COMPANY WHERE SALARY < 20000
    )
    SELECT sum(n) FROM t;

    得到结果如下:

     sum
    -------
     25000
    (1 row)

    下面我们建立一张和 COMPANY 表相似的 COMPANY1 表,使用 DELETE 语句和 WITH 子句删除 COMPANY 表中 SALARY(工资) 字段大于等于 30000 的数据,并将删除的数据插入 COMPANY1 表,实现将 COMPANY 表数据转移到 COMPANY1 表中:

    CREATE TABLE COMPANY1(
       ID INT PRIMARY KEY     NOT NULL,
       NAME           TEXT    NOT NULL,
       AGE            INT     NOT NULL,
       ADDRESS        CHAR(50),
       SALARY         REAL
    );
    
    
    WITH moved_rows AS (
       DELETE FROM COMPANY
       WHERE
          SALARY >= 30000
       RETURNING *
    )
    INSERT INTO COMPANY1 (SELECT * FROM moved_rows);

    得到结果如下:

    INSERT 0 3

    此时,CAMPANY 表和 CAMPANY1 表的数据如下:

    runoobdb=# SELECT * FROM COMPANY;
     id | name  | age |  address   | salary
    ----+-------+-----+------------+--------
      1 | Paul  |  32 | California |  20000
      2 | Allen |  25 | Texas      |  15000
      3 | Teddy |  23 | Norway     |  20000
      7 | James |  24 | Houston    |  10000
    (4 rows)
    
    
    runoobdb=# SELECT * FROM COMPANY1;
     id | name  | age | address | salary
    ----+-------+-----+-------------+--------
      4 | Mark  |  25 | Rich-Mond   |  65000
      5 | David |  27 | Texas       |  85000
      6 | Kim   |  22 | South-Hall  |  45000
    (3 rows)
  • 相关阅读:
    超棒的前端开发界面套件 InK
    现代浏览器的web音频javascript类库 Howler.js
    富有创意的菱形响应式页面设计
    创意味儿十足的web布局及交互设计
    一个超酷的横向多列响应式布局效果
    帮助你生成响应式布局的CSS模板 xyCSS
    免费素材大荟萃:免费图标和UI设计
    使用浏览器生成超棒的midi音乐 midi.js
    JavaScript 和 .NET 中的 JavaScript Object Notation (JSON) 简介
    推荐一批基于web的开源html文本编辑器(40+)
  • 原文地址:https://www.cnblogs.com/oxspirt/p/13962347.html
Copyright © 2011-2022 走看看