When you really want to force a certain join type?
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)
Re: When you really want to force a certain join type?
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
Re: When you really want to force a certain join type?
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?
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