Thread

Commits

  1. Fix UNION planner estimate_num_groups with varno==0

  2. Fix possible usage of incorrect UPPERREL_SETOP RelOptInfo

  3. Teach planner to short-circuit EXCEPT/INTERSECT with dummy inputs

  4. Fix incorrect targetlist in dummy UNIONs

  5. Teach UNION planner to remove dummy inputs

  1. Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    David Rowley <dgrowleyml@gmail.com> — 2025-10-02T02:22:34Z

    In [1] there was a report that set operations didn't correctly detect
    when inputs were provably empty sets.  While this is not the bug that
    the report claimed it to be, as it's just a missing optimisation, I
    did decide to look at it to check if there was much performance to
    gain from doing this.
    
    The short of it is, Yes, there are cases when this can help query
    performance. Primarily, this seems to come from when the code detects
    that an EXCEPT ALL has an empty right-hand input. In this case, we can
    scan the left-hand input and forego the SetOp node completely.
    
    With EXCEPT (without ALL), deduplication is still required, however
    that can be done with an Aggregate node on the left input rather than
    using the slightly less efficient SetOp node.
    
    If I create two tables with a single int column containing 1 million
    rows each, ANALYZE them and run some queries with and without the
    patch, I see:
    
    (work_mem=256MB, pgbench -M simple -T 10)
    
    master:
    EXCEPT ALL left dummy : tps = 8466.587802
    EXCEPT ALL right dummy : tps = 3.160083
    EXCEPT left dummy : tps = 8433.607519
    EXCEPT right dummy : tps = 3.178104
    INTERSECT (all types) : tps = 8392.695606
    UNION left dummy : tps = 3.406355
    
    patched:
    EXCEPT ALL left dummy : tps = 8973.958896 (+5.99%)
    EXCEPT ALL right dummy : tps = 53.583312 (+1595.63%)
    EXCEPT left dummy : tps = 8736.716176 (+3.59%)
    EXCEPT right dummy : tps = 3.385520 (+6.53%)
    INTERSECT (all types) : tps = 8759.123942 (+4.37%)
    UNION left dummy : tps = 3.590264 (+5.40%)
    
    You can see EXCEPT ALL with the empty right-hand input became ~15x
    faster, and all the others became ~5% faster.
    
    There are some additional benefits aside from the performance as it's
    possible to provide better row estimates in certain cases.  For
    example, if a UNION query removes all apart from 1 input, we can do
    estimate_num_groups() on that input.  Otherwise, we're left to the
    assumption that all rows are unique, which certainly could cause some
    trouble later in planning for queries consuming the results of set
    operations in subqueries. EXCEPT with an empty right-hand input also
    benefits from improved row estimates for the same reason.
    
    For me, this seems worth doing. Set operations have been drawn out of
    the dark ages with the last few releases, and I feel this makes them
    more aligned to the set of optimisations we've come to expect in other
    parts of the planner.
    
    I'm happy to hear other opinions.
    
    Patch attached.
    
    David
    
    [1] https://postgr.es/m/18904-c5fea7892f4d26ed@postgresql.org
    
  2. Re: Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    Tom Lane <tgl@sss.pgh.pa.us> — 2025-10-02T03:21:48Z

    David Rowley <dgrowleyml@gmail.com> writes:
    > In [1] there was a report that set operations didn't correctly detect
    > when inputs were provably empty sets.  While this is not the bug that
    > the report claimed it to be, as it's just a missing optimisation, I
    > did decide to look at it to check if there was much performance to
    > gain from doing this.
    
    I'm kind of resistant to the amount of code this patch adds in
    comparison to the likely benefit.  Sure, a badly written query can
    profit, but is it worth debugging and maintaining a couple hundred
    lines of code for that?
    
    The first few hunks of changes seem fine by this light, but I think
    you're expending too much effort on the EXCEPT-with-dummy-inputs
    cases.  I'm wondering if it could be shortened a great deal by
    handling left-input-dummy and EXCEPT-ALL-with-right-input-dummy
    but leaving the EXCEPT-with-right-input-dummy case unimproved.
    
    			regards, tom lane
    
    
    
    
  3. Re: Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    David Rowley <dgrowleyml@gmail.com> — 2025-10-02T12:09:27Z

    On Thu, 2 Oct 2025 at 16:21, Tom Lane <tgl@sss.pgh.pa.us> wrote:
    > I'm wondering if it could be shortened a great deal by
    > handling left-input-dummy and EXCEPT-ALL-with-right-input-dummy
    > but leaving the EXCEPT-with-right-input-dummy case unimproved.
    
    Good idea. Less code and still get to keep the one that did well in
    the benchmark. See attached.
    
    I ended up splitting the patch in two. 0001 for UNION only, then 0002
    for the INTERSECT and EXCEPT.
    
    David
    
  4. Re: Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    Zhang Mingli <zmlpostgres@gmail.com> — 2025-10-02T13:55:36Z

    Hi,
    
    David Rowley <dgrowleyml@gmail.com>于2025年10月2日 周四20:09写道:
    
    > On Thu, 2 Oct 2025 at 16:21, Tom Lane <tgl@sss.pgh.pa.us> wrote:
    > > I'm wondering if it could be shortened a great deal by
    > > handling left-input-dummy and EXCEPT-ALL-with-right-input-dummy
    > > but leaving the EXCEPT-with-right-input-dummy case unimproved.
    >
    > Good idea. Less code and still get to keep the one that did well in
    > the benchmark. See attached.
    >
    > I ended up splitting the patch in two. 0001 for UNION only, then 0002
    > for the INTERSECT and EXCEPT.
    >
    > David
    
    
    It seems that the optimization for `UNION ALL` is already implemented in
    the patch: it removes empty sub-paths and preserves the remaining ones.
    Should we add a test case to formally validate this behavior like Union
    cases?
    
  5. Re: Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    Tom Lane <tgl@sss.pgh.pa.us> — 2025-10-02T15:18:56Z

    David Rowley <dgrowleyml@gmail.com> writes:
    > Good idea. Less code and still get to keep the one that did well in
    > the benchmark. See attached.
    
    0001's change in is_dummy_rel() seems like a complete hack, especially
    since you didn't bother to change the adjacent comment.  Why is that
    necessary?
    
    v2 LGTM otherwise.
    
    			regards, tom lane
    
    
    
    
  6. Re: Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    David Rowley <dgrowleyml@gmail.com> — 2025-10-02T20:32:18Z

    On Fri, 3 Oct 2025 at 04:18, Tom Lane <tgl@sss.pgh.pa.us> wrote:
    > 0001's change in is_dummy_rel() seems like a complete hack, especially
    > since you didn't bother to change the adjacent comment.  Why is that
    > necessary?
    
    build_setop_child_paths() wraps the child inputs in SubqueryScanPaths,
    so we need to see through those.
    
    An alternative way would be to propagate those during build_setop_child_paths()
    
    --- a/src/backend/optimizer/prep/prepunion.c
    +++ b/src/backend/optimizer/prep/prepunion.c
    @@ -523,6 +523,13 @@ build_setop_child_paths(PlannerInfo *root, RelOptInfo *rel,
      bool is_sorted;
      int presorted_keys;
    
    +     /* If the input rel is dummy, propagate that to this query level */
    +     if (is_dummy_rel(final_rel))
    +     {
    +         mark_dummy_rel(rel);
    +         continue;
    +     }
    +
    
    As attached.
    
    > v2 LGTM otherwise.
    
    Thanks
    
    David
    
  7. Re: Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    Tom Lane <tgl@sss.pgh.pa.us> — 2025-10-02T21:02:44Z

    David Rowley <dgrowleyml@gmail.com> writes:
    > On Fri, 3 Oct 2025 at 04:18, Tom Lane <tgl@sss.pgh.pa.us> wrote:
    >> 0001's change in is_dummy_rel() seems like a complete hack, especially
    >> since you didn't bother to change the adjacent comment.  Why is that
    >> necessary?
    
    > build_setop_child_paths() wraps the child inputs in SubqueryScanPaths,
    > so we need to see through those.
    
    Ah.
    
    > An alternative way would be to propagate those during build_setop_child_paths()
    
    That answer works for me.  I was expecting you to just document the
    need for the extra check in is_dummy_rel ;-) ... but this way is
    perhaps better.
    
    			regards, tom lane
    
    
    
    
  8. Re: Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    David Rowley <dgrowleyml@gmail.com> — 2025-10-02T22:24:45Z

    On Fri, 3 Oct 2025 at 02:55, Mingli Zhang <zmlpostgres@gmail.com> wrote:
    > It seems that the optimization for `UNION ALL` is already implemented in the patch: it removes empty sub-paths and preserves the remaining ones.
    > Should we add a test case to formally validate this behavior like Union cases?
    
    If I were to do that, I'd have to come up with something that's
    flatten_simple_union_all() proof. Maybe something like varying types
    in the targetlist. I think it's probably not really worthwhile since
    it's not testing any new code that is not already being tested by the
    tests that I did add.
    
    David
    
    
    
    
  9. Re: Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    David Rowley <dgrowleyml@gmail.com> — 2025-10-04T03:55:56Z

    On Fri, 3 Oct 2025 at 10:02, Tom Lane <tgl@sss.pgh.pa.us> wrote:
    >
    > David Rowley <dgrowleyml@gmail.com> writes:
    > > An alternative way would be to propagate those during build_setop_child_paths()
    >
    > That answer works for me.  I was expecting you to just document the
    > need for the extra check in is_dummy_rel ;-) ... but this way is
    > perhaps better.
    
    So, I pushed the UNION portion earlier, but on hacking more on the
    EXCEPT/INTERSECT patch, I noticed that I don't have the target lists
    correct when marking the top-level set op as dummy. I had thought it
    was ok to use the target list of the first child. I did that to make
    EXPLAIN VERBOSE work as it chokes on the varno==0 top-level setop
    targetlist as generated by generate_append_tlist(). However, using the
    target list of the first child isn't right as createplan will choke on
    not finding PathKeys to sort.
    
    It looks like the normal UNION case side steps this issue for T_SetOp
    by applying set_dummy_tlist_references() in setrefs.c. That doesn't
    happen for Result since we may have something to evaluate there.
    
    I'm just considering the best fix and can think of two options:
    
    1) Move away from using varno==0 in generate_append_tlist(). Use varno==1, or;
    2) Add handling in setrefs.c for T_Result to adjust varno==0 Vars to
    use varno==1 vars.
    
    The attached v4-0001 does #2, but wondering if #1 should be explored first.
    
    David
    
  10. Re: Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    Tom Lane <tgl@sss.pgh.pa.us> — 2025-10-04T15:43:48Z

    David Rowley <dgrowleyml@gmail.com> writes:
    > I'm just considering the best fix and can think of two options:
    
    > 1) Move away from using varno==0 in generate_append_tlist(). Use varno==1, or;
    > 2) Add handling in setrefs.c for T_Result to adjust varno==0 Vars to
    > use varno==1 vars.
    
    > The attached v4-0001 does #2, but wondering if #1 should be explored first.
    
    I don't recall the details ATM, but if you poke around you will find
    multiple comments complaining about how that varno-zero convention is
    problematic or requires code to do something unusual.  So I'd be in
    favor of trying to get rid of it, but I'm not entirely sure what to do
    instead, and the ramifications might be wider than you realize.
    
    In particular it's not clear to me why varno==1 is better?  As best
    I can recall without diving into code, the fundamental mismatch is
    that varno zero doesn't correspond to any RTE.  It would be better
    if the Vars matched the subquery RTEs that are at the base of the
    set-operation nest, so that there were a useful referent as to where
    a Var came from.  Arbitrarily setting varno=1 sounds like the worst
    case: we could neither identify a Var with a source subquery
    accurately, nor realize that its varno is phony.
    
    			regards, tom lane
    
    
    
    
  11. Re: Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    David Rowley <dgrowleyml@gmail.com> — 2025-10-05T00:56:44Z

    On Sun, 5 Oct 2025 at 04:43, Tom Lane <tgl@sss.pgh.pa.us> wrote:
    > In particular it's not clear to me why varno==1 is better?  As best
    > I can recall without diving into code, the fundamental mismatch is
    > that varno zero doesn't correspond to any RTE.  It would be better
    > if the Vars matched the subquery RTEs that are at the base of the
    > set-operation nest, so that there were a useful referent as to where
    > a Var came from.  Arbitrarily setting varno=1 sounds like the worst
    > case: we could neither identify a Var with a source subquery
    > accurately, nor realize that its varno is phony.
    
    I've not tried it yet, but the idea with varno==1 is that is that it
    does index the first UNION child's RTE. For the case at hand, all the
    children are dummies.  I thought doing this was ok because Appends and
    MergeAppends show the target entries for the first child, and cases
    like the following do show Vars from RTEs that don't get scanned in
    the plan:
    
    # explain verbose select * from t where 1=2 order by 1;
    
     Sort  (cost=0.01..0.02 rows=0 width=0)
       Output: a, b
       Sort Key: t.a
       ->  Result  (cost=0.00..0.00 rows=0 width=0)
             Output: a, b
             Replaces: Scan on t
             One-Time Filter: false
    
    Another alternative that I'm thinking about which might be better is
    to double-down on the varno==0 and invent a special varno and define
    SETOP_VAR. I'd feel better about doing that as I didn't feel good
    about coding the magic number for the if(var->varno == 0) check in
    set_plan_refs() to make the T_Result work in EXPLAIN VERBOSE.
    
    David
    
    
    
    
  12. Re: Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    David Rowley <dgrowleyml@gmail.com> — 2025-10-07T01:19:05Z

    On Sun, 5 Oct 2025 at 13:56, David Rowley <dgrowleyml@gmail.com> wrote:
    > Another alternative that I'm thinking about which might be better is
    > to double-down on the varno==0 and invent a special varno and define
    > SETOP_VAR. I'd feel better about doing that as I didn't feel good
    > about coding the magic number for the if(var->varno == 0) check in
    > set_plan_refs() to make the T_Result work in EXPLAIN VERBOSE.
    
    I decided not to do it that way and instead just added code to create
    a varno==1 Var in setrefs.c.  This basically amounts to following on
    with the varno==0 hack used in prepunion.c.
    
    The reason I didn't go down the route of SETOP_VAR was that it's still
    a hack, it's just making it look a bit more official.  I suppose the
    correct way to fix all this and get rid of the varno==0 stuff forever
    is to have a proper top-level RTE for the top-level set operation and
    make it so each child is an OTHER_MEMBER rel at that query level. It
    felt like going a bit too far to do something like that to fix this
    bug, so I didn't explore that further.
    
    David
    
    
    
    
  13. Re: Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    Tom Lane <tgl@sss.pgh.pa.us> — 2025-10-07T01:48:35Z

    David Rowley <dgrowleyml@gmail.com> writes:
    > The reason I didn't go down the route of SETOP_VAR was that it's still
    > a hack, it's just making it look a bit more official.  I suppose the
    > correct way to fix all this and get rid of the varno==0 stuff forever
    > is to have a proper top-level RTE for the top-level set operation and
    > make it so each child is an OTHER_MEMBER rel at that query level. It
    > felt like going a bit too far to do something like that to fix this
    > bug, so I didn't explore that further.
    
    Yeah, I think "more RTEs" is the ultimate solution here, but it's
    never risen to the top of my to-do list either.  I was kind of
    thinking about an RTE per set-op child, though.  Not sure if one
    for the top-level op, or one for an intermediate op, would help;
    though it's certainly possible it would.
    
    			regards, tom lane
    
    
    
    
  14. Re: Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    Alexander Law <exclusion@gmail.com> — 2025-11-04T08:00:00Z

    Hello David,
    
    04.10.2025 06:55, David Rowley wrote:
    > On Fri, 3 Oct 2025 at 10:02, Tom Lane <tgl@sss.pgh.pa.us> wrote:
    >> David Rowley <dgrowleyml@gmail.com> writes:
    >>> An alternative way would be to propagate those during build_setop_child_paths()
    >> That answer works for me.  I was expecting you to just document the
    >> need for the extra check in is_dummy_rel ;-) ... but this way is
    >> perhaps better.
    > So, I pushed the UNION portion earlier, but on hacking more on the
    > EXCEPT/INTERSECT patch, I noticed that I don't have the target lists
    > correct when marking the top-level set op as dummy. ...
    
    Please look at a new anomaly, introduced with 03d40e4b5:
    CREATE TABLE t(i integer);
    CREATE TABLE pt(i integer) PARTITION BY LIST(i);
    
    SET enable_seqscan = 'off';
    SELECT * FROM t UNION SELECT * FROM t
    UNION ALL
    SELECT * FROM pt;
    produces:
    ERROR:  XX000: unrecognized node type: 0
    LOCATION:  create_plan_recurse, createplan.c:538
    
    
    Best regards.
    Alexander
    
    
    
    
  15. Re: Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    Richard Guo <guofenglinux@gmail.com> — 2025-11-04T09:19:34Z

    On Tue, Nov 4, 2025 at 5:00 PM Alexander Lakhin <exclusion@gmail.com> wrote:
    > Please look at a new anomaly, introduced with 03d40e4b5:
    > CREATE TABLE t(i integer);
    > CREATE TABLE pt(i integer) PARTITION BY LIST(i);
    >
    > SET enable_seqscan = 'off';
    > SELECT * FROM t UNION SELECT * FROM t
    > UNION ALL
    > SELECT * FROM pt;
    > produces:
    > ERROR:  XX000: unrecognized node type: 0
    > LOCATION:  create_plan_recurse, createplan.c:538
    
    I looked into this.  The child relation with relid 3 (the scan on the
    partitioned table) is a dummy, so it is skipped in
    generate_union_paths().  As a result, the final setop relation ends up
    the same as the child relation with relid set to (1, 2).  Then,
    generate_union_paths() creates an Append path using this relation's
    cheapest path as its subpath.  Somehow, add_path() determines that
    this new Append path dominates the original cheapest path, causing the
    original cheapest path to be freed.  This leads to the final Append
    path referencing a subpath that has already been freed.
    
    - Richard
    
    
    
    
  16. Re: Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    David Rowley <dgrowleyml@gmail.com> — 2025-11-04T09:54:37Z

    On Tue, 4 Nov 2025 at 21:00, Alexander Lakhin <exclusion@gmail.com> wrote:
    > Please look at a new anomaly, introduced with 03d40e4b5:
    > CREATE TABLE t(i integer);
    > CREATE TABLE pt(i integer) PARTITION BY LIST(i);
    >
    > SET enable_seqscan = 'off';
    > SELECT * FROM t UNION SELECT * FROM t
    > UNION ALL
    > SELECT * FROM pt;
    > produces:
    > ERROR:  XX000: unrecognized node type: 0
    
    Thanks for the report.
    
    This seems to be due to the fact that the same UPPERREL_SETOP
    RelOptInfo is being used for the UNION and UNION ALL. When doing the
    add_path() for the UNION ALL at prepunion.c:1030, the sole child path
    of the Append being added is an AggPath for the previous UNION
    operation. When we add_path() the AppendPath, the previous AggPath is
    already in the pathlist of the result_rel.  add_path() ends up freeing
    the old Path due to the new AppendPath being better and the end result
    is the Append's subpath is free'd.
    
    The reason we end up with the same result_rel is that we're not
    passing all the relids in fetch_upper_rel(root, UPPERREL_SETOP,
    relids) due to having removed dummy rels. I guess the fix might be
    something like record the relids even when skipping dummy relations.
    I'll go and explore that as an option.
    
    David
    
    
    
    
  17. Re: Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    David Rowley <dgrowleyml@gmail.com> — 2025-11-04T10:00:09Z

    On Tue, 4 Nov 2025 at 22:54, David Rowley <dgrowleyml@gmail.com> wrote:
    > The reason we end up with the same result_rel is that we're not
    > passing all the relids in fetch_upper_rel(root, UPPERREL_SETOP,
    > relids) due to having removed dummy rels. I guess the fix might be
    > something like record the relids even when skipping dummy relations.
    > I'll go and explore that as an option.
    
    This seems to fix it. I'll study it more in the morning (it's late in
    my time zone).
    
    David
    
  18. Re: Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    Richard Guo <guofenglinux@gmail.com> — 2025-11-04T10:02:18Z

    On Tue, Nov 4, 2025 at 6:19 PM Richard Guo <guofenglinux@gmail.com> wrote:
    > On Tue, Nov 4, 2025 at 5:00 PM Alexander Lakhin <exclusion@gmail.com> wrote:
    > > Please look at a new anomaly, introduced with 03d40e4b5:
    > > CREATE TABLE t(i integer);
    > > CREATE TABLE pt(i integer) PARTITION BY LIST(i);
    > >
    > > SET enable_seqscan = 'off';
    > > SELECT * FROM t UNION SELECT * FROM t
    > > UNION ALL
    > > SELECT * FROM pt;
    > > produces:
    > > ERROR:  XX000: unrecognized node type: 0
    > > LOCATION:  create_plan_recurse, createplan.c:538
    >
    > I looked into this.  The child relation with relid 3 (the scan on the
    > partitioned table) is a dummy, so it is skipped in
    > generate_union_paths().  As a result, the final setop relation ends up
    > the same as the child relation with relid set to (1, 2).  Then,
    > generate_union_paths() creates an Append path using this relation's
    > cheapest path as its subpath.  Somehow, add_path() determines that
    > this new Append path dominates the original cheapest path, causing the
    > original cheapest path to be freed.  This leads to the final Append
    > path referencing a subpath that has already been freed.
    
    I think a quick fix is to always include the involved child relids
    when building the relid set for the setop relation, even if the child
    is dummy.  A dummy child does not yield any rows, but theoretically it
    is still a relation that we should account for.
    
    - Richard
    
  19. Re: Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    David Rowley <dgrowleyml@gmail.com> — 2025-11-04T22:55:13Z

    On Tue, 4 Nov 2025 at 23:00, David Rowley <dgrowleyml@gmail.com> wrote:
    >
    > On Tue, 4 Nov 2025 at 22:54, David Rowley <dgrowleyml@gmail.com> wrote:
    > > The reason we end up with the same result_rel is that we're not
    > > passing all the relids in fetch_upper_rel(root, UPPERREL_SETOP,
    > > relids) due to having removed dummy rels. I guess the fix might be
    > > something like record the relids even when skipping dummy relations.
    > > I'll go and explore that as an option.
    >
    > This seems to fix it. I'll study it more in the morning (it's late in
    > my time zone).
    
    I went over this again this morning. I considered adding the following test:
    
    -- Try a more complex case with multiple chained UNIONs
    EXPLAIN (COSTS OFF)
    SELECT two FROM tenk1
    UNION
    SELECT four FROM tenk1
    UNION ALL
    SELECT ten FROM tenk1 WHERE 1=2;
    
    but it seems that enable_seqscan does also need to be disabled as
    otherwise add_path() just finds the new and old path to cost the same
    and rejects the new path.  With enable_seqscan = off,
    compare_path_costs_fuzzily() will find the old path to have
    disabled_node = 2 and the new one to have disabled_nodes = 0, so
    accepts the new and pfree's the old.
    
    I finally decided that it was a bit too obscure a scenario to test to
    verify that the same silly mistake didn't reappear.
    
    Thanks again for the report and the simple recreation steps.
    
    David
    
    
    
    
  20. Re: Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    Alexander Law <exclusion@gmail.com> — 2025-11-05T04:00:00Z

    Hello David!
    
    05.11.2025 00:55, David Rowley wrote:
    > I finally decided that it was a bit too obscure a scenario to test to
    > verify that the same silly mistake didn't reappear.
    >
    > Thanks again for the report and the simple recreation steps.
    
    Thank you for the fix!
    
    But while playing around, I've just discovered another interesting error:
    
    SET enable_seqscan = 'off';
    SELECT * FROM t EXCEPT SELECT * FROM t
    UNION SELECT * FROM pt;
    
    leads to:
    ERROR:  XX000: no relation entry for relid 0
    LOCATION:  find_base_rel, relnode.c:541
    
    Best regards,
    Alexander
    
  21. Re: Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    David Rowley <dgrowleyml@gmail.com> — 2025-11-05T05:26:53Z

    On Wed, 5 Nov 2025 at 17:00, Alexander Lakhin <exclusion@gmail.com> wrote:
    > But while playing around, I've just discovered another interesting error:
    >
    > SET enable_seqscan = 'off';
    > SELECT * FROM t EXCEPT SELECT * FROM t
    > UNION SELECT * FROM pt;
    >
    > leads to:
    > ERROR:  XX000: no relation entry for relid 0
    > LOCATION:  find_base_rel, relnode.c:541
    
    :-(
    
    This happens because of the code I added to try to give better
    estimates to the number of groups when only a single child remains in
    the UNION. Because the only surviving Path is for the EXCEPT set-op,
    there are Vars with varno==0, which estimate_num_groups() does not
    like.  I'm considering restricting that code Path to where the
    path->parent->reloptkind != RELOPT_UPPER_REL. It doesn't feel great
    adding the special case, but likely having better row estimates is
    worthwhile, when it's possible. I'll sit on that for a bit and see if
    I can think of a better way.
    
    (Yet another reason we should probably fix the varno==0 issue.)
    
    Thanks for the report and reproducer, again!
    
    David
    
    
    
    
  22. Re: Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    David Rowley <dgrowleyml@gmail.com> — 2025-11-05T09:44:58Z

    On Wed, 5 Nov 2025 at 18:26, David Rowley <dgrowleyml@gmail.com> wrote:
    > I'm considering restricting that code Path to where the
    > path->parent->reloptkind != RELOPT_UPPER_REL.
    
    I couldn't think of anything better, so here's a patch to that effect.
    
    David
    
  23. Re: Teaching planner to short-circuit empty UNION/EXCEPT/INTERSECT inputs

    David Rowley <dgrowleyml@gmail.com> — 2025-11-06T03:35:43Z

    On Wed, 5 Nov 2025 at 22:44, David Rowley <dgrowleyml@gmail.com> wrote:
    >
    > On Wed, 5 Nov 2025 at 18:26, David Rowley <dgrowleyml@gmail.com> wrote:
    > > I'm considering restricting that code Path to where the
    > > path->parent->reloptkind != RELOPT_UPPER_REL.
    >
    > I couldn't think of anything better, so here's a patch to that effect.
    
    I've pushed this.
    
    David