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
56 changes: 47 additions & 9 deletions test/check_ledger.tsv

Large diffs are not rendered by default.

46 changes: 44 additions & 2 deletions test/check_ledger_budget.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
#
# Adding a check to a suite that is already covered does not move it, which is
# what makes it safe to bound.
suites_not_covered 199
suites_not_covered 197
#
# checks_never_observed_red -- A CENSUS. NOT a ceiling, and it must not become
# one.
Expand Down Expand Up @@ -3343,4 +3343,46 @@ suites_not_covered 199
# never 2988 - 16 = 2972
# dated 1201 + 16 = 1217
# 2972 + 1217 == 4189
checks_never_observed_red 2972
#
# #1341, native_groupagg_batch's NULL-key mutation (OffgridwithJD, from jdatcmd's
# #1485 review): 2972 -> 2969. The grouped fold's gather dropping the NULL-key
# flag folds the NULL group into k = 0 on the batch path only. Nine checks redden,
# shell and twin alike; three of them (plain, two keys and count(*) against the
# heap mirror) entered never and are now dated, and the other six gain the
# mutation in their history. No row is added or removed.
#
# ledger rows 4189 + 0 = 4189
# never 2972 - 3 = 2969
# dated 1217 + 3 = 1220
# 2969 + 1220 == 4189
#
# #1341, native_fetch_position seeded, with its twin (OffgridwithJD): 2969 -> 2975,
# and suites_not_covered 199 -> 198. The suite had no rows. It enters with all 12
# checks from five-major runs (12/12 on each of 15-19). 6 are dated by four
# mutations of pgcolumnar_fetch_row: the rank counting rows, the rank one too far,
# the rank walking the validity bits, and a varying-length value reached by
# decoding every earlier one. The two timing arms are dated by the walks only now
# that the fetches go through the index and the timed pass is a read. Six arrive
# never: the one-row-group fixture fact and five premises.
#
# ledger rows 4189 + 12 = 4201
# never 2969 + 6 = 2975
# dated 1220 + 6 = 1226
# 2975 + 1226 == 4201
#
# #1341, native_fetch_cache seeded, with its twin (OffgridwithJD): 2975 -> 2991,
# and suites_not_covered 198 -> 197. The suite had no rows. It enters with all 26
# checks from five-major runs (26/26 on each of 15-19). 10 are dated by ten
# mutations: the cache never hitting, the key without the storage id, no
# earlier-command reject, each of the four recheck fields dropped, the discard
# dropped from executor end and from transaction end, and the cap dropping every
# column. Sixteen arrive never: four premises and the four #797 plan premises;
# the two #801 geometry records; the four correctness arms; and the #359 ratio,
# which the cap-drops-every-column mutation cannot redden on this fixture (it
# slows one projected column as much as five, measured 46 -> 286 ms in-suite).
#
# ledger rows 4201 + 26 = 4227
# never 2975 + 16 = 2991
# dated 1226 + 10 = 1236
# 2991 + 1236 == 4227
checks_never_observed_red 2991
7 changes: 6 additions & 1 deletion test/native_fetch_cache.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ build() { # table, rows-per-group
CREATE INDEX ${1}_id ON $1 (id);"
}

# index-driven so every row goes through the fetch path rather than a scan
# Every updated row goes through the fetch path whatever the scan plans: the
# UPDATE fetches its target rows by number. Measured (#1341) on PG18: this
# UPDATE plans as Custom Scan (PgColumnarScan) under these SETs, and with the
# cache made to miss on every lookup the ratio below still goes red (844 ms
# against 93 ms). So the SETs are not what makes it a fetch, and no plan
# premise is needed here, unlike the point queries further down.
upd_ms() {
local start end
start=$(date +%s%N)
Expand Down
44 changes: 41 additions & 3 deletions test/native_fetch_position.sh
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,32 @@ check "the rows are in one row group" \

# Fetch by row number is what an index scan drives, so force one. Each leading
# SET prints its own line ahead of the result, so these queries take the last.
#
# enable_seqscan=off does NOT turn the columnar custom scan off, and it was all
# this suite set: measured, the single-row fetch, the near and far UPDATEs and
# the mirror join all planned as Custom Scan (PgColumnarScan), so none of them
# fetched by row number. A rank off by one left the single-row arm green, and a
# walk over every earlier row left both timing arms green. The custom scan is
# off now, and each measurement asserts first that its plan reads the index.
FORCE="SET enable_seqscan = off; SET enable_bitmapscan = off;
SET max_parallel_workers_per_gather = 0;"
SET max_parallel_workers_per_gather = 0;
SET pgcolumnar.enable_custom_scan = off;"

# reads_index QUERY INDEX -> yes, or the plan's first line: the premise that a
# query fetches by row number through INDEX rather than scanning
reads_index() {
local plan
plan="$(q "$FORCE EXPLAIN (COSTS OFF) $1")"
case "$plan" in
*"Index Scan using $2 "*) echo yes ;;
*) echo "no: $(grep -m1 -E 'Scan|Update' <<<"$plan" | sed 's/^ *//')" ;;
esac
}

check "premise: the mirror comparison fetches through the index" \
"$(reads_index "SELECT count(*) FROM fp_c c JOIN fp_h h USING (id) WHERE c.fixed IS DISTINCT FROM h.fixed" fp_c_id)" "yes"
check "premise: a single-row fetch goes through the index" \
"$(reads_index "SELECT fixed FROM fp_c WHERE id = 1001" fp_c_id)" "yes"

# rows spread across the whole group, including the far end where the old walk
# was longest
Expand Down Expand Up @@ -103,7 +127,7 @@ FAR_HI=$ROWS
upd_once() { # id-low, id-high -> milliseconds for one pass
local start end
start=$(date +%s%N)
psql_run "$FORCE UPDATE fp_c SET fixed = fixed + 1
psql_run "$FORCE SELECT sum(fixed), sum(length(txt)) FROM fp_c
WHERE id BETWEEN $1 AND $2;" >/dev/null 2>&1
end=$(date +%s%N)
echo $(( (end - start) / 1000000 ))
Expand All @@ -130,6 +154,17 @@ upd_ms() { # id-low, id-high -> median of three passes
printf '%s\n%s\n%s\n' "$a" "$b" "$c" | sort -n | sed -n 2p
}

# The timed pass is a SELECT, not the UPDATE it was. A columnar UPDATE writes
# each row again into a new row group, so only the first of three passes
# fetched from the far end of the big group; the second and third fetched from
# the start of the new one, and the median was always one of those. Measured
# with the rank walking every earlier row: the UPDATE form read near 122 ms,
# far 132 ms, green. A read leaves every row where it was, so all three passes
# fetch the same positions. sum(length(txt)) makes it decode a varying-length
# column, which the offset table serves.
check "premise: the timed reads fetch through the index" \
"$(reads_index "SELECT sum(fixed), sum(length(txt)) FROM fp_c WHERE id BETWEEN $FAR_LO AND $FAR_HI" fp_c_id)" "yes"

near="$(upd_ms $NEAR_LO $NEAR_HI)"
far="$(upd_ms $FAR_LO $FAR_HI)"
echo "-- $N fetches, median of three: first tenth ${near} ms, last tenth ${far} ms"
Expand Down Expand Up @@ -157,7 +192,7 @@ build() { # table, rows
fetch_once() { # table, table-rows, count -> milliseconds for one pass
local start end
start=$(date +%s%N)
psql_run "$FORCE UPDATE $1 SET v = v + 1 WHERE id > $(( $2 - $3 ));" \
psql_run "$FORCE SELECT sum(v), sum(length(t)) FROM $1 WHERE id > $(( $2 - $3 ));" \
>/dev/null 2>&1
end=$(date +%s%N)
echo $(( (end - start) / 1000000 ))
Expand Down Expand Up @@ -205,6 +240,9 @@ build fp_small $((ROWS / 2))
build fp_big "$ROWS"

K=$((ROWS / 20))
check "premise: the doubled group's reads fetch through its index" \
"$(reads_index "SELECT sum(v), sum(length(t)) FROM fp_big WHERE id > $(( ROWS - K ))" fp_big_id)" "yes"

small="$(fetch_ms fp_small $((ROWS / 2)) $K)"
big="$(fetch_ms fp_big "$ROWS" $K)"
echo "-- $K fetches at the far end, median of three: group of $((ROWS / 2)) took ${small} ms, group of $ROWS took ${big} ms"
Expand Down
28 changes: 28 additions & 0 deletions test/pytest/TESTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ behaviour, the source of that number is named.
- [164. test_native_saop_pushdown.py: IN-lists and = ANY(array) prune row groups](#164-test_native_saop_pushdownpy-in-lists-and--anyarray-prune-row-groups)
- [165. test_native_groupagg_batch.py: the grouped aggregate folds column-at-a-time](#165-test_native_groupagg_batchpy-the-grouped-aggregate-folds-column-at-a-time)
- [166. test_native_fetch_projection.py: a fetch by row number decodes only what its caller needs](#166-test_native_fetch_projectionpy-a-fetch-by-row-number-decodes-only-what-its-caller-needs)
- [167. test_native_fetch_position.py: a fetch by row number costs the same anywhere in its group](#167-test_native_fetch_positionpy-a-fetch-by-row-number-costs-the-same-anywhere-in-its-group)
- [168. test_native_fetch_cache.py: the fetch-by-row-number cache](#168-test_native_fetch_cachepy-the-fetch-by-row-number-cache)

## 1. How to read a test in here

Expand Down Expand Up @@ -7691,3 +7693,29 @@ The pytest twin of `test/native_fetch_projection.sh` (ported for #1341), every c
| `test_a_projection_covering_every_column_decodes_nothing_and_is_right` | with the computed base-column set empty, every row is reconstructed with the right values |
| `test_no_caller_decodes_every_column_and_index_deletion_reads_no_row` | the projection path calls no full decode, and index deletion asks no row whether it is live |
| `test_every_needed_set_test_consults_the_all_columns_flag` | every needed-set membership test in the reader consults the explicit all-columns flag |

## 167. test_native_fetch_position.py: a fetch by row number costs the same anywhere in its group

The pytest twin of `test/native_fetch_position.sh` (ported for #1341), every check by the shell's name. A cached row group still walked to the row: it counted the validity bits below the row, then decoded and discarded that many values (#143). A rank prefix over the validity bitmap and an offset table for varying-length columns make each fetch O(1). Every fetch here runs with the columnar custom scan off, and each measurement first asserts that its plan reads the index. With only `enable_seqscan=off`, every query scanned instead and none fetched by row number. The timing ratios read the same ranges three times. An UPDATE would write its rows into a new group, so later passes would fetch elsewhere.

### Every test

| test | what it holds |
| --- | --- |
| `test_every_fetched_value_matches_the_heap_mirror` | the group is one row group, and fixed, varying-length and heavily-null columns fetched through the index match a heap mirror, at every probed position |
| `test_the_cost_does_not_depend_on_the_position` | fetching the last twentieth of the group costs under three times the first |
| `test_a_bigger_group_does_not_cost_more_per_fetch` | the same far-end fetches in a group twice the size cost under 1.8 times as much |
| `test_the_indexes_that_make_it_constant_are_in_the_fetch` | `pgcolumnar_fetch_row` takes the rank from the prefix and a varying-length value from the offset table |

## 168. test_native_fetch_cache.py: the fetch-by-row-number cache

The pytest twin of `test/native_fetch_cache.sh` (ported for #1341), every check by the shell's name. A fetch by row number decoded a whole row group per row (#143), and a statement-scoped cache of the decoded group removes the repeat. The cache is timed as a ratio: the same index fetches from one big row group and from ten small ones. Each reading runs on a freshly built fixture, because an UPDATE moves the rows it reads into a new group (#801). Then come the scoping guards in the source, a wide group's decode scratch (#353), and crossing the cache cap (#359).

### Every test

| test | what it holds |
| --- | --- |
| `test_the_cache_makes_one_big_group_cost_what_ten_small_ones_do` | each reading measured the geometry it meant to, one big group costs under three times ten small ones, and the updated rows are right |
| `test_the_scoping_guards_are_in_the_source` | the cache key includes the storage id, an earlier command's entry is rejected, a hit re-checks four fields, and the cache is discarded at executor end and transaction end |
| `test_a_wide_groups_decode_scratch_does_not_blow_the_cap` | a wide group's point query drives the fetch path, costs under five times a small group's, and answers the same |
| `test_crossing_the_cap_costs_per_column` | four and five projected columns drive the fetch path, five costs under twelve times four, and every over-cap column reads back its value |
6 changes: 5 additions & 1 deletion test/pytest/expected_tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -795,4 +795,8 @@ guard_tests 438
# eight tests, by collection over the cluster files on this tree.
# cluster_tests 718 -> 722 for #1341's test_native_fetch_projection.py, the twin of native_fetch_projection.sh:
# four tests, by collection over the cluster files on this tree.
cluster_tests 722
# cluster_tests 722 -> 726 for #1341's test_native_fetch_position.py, the twin of native_fetch_position.sh:
# four tests, by collection over the cluster files on this tree.
# cluster_tests 726 -> 730 for #1341's test_native_fetch_cache.py, the twin of native_fetch_cache.sh:
# four tests, by collection over the cluster files on this tree.
cluster_tests 730
2 changes: 2 additions & 0 deletions test/pytest/test_compare_to_bash.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@
"native_dict_underfill",
"native_dml",
"native_encdesc_golden",
"native_fetch_cache",
"native_fetch_coalesce",
"native_fetch_position",
"native_fetch_projection",
"native_groupagg",
"native_groupagg_batch",
Expand Down
Loading
Loading