【问题标题】:Improve query performance for union of CTE + normal select提高 CTE + 普通选择联合的查询性能
【发布时间】:2013-01-16 05:42:19
【问题描述】:

Convert recursive function to view 的基础上,我想通过创建节点父节点的快照来加快从图中任意节点到其根节点的路径检索。这个想法是递归树遍历受中间快照的限制,这些快照避免了任何进一步的递归,从而加快了执行时间。我没有进行负载测试,所以除了这个简单的示例之外,我不知道它的性能如何,但早期的试验已经表明存在一些瓶颈。我很高兴 cmets 如何加快/简化查询。我正在使用 Postgres 9.2.2.0 (20)。

DROP TABLE IF EXISTS revision CASCADE;
CREATE TABLE revision (
  id serial primary key,
  parent_revision_id int references revision(id),
  username varchar(128),
  ts timestamp without time zone
);

DROP TABLE IF EXISTS revision_snapshot CASCADE;
CREATE TABLE revision_snapshot (
  id serial primary key,
  revision_id int,
  parent_revision_id int,
  depth int
);

CREATE OR REPLACE FUNCTION create_revision_snapshot(_rev int)
  RETURNS void AS
$func$
DELETE FROM revision_snapshot WHERE revision_id=$1;
INSERT INTO revision_snapshot (revision_id, parent_revision_id, depth) 
  (SELECT $1, id, depth FROM revision_tree($1));
$func$ LANGUAGE sql;


-- Recursively return path from '_rev' to root
CREATE OR REPLACE FUNCTION revision_tree(_rev int)
 RETURNS TABLE(id int, parent_revision_id int, depth int) AS
$func$
   WITH RECURSIVE rev_list(id, parent_revision_id, depth) AS (
      SELECT t.id, t.parent_revision_id, 1
      FROM   revision t
      WHERE  t.id = $1

      UNION ALL
      SELECT t.id, t.parent_revision_id, r.depth + 1
      FROM   rev_list r
      JOIN   revision t ON t.id = r.parent_revision_id
   )
   SELECT t.id, t.parent_revision_id, t.depth
   FROM   rev_list t
   ORDER  BY t.id;
$func$ LANGUAGE sql;


-- Fast version of 'revision_tree' (to be). This version will return the 
-- revision tree making use of snapshots (recursively returning the path from 
-- specified revision id to last snapshot of the path to the root + the snapshot)
CREATE OR REPLACE FUNCTION revision_tree_perf(_rev int)
  RETURNS TABLE(parent_revision_id int) AS
$func$
BEGIN
  CREATE TEMP TABLE graph_result ON COMMIT DROP AS
  WITH RECURSIVE rev_list(id, parent_revision_id, depth) AS (
      SELECT t.id, t.parent_revision_id, 1
      FROM   revision t
      WHERE  t.id = $1
      UNION ALL
      SELECT t.id, t.parent_revision_id, r.depth + 1
      FROM   rev_list r
      JOIN   revision t ON t.id = r.parent_revision_id
      WHERE  not(t.id in (select revision_id from revision_snapshot))
   )
   SELECT t.id, t.parent_revision_id, t.depth
   FROM   rev_list t
   ORDER  BY t.id;
   RETURN QUERY
   SELECT g.parent_revision_id FROM graph_result AS g WHERE g.parent_revision_id IS NOT NULL 
   UNION
   SELECT s.parent_revision_id FROM revision_snapshot AS s WHERE 
     s.revision_id = (SELECT min(q.parent_revision_id) FROM graph_result as q) ORDER BY parent_revision_id;
END;
$func$
LANGUAGE 'plpgsql';


-- Example tree
--
--                                                   +-- <10>
--                                                  /
--                         +-- <4> -- <8> --- <9> -+- <11> --- <15> --- <16> --- <17>
--                        /                                    
--  <1> --- <2> --- <3> -+                                     
--                        \                                    
--                         +-- <5> --- <6> --- <7> --- <12> -+- <14> --- <18>
--                                                            \
--                                                             \
--                                                              \
--                                                               \
--                                                                +-- <13> --- <19> --- <20> --- <21>
--


INSERT INTO revision (username, ts, parent_revision_id) VALUES
  ('someone', now(), null)   -- 1
  ,('someone', now(), 1)     -- 2
  ,('someone', now(), 2)     -- 3
  ,('someone', now(), 3)     -- 4
  ,('someone', now(), 3)     -- 5
  ,('someone', now(), 5)     -- 6
  ,('someone', now(), 6)     -- 7
  ,('someone', now(), 4)     -- 8
  ,('someone', now(), 8)     -- 9
  ,('someone', now(), 9)     -- 10
  ,('someone', now(), 9)     -- 11
  ,('someone', now(), 7)     -- 12
  ,('someone', now(), 12)    -- 13
  ,('someone', now(), 12)    -- 14
  ,('someone', now(), 11)    -- 15
  ,('someone', now(), 15)    -- 16
  ,('someone', now(), 16)    -- 17
  ,('someone', now(), 14)    -- 18
  ,('someone', now(), 13)    -- 19
  ,('someone', now(), 19)    -- 20
  ,('someone', now(), 20);   -- 21


-- Create a revision snapsnot
select create_revision_snapshot(13);

-- This query is meant to be faster ...
select * from revision_tree_perf(21);

-- ... than this one
select * from revision_tree(21);

上面的例子

select create_revision_snapshot(13);
select * from revision_tree_perf(21);

旨在产生一个记录集,该记录集表示从 21 到根的路径,即(21, 20, 19, 13, 12, 7, 6, 5, 3, 2, 1)。部分解决方案是通过遍历树(21 到 13,因为有 13 的快照,因此无需再遍历树)并使用从 13 到根的已经“缓存”路径(取自修订快照)。希望这会让它更容易理解......

更新:
我想出了一个潜在的改进。这只是在黑暗中刺伤,但我可以想象exists 子句非常昂贵。我现在在修订表中标记了快照的存在:

CREATE TABLE revision (
  id serial primary key,
  parent_revision_id int references revision(id),
  username varchar(128),
  has_snapshot boolean default false,
  ts timestamp without time zone
);

CREATE OR REPLACE FUNCTION create_revision_snapshot(_rev int) RETURNS void AS $func$
  DELETE FROM revision_snapshot WHERE revision_id=$1;
  INSERT INTO revision_snapshot (revision_id, parent_revision_id, depth) 
    (SELECT $1, id, depth FROM revision_tree($1));
  -- Mark revision table to avoid costly exists/in clause
  UPDATE revision SET has_snapshot = true WHERE id=$1;
$func$ LANGUAGE sql;

这会将revision_tree_perf SP 的 CTE 部分更改为

WITH RECURSIVE rev_list(id, parent_revision_id, depth) AS (
  SELECT t.id, t.parent_revision_id, 1 -- AS depth
  FROM   revision t
  WHERE  t.id = $1
  UNION ALL
  SELECT t.id, t.parent_revision_id, r.depth + 1
  FROM   rev_list r
  JOIN   revision t ON t.id = r.parent_revision_id
  WHERE  t.has_snapshot = false
)
SELECT t.id, t.parent_revision_id, t.depth FROM   rev_list t ORDER BY t.id;

这应该会很快执行。难题的另一部分是从has_snapshot=true 的修订ID 返回revision_snapshot 的内容并将这两个结果连接起来。问题是如何从 CTE 获取此修订 ID。我可以将 CTE 的查询结果存储在临时表中并查询修订 ID,或者建议不要编写 CTE 而将其编写为循环。这样,人们可以跟踪循环退出的修订 id(当has_snapshot = true 时)。但我不确定这与 CTE 相比如何。

人们如何看待这种方法?

【问题讨论】:

  • DDL 数量正确的优秀问题。顺便说一句:VALUES 也接受逗号列表。
  • 谢谢。我知道逗号列表(它在另一个线程中被建议),但我更喜欢冗长的方式来使用它。
  • 如果“19”的树被请求,系统是否也应该确保“12”和“3”的子树也被缓存?
  • 顺便说一句:不要认为您的问题被忽略了。这只是一个很难的问题,比其他问题更难。这个周末我会研究一下。 (还有很多其他人也会。相信我......)
  • @vyegorov:不一定。仅当缓存值存在时才应考虑它们。如果不存在快照条目,则需要递归 CTE 探索路径。但是,如果沿路径存在快照,则应使用它(在您的示例中,对于请求的“19”,只会从树中获取 19 个,其余路径由快照查找组装,其中包含来自“ 13"(假设它包含一个))。我在示例中添加了解释,请查看是否需要更多散文。

标签: performance postgresql stored-procedures common-table-expression postgresql-9.2


【解决方案1】:

这是一个完全修订的版本
在我的最小测试中,使用 revision_snapshot 的新函数现在实际上更快了。
我做了很多改变。最重要的是:

  • 不要将列添加到原始表中。这可能会稍微加快查询速度,但也会在主表中引入成本和开销。如果您对表所做的所有事情都是执行此功能,则可能会付出代价,但在现实生活中,这只是众多任务之一。

  • 从函数中删除临时表。只需 CTE 就可以更便宜地完成。

  • 修复ORDER BY,这是错误的。

  • 更多信息,请阅读代码中的cmets

也可以和->sqlfiddle一起玩。

CREATE TABLE revision (
  revision_id serial PRIMARY KEY -- Don't use useless name "id", that's an anti-pattern of ORMs
 ,parent_revision_id int  NOT NULL REFERENCES revision(revision_id) DEFERRABLE
  -- must be DEFERRABLE for self-reference of root
 ,ts timestamp  NOT NULL -- all columns NOT NULL 
 ,has_snapshot boolean NOT NULL DEFAULT FALSE -- columns ordered for perfect packing and performance
 ,username text NOT NULL
);

CREATE TABLE revision_snapshot (
  depth int PRIMARY KEY
 ,revision_id int
);  -- simplified

-- Recursively return path from '_revision_id' to root
CREATE OR REPLACE FUNCTION revision_tree(_revision_id int)
  RETURNS TABLE(depth int, revision_id int) AS
$func$
   WITH RECURSIVE l AS (
      SELECT 1::int AS depth, r.parent_revision_id AS revision_id
      FROM   revision r
      WHERE  r.revision_id = $1

      UNION ALL
      SELECT l.depth + 1, r.parent_revision_id  -- AS revision_id
      FROM   l
      JOIN   revision r USING (revision_id)
      WHERE  r.parent_revision_id <> 0
   )
   SELECT *
   FROM   l
   ORDER  BY l.depth; -- NOT revision_id!
$func$ LANGUAGE sql;


CREATE OR REPLACE FUNCTION create_revision_snapshot(_revision_id int)
  RETURNS void AS
$func$
   -- for tiny tables, DELETE is faster than TRUNCATE
   DELETE FROM revision_snapshot;

   INSERT INTO revision_snapshot (depth, revision_id) 
   SELECT depth, revision_id
   FROM   revision_tree($1);
$func$ LANGUAGE sql;


-- Faster version of 'revision_tree'.
-- Stops recursion as soon as  revision_snapshot covers the "last mile" to root
CREATE OR REPLACE FUNCTION revision_tree_perf(_revision_id int)
  RETURNS TABLE(revision_id int) AS
$func$
BEGIN
   RETURN QUERY  -- works without expensive temp table
   WITH RECURSIVE l AS (
      SELECT 1::int AS depth, r.parent_revision_id AS revision_id  -- trim cruft, only two columns needed
      FROM   revision r
      WHERE  r.revision_id = $1

      UNION ALL
      SELECT l.depth + 1, r.parent_revision_id  -- AS revision_id
      FROM   l
      JOIN   revision r USING (revision_id)
      WHERE  r.parent_revision_id <> 0  -- stop condition needed, since parent_revision_id IS NOT NULL
      AND    NOT EXISTS (  -- NOT EXISTS faster than IN (SELECT...)
         SELECT 1 FROM revision_snapshot s WHERE s.revision_id = l.revision_id)
      )
   (  -- extra parens needed for separate ORDER BY in UNION ALL
   SELECT l.revision_id
   FROM   l
   ORDER  BY l.depth  -- NOT revision_id! Bug just didn't show because the test ids were ordered.
   )
   UNION ALL  -- NOT: UNION - correct and faster
   (
   SELECT s.revision_id
   FROM   revision_snapshot s
   WHERE  s.depth > (
      SELECT s0.depth
      FROM   revision_snapshot s0
      JOIN   l USING (revision_id)
      )  -- must be exactly 1 value - follows logically from CTE
   ORDER  BY s.depth
   );
END  -- no ; needed here
$func$ LANGUAGE plpgsql; -- DO NOT quote language name!

-- Example tree
--
--                                                      +-- <10>
--                                                     /
--                              +- <4> -- <8> -- <9> -+- <11> -- <15> -- <16> -- <17>
--                             /                                    
--  <0> -- <1> -- <2> -- <3> -+
--                             \                                    
--                              +- <5> -- <6> -- <7> -- <12> -+- <14> -- <18>
--                                                             \
--                                                              \
--                                                               \
--                                                                \
--                                                                 +- <13> -- <19> -- <20> -- <21>
--

INSERT INTO revision (revision_id, username, ts, parent_revision_id) VALUES
   (0, 'root',    now(), 0)  -- referencing itself
  ,(1, 'someone', now(), 0)
  ,(2, 'someone', now(), 1)
  ,(3, 'someone', now(), 2)
  ,(4, 'someone', now(), 3)
  ,(5, 'someone', now(), 3)
  ,(6, 'someone', now(), 5)
  ,(7, 'someone', now(), 6)
  ,(8, 'someone', now(), 4)
  ,(9, 'someone', now(), 8)
  ,(10,'someone', now(), 9)
  ,(11,'someone', now(), 9)
  ,(12,'someone', now(), 7)
  ,(13,'someone', now(), 12)
  ,(14,'someone', now(), 12)
  ,(15,'someone', now(), 11)
  ,(16,'someone', now(), 15)
  ,(17,'someone', now(), 16)
  ,(18,'someone', now(), 14)
  ,(19,'someone', now(), 13)
  ,(20,'someone', now(), 19)
  ,(21,'someone', now(), 20);

ANALYZE revision; 

-- Create a revision snapsnot
select create_revision_snapshot(13);

ANALYZE revision_snapshot;

呼叫:

这个查询现在应该更快了:

SELECT * FROM revision_tree_perf(21);

..比这个:

SELECT * FROM revision_tree(21);

【讨论】:

  • 感谢完全修订的版本。它运行良好,快照的性能改进非常明显。唯一让我感到困惑的是函数的最后一部分(即UNION ALL 之后的部分)。您选择快照表中所有深度高于...的条目是什么?首先,它不是更小的深度,因为您正在查看更接近根的末端(而不是更远且已经检索到的递归位)。你愿意详细说明吗?我不是反驳你的答案,我只是想多了解一点......
【解决方案2】:

[这不是答案;不幸的是,我的幼稚观点比 OQ 中的物化版本;-]

这里有一个 sn-p 来增加修订表的数量(显然是在插入 21 个 VALUES 之后):

        -- clone the revision-table to make it larger
INSERT INTO revision (username, ts, parent_revision_id)
SELECT 'user' || src.id || 'clone' ||gs
        , src.ts+ gs*'1 min'::interval
        , src.parent_revision_id+ (21*gs)
FROM revision src
JOIN generate_series(1,10000) gs ON 1=1
        ;
-- SELECT * FROM revision;

VACUUM ANALYZE revision;

更新:

我设法创建了自己的函数版本,它似乎比原始版本稍快(即使在原始版本之前调用)

DROP FUNCTION fnc_naive(_me INTEGER);
CREATE OR REPLACE FUNCTION fnc_naive(_me INTEGER)
RETURNS TABLE(fixed int, this int, ancestor int, depth int) AS $body$
    WITH RECURSIVE tree(fixed, this, some_ancestor, the_level) AS (
      SELECT t.id AS fixed, t.id AS this
        , t.parent_revision_id AS some_ancestor, 1 AS the_level
      FROM   revision t
      WHERE  t.id = $1
      UNION ALL
      SELECT tr.fixed AS fixed, rev.id AS this
        , rev.parent_revision_id AS some_ancestor
        , tr.the_level+1 AS the_level
      FROM   tree tr
      JOIN   revision rev ON rev.id = tr.some_ancestor
   )
   SELECT t.fixed, t.this, t.some_ancestor, t.the_level
        FROM   tree t
        ORDER  BY t.this
        ;
$body$ LANGUAGE sql;

-- Altered "Fast" version of 'revision_tree' (to be). This version will return the 
-- revision tree making use of snapshots (recursively returning the path from 
-- specified revision id to last snapshot of the path to the root + the snapshot)
-- Changes:
--      replaced the IN(subselect) by a corresponding EXISTS (subselect)
--      Replaced the = (select(min() subselect) by the corresponding NOT EXISTS
CREATE OR REPLACE FUNCTION revision_tree_perf_wp(_rev int)
RETURNS TABLE(parent_revision_id int) AS
$$
BEGIN
  CREATE TEMP TABLE tmp_graph_result_wp ON COMMIT DROP AS
  WITH RECURSIVE rev_list(id, parent_revision_id, depth) AS (
      SELECT t.id, t.parent_revision_id, 1
      FROM   revision t
      WHERE  t.id = $1
      UNION ALL
      SELECT t.id, t.parent_revision_id, r.depth + 1
      FROM   rev_list r
      JOIN   revision t ON t.id = r.parent_revision_id
      WHERE  NOT EXISTS (SELECT *
                FROM revision_snapshot nx
                WHERE t.id = nx.revision_id
                )
        )
   SELECT t.id, t.parent_revision_id, t.depth
   FROM   rev_list t
        ;
   RETURN QUERY
   SELECT g.parent_revision_id FROM tmp_graph_result_wp AS g
    WHERE g.parent_revision_id IS NOT NULL
   UNION
   SELECT s.parent_revision_id FROM revision_snapshot AS s
    WHERE NOT EXISTS ( SELECT *
          FROM tmp_graph_result_wp nx
          WHERE nx.parent_revision_id < s.revision_id
          )
   ORDER BY parent_revision_id
        ;
END;
$$
LANGUAGE 'plpgsql';

附加说明:我不理解“revision_snapshot”和表以及临时表“graph_result”的(预期)用法。在我看来,朴素的 treewalk 将总是被执行,加上一个额外的“存在于临时和/或缓存表中)在任何情况下:朴素版本比“缓存”版本快得多。

【讨论】:

  • 谢谢。 “revision_snapshot”的预期用途是限制树遍历的执行时间,直到找到“revision_snapshot”中的修订。随着树的深度增加,执行时间将增加(即从请求的节点到根的步行)。所以想法是限制步行并依赖“revision_snapshot”中的“缓存”路径。本质上,结果应该是{Path from requested node until last snapshot revision on path (call it 'n')} UNION {select * from revision_snapshot where revision_id=n} 的并集。感谢EXISTS的东西。
  • 天真的版本更快,因为测试数据集相当小。它最终会遇到一个限制,因为我确信 postgres 有一些最大的递归深度(MS SQL Server 有)。此外,当必须探索大树时,可扩展性是一个问题(长路径比短路径花费更长的时间;通过快照,这可以在一定程度上得到控制)。
  • 不,递归 CTE 没有最大递归深度。它们是通过迭代而不是递归来执行的。 (触发器等有最大递归深度,但这是一个不同的问题)
  • 这很有趣。我不知道 CTE 递归是无限的。然而,我的观点仍然是深树需要很长时间才能探索。我将运行您的表填充脚本,看看在什么时候使用快照比探索树更快。
  • 我通过加深树 (INSERT INTO revision (username, ts, parent_revision_id) SELECT 'user', now(), s.a FROM generate_series(21, 1000000) as s(a);) 进行了一些性能测试。如果我使用朴素的递归查询来查询第 900,000 个元素,我会在 4 秒内获得结果。对 850,000 的快照和“高性能”查询(只留下 50,000 个要从 CTE 检索的元素)执行相同的操作超过 5 分钟(在完成之前终止)。返回 850,000 条快照记录只需要 100 毫秒,所以其他的东西肯定需要这么多时间......
猜你喜欢
  • 1970-01-01
  • 2019-09-06
  • 1970-01-01
  • 2016-01-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-05-06
相关资源
最近更新 更多