Re: When you really want to force a certain join type?

2022-12-28 Thread Tom Lane
Gunther Schadow  writes:
> Also, why even use the RECURSIVE keyword, DB2 didn't need it, and the 
> query analyzer should immediately see the recursion, so no need to have 
> that keyword.

Our reading of the SQL spec is that it's required.  The scope of
visibility of CTE names is different depending on whether you
write RECURSIVE or not, so it's not a question of "the query analyzer
should see it": the analyzer is required NOT to see it.

DB2 generally has a reputation for agreeing with the spec,
so I'm surprised to hear that they're not doing this per spec.

regards, tom lane




Re: When you really want to force a certain join type?

2022-12-28 Thread Gunther Schadow

On 12/28/2022 10:48 AM, Justin Pryzby wrote:

Maybe the new parameter in v15 would help.

https://www.postgresql.org/docs/15/runtime-config-query.html#GUC-RECURSIVE-WORKTABLE-FACTOR
recursive_worktable_factor (floating point)

 Sets the planner's estimate of the average size of the working table
 of a recursive query, as a multiple of the estimated size of the
 initial non-recursive term of the query. This helps the planner
 choose the most appropriate method for joining the working table to
 the query's other tables. The default value is 10.0. A smaller value
 such as 1.0 can be helpful when the recursion has low “fan-out” from
 one step to the next, as for example in shortest-path queries. Graph
 analytics queries may benefit from larger-than-default values.


Thanks that's something I will try after I upgraded.

But speaking of such other uses for recursive queries, I can say I have 
quite a bit of experience of turning graph related "traversal" and 
search and optimization and classification queries into SQL, in short, 
computing the transitive closure. And usually I have stayed away from 
the recursive WITH query and instead set up a start table and then 
perform the iterative step. And there are two ways to go about it. Say 
you have a graph, simple nodes and arcs. You want to find all paths 
through the graph.


Now you can set up start nodes and then extend them at the end by 
joining the recursive table to the simple arc table and extend your path 
every time. This is what the WITH RECURSIVE supports. These queries 
linearly iterate as many times as the length of the longest path.


with recursive arcs as (
  select source, target, 1 as distance, ...
    from ...
), paths as (
  select * from arcs
  union all
  select a.source, b.target, a.distance + 1 as distance, ...
    from paths a inner join_*arcs *_b
  on(b.source = a.target)
)
select * from paths

But another way is to join paths with paths. It would be this, which I 
think I have seen postgresql unable to deal with:


with recursive arcs as (
  select source, target, 1 as distance, ...
    from ...
), paths as (
  select * from arcs
  union all
  select a.source, b.target, a.distance + 1 as distance, ...
    from paths a inner join_*paths *_b
  on(b.source = a.target)
)
select * from paths

So, instead of the recursive union to join back to the fixed table, it 
joins the recursive table to the recursive table, and the benefit of 
that is that these queries converge much quicker. Instead of going 10 
iterations to find a path of length 10, you go 1 iteration to find all 
paths of 2 (if distance 1 is the base table of all arcs), then next you 
find paths of up to 4 then you find paths of up to 8, then 16, 32, ... 
This converges much faster. I usually do that as follows


create table paths as
select source, target, 1 as distance, ...
  from arcs;

prepare rep as
insert into paths(source, target, distance, ...)
select a.source, b.target, a.distance + b.distance as distance, ...
  from paths a inner join paths b on(b.source = a.target)
except
select * from paths;

execute rep;
execute rep;
...

or instead of the except, in order to minimize distances:

where not exists (select 1 from paths x
   where x.source = a.source
 and x.target = a.target
 and x.distance < a.distance)

I have even done a group by in the recursive step which replaces the 
paths relation at every iteration (e.g. with only minimal distance paths).


Since this converges so rapidly I often prefer that approach over a 
recursive union query.


I think in IBM DB2 allowed to join the recursive table with itself. Is 
this something you want to support at some time?


Also, why even use the RECURSIVE keyword, DB2 didn't need it, and the 
query analyzer should immediately see the recursion, so no need to have 
that keyword.


regards,
-Gunther


Re: When you really want to force a certain join type?

2022-12-28 Thread Justin Pryzby
On Wed, Dec 28, 2022 at 10:39:14AM -0500, Gunther Schadow wrote:
> I have a complex query which essentially runs a finite state automaton
> through a with recursive union, adding the next state based on the
> previous.  This is run at 100,000 or a million start states at the same
> time, picking a new record (token), matching it to the FSA (a three-way
> join:

> There are 100s of thousands of states. This join has a HUGE fan out if it is

> I doubt that I can find any trick to give to the planner better data which
> it can then use to figure out that the merge join is a bad proposition.

> Note, for my immediate relief I have forced it by simply set
> enable_mergejoin=off. This works fine, except, it converts both into a
> nested loop, but the upper merge join was not a problem, and sometimes (most
> often) nested loop is a bad choice for bulk data. It's only for this
> recursive query it sometimes makes sense.

Maybe the new parameter in v15 would help.

https://www.postgresql.org/docs/15/runtime-config-query.html#GUC-RECURSIVE-WORKTABLE-FACTOR
recursive_worktable_factor (floating point)

Sets the planner's estimate of the average size of the working table
of a recursive query, as a multiple of the estimated size of the
initial non-recursive term of the query. This helps the planner
choose the most appropriate method for joining the working table to
the query's other tables. The default value is 10.0. A smaller value
such as 1.0 can be helpful when the recursion has low “fan-out” from
one step to the next, as for example in shortest-path queries. Graph
analytics queries may benefit from larger-than-default values.

-- 
Justin




When you really want to force a certain join type?

2022-12-28 Thread Gunther Schadow
I have a complex query which essentially runs a finite state automaton 
through a with recursive union, adding the next state based on the 
previous.  This is run at 100,000 or a million start states at the same 
time, picking a new record (token), matching it to the FSA (a three-way 
join:


   token inner join next token * state-transition-table -> next state

I know this doesn't really tell you much. The following might give you a 
glimpse:


with recursive Token as (
  select * from steps left outer join token using(event)
  limit 10
), StartStates as (
select pathId, start, end, m.new_state as state, m.goalId
  from Token w inner join FSA m
on(m.token = w.token and m.old_state = w.state)
), Phrase as (
select pathId, start, end, state, goalId
  from StartStates
union all
select p.pathId, p.start, n.end, n.new_state as state, n.goalId
  from Phrase p
  inner join (
select pathId, start, end, old_state as state, new_state, f.goalId
  from Token inner join FSA f using(token)
  ) n using(pathId, end, state)

There are 100s of thousands of states. This join has a HUGE fan out if 
it is not immediately limited by the chaining criterion on the 
old_state. So any attempt to use merge join or hash join which will 
compute the whole big thing and only then apply the chaining criterion, 
will just create massive amounts of sort load and/or humongous hash 
tables only to throw the vast majority away every time. But when it runs 
through nested loops, the indexes help to make it really quick.


I cannot show you the exact data, but I can show you the plan that works 
amazingly fast:


 Insert on good_paths  (cost=912224.51..912228.71 rows=240 width=302)
   CTE token
 ->  Limit  (cost=46728.24..81127.75 rows=10 width=519)
   ->  Hash Left Join  (cost=46728.24..115752.23 rows=200654 width=519)
   ... this is creating the start states
   CTE path
 ->  Recursive Union  (cost=23293.75..831082.45 rows=241 width=278)
   ->  Merge Join  (cost=23293.75..289809.83 rows=171 width=278)
 Merge Cond: ((m.old_state = w_1.state) AND (m.token = 
w_1.token))
 ->  Index Scan using fsa_old_state_token_idx on fsa m  
(cost=0.43..245365.63 rows=4029834 width=28)
 ->  Materialize  (cost=23293.32..23793.32 rows=10 
width=278)
   ->  Sort  (cost=23293.32..23543.32 rows=10 width=278)
 Sort Key: w_1.state, w_1.token
 ->  CTE Scan on token w_1  (cost=0.00..2000.00 
rows=10 width=278)
   ->  Nested Loop  (cost=18295.78..54126.78 rows=7 width=278)
 ->  Merge Join  (cost=18295.35..19120.16 rows=4275 width=340)
   Merge Cond: ((token.pathid = p.pathid) AND (token.start 
= p.end))
   ->  Sort  (cost=18169.32..18419.32 rows=10 width=160)
 Sort Key: token.pathid, token.start
 ->  CTE Scan on token (cost=0.00..2000.00 
rows=10 width=160)
   ->  Sort  (cost=126.03..130.30 rows=1710 width=212)
 Sort Key: p.pathid, p.end
 ->  WorkTable Scan on path p  (cost=0.00..34.20 
rows=1710 width=212)
 ->  Index Scan using fsa_old_state_token_idx on fsa f  
(cost=0.43..8.18 rows=1 width=28)

Now, when that initial token list (of start states) grows beyond this 
limit of 100,000, the execution plan flips:


 Insert on good_paths  (cost=2041595.63..2041606.66 rows=630 width=302)
   CTE token
 ->  Limit  (cost=46728.24..115752.23 rows=200654 width=519)
   ->  Hash Left Join  (cost=46728.24..115752.23 rows=200654 width=519)
   ... this is creating the start states
   CTE path
 ->  Recursive Union  (cost=47749.96..1925801.45 rows=633 width=278)
   ->  Merge Join  (cost=47749.96..315274.30 rows=343 width=278)
 Merge Cond: ((m.old_state = w_1.state) AND (m.token = 
w_1.token))
 ->  Index Scan using fsa_old_state_token_idx on fsa m  
(cost=0.43..245365.63 rows=4029834 width=28)
 ->  Materialize  (cost=47749.53..48752.80 rows=200654 
width=278)
   ->  Sort  (cost=47749.53..48251.16 rows=200654 width=278)
 Sort Key: w_1.state, w_1.token
 ->  CTE Scan on token w_1  (cost=0.00..4013.08 
rows=200654 width=278)
   ->  Merge Join  (cost=158013.87..161051.45 rows=29 width=278)
 Merge Cond: ((token.token = f.token) AND (token.pathid = 
p.pathid) AND (token.start = p.end))
 ->  Sort  (cost=37459.53..37961.16 rows=200654 width=160)
   Sort Key: token.token, token.pathid, token.start
   ->  CTE Scan on token (cost=0.00..4013.08 rows=200654 
width=160)
 ->  Materialize  (cost=120554.35..120966.44 rows=82419 
width=228)