Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion test/native_groupagg.sh
Original file line number Diff line number Diff line change
Expand Up @@ -563,14 +563,20 @@ HIGHK="SELECT k, count(*), sum(i) FROM %T GROUP BY k"
# or its error. The query runs itself: wrapped in SELECT count(*) FROM (...), the
# planner drops the unused aggregates and the node refuses that shape, so the
# wrapper would measure a different plan.
# Both helpers turn hash aggregation and sorting off, so the grouped path wins
# whenever the gate offers it: these arms test the gate, not the cost model. A
# declined query still runs, on the disabled plan. Without this, a gate that let a
# skewed key through could still lose on cost, and the arm would stay green.
groups_or_error() {
local out
out="$(env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres -d "$PGC_DB" -At -q \
-c "SET enable_hashagg = off" -c "SET enable_sort = off" \
-c "SET pgcolumnar.groupagg_max_groups = $1" -c "$2" 2>&1)"
if grep -q '^ERROR' <<<"$out"; then grep -m1 '^ERROR' <<<"$out"; else grep -c '' <<<"$out"; fi
}
under_cap() { # under_cap CAP QUERY -> 1 when the grouped node is planned under that cap
env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres -d "$PGC_DB" -At \
env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres -d "$PGC_DB" -At -q \
-c "SET enable_hashagg = off" -c "SET enable_sort = off" \
-c "SET pgcolumnar.groupagg_max_groups = $1" -c "EXPLAIN (COSTS OFF) $2" 2>&1 \
| grep -c 'Columnar Vectorized Group Keys'
}
Expand Down Expand Up @@ -608,4 +614,16 @@ rc_out="$(env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postg
check "a cached plan run under a lower cap stops at the runtime cap, with its hint" \
"$(grep -c 'Columnar Vectorized Group Keys' <<<"$rc_out")|$(grep -c 'exceeded pgcolumnar.groupagg_max_groups (1000)' <<<"$rc_out")|$(grep -c 'HINT: Raise pgcolumnar.groupagg_max_groups' <<<"$rc_out")" "1|1|1"

# A NULL key is one more group, and the bucket bound must count it. @OffgridwithJD's
# shape on #1330: buckets at 00:00:59, 00:01:30 and 00:02:00 fill the bound of 3
# (floor(61 s / 60 s) + 2), so the NULL rows make a fourth group. Under a cap of 3 a
# bound without the NULL group takes the path and errors.
psql_run "CREATE TABLE gate_nb (ts timestamptz, v int) USING pgcolumnar;
INSERT INTO gate_nb SELECT '2026-01-01 00:00:59+00', g FROM generate_series(1, 50) g;
INSERT INTO gate_nb SELECT '2026-01-01 00:01:30+00', g FROM generate_series(1, 50) g;
INSERT INTO gate_nb SELECT '2026-01-01 00:02:00+00', g FROM generate_series(1, 50) g;
INSERT INTO gate_nb SELECT NULL, g FROM generate_series(1, 50) g;"
check "a NULL key counts as a group in the bucket bound: four groups under a cap of 3 are declined and all answered" \
"$(groups_or_error 3 "SELECT date_trunc('minute', ts) b, count(*) FROM gate_nb GROUP BY b")" "4"

pgc_summary
1 change: 1 addition & 0 deletions test/pytest/TESTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5662,6 +5662,7 @@ every major for this pair, which is the designed third state and not a failure.
| `test_the_bound_reads_the_zone_maps_in_one_catalog_scan` | planning a bucketed GROUP BY over 50 row groups scans zone_map and its index once in all, not once per group (#1304) |
| `test_a_mixed_timestamp_predicate_never_narrows_the_bound` | a cross-type predicate under a non-UTC TimeZone never narrows the estimate below the true group count (#1292); the matching-type one still gets a tight bound |
| `test_on_by_default_and_gated_on_an_upper_bound` | the grouped path is on by default and taken only when an upper bound on the groups (the row count, or the time buckets) is within the cap: a bucket query over more rows than the cap is taken, a plain key over more rows is declined (answering as heap does) until the cap is raised, and a skewed key that fools ANALYZE is declined and answers every group; a cached plan run under a lower cap stops at the runtime cap with its hint (#1292, #1330 review) |
| `test_a_null_key_counts_as_a_group_in_the_bucket_bound` | three minute buckets fill the bucket bound and NULL rows add a fourth group; under a cap of 3 the path is declined and all four groups answer, where a bound without the NULL group errors (#1330 review) |

## 72. test_analyze_function.py: statistics collected by reading, not by sampling

Expand Down
6 changes: 5 additions & 1 deletion test/pytest/expected_tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -606,4 +606,8 @@ guard_tests 419
#
# cluster_tests 527 -> 528 for #1297's test_partition_drop_plan, by collection on
# this branch rebased onto b55cb23 (#1330).
cluster_tests 528
#
# cluster_tests 528 -> 529 for the #1330 review's NULL-group arm in
# test_native_groupagg.py, one test, by collection on this branch rebased onto
# 614eb02 (#1331).
cluster_tests 529
36 changes: 32 additions & 4 deletions test/pytest/test_native_groupagg.py
Original file line number Diff line number Diff line change
Expand Up @@ -730,25 +730,39 @@ def test_a_mixed_timestamp_predicate_never_narrows_the_bound(pgc_conn, expect):

# ---- on by default, gated on an upper bound (#1292 phase 2) -----------------

def _under_cap(conn, cap, sql):
def _gate_only(conn, cap):
"""Hash aggregation and sorting off, so the grouped path wins whenever the gate
offers it: the arms below test the gate, not the cost model. A declined query
still runs, on the disabled plan."""
_exec(conn, "SET enable_hashagg = off")
_exec(conn, "SET enable_sort = off")
_exec(conn, f"SET pgcolumnar.groupagg_max_groups = {cap}")


def _gate_reset(conn):
for name in ("enable_hashagg", "enable_sort", "pgcolumnar.groupagg_max_groups"):
_exec(conn, f"RESET {name}")


def _under_cap(conn, cap, sql):
_gate_only(conn, cap)
try:
return _plan(conn, sql)
finally:
_exec(conn, "RESET pgcolumnar.groupagg_max_groups")
_gate_reset(conn)


def _groups_or_error(conn, cap, sql):
"""The number of groups the query returns under that cap, or its error. The query
runs itself: wrapped in count(*), the planner drops the unused aggregates and the
node refuses that shape, so a wrapper would measure a different plan."""
_exec(conn, f"SET pgcolumnar.groupagg_max_groups = {cap}")
_gate_only(conn, cap)
try:
return str(len(_rows(conn, sql)))
except psycopg.Error as exc:
return f"ERROR: {exc.diag.message_primary}"
finally:
_exec(conn, "RESET pgcolumnar.groupagg_max_groups")
_gate_reset(conn)


def test_on_by_default_and_gated_on_an_upper_bound(pgc_conn, expect):
Expand Down Expand Up @@ -818,3 +832,17 @@ def test_on_by_default_and_gated_on_an_upper_bound(pgc_conn, expect):
_exec(conn, "DEALLOCATE gk")
expect.text(f"{1 if planned else 0}|{got}", "1|1|1",
"a cached plan run under a lower cap stops at the runtime cap, with its hint")


def test_a_null_key_counts_as_a_group_in_the_bucket_bound(pgc_conn, expect):
"""@OffgridwithJD's shape on #1330: three minute buckets fill the bound exactly
(lo 00:00:59, hi 00:02:00: floor(61 s / 60 s) + 2 = 3), and NULL rows add a
fourth group. Under a cap of 3, a bound without the NULL group takes the path
and errors. This file's own rows: other second offsets, 40 rows a group.
"""
conn = pgc_conn
_exec(conn, "CREATE TABLE nul_b (ts timestamptz, v int) USING pgcolumnar")
for ts in ("'2026-03-01 10:00:59+00'", "'2026-03-01 10:01:05+00'", "'2026-03-01 10:02:00+00'", "NULL"):
_exec(conn, f"INSERT INTO nul_b SELECT {ts}, g FROM generate_series(1, 40) g")
expect.text(_groups_or_error(conn, 3, "SELECT date_trunc('minute', ts) b, count(*) FROM nul_b GROUP BY b"), "4",
"a NULL key counts as a group in the bucket bound: four groups under a cap of 3 are declined and all answered")
Loading