Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    primarykey
    data
    text
    <p>Here is a <strong>completely revised version</strong>.<br> In my minimal test, the new function using the <code>revision_snapshot</code> is actually faster now.<br> I made numerous changes. Most importantly:</p> <ul> <li><p>Don't add column to original table. That may speed up the query marginally, but it also introduces cost and overhead in the main table. May pay if all you ever do with the table is executing this function, but in real life, that's just one of many tasks.</p></li> <li><p>Remove temporary table from function. Can be done much cheaper with just the CTE.</p></li> <li><p>Fix <code>ORDER BY</code>, which was wrong.</p></li> <li><p>For more, read the <strong>comments</strong> in the code.</p></li> </ul> <p>Also as <a href="http://www.sqlfiddle.com/#!12/40d89/2" rel="nofollow">->sqlfiddle</a> to play with.</p> <pre><code>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 &lt;&gt; 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 &lt;&gt; 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 &gt; ( 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 -- -- +-- &lt;10&gt; -- / -- +- &lt;4&gt; -- &lt;8&gt; -- &lt;9&gt; -+- &lt;11&gt; -- &lt;15&gt; -- &lt;16&gt; -- &lt;17&gt; -- / -- &lt;0&gt; -- &lt;1&gt; -- &lt;2&gt; -- &lt;3&gt; -+ -- \ -- +- &lt;5&gt; -- &lt;6&gt; -- &lt;7&gt; -- &lt;12&gt; -+- &lt;14&gt; -- &lt;18&gt; -- \ -- \ -- \ -- \ -- +- &lt;13&gt; -- &lt;19&gt; -- &lt;20&gt; -- &lt;21&gt; -- 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; </code></pre> <p>Call:</p> <p>This query should be faster now:</p> <pre><code>SELECT * FROM revision_tree_perf(21); </code></pre> <p>.. than this one:</p> <pre><code>SELECT * FROM revision_tree(21); </code></pre>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload