Skip to content

The grouped parallel arm loses to the serial node because the Finalize is priced off estimate_num_groups on an expression key #369

Description

@jdatcmd

Summary

Split out of #349, which is otherwise complete. #366 made the grouped vectorized path
parallel-aware and it works — but the planner declines it on exactly the shapes where
it wins most, and the reason is a costing asymmetry rather than anything about the
node.

20M-row TSBS-shaped fixture, PG18 assert, 4 workers:

shape core serial node parallel arm
G1 (1 metric, 12h window) 6,144 ms 4,555 ms 4,587 ms (serial chosen)
G2 (10 metrics, 12h window) 12,410 ms 9,225 ms 7,493 ms (serial chosen)
G3 (full scan, group by host) 892 ms 896 ms 600 ms (parallel chosen)

Forced onto G1 the parallel arm runs in 1,184 ms against the serial node's 4,555
a 3.9x win the planner is declining.

Mechanism

The planner's own numbers on G1:

partialScan=15,042   ppath=16,292   gather=16,292   final=44,324   serial=20,042
dNumGroups=200,000   (actual groups ~8,000)

Our partial node under its Gather costs 16,292 against the serial node's 20,042
— it wins on everything it actually does. The core Finalize HashAggregate on top adds
28,031, and that term is priced off dNumGroups.

For a date_trunc(...) grouping key estimate_num_groups cannot estimate distinctness
and returns a count near the input row count: 200,000 estimated against ~8,000 actual
here, and 2,000,000 against 48,000 on a larger fixture. The finalize is overpriced by
that ratio and loses the comparison on its own.

Why it is structural, not a tuning miss

The serial node emits finished values, so it pays no finalize at all. #350
deliberately priced it per input row with no per-output-group term — an earlier version
charged per group and autoanalyze's group estimate flipped the plan choice, so the node
sometimes did not run.

So any two-phase plan pays a group-count-driven finalize and the serial node does not.
When the group estimate is inflated, the serial node wins by construction, whatever
the parallel arm actually costs.

The mechanism is confirmed, not inferred

G3's grouping key is a plain column, where estimate_num_groups is accurate, and there
the parallel arm is chosen and is faster. Accurate estimate → chosen; inflated
estimate → declined. Same code, same box, same session.

Fix directions

  1. Charge the serial node for the group hash table it builds, so both arms pay a
    group-count term. Closest to correct, and the reason fix: charge the grouped aggregate path for the folding it does (#349) #350 avoided it (autoanalyze
    flipping the choice) is worth re-testing rather than assumed — it may have been an
    artifact of charging per output group instead of per hash entry built.
  2. Cost the finalize off something less brittle than estimate_num_groups on an
    expression key
    — e.g. clamp it by the partial arm's own row estimate.
  3. Leave it, and accept that the parallel arm only fires on plain-column keys.

(1) and (2) both change plan choice on shapes beyond this one, which is why #366 did
not fold either in: each deserves its own measurement.

Not urgent

pgcolumnar.enable_group_vectorization and enable_parallel_vector_agg are both off
by default
, so no default install is affected. Post-alpha in my view; noted on #367.

Related

#349 (parent, complete), #366 (the parallel arm), #350 (the serial node's costing).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions