In this, hopefully 2nd to last, post in the series, I will cover the rest of usually happening operations that you can see in your explain outputs.

Unique

Name seems to be clear about what's going on here – it removes duplicate data.

This might happen, for example, when you're doing:

select distinct field from table

But in newer Pgs this query will usually be done using HashAggregate.

The problem with Unique is that is requires data to be sorted. Not because it needs data in any particular order – but it needs it so that all rows with the same value will be “together".

This makes it really cool (when possible to use) because it doesn't use virtually any memory. It just checks if value in previous row is the same as in current, and if yes – discards it. That's all.

So, we can force usage of it, by pre-sorting data:

$ explain select distinct relkind from (select relkind from pg_class order by relkind) as x;
                              QUERY PLAN
-----------------------------------------------------------------------
 Unique  (cost=22.88..27.26 rows=4 width=1)
   ->  Sort  (cost=22.88..23.61 rows=292 width=1)
         Sort Key: pg_class.relkind
         ->  Seq Scan on pg_class  (cost=0.00..10.92 rows=292 width=1)
(4 rows)

Append

This plan simply runs multiple sub-operations, and returns all the rows that were returned as one resultset.

This is used by UNION/UNION ALL queries:

$ explain select oid from pg_class union all select oid from pg_proc union all select oid from pg_database;
                           QUERY PLAN                            
-----------------------------------------------------------------
 Append  (cost=0.00..104.43 rows=2943 width=4)
   ->  Seq Scan on pg_class  (cost=0.00..10.92 rows=292 width=4)
   ->  Seq Scan on pg_proc  (cost=0.00..92.49 rows=2649 width=4)
   ->  Seq Scan on pg_database  (cost=0.00..1.02 rows=2 width=4)
(4 rows)

In here you can see append running three scans on three tables and returning all the rows together.

Please note that I used UNION ALL. If I'd used UNION, we would get:

$ explain select oid from pg_class union select oid from pg_proc union select oid from pg_database;
                              QUERY PLAN                               
-----------------------------------------------------------------------
 HashAggregate  (cost=141.22..170.65 rows=2943 width=4)
   ->  Append  (cost=0.00..133.86 rows=2943 width=4)
         ->  Seq Scan on pg_class  (cost=0.00..10.92 rows=292 width=4)
         ->  Seq Scan on pg_proc  (cost=0.00..92.49 rows=2649 width=4)
         ->  Seq Scan on pg_database  (cost=0.00..1.02 rows=2 width=4)
(5 rows)

This is because UNION removes duplicate rows – which is, in this case, done using HashAggregate operation.

Result

This happens mostly in very simple test queries. This operation is used when your query selects some constant value (or values):

$ explain select 1, 2;
                QUERY PLAN                
------------------------------------------
 Result  (cost=0.00..0.01 rows=1 width=0)
(1 row)

Aside from test queries it can be sometimes seen in queries that do “insert, but don't if it would be duplicate" kind of thing:

$ explain insert into t (i) select 1 where not exists (select * from t where i = 1);
                             QUERY PLAN                              
---------------------------------------------------------------------
 Insert on t  (cost=3.33..3.35 rows=1 width=4)
   ->  Result  (cost=3.33..3.34 rows=1 width=0)
         One-Time Filter: (NOT $0)
         InitPlan 1 (returns $0)
           ->  Seq Scan on t t_1  (cost=0.00..40.00 rows=12 width=0)
                 Filter: (i = 1)
(6 rows)

Values Scan

Just like Result above, Values Scan is for returning simple, entered in query, data, but this time – it can be whole recordset, based on VALUES() functionality.

In case you don't know, you can select multiple rows with multiple columns, without any table, just by using VALUES syntax, like here:

$ select * from ( values (1, 'hubert'), (2, 'depesz'), (3, 'lubaczewski') ) as t (a,b);
 a |      b      
---+-------------
 1 | hubert
 2 | depesz
 3 | lubaczewski
(3 rows)

Such query plan looks like:

                          QUERY PLAN                          
--------------------------------------------------------------
 Values Scan on "*VALUES*"  (cost=0.00..0.04 rows=3 width=36)
(1 row)

It is also most commonly used in INSERTs, but it has other uses too, like custom sorting.

GroupAggregate

This is similar to previously described HashAggregate.

The difference is that for GroupAggregate to work data has to be sorted using whatever column(s) you used for your GROUP BY clause.

Just like Unique – GroupAggregate uses very little memory, but forces ordering of data.

Example:

$ explain select relkind, count(*) from (select relkind from pg_class order by relkind) x group by relkind;
                              QUERY PLAN                               
-----------------------------------------------------------------------
 GroupAggregate  (cost=22.88..28.03 rows=4 width=1)
   ->  Sort  (cost=22.88..23.61 rows=292 width=1)
         Sort Key: pg_class.relkind
         ->  Seq Scan on pg_class  (cost=0.00..10.92 rows=292 width=1)
(4 rows)

HashSetOp

This operation is used by INTERSECT/EXCEPT operations (with optional “ALL" modifier).

It works by running sub-operation of Append for a pair of sub-queries, and then, based on result and optional ALL modifier, it figures which rows should be returned. I haven't digged in the source code so I can't tell you exactly how it works, but given the name and the operation, it looks like a simple counter-based solution.

In here we can see that unlike UNION, these operations work on two sources of data:

$ explain select * from (select oid from pg_Class order by oid) x intersect all select * from (select oid from pg_proc order by oid) y;
                                                 QUERY PLAN                                                  
-------------------------------------------------------------------------------------------------------------
 HashSetOp Intersect All  (cost=0.15..170.72 rows=292 width=4)
   ->  Append  (cost=0.15..163.36 rows=2941 width=4)
         ->  Subquery Scan on "*SELECT* 1"  (cost=0.15..18.37 rows=292 width=4)
               ->  Index Only Scan using pg_class_oid_index on pg_class  (cost=0.15..12.53 rows=292 width=4)
         ->  Subquery Scan on "*SELECT* 2"  (cost=0.28..145.00 rows=2649 width=4)
               ->  Index Only Scan using pg_proc_oid_index on pg_proc  (cost=0.28..92.02 rows=2649 width=4)
(6 rows)

but with three sources, we get more complex tree:

$ explain select * from (select oid from pg_Class order by oid) x intersect all select * from (select oid from pg_proc order by oid) y intersect all select * from (Select oid from pg_database order by oid) as w;
                                                          QUERY PLAN                                                           
-------------------------------------------------------------------------------------------------------------------------------
 HashSetOp Intersect All  (cost=1.03..172.53 rows=2 width=4)
   ->  Append  (cost=1.03..171.79 rows=294 width=4)
         ->  Subquery Scan on "*SELECT* 3"  (cost=1.03..1.07 rows=2 width=4)
               ->  Sort  (cost=1.03..1.03 rows=2 width=4)
                     Sort Key: pg_database.oid
                     ->  Seq Scan on pg_database  (cost=0.00..1.02 rows=2 width=4)
         ->  Result  (cost=0.15..170.72 rows=292 width=4)
               ->  HashSetOp Intersect All  (cost=0.15..170.72 rows=292 width=4)
                     ->  Append  (cost=0.15..163.36 rows=2941 width=4)
                           ->  Subquery Scan on "*SELECT* 1"  (cost=0.15..18.37 rows=292 width=4)
                                 ->  Index Only Scan using pg_class_oid_index on pg_class  (cost=0.15..12.53 rows=292 width=4)
                           ->  Subquery Scan on "*SELECT* 2"  (cost=0.28..145.00 rows=2649 width=4)
                                 ->  Index Only Scan using pg_proc_oid_index on pg_proc  (cost=0.28..92.02 rows=2649 width=4)
(13 rows)

CTE Scan

This is similar to previously mentioned Materialized operation. It runs a part of a query, and stores output so that it can be used by other part (or parts) of the query.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
$ explain analyze with x as (select relname, relkind from pg_class) select relkind, count(*), (select count(*) from x) from x group by relkind;
                                                   QUERY PLAN                                                    
-----------------------------------------------------------------------------------------------------------------
 HashAggregate  (cost=24.80..26.80 rows=200 width=1) (actual time=0.466..0.468 rows=6 loops=1)
   CTE x
     ->  Seq Scan on pg_class  (cost=0.00..10.92 rows=292 width=65) (actual time=0.009..0.127 rows=295 loops=1)
   InitPlan 2 (returns $1)
     ->  Aggregate  (cost=6.57..6.58 rows=1 width=0) (actual time=0.085..0.085 rows=1 loops=1)
           ->  CTE Scan on x x_1  (cost=0.00..5.84 rows=292 width=0) (actual time=0.000..0.055 rows=295 loops=1)
   ->  CTE Scan on x  (cost=0.00..5.84 rows=292 width=1) (actual time=0.012..0.277 rows=295 loops=1)
 Total runtime: 0.524 ms
(8 rows)

Please note that pg_class is scanned only once – line #6. But it's results are stored in “x", and then scanned twice – inside aggregate (line #9) and HashAggregate (10).

How is it different from Materialize? To answer fully, one would need to jump into sources, but I would say that the difference stems from simple fact that CTE's are user defined. While Materialize is helper operation that Pg chooses to use when (it thinks) it makes sense.

The very important thing is that CTEs are ran just as specified. So they can be used to circumvent some not-so-good optimizations that planner normally can do.

InitPlan

This plan happens whenever there is a part of your query that can (or have to) be calculated before anything else, and it doesn't depend on anything in the rest of your query.

For example, let's assume you'd want such query:

$ explain select * from pg_class where relkind = (select relkind from pg_class order by random() limit 1);
                                        QUERY PLAN                                        
------------------------------------------------------------------------------------------
 Seq Scan on pg_class  (cost=13.11..24.76 rows=73 width=203)
   Filter: (relkind = $0)
   InitPlan 1 (returns $0)
     ->  Limit  (cost=13.11..13.11 rows=1 width=1)
           ->  Sort  (cost=13.11..13.84 rows=292 width=1)
                 Sort Key: (random())
                 ->  Seq Scan on pg_class pg_class_1  (cost=0.00..11.65 rows=292 width=1)
(7 rows)

In this case – getting the limit/sort/seq-scan is needed to run before normal seq scan on pg_class – because Pg will have to compare relkind value with the value returned by subquery.

On the other hand, if I'd write:

$ explain select *, (select length('depesz')) from pg_class;
                         QUERY PLAN                          
-------------------------------------------------------------
 Seq Scan on pg_class  (cost=0.01..10.93 rows=292 width=203)
   InitPlan 1 (returns $0)
     ->  Result  (cost=0.00..0.01 rows=1 width=0)
(3 rows)

Pg correctly sees that the subselect column does not depend on any data from pg_class table, so it can be run just once, and doesn't have to redo the length-calculation for every row.

Of course you can have many init plans, like in here:

$ explain select *, (select length('depesz')) from pg_class where relkind = (select relkind from pg_class order by random() limit 1);
                                        QUERY PLAN                                        
------------------------------------------------------------------------------------------
 Seq Scan on pg_class  (cost=13.12..24.77 rows=73 width=203)
   Filter: (relkind = $1)
   InitPlan 1 (returns $0)
     ->  Result  (cost=0.00..0.01 rows=1 width=0)
   InitPlan 2 (returns $1)
     ->  Limit  (cost=13.11..13.11 rows=1 width=1)
           ->  Sort  (cost=13.11..13.84 rows=292 width=1)
                 Sort Key: (random())
                 ->  Seq Scan on pg_class pg_class_1  (cost=0.00..11.65 rows=292 width=1)
(9 rows)

There is one important thing, though – numbering of init plans within single query is “global", and not “per operation".

SubPlan

SubPlans are a bit similar to NestedLoop. In this way that these can be called many times.

SubPlan is called to calculate data from a subquery, that actually does depends on current row.

For example:

$ explain analyze select c.relname, c.relkind, (Select count(*) from pg_Class x where c.relkind = x.relkind) from pg_Class c;
                                                      QUERY PLAN                                                       
-----------------------------------------------------------------------------------------------------------------------
 Seq Scan on pg_class c  (cost=0.00..3468.93 rows=292 width=65) (actual time=0.135..26.717 rows=295 loops=1)
   SubPlan 1
     ->  Aggregate  (cost=11.83..11.84 rows=1 width=0) (actual time=0.090..0.090 rows=1 loops=295)
           ->  Seq Scan on pg_class x  (cost=0.00..11.65 rows=73 width=0) (actual time=0.010..0.081 rows=93 loops=295)
                 Filter: (c.relkind = relkind)
                 Rows Removed by Filter: 202
 Total runtime: 26.783 ms
(7 rows)

For every row that is returned by scan on “pg_class as c", Pg has to run SubPlan, which checks how many rows in pg_class have the same (as currently processed row) value in relkind column.

Please note “loops=295″ in the “Seq Scan on pg_class x" line, and matching “rows=295″ in the earlier “Seq Scan on pg_class c" node.

Other ?

Yes. There are other operations. Some of them are too rare to care about (especially since you do have the great source of knowledge: sources), some are (I suspect) older versions of newer nodes.

If you have a plan with operation I did not cover, and you don't understand it – please let me know in comments – link to explain output on explain.depesz.com, operation name, and Pg version that you have it in. Will comment on such cases with whatever information I might find.

Leave a comment