Re: pg_plan_advice

Ajay Pal <ajay.pal.k@gmail.com>

From: Ajay Pal <ajay.pal.k@gmail.com>
To: Robert Haas <robertmhaas@gmail.com>
Cc: Jakub Wartak <jakub.wartak@enterprisedb.com>, Lukas Fittl <lukas@fittl.com>, Jacob Champion <jacob.champion@enterprisedb.com>, Dian Fay <di@nmfay.com>, Matheus Alcantara <matheusssilv97@gmail.com>, PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
Date: 2026-01-27T12:48:46Z
Lists: pgsql-hackers
Thank you Robert for clarification.

On Tue, Jan 27, 2026 at 6:02 PM Robert Haas <robertmhaas@gmail.com> wrote:
>
> On Tue, Jan 27, 2026 at 2:49 AM Ajay Pal <ajay.pal.k@gmail.com> wrote:
> > #1 Grouped Hash Join, This forces the join of dim1 and dim2 to happen
> > first, and then places that resulting set on the inner side of a Hash
> > Join against fact.
> > but the planner partially matches the generated advice.
> >
> > -- We want (dim1 JOIN dim2) to be the inner side of a Hash Join
> > SET LOCAL pg_plan_advice.advice = 'HASH_JOIN((dim1 dim2))';
> >
> > postgres=*# EXPLAIN (COSTS OFF, PLAN_ADVICE)
> > SELECT * FROM fact
> >                   JOIN dim1 ON fact.d1_id = dim1.id
> >                   JOIN dim2 ON fact.d2_id = dim2.id;
> >                         QUERY PLAN
> > -----------------------------------------------------------
> >  Nested Loop
> >    Disabled: true
> >    ->  Nested Loop
> >          Disabled: true
> >          ->  Seq Scan on fact
> >          ->  Index Scan using dim1_pkey on dim1
> >                Index Cond: (id = fact.d1_id)
> >    ->  Index Scan using dim2_pkey on dim2
> >          Index Cond: (id = fact.d2_id)
> >  Supplied Plan Advice:
> >    HASH_JOIN((dim1 dim2)) /* partially matched */
> >  Generated Plan Advice:
> >    JOIN_ORDER(fact dim1 dim2)
> >    NESTED_LOOP_PLAIN(dim1 dim2)
> >    SEQ_SCAN(fact)
> >    INDEX_SCAN(dim1 public.dim1_pkey dim2 public.dim2_pkey)
> >    NO_GATHER(fact dim1 dim2)
> > (17 rows)
>
> Thanks for the report, but this is actually correct behavior. There's
> no join clause between dim1 and dim2, so the planner doesn't consider
> a dim1-dim2 join. This is a good example of the phenomenon described
> in the documentation: you can't force the planner to create an
> arbitrary plan that it wouldn't otherwise have considered. I might
> tweak the documentation wording a little to try  to mention that this
> is another way "partially matched" can happen, but there's no bug
> here.
>
> > #2 Multiple Instances of Same Table in Subqueries, here target the
> > second instance of dim1 inside the subquery 'sq'. both seq_scan and
> > index_scan advices are not matching.
> >
> > SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(dim1#2@sq)
> > INDEX_SCAN(dim1@sq dim1_pkey)';
> >
> > postgres=*# EXPLAIN (COSTS OFF, PLAN_ADVICE)
> > SELECT * FROM fact
> > JOIN (
> >     SELECT a.id FROM dim1 a
> >     JOIN dim1 b ON a.id = b.id
> >     OFFSET 0
> > ) sq ON fact.d1_id = sq.id;
> >                     QUERY PLAN
> > ---------------------------------------------------
> >  Hash Join
> >    Hash Cond: (fact.d1_id = b.id)
> >    ->  Seq Scan on fact
> >    ->  Hash
> >          ->  Seq Scan on dim1 b
> >  Supplied Plan Advice:
> >    SEQ_SCAN(dim1#2@sq) /* not matched */
> >    INDEX_SCAN(dim1@sq dim1_pkey) /* not matched */
> >  Generated Plan Advice:
> >    JOIN_ORDER(fact sq)
> >    HASH_JOIN(sq)
> >    SEQ_SCAN(b@sq fact)
> >    NO_GATHER(fact b@sq)
> > (13 rows)
>
> I'm not sure what why you expected this to work. You can see what the
> correct relation identifiers are from the generated plan advice, and
> you've used something else, so it doesn't match. It's documented in
> both the SGML documentation and the README that relation identifiers
> are based on the relation alias, not the relation name.
>
> In general, this seems like a good to reiterate that this is first and
> foremost a plan stability feature. More than anything, these examples
> show that if you try to write your own plan advice from scratch to
> force a novel plan that the planner has never produced itself, you may
> not have much luck. If you do want to try to produce a novel plan, you
> should at least look at the generated plan advice and adapt it instead
> of starting from scratch. And if you find, when trying to produce a
> novel plan, that it doesn't work, you need to consider the possibility
> that this is because the optimizer did not ever consider that plan,
> and that is why pg_plan_advice is unable to induce the planner to
> prefer it. That's not to say there can't be any remaining bugs in
> pg_plan_advice; there probably are. But it also is absolutely not a
> "write your own plan and do anything you like" feature.
>
> --
> Robert Haas
> EDB: http://www.enterprisedb.com



Commits

Same data as JSON: GET /api/v1/messages/:b64id/commits the thread's linked commits as JSON, with link sources. API reference →
  1. pg_plan_advice: Fix another unique-semijoin bug.

  2. pg_plan_advice: Export feedback-related definitions.

  3. pg_plan_advice: Fix a bug when a subquery is pruned away entirely.

  4. pg_plan_advice: Add alternatives test to Makefile.

  5. pg_plan_advice: Handle non-repeatable TABLESAMPLE scans.

  6. pg_stash_advice: Allow stashed advice to be persisted to disk.

  7. Add pg_stash_advice contrib module.

  8. pg_plan_advice: Avoid assertion failure with partitionwise aggregate.

  9. pg_plan_advice: Invent DO_NOT_SCAN(relation_identifier).

  10. Add an alternative_plan_name field to PlannerInfo.

  11. pg_plan_advice: Refactor to invent pgpa_planner_info

  12. Respect disabled_nodes in fix_alternative_subplan.

  13. get_memoize_path: Don't exit quickly when PGS_NESTLOOP_PLAIN is unset.

  14. test_plan_advice: Set TAP test priority 50 in meson.build.

  15. pg_plan_advice: Avoid a crash under GEQO.

  16. Test pg_plan_advice using a new test_plan_advice module.

  17. pg_plan_advice: Always install pg_plan_advice.h, and in the right place

  18. pg_plan_advice: Fix failures to accept identifier keywords.

  19. Add pg_plan_advice contrib module.

  20. Allow extensions to mark an individual index as disabled.

  21. Replace get_relation_info_hook with build_simple_rel_hook.

  22. Store information about Append node consolidation in the final plan.

  23. Store information about elided nodes in the final plan.

  24. Store information about range-table flattening in the final plan.

  25. Pass cursorOptions to planner_setup_hook.

  26. Fix PGS_CONSIDER_NONPARTIAL interaction with Materialize nodes.

  27. Fix mistakes in commit 4020b370f214315b8c10430301898ac21658143f

  28. Allow for plugin control over path generation strategies.

  29. Update some comments for fasthash

  30. Allow passing a pointer to GetNamedDSMSegment()'s init callback.

  31. Don't reset the pathlist of partitioned joinrels.

  32. Treat number of disabled nodes in a path as a separate cost metric.