I'm learning PostgreSQL EXPLAIN plan nodes. Currently, I investigate Materialize node. Here is a query I found in the blog post (https://www.depesz.com/2013/05/09/explaining-the-unexplainable-part-3/) and a plan I obtained by myself (structurally it is the same as in the blog post):
set work_mem= '1GB';
explain analyze select * from
(select * from pg_class order by oid) as c
join
(select * from pg_attribute a order by attrelid) as a
on c.oid = a.attrelid;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Merge Join (cost=34.27..333.37 rows=2913 width=504) (actual time=0.823..28.413 rows=2913 loops=1)
Merge Cond: (pg_class.oid = a.attrelid)
-> Sort (cost=33.99..34.97 rows=395 width=265) (actual time=0.739..1.084 rows=395 loops=1)
Sort Key: pg_class.oid
Sort Method: quicksort Memory: 130kB
-> Seq Scan on pg_class (cost=0.00..16.95 rows=395 width=265) (actual time=0.038..0.285 rows=395 loops=1)
-> Materialize (cost=0.28..257.05 rows=2913 width=239) (actual time=0.060..11.702 rows=2913 loops=1)
-> Index Scan using pg_attribute_relid_attnum_index on pg_attribute a (cost=0.28..220.63 rows=2913 width=239) (actual time=0.050..6.827 rows=2913 loops=1)
Planning Time: 1.472 ms
Execution Time: 29.617 ms
(10 rows)
I can't understand the argumentation of the blog author about why Postgres is using Materialize here.
... Merge Join has to match several criteria. Some are obvious (data has to be sorted) and some are not so obvious as are more technical (data has to be scrollable back and forth).
Because of this (these not so obvious criteria) sometimes Pg will have to Materialize the data coming from source (Index Scan in our case) so that it will have all the necessary features when using it.
Why Merge Join needs data to be scrollable back and forth? According to my understanding of Merge Join it iterates forward both data sets simultaneously with two pointers. There is no case in Merge Join algorithm when it goes backward. Anyway, as I understand, Index Scan actually is "scrollable back and forth". I saw "Index Scan Backward" many times. So why Postgres has to materialize it?
I found the confirmation of the blog author opinion in other source, the old book "PostgreSQL: The comprehensive guide to building, programming, and administering PostgreSQL databases", 2nd Edition by Korry Douglas and Susan Douglas:
Materialize will also be used for some merge-join operations. In particular, if the inner input set of a Merge Join operator is not produced by a Seq Scan, an Index Scan, a Sort, or a Materialize operator, the planner/optimizer will insert a Materialize operator into the plan. The reasoning behind this rule is not obviousit has more to do with the capabilities of the other operators than with the performance or the structure of your data. The Merge Join operator is complex; one requirement of Merge Join is that the input sets must be ordered by the join columns. A second requirement is that the inner input set must be repositionable; that is, Merge Join needs to move backward and forward through the input set. Not all ordered operators can move backward and forward. If the inner input set is produced by an operator that is not repositionable, the planner/optimizer will insert a Materialize.
In my case the inner input set is produced by Index Scan, so according to the book, there should be no Materialize node in this plan.
Then I decided to modify the query in such way that planner will not use Materialize, but Merge Join will be still present. This is what I came up with:
set enable_hashjoin = off;
set work_mem= '1GB';
explain analyze select * from
(select * from pg_class order by oid) as c
join
(select * from pg_attribute a) as a
on c.oid = a.attrelid;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------
Merge Join (cost=34.27..296.96 rows=2913 width=504) (actual time=0.554..10.103 rows=2913 loops=1)
Merge Cond: (pg_class.oid = a.attrelid)
-> Sort (cost=33.99..34.97 rows=395 width=265) (actual time=0.491..0.645 rows=395 loops=1)
Sort Key: pg_class.oid
Sort Method: quicksort Memory: 130kB
-> Seq Scan on pg_class (cost=0.00..16.95 rows=395 width=265) (actual time=0.021..0.177 rows=395 loops=1)
-> Index Scan using pg_attribute_relid_attnum_index on pg_attribute a (cost=0.28..220.63 rows=2913 width=239) (actual time=0.041..2.296 rows=2913 loops=1)
Planning Time: 1.188 ms
Execution Time: 10.731 ms
(9 rows)
I deleted order by attrelid
from 2nd subquery and disabled Hash Join forcibly. This plan is the same as previous one except Materialize node. So, I conclude that Merge Join is not the reason why planner use Materialize in previous one. This plan is less expensive, but I suppose the result to be the same.
I will be grateful if you help me to solve some of this puzzles:
- Does Merge Join really need to be able to iterate inner dataset backward? In which cases? Is the result of Index Scan "backward iterable" in terms of Merge Join requirements?
- Why Postgres planner uses Materialize in the first plan even though it costs more? What purpose it serves here? Why Postgres planner does not use Materialize in the second plan?
ORDER BY
in the derived tables? In theory Postgres should simply ignore them and run the query as if it was written like this:select * from pg_class c join pg_attribute a on c.oid = a.attrelid
ORDER BY
is used by blog post author here to stimulate Postgres to chose Merge Join rather Hash Join. It is an educational query, mostly to illustrate how Merge Join uses sorted data, I suppose.