Hi David,
On Sat, Oct 24, 2026 at 11:21 PM David Rowley
<[email protected]> wrote:
> I'm only interested in discussion here, as there are
> obviously trade-offs to adding an index.
On your schema at full scale, pg_upgrade takes 1121 s on master and
about two minutes with the index, from v10 or from your patch alike:
both use it in the same three lookups. The price is on writes, since
every constraint gets an index entry, under zero if it is not a
foreign key: creating 1,000 tables with 100 NOT NULL columns each
takes 4.5% more instructions and 5.3% more WAL.
v10, attached, is just the plain index Tom suggested in [1].
heap_truncate_find_FKs() keeps its seqscan: v9-0002 probed the index
once per relation, and that made a commit with 1,000 ON COMMIT DELETE
ROWS tables 14-24% slower on a fresh database. The stats.sql check
now reads this backend's own counter inside a transaction, so a
parallel test can't hide a revert. catversion is not bumped.
Details and scripts attached.
[1] https://www.postgresql.org/message-id/1298334.1790896057%40sss.pgh.pa.us
Regards,
Manu
From b5b972a4bb9a6b5383ab5ad3340fd87aa77f7d30 Mon Sep 17 00:00:00 2001
From: Manuel Reyes Bravo <[email protected]>
Date: Sun, 4 Oct 2026 19:18:35 -0300
Subject: [PATCH v10] Index pg_constraint.confrelid to avoid seqscans of the
catalog
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Three places look up the foreign keys that reference one relation by
scanning all of pg_constraint for a matching confrelid:
- CloneFkReferenced() (ATTACH PARTITION, PARTITION OF, and
so every partition pg_upgrade restores)
- GetParentedForeignKeyRefs() (DETACH PARTITION)
- ATPrepChangePersistence() (ALTER TABLE ... SET UNLOGGED)
That was cheap while pg_constraint stayed small. Since not-null
constraints gained pg_constraint rows (commit 14e87ffa5c5), these scans
grow with the total number of constraints in the database, and
pg_upgrade pays one per partition it restores.
Add a non-unique btree index on confrelid, like the existing one on
contypid, and use it in those three places. As with contypid, the rows
of constraints other than foreign keys are indexed under zero, so every
constraint created, including each not-null constraint, pays for one
more index insertion.
Check in stats.sql that each of the three lookups scans the new index.
The check reads this backend's counters for the current transaction,
so the tests running in parallel, which attach and detach partitions
too, cannot hide a revert.
heap_truncate_find_FKs() also scans pg_constraint by confrelid, but for
a whole list of relations. Probing the index once per relation costs
more than one pass over a small catalog, so it keeps its seqscan.
Reported-by: Bernhard Wonisch <[email protected]>
Suggested-by: Tom Lane <[email protected]>
Reviewed-by: Ãlvaro Herrera <[email protected]>
Discussion: https://postgr.es/m/trinity-08d3329b-f6b2-4c11-91d7-2198e50bf4fc-1790681922354@trinity-msg-rest-gmx-gmx-live-58cc8f554d-c56lm
Discussion: https://postgr.es/m/[email protected]
Discussion: https://postgr.es/m/CAApHDvpxGcb-_ddd+qL+F=vmv4d3wznbuci3wqyj0hcfnc9...@mail.gmail.com
---
src/backend/catalog/heap.c | 5 ++--
src/backend/commands/tablecmds.c | 46 ++++++++++++++++++-----------
src/include/catalog/pg_constraint.h | 1 +
src/test/regress/expected/stats.out | 32 ++++++++++++++++++++
src/test/regress/sql/stats.sql | 28 ++++++++++++++++++
5 files changed, 92 insertions(+), 20 deletions(-)
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index ce3d3466126..cce8f2d139e 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -3848,8 +3848,9 @@ heap_truncate_find_FKs(List *relationIds)
oids = list_copy(relationIds);
/*
- * Must scan pg_constraint. Right now, it is a seqscan because there is
- * no available index on confrelid.
+ * Must scan pg_constraint. It is a seqscan although confrelid is
+ * indexed, because probing the index once for each relation in the list
+ * costs more than one pass over a small catalog.
*/
fkeyRel = table_open(ConstraintRelationId, AccessShareLock);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 97e16f3eb47..9939d8de5b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -11350,7 +11350,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
AttrMap *attmap;
ListCell *cell;
SysScanDesc scan;
- ScanKeyData key[2];
+ ScanKeyData key;
HeapTuple tuple;
List *clone = NIL;
Relation trigrel;
@@ -11365,19 +11365,23 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
* scan could return children first.)
*/
pg_constraint = table_open(ConstraintRelationId, RowShareLock);
- ScanKeyInit(&key[0],
+ ScanKeyInit(&key,
Anum_pg_constraint_confrelid, BTEqualStrategyNumber,
F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(parentRel)));
- ScanKeyInit(&key[1],
- Anum_pg_constraint_contype, BTEqualStrategyNumber,
- F_CHAREQ, CharGetDatum(CONSTRAINT_FOREIGN));
- /* This is a seqscan, as we don't have a usable index ... */
- scan = systable_beginscan(pg_constraint, InvalidOid, true,
- NULL, 2, key);
+
+ /*
+ * Scan pg_constraint using the index on confrelid, which is only set
+ * nonzero by FK constraints. Filtering on contype in the loop is
+ * therefore just a safety check.
+ */
+ scan = systable_beginscan(pg_constraint, ConstraintConfRelidIndexId, true,
+ NULL, 1, &key);
while ((tuple = systable_getnext(scan)) != NULL)
{
Form_pg_constraint constrForm = (Form_pg_constraint) GETSTRUCT(tuple);
+ if (constrForm->contype != CONSTRAINT_FOREIGN)
+ continue;
clone = lappend_oid(clone, constrForm->oid);
}
systable_endscan(scan);
@@ -19575,8 +19579,8 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
pg_constraint = table_open(ConstraintRelationId, AccessShareLock);
/*
- * Scan conrelid if changing to permanent, else confrelid. This also
- * determines whether a useful index exists.
+ * Scan conrelid if changing to permanent, else confrelid. Each column
+ * leads an index, so this also determines which index to use.
*/
ScanKeyInit(&skey[0],
toLogged ? Anum_pg_constraint_conrelid :
@@ -19584,7 +19588,8 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(RelationGetRelid(rel)));
scan = systable_beginscan(pg_constraint,
- toLogged ? ConstraintRelidTypidNameIndexId : InvalidOid,
+ toLogged ? ConstraintRelidTypidNameIndexId :
+ ConstraintConfRelidIndexId,
true, NULL, 1, skey);
while (HeapTupleIsValid(tuple = systable_getnext(scan)))
@@ -22673,7 +22678,7 @@ GetParentedForeignKeyRefs(Relation partition)
Relation pg_constraint;
HeapTuple tuple;
SysScanDesc scan;
- ScanKeyData key[2];
+ ScanKeyData key;
List *constraints = NIL;
/*
@@ -22687,19 +22692,24 @@ GetParentedForeignKeyRefs(Relation partition)
/* Search for constraints referencing this table */
pg_constraint = table_open(ConstraintRelationId, AccessShareLock);
- ScanKeyInit(&key[0],
+ ScanKeyInit(&key,
Anum_pg_constraint_confrelid, BTEqualStrategyNumber,
F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(partition)));
- ScanKeyInit(&key[1],
- Anum_pg_constraint_contype, BTEqualStrategyNumber,
- F_CHAREQ, CharGetDatum(CONSTRAINT_FOREIGN));
- /* XXX This is a seqscan, as we don't have a usable index */
- scan = systable_beginscan(pg_constraint, InvalidOid, true, NULL, 2, key);
+ /*
+ * Scan pg_constraint using the index on confrelid, which is only set
+ * nonzero by FK constraints. Filtering on contype in the loop is
+ * therefore just a safety check.
+ */
+ scan = systable_beginscan(pg_constraint, ConstraintConfRelidIndexId, true,
+ NULL, 1, &key);
while ((tuple = systable_getnext(scan)) != NULL)
{
Form_pg_constraint constrForm = (Form_pg_constraint) GETSTRUCT(tuple);
+ if (constrForm->contype != CONSTRAINT_FOREIGN)
+ continue;
+
/*
* We only need to process constraints that are part of larger ones.
*/
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index e8d27546ed9..47d57f29488 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -185,6 +185,7 @@ DECLARE_UNIQUE_INDEX(pg_constraint_conrelid_contypid_conname_index, 2665, Constr
DECLARE_INDEX(pg_constraint_contypid_index, 2666, ConstraintTypidIndexId, pg_constraint, btree(contypid oid_ops));
DECLARE_UNIQUE_INDEX_PKEY(pg_constraint_oid_index, 2667, ConstraintOidIndexId, pg_constraint, btree(oid oid_ops));
DECLARE_INDEX(pg_constraint_conparentid_index, 2579, ConstraintParentIndexId, pg_constraint, btree(conparentid oid_ops));
+DECLARE_INDEX(pg_constraint_confrelid_index, 9370, ConstraintConfRelidIndexId, pg_constraint, btree(confrelid oid_ops));
MAKE_SYSCACHE(CONSTROID, pg_constraint_oid_index, 16);
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index 8b15471248b..9a7a8c50e76 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -2154,4 +2154,36 @@ SELECT fastpath_exceeded > :backend_fastpath_exceeded_before
(1 row)
DROP TABLE part_test;
+-- Test that the lookups of foreign keys by referenced relation use
+-- pg_constraint_confrelid_index. The counter is this backend's, for the
+-- current transaction, so other sessions cannot change the result.
+CREATE TABLE confrelid_pk (id int PRIMARY KEY) PARTITION BY LIST (id);
+CREATE TABLE confrelid_fk (pid int REFERENCES confrelid_pk);
+CREATE TABLE confrelid_ul (a int);
+CREATE FUNCTION confrelid_idx_scan() RETURNS bigint LANGUAGE sql AS
+ $$SELECT pg_stat_get_xact_idx_numscans('pg_constraint_confrelid_index'::regclass)$$;
+BEGIN;
+SELECT confrelid_idx_scan() AS confrelid_scans \gset
+-- CloneFkReferenced()
+CREATE TABLE confrelid_pk1 PARTITION OF confrelid_pk FOR VALUES IN (1);
+SELECT confrelid_idx_scan() > :confrelid_scans AS clone_fk_referenced,
+ confrelid_idx_scan() AS confrelid_scans \gset
+-- GetParentedForeignKeyRefs()
+ALTER TABLE confrelid_pk DETACH PARTITION confrelid_pk1;
+SELECT confrelid_idx_scan() > :confrelid_scans AS get_parented_fk_refs,
+ confrelid_idx_scan() AS confrelid_scans \gset
+-- ATPrepChangePersistence()
+ALTER TABLE confrelid_ul SET UNLOGGED;
+SELECT confrelid_idx_scan() > :confrelid_scans AS change_persistence \gset
+COMMIT;
+SELECT :'clone_fk_referenced' AS clone_fk_referenced,
+ :'get_parented_fk_refs' AS get_parented_fk_refs,
+ :'change_persistence' AS change_persistence;
+ clone_fk_referenced | get_parented_fk_refs | change_persistence
+---------------------+----------------------+--------------------
+ t | t | t
+(1 row)
+
+DROP TABLE confrelid_fk, confrelid_pk, confrelid_pk1, confrelid_ul;
+DROP FUNCTION confrelid_idx_scan();
-- End of Stats Test
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 674637e172b..3fbe32274fe 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -1070,4 +1070,32 @@ SELECT fastpath_exceeded > :backend_fastpath_exceeded_before
DROP TABLE part_test;
+-- Test that the lookups of foreign keys by referenced relation use
+-- pg_constraint_confrelid_index. The counter is this backend's, for the
+-- current transaction, so other sessions cannot change the result.
+CREATE TABLE confrelid_pk (id int PRIMARY KEY) PARTITION BY LIST (id);
+CREATE TABLE confrelid_fk (pid int REFERENCES confrelid_pk);
+CREATE TABLE confrelid_ul (a int);
+CREATE FUNCTION confrelid_idx_scan() RETURNS bigint LANGUAGE sql AS
+ $$SELECT pg_stat_get_xact_idx_numscans('pg_constraint_confrelid_index'::regclass)$$;
+BEGIN;
+SELECT confrelid_idx_scan() AS confrelid_scans \gset
+-- CloneFkReferenced()
+CREATE TABLE confrelid_pk1 PARTITION OF confrelid_pk FOR VALUES IN (1);
+SELECT confrelid_idx_scan() > :confrelid_scans AS clone_fk_referenced,
+ confrelid_idx_scan() AS confrelid_scans \gset
+-- GetParentedForeignKeyRefs()
+ALTER TABLE confrelid_pk DETACH PARTITION confrelid_pk1;
+SELECT confrelid_idx_scan() > :confrelid_scans AS get_parented_fk_refs,
+ confrelid_idx_scan() AS confrelid_scans \gset
+-- ATPrepChangePersistence()
+ALTER TABLE confrelid_ul SET UNLOGGED;
+SELECT confrelid_idx_scan() > :confrelid_scans AS change_persistence \gset
+COMMIT;
+SELECT :'clone_fk_referenced' AS clone_fk_referenced,
+ :'get_parented_fk_refs' AS get_parented_fk_refs,
+ :'change_persistence' AS change_persistence;
+DROP TABLE confrelid_fk, confrelid_pk, confrelid_pk1, confrelid_ul;
+DROP FUNCTION confrelid_idx_scan();
+
-- End of Stats Test
CF 7365 v10: measurements
=========================
All on master a054b77e40d, production builds (-O2 -g, no cassert):
master, and master + v10-0001. Counts are user-space instructions under
perf stat, 3 runs per build in rotated order, median. The scripts are
attached (david-repro.sh.txt, ops-counts.sh.txt, fk-lookup-counts.sh.txt).
1. David's schema at full scale
-------------------------------
957,311 constraints (859,329 NOT NULL, 32,686 PK, 32,625 CHECK,
32,623 FK, 48 UNIQUE) over 32,300 partitions; every FK references the
same table, dim_ref, so master's scans read every row and find nothing.
pg_upgrade from master, on a 24-thread laptop in light desktop use:
master, 0001 and the index from David's last message, 3 rounds in
rotated order, then 3 more master runs. A run counts only if the 1-min
load average stayed below 5 (sampled every 30 s; for the last three
runs, read at the end of each run). Three master runs overlapped load
spikes of 12 to 56 and are listed but not counted:
build started restore total load
master 11-03 18:43 1443.5s 1481.3s 56.54 disturbed, not
counted
0001 11-03 19:08 94.2s 116.9s 3.36
David's patch 11-03 19:11 85.5s 118.3s 2.51
0001 11-03 19:13 93.6s 115.7s 2.45
David's patch 11-03 19:15 86.5s 122.1s 3.12
master 11-03 19:17 1085.6s 1120.6s 3.65
David's patch 11-03 19:36 85.9s 119.6s 3.04
master 11-03 19:39 1411.5s 1447.0s 55.23 disturbed, not
counted
0001 11-03 20:04 79.0s 99.7s 1.90
master 11-03 21:21 983.0s 1015.0s 1.27
master 11-03 21:38 1462.5s 1501.7s 12.12 disturbed, not
counted
master 11-03 22:05 1093.3s 1128.8s 3.18
median of the quiet runs:
master n=3 restore 1085.6s total 1120.6s (total range
1015.0-1128.8)
0001 n=3 restore 93.6s total 115.7s (total range 99.7-116.9)
David's patch n=3 restore 85.9s total 119.6s (total range
118.3-122.1)
master / 0001: 11.6x the restore, 9.7x the whole pg_upgrade
The same schema with a plain pg_restore --schema-only into a fresh
cluster, instructions of the whole server:
master rc=0 pg_constraint rows 957,311 server instructions 9.937T
0001 rc=0 pg_constraint rows 957,311 server instructions 0.667T
2. The three lookups, and the writes, by size of pg_constraint
--------------------------------------------------------------
0001 against master, 50 times each operation; nn = David's mix (90% NOT
NULL, 3% FK), fk = almost only FKs. attach = CREATE + ATTACH PARTITION
+ DETACH + DROP of a partition of a referenced table; partof = CREATE
... PARTITION OF + DETACH + DROP; detachc = DETACH ... CONCURRENTLY;
unlogged = SET UNLOGGED and LOGGED of a referenced table; fkadd = ADD +
DROP of a FOREIGN KEY; dropfk = CREATE + DROP of a table with 10 FKs.
mix rows attach partof detachc unlogged fkadd dropfk
nn 205 -2.0% -2.2% -2.4% -1.6% +0.4% +0.4%
fk 10,405 -50.1% -51.8% -49.9% -39.1% +0.4% +0.5%
nn 10,211 -48.6% -50.1% -48.2% -37.4% +0.4% +0.4%
fk 102,205 -90.5% -91.0% -90.4% -85.8% +0.4% +0.4%
nn 100,271 -90.0% -90.6% -89.9% -85.1% +0.4% +0.1%
nn 1,000,871 -99.0% -99.1% -99.0% -98.5% +0.5% +0.5%
100,000 NOT NULL (1,000 tables x 100 columns), 3 runs each, min-max;
churn = 5 x (drop all 1,000 tables, create them again):
instructions master 19.91G-19.91G 0001 20.80G-20.80G +4.5%
WAL bytes master 121.68M-121.68M 0001 128.17M-128.17M +5.3%
sizes pg_constraint 17.47M, confrelid index 0.66M
churn heap 17.47M -> 104.62M, index 0.66M -> 3.88M
On David's 957,311 rows (924,688 with confrelid = 0) a btree on
confrelid is 6.65 MB, 21.55 MB without deduplication; pg_constraint
itself is 184 MB.
3. heap_truncate_find_FKs() is not changed
------------------------------------------
TRUNCATE and ON COMMIT DELETE ROWS keep their seqscan; on a fresh
database (194 rows) 0001 reads the same rows as master and the same
instructions, within noise:
master 0001 rows m/0001, idx
ON COMMIT, N=1, per commit 0.14M 0.14M +0.1% 19400/19400, 0
ON COMMIT, N=100, per commit 3.48M 3.48M -0.0% 19400/19400, 0
ON COMMIT, N=1000, per commit 33.91M 33.91M +0.0% 19400/19400, 0
TRUNCATE N=1, per stmt 0.96M 0.96M +0.2% 19400/19400, 0
TRUNCATE N=10, per stmt 3.92M 3.92M +0.1% 19400/19400, 0
TRUNCATE N=100, per stmt 25.54M 25.51M -0.1% 19400/19400, 0
TRUNCATE N=1000, per stmt 262.16M 263.06M +0.3% 1940/1940, 0
CASCADE chain 10 20.53M 20.63M +0.5% 6690/6690, 0
CASCADE chain 100 83.91M 83.95M +0.0% 147900/147900, 0
CASCADE chain 1000 4.07G 4.07G +0.0% 9579000/9579000, 0
4. Correctness, in short
------------------------
- make check 239/239, cassert, 0 warnings; meson + clang with -Dwerror
and gcc with -Werror clean; headerscheck and cpluspluscheck add no
lines; pgindent clean.
- The stats.sql check reads this backend's counter inside a transaction.
On a build with CloneFkReferenced() back to a heap scan, and another
session attaching and detaching an indexed partition in a loop, the
old check (global idx_scan) said "index used" in 34 of 50 runs, the new
one in 0 of 50.
- Mutants: each lookup back to a heap scan, the key on the wrong column,
the wrong index, the index on another column: all fail make check.
- The whole regression suite with ignore_system_indexes = on, one test
at a time: master and 0001 fail the same tests with the same diffs,
except stats.sql (its index check) and toast OIDs in two warnings.
- isolation: ATTACH, DETACH CONCURRENTLY and TRUNCATE CASCADE against a
concurrent ADD FOREIGN KEY, 6 permutations: same output as master.
- pg_upgrade from 14, 17, 18.6, 19beta3 and master (David's schema at
1/10, plus FKs to partitioned tables and a cycle): pg_dump
--schema-only identical, amcheck clean on all 6 pg_constraint indexes;
from 18 on, the user constraints are identical too (from 14 and 17 the
not-null rows are new, and the clones of FKs onto referenced
partitions get master's names).
#!/usr/bin/env bash
# David Rowley's pg_upgrade case at full scale (his script, his defaults:
# 323 partitioned tables x 100 partitions, 3,970 tables x 200 NOT NULL
# columns), upgraded from master to each build.
#
# setup build the old cluster once, with master's binaries
# upgrade <build> <run> pg_upgrade -b master -B <build>; every output line
# gets a timestamp, like "| ts" in David's mail
#
# OLD_BIN=<master bindir> BINS="master=<dir> v10=<dir> ..." WORK=<dir>
# REPRO_SQL=<path to pg_upgrade_catalog_repro.sql> bash david-repro.sh.txt ...
#
# With VACLOOP=1, the run also does what David tried as a workaround: VACUUM
# (FREEZE, SKIP_DATABASE_STATS) pg_constraint in the new cluster every 3 s
# while pg_upgrade restores the schema.
#
# The restore time is the one David quoted: from the line "Restoring global
# objects in the new cluster ... ok" to the line "Restoring database schemas
# in the new cluster ... ok". pg_upgrade runs with --no-sync, so the fsync of
# the new cluster at the end does not enter the total either.
set -euo pipefail
: "${WORK:?}" "${OLD_BIN:?}" "${BINS:?}"
PORT=${PORT:-61540}
mkdir -p "$WORK"
bin() { local kv; for kv in $BINS; do [ "${kv%%=*}" = "$1" ] && { echo
"${kv#*=}"; return; }; done; exit 2; }
stamp() { perl -MTime::HiRes=time -MPOSIX=strftime -ne \
'BEGIN{$|=1} $t=time; printf "[%s.%03d] %s", strftime("%H:%M:%S", localtime
$t), ($t-int $t)*1000, $_'; }
case ${1:?stage} in
setup)
: "${REPRO_SQL:?}"
rm -rf "$WORK/old"
"$OLD_BIN/initdb" -D "$WORK/old" -U postgres -A trust >/dev/null
cat >>"$WORK/old/postgresql.conf" <<EOF
max_locks_per_transaction = 4096
fsync = off
synchronous_commit = off
autovacuum = off
listen_addresses = ''
EOF
"$OLD_BIN/pg_ctl" -D "$WORK/old" -l "$WORK/old.log" -w -o "-p $PORT -k $WORK"
start >/dev/null
"$OLD_BIN/createdb" -h "$WORK" -p $PORT -U postgres test
tr -d '\r' <"$REPRO_SQL" | "$OLD_BIN/psql" -h "$WORK" -p $PORT -U postgres -d
test -X -q \
-v ON_ERROR_STOP=1 -f - >"$WORK/setup.out" 2>&1
q() { "$OLD_BIN/psql" -h "$WORK" -p $PORT -U postgres -d test -X -qtA -F' '
-c "$1"; }
echo "old cluster: $("$OLD_BIN/postgres" --version)"
echo "pg_constraint by contype: $(q "SELECT coalesce(contype::text, 'all'),
count(*) FROM pg_constraint
GROUP BY ROLLUP (contype) ORDER BY 2 DESC" | tr '\n' ' ')"
echo "partitions: $(q "SELECT count(*) FROM pg_class WHERE relpartbound IS
NOT NULL")" \
"partitioned tables: $(q "SELECT count(*) FROM pg_class WHERE relkind =
'p'")"
"$OLD_BIN/pg_ctl" -D "$WORK/old" -m fast -w stop >/dev/null
;;
upgrade)
S=${2:?build}; R=${3:?run}; B=$(bin "$S"); D=$WORK/new; U=$WORK/up-$S-$R
rm -rf "$D" "$U"; mkdir -p "$U"
"$B/initdb" -D "$D" -U postgres -A trust >/dev/null
echo "max_locks_per_transaction = 4096" >>"$D/postgresql.conf"
cd "$U"
# RSS of the servers pg_upgrade starts (old and new cluster) and their
children only,
# not of other clusters on the machine
( while sleep 5; do
pids=$(for f in "$WORK/old/postmaster.pid" "$D/postmaster.pid"; do
if [ -f "$f" ]; then p=$(head -1 "$f"); echo "$p"; ps --ppid
"$p" -o pid= || :; fi; done)
if [ -n "$pids" ]; then ps -o rss= -p "$(echo $pids | tr ' ' ',')" | awk
'{s+=$1} END{print s+0}' || :; fi
done ) >"$U/rss.log" &
rp=$!
vp=""
if [ -n "${VACLOOP:-}" ]; then
( while sleep 3; do
"$B/psql" -h "$U" -p $((PORT + 1)) -U postgres -d test -X -q \
-c "VACUUM (FREEZE, SKIP_DATABASE_STATS) pg_constraint"
>>"$U/vacloop.log" 2>&1 &&
echo "VACUUM done $(date +%T)" >>"$U/vacloop.log" || :
done ) &
vp=$!
fi
t0=$(date +%s.%N)
"$B/pg_upgrade" -b "$OLD_BIN" -B "$B" -d "$WORK/old" -D "$D" -U postgres \
--no-sync -s "$U" -p $PORT -P $((PORT + 1)) 2>&1 | stamp
>"$U/pg_upgrade.out" || true
t1=$(date +%s.%N)
kill $rp $vp 2>/dev/null || true
grep -q 'Upgrade Complete' "$U/pg_upgrade.out" || { tail -20
"$U/pg_upgrade.out"; exit 1; }
secs() { sed -n "s/^\[\([0-9:.]*\)\] $1.*/\1/p" "$U/pg_upgrade.out" |
awk -F: '{print $1*3600 + $2*60 + $3}'; }
g=$(secs 'Restoring global objects in the new cluster'); s=$(secs 'Restoring
database schemas in the new cluster')
restore=$(awk -v a="$g" -v b="$s" 'BEGIN{d=b-a; if (d<0) d+=86400; printf
"%.1f", d}')
total=$(awk -v a="$t0" -v b="$t1" 'BEGIN{printf "%.1f", b-a}')
"$B/pg_ctl" -D "$D" -l "$U/server.log" -w -o "-p $PORT -k $U -c
listen_addresses=''" start >/dev/null
q() { "$B/psql" -h "$U" -p $PORT -U postgres -d test -X -qtA -F' ' -c "$1"; }
n=$(q "SELECT count(*) FROM pg_constraint")
sz=$(q "SELECT pg_relation_size('pg_constraint') || ' ' ||
coalesce((SELECT pg_relation_size(indexrelid)::text FROM pg_index
WHERE indrelid = 'pg_constraint'::regclass
AND indexrelid::regclass::text =
'pg_constraint_confrelid_index'), '-')")
"$B/pg_ctl" -D "$D" -m fast -w stop >/dev/null
set -- $sz
[ -n "${VACLOOP:-}" ] && S="$S+vacfreeze($(grep -c '^VACUUM done'
"$U/vacloop.log")vacuums)"
echo "upgrade $S run=$R restore_schemas_s=$restore pg_upgrade_total_s=$total"
\
"pg_constraint_rows=$n pg_constraint_bytes=$1 confrelid_index_bytes=$2" \
"peak_postgres_rss_kb=$(sort -n "$U/rss.log" | tail -1)"
;;
*) echo "stage?"; exit 2;;
esac
#!/usr/bin/env bash
# Counts, not times, for the operations that CF 7365 touches or that pay for
# the new index (section 5 of our notes). Single-user backend under perf
# stat (user-space instructions), as in fk-lookup-counts.sh.txt.
#
# BINS="master=<bindir> v10=<bindir>" CATS="master=master v10=idx"
WORK=<dir>
#
# cat-setup <build> <mix> <K> catalog with about K pg_constraint rows:
# mix nn = 90% NOT NULL, 3% FK (David's mix)
# mix fk = almost only FKs
# plus the objects the operations below use
# ops <build> <mix> <K> <run> each operation, 50 times, in that catalog
# write <build> <run> cost of creating 100,000 NOT NULL
# constraints (1,000 tables x 100 columns):
# instructions, WAL bytes, sizes after
# churn <build> create and drop those tables 5 times:
# index size before/after, then VACUUM
set -euo pipefail
: "${WORK:?}" "${BINS:?}" "${CATS:?}"
mkdir -p "$WORK"
lookup() { local k=$1 kv; shift; for kv in $*; do [ "${kv%%=*}" = "$k" ] && {
echo "${kv#*=}"; return; }; done
echo "unknown build $k" >&2; exit 2; }
bin() { lookup "$1" $BINS; }
cat_of() { lookup "$1" $CATS; }
instr() { awk -F, '$3 ~ /^instructions/ {print $1}' "$1"; }
val() { sed -n "s/^[[:space:]]*[0-9]*: $2 = \"\([^\"]*\)\".*/\1/p" "$1" | tail
-1; }
sb() { "$1/postgres" --single -j -D "$2" -c stats_fetch_consistency=none
postgres; }
newdb() {
rm -rf "$1"; "$2/initdb" -D "$1" -U postgres -A trust >/dev/null
printf 'fsync = off\nautovacuum = off\nmax_locks_per_transaction = 1024\n'
>>"$1/postgresql.conf"
}
# run <bin> <datadir> <out> <sql file>: instructions of the whole session
run() { perf stat -x, -e instructions:u -o "$3.perf" -- \
"$1/postgres" --single -j -D "$2" -c stats_fetch_consistency=none
postgres <"$4" >"$3.out" 2>&1
local ne; ne=$(grep -c 'ERROR:' "$3.out" || true)
[ "$ne" = 0 ] || { echo "errors=$ne"; grep -m3 'ERROR:' "$3.out"; exit
1; }
instr "$3.perf"; }
rep() { local n=$1 i; shift; for i in $(seq "$n"); do printf "$@"; printf
'\n\n'; done; }
size_sql="SELECT pg_relation_size('pg_constraint') AS heapb,
coalesce((SELECT pg_relation_size(indexrelid) FROM pg_index WHERE
indexrelid::regclass::text
= 'pg_constraint_confrelid_index'), 0) AS idxb,
(SELECT count(*) FROM pg_constraint) AS nrows;"
case ${1:?stage} in
cat-setup)
C=${2:?}; MIX=${3:?}; K=${4:?}; B=$(bin "$C"); D=$WORK/cat-$(cat_of
"$C")-$MIX-$K
newdb "$D" "$B"
{
# the objects of the operations
printf 'CREATE TABLE pk (id int PRIMARY KEY);\n\n'
printf 'CREATE TABLE pp (id int PRIMARY KEY) PARTITION BY RANGE (id);\n\n'
printf 'CREATE TABLE pp_0 PARTITION OF pp FOR VALUES FROM (0) TO (10);\n\n'
printf 'CREATE TABLE ppref (p int REFERENCES pp);\n\n'
printf 'CREATE TABLE ul (id int PRIMARY KEY);\n\nCREATE TABLE ulref (u int
REFERENCES ul);\n\n'
printf 'CREATE TABLE fkc (id int);\n\n'
# bulk: 100-constraint tables up to K rows
t=$(( K / 100 ))
for i in $(seq "$t"); do
if [ "$MIX" = nn ] && [ $(( i % 30 )) -ne 0 ]; then
printf 'CREATE TABLE b%s (%s);\n\n' "$i" "$(seq -s, -f 'c%g int NOT
NULL' 1 100 | sed 's/,$//')"
else
printf 'CREATE TABLE m%s (id int PRIMARY KEY);\n\n' "$i"
printf 'CREATE TABLE b%s (%s);\n\n' "$i" "$(seq -s, -f "c%g int
REFERENCES m$i" 1 100 | sed 's/,$//')"
fi
done; } | sb "$B" "$D" >/dev/null
printf '%s\n' "$size_sql" | sb "$B" "$D" >"$D.size" 2>/dev/null
echo "cat-setup $(cat_of "$C") mix=$MIX K=$K rows=$(val "$D.size" nrows)" \
"fk=$(sb "$B" "$D" <<<"SELECT count(*) AS f FROM pg_constraint WHERE
contype = 'f';" 2>/dev/null | sed -n 's/.*f = "\([0-9]*\)".*/\1/p')" \
"pg_constraint_bytes=$(val "$D.size" heapb) confrelid_index_bytes=$(val
"$D.size" idxb)"
;;
ops)
S=${2:?}; MIX=${3:?}; K=${4:?}; R=${5:?}; B=$(bin "$S"); D=$WORK/cat-$(cat_of
"$S")-$MIX-$K
O=$WORK/ops-$S-$MIX-$K-$R; mkdir -p "$O"
N=50
# attach: CREATE + ATTACH (CloneFkReferenced) + DETACH
(GetParentedForeignKeyRefs) + DROP
rep $N 'CREATE TABLE pp_x (id int NOT NULL);\n\nALTER TABLE pp ATTACH
PARTITION pp_x FOR VALUES FROM (10) TO (20);\n\nALTER TABLE pp DETACH PARTITION
pp_x;\n\nDROP TABLE pp_x;' >"$O/attach.sql"
rep $N 'CREATE TABLE pp_x (id int NOT NULL);\n\nDROP TABLE pp_x;'
>"$O/attach0.sql"
# partof: CREATE TABLE ... PARTITION OF a referenced table
(CloneFkReferenced),
# DETACH, DROP
rep $N 'CREATE TABLE pp_x PARTITION OF pp FOR VALUES FROM (10) TO
(20);\n\nALTER TABLE pp DETACH PARTITION pp_x;\n\nDROP TABLE pp_x;'
>"$O/partof.sql"
# detachc: ATTACH + DETACH CONCURRENTLY
rep $N 'CREATE TABLE pp_x (id int NOT NULL);\n\nALTER TABLE pp ATTACH
PARTITION pp_x FOR VALUES FROM (10) TO (20);\n\nALTER TABLE pp DETACH PARTITION
pp_x CONCURRENTLY;\n\nDROP TABLE pp_x;' >"$O/detachc.sql"
# unlogged: SET UNLOGGED (confrelid lookup) + SET LOGGED, on a referenced
table
# (ul is referenced by ulref, so both must be switched together)
rep $N 'ALTER TABLE ulref SET UNLOGGED;\n\nALTER TABLE ul SET
UNLOGGED;\n\nALTER TABLE ul SET LOGGED;\n\nALTER TABLE ulref SET LOGGED;'
>"$O/unlogged.sql"
# fkadd: ADD + DROP of a FOREIGN KEY constraint
rep $N 'ALTER TABLE fkc ADD CONSTRAINT fkc_f FOREIGN KEY (id) REFERENCES
pk;\n\nALTER TABLE fkc DROP CONSTRAINT fkc_f;' >"$O/fkadd.sql"
# dropfk: CREATE + DROP of a table with 10 FKs
rep $N 'CREATE TABLE dfk (%s);\n\nDROP TABLE dfk;' "$(seq -s, -f 'c%g int
REFERENCES pk' 1 10 | sed 's/,$//')" >"$O/dropfk.sql"
line="ops $S mix=$MIX K=$K run=$R x$N:"
for op in attach attach0 partof detachc unlogged fkadd dropfk; do
line+=" $op=$(run "$B" "$D" "$O/$op" "$O/$op.sql")"
done
echo "$line"
;;
write)
S=${2:?}; R=${3:?}; B=$(bin "$S"); D=$WORK/write-$S-$R; O=$WORK/wr-$S-$R
newdb "$D" "$B"
{ printf 'SELECT pg_current_wal_insert_lsn() AS l0;\n\n'
for i in $(seq 1000); do
printf 'CREATE TABLE nn%s (%s);\n\n' "$i" "$(seq -s, -f 'c%g int NOT
NULL' 1 100 | sed 's/,$//')"
done
printf 'SELECT pg_current_wal_insert_lsn() AS l1;\n\n%s\n\n' "$size_sql"; }
>"$O.sql"
{ for i in $(seq 1000); do
printf 'CREATE TABLE nn%s (%s);\n\n' "$i" "$(seq -s, -f 'c%g int' 1 100 |
sed 's/,$//')"
done; } >"$O-ctl.sql"
i1=$(run "$B" "$D" "$O" "$O.sql")
wal=$(sb "$B" "$D" <<<"SELECT pg_wal_lsn_diff('$(val "$O.out" l1)', '$(val
"$O.out" l0)')::bigint AS w;" 2>/dev/null | sed -n 's/.*w =
"\([0-9]*\)".*/\1/p')
newdb "$D" "$B"
i0=$(run "$B" "$D" "$O-ctl" "$O-ctl.sql")
echo "write $S run=$R 1000 tables x 100 NOT NULL: instructions=$i1
control(nullable)=$i0" \
"per constraint=$(( (i1 - i0) / 100000 )) wal_bytes=$wal
pg_constraint_rows=$(val "$O.out" nrows)" \
"pg_constraint_bytes=$(val "$O.out" heapb) confrelid_index_bytes=$(val
"$O.out" idxb)"
rm -rf "$D"
;;
churn)
S=${2:?}; B=$(bin "$S"); D=$WORK/churn-$S; O=$WORK/ch-$S
newdb "$D" "$B"
mk() { for i in $(seq 1000); do
printf 'CREATE TABLE nn%s (%s);\n\n' "$i" "$(seq -s, -f 'c%g int NOT
NULL' 1 100 | sed 's/,$//')"; done; }
dr() { for i in $(seq 1000); do printf 'DROP TABLE nn%s;\n\n' "$i"; done; }
mk | sb "$B" "$D" >/dev/null
printf '%s\n' "$size_sql" | sb "$B" "$D" >"$O.0" 2>/dev/null
for c in 1 2 3 4 5; do dr | sb "$B" "$D" >/dev/null; mk | sb "$B" "$D"
>/dev/null; done
printf '%s\n' "$size_sql" | sb "$B" "$D" >"$O.5" 2>/dev/null
printf 'VACUUM pg_constraint;\n\n%s\n' "$size_sql" | sb "$B" "$D" >"$O.v"
2>/dev/null
echo "churn $S 100,000 NOT NULL created, then 5 x (drop all, create all):" \
"start heap=$(val "$O.0" heapb) idx=$(val "$O.0" idxb);" \
"after heap=$(val "$O.5" heapb) idx=$(val "$O.5" idxb);" \
"after VACUUM heap=$(val "$O.v" heapb) idx=$(val "$O.v" idxb)"
;;
*) echo "stage?"; exit 2;;
esac
#!/usr/bin/env bash
# Counts, not times, for the pg_constraint lookups by confrelid (CF 7365).
# Same method as the v9 measurements: a single-user backend under perf stat
# (user-space instructions), plus the pg_constraint seqscan counters and the
# confrelid index counter. Any number of builds, by name:
#
# BINS="master=<bindir> probe=<bindir> any=<bindir>"
# CATS="master=master probe=idx any=idx" (builds that share a catalog)
# WORK=<scratch dir>
# SBOPTS="-c ignore_system_indexes=on" (optional: the no-index path)
#
# spread-setup <cat-build> <K> old table + FK, K FKs between, new table + FK
# spread <build> <K> <run> 100 x TRUNCATE old_pk, old_fk, new_pk, new_fk
# list-setup <cat-build> <N> N tables with a trigger (+ N without)
# list <build> <N> <run> TRUNCATE of the N tables in one statement
# cascade-setup/cascade <N> TRUNCATE c1 CASCADE down a chain of N tables
# wide-setup/wide <N> TRUNCATE w0 CASCADE, N tables reference w0
# part-setup/part <P> <Q> partitioned pp (P parts) referenced by Q
tables
# oncommit-setup <cat-build>
# oncommit <build> <N> <run> [<K>] N ON COMMIT DELETE ROWS temp tables
# with a trigger; with K, in the spread catalog of that K
# errpath-setup/errpath <N> TRUNCATE e1..eN that fails on eN
set -euo pipefail
: "${WORK:?}" "${BINS:?}" "${CATS:?}"
mkdir -p "$WORK"
lookup() { local k=$1 kv; shift; for kv in $*; do [ "${kv%%=*}" = "$k" ] && {
echo "${kv#*=}"; return; }; done
echo "unknown build $k" >&2; exit 2; }
bin() { lookup "$1" $BINS; }
cat_of() { lookup "$1" $CATS; }
instr() { awk -F, '$3 ~ /^instructions/ {print $1}' "$1"; }
val() { sed -n "s/^[[:space:]]*[0-9]*: $2 = \"\([^\"]*\)\".*/\1/p" "$1" | tail
-1; }
sb() { "$1/postgres" --single -j -D "$2" -c stats_fetch_consistency=none
${SBOPTS:-} postgres; }
newdb() {
rm -rf "$1"; "$2/initdb" -D "$1" -U postgres -A trust >/dev/null
printf 'fsync = off\nautovacuum = off\nmax_locks_per_transaction = 1024\n'
>>"$1/postgresql.conf"
}
COUNTERS="SELECT seq_scan AS SX, seq_tup_read AS TX,
coalesce((SELECT idx_tup_read FROM pg_stat_sys_indexes
WHERE indexrelname = 'pg_constraint_confrelid_index'), 0) AS IX
FROM pg_stat_sys_tables WHERE relname = 'pg_constraint';"
# measure <bin> <datadir> <out prefix> <reps> <statement> [<sql first>]:
# prints: instructions seq_scan seq_tup_read confrelid_idx_tup_read
# MEASURE_ERRORS=<n>: each statement is expected to fail, n errors in all
measure() {
local f=$3.sql c0 c1
c0=${COUNTERS//X/0}; c1=${COUNTERS//X/1}
{ [ -n "${6:-}" ] && printf '%s\n\n' "$6"
printf 'SELECT pg_stat_force_next_flush();\n\nSELECT 1;\n\n%s\n\n' "$c0"
for _ in $(seq "$4"); do printf '%s\n\n' "$5"; done
printf 'SELECT pg_stat_force_next_flush();\n\nSELECT 1;\n\n%s\n\n' "$c1"
} >"$f"
perf stat -x, -e instructions:u -o "$3.perf" -- \
"$1/postgres" --single -j -D "$2" -c stats_fetch_consistency=none
${SBOPTS:-} postgres \
<"$f" >"$3.out" 2>&1
local ne; ne=$(grep -c 'ERROR:' "$3.out" || true)
[ "$ne" = "${MEASURE_ERRORS:-0}" ] || { echo "errors=$ne"; grep -m3 'ERROR:'
"$3.out"; exit 1; }
echo "$(instr "$3.perf") $(( $(val "$3.out" s1) - $(val "$3.out" s0) ))" \
"$(( $(val "$3.out" t1) - $(val "$3.out" t0) )) $(( $(val "$3.out" i1) -
$(val "$3.out" i0) ))"
}
fkrows() { sb "$1" "$2" <<<"SELECT count(*) AS n, count(*) FILTER (WHERE
contype = 'f') AS f FROM pg_constraint;" 2>/dev/null |
sed -n 's/.* [nf] = "\([0-9]*\)".*/\1/p' | paste -sd/; }
case ${1:?stage} in
spread-setup)
C=${2:?}; K=${3:?}; B=$(bin "$C"); D=$WORK/spread-$(cat_of "$C")
if [ ! -d "$D" ]; then
newdb "$D" "$B"
sb "$B" "$D" >/dev/null <<'SQL'
CREATE TABLE old_pk (id int PRIMARY KEY);
CREATE TABLE old_fk (id int REFERENCES old_pk);
CREATE TABLE plain1 (id int);
CREATE TABLE plain2 (id int);
CREATE TABLE plain3 (id int);
CREATE TABLE plain4 (id int);
SQL
fi
have=$(sb "$B" "$D" <<<"SELECT count(*) AS n FROM pg_class WHERE relkind =
'r' AND relname LIKE 'mid\_%';" 2>/dev/null |
sed -n 's/.*n = "\([0-9]*\)".*/\1/p')
# one referenced table per 100 foreign keys; each gets 100 FKs from its own
table
for j in $(seq $((have + 1)) $((K / 100))); do
printf 'CREATE TABLE mid_%s (id int PRIMARY KEY);\n\n' "$j"
printf 'CREATE TABLE ref_%s (%s);\n\n' "$j" \
"$(seq -s, -f "c%g int REFERENCES mid_$j" 1 100 | sed 's/,$//')"
done | sb "$B" "$D" >/dev/null
sb "$B" "$D" >/dev/null <<'SQL'
DROP TABLE IF EXISTS new_fk, new_pk;
CREATE TABLE new_pk (id int PRIMARY KEY);
CREATE TABLE new_fk (id int REFERENCES new_pk);
SQL
echo "spread-setup $(cat_of "$C") K=$K pg_constraint rows/fk=$(fkrows "$B"
"$D")"
;;
spread)
S=${2:?}; K=${3:?}; R=${4:?}; B=$(bin "$S"); D=$WORK/spread-$(cat_of "$S");
O=$WORK/sp-$S-$K-$R
set -- $(measure "$B" "$D" "$O-fk" 100 'TRUNCATE old_pk, old_fk, new_pk,
new_fk;') \
$(measure "$B" "$D" "$O-plain" 100 'TRUNCATE plain1, plain2, plain3,
plain4;')
echo "spread $S K=$K run=$R x100 TRUNCATE old+new: instructions=$1
seq_scan=$2" \
"seq_tup_read=$3 confrelid_idx_tup_read=$4; control: instructions=$5;" \
"lookup per TRUNCATE=$(( ($1 - $5) / 100 ))"
;;
list-setup)
C=${2:?}; N=${3:?}; B=$(bin "$C"); D=$WORK/list-$(cat_of "$C")-$N
newdb "$D" "$B"
{ printf 'CREATE FUNCTION f() RETURNS trigger LANGUAGE plpgsql AS $$BEGIN
RETURN NEW; END$$;\n\n'
for i in $(seq "$N"); do
printf 'CREATE TABLE t%s (id int);\n\nCREATE TRIGGER g BEFORE INSERT ON
t%s FOR EACH ROW EXECUTE FUNCTION f();\n\n' "$i" "$i"
printf 'CREATE TABLE p%s (id int);\n\n' "$i"
done; } | sb "$B" "$D" >/dev/null
echo "list-setup $(cat_of "$C") N=$N pg_constraint rows/fk=$(fkrows "$B"
"$D")"
;;
list)
S=${2:?}; N=${3:?}; R=${4:?}; B=$(bin "$S"); D=$WORK/list-$(cat_of "$S")-$N;
O=$WORK/li-$S-$N-$R
reps=100; [ "$N" -ge 1000 ] && reps=10
set -- $(measure "$B" "$D" "$O-trg" $reps "TRUNCATE $(seq -s, -f 't%g' 1 "$N"
| sed 's/,$//');") \
$(measure "$B" "$D" "$O-plain" $reps "TRUNCATE $(seq -s, -f 'p%g' 1
"$N" | sed 's/,$//');")
echo "list $S N=$N run=$R x$reps TRUNCATE: instructions=$1 seq_tup_read=$3" \
"confrelid_idx_tup_read=$4; control: instructions=$5;" \
"per TRUNCATE: total=$(( $1 / reps )) lookup=$(( ($1 - $5) / reps ))"
;;
cascade-setup)
C=${2:?}; N=${3:?}; B=$(bin "$C"); D=$WORK/cascade-$(cat_of "$C")-$N
newdb "$D" "$B"
{ printf 'CREATE TABLE c1 (id int PRIMARY KEY);\n\n'
for i in $(seq 2 "$N"); do
printf 'CREATE TABLE c%s (id int PRIMARY KEY, p int REFERENCES c%s);\n\n'
"$i" $((i - 1))
done; } | sb "$B" "$D" >/dev/null
echo "cascade-setup $(cat_of "$C") N=$N pg_constraint rows/fk=$(fkrows "$B"
"$D")"
;;
cascade)
S=${2:?}; N=${3:?}; R=${4:?}; B=$(bin "$S"); D=$WORK/cascade-$(cat_of
"$S")-$N; O=$WORK/ca-$S-$N-$R
set -- $(measure "$B" "$D" "$O" 3 'TRUNCATE c1 CASCADE;')
left=$({ printf 'INSERT INTO c1 VALUES (1);\n\n'
for i in $(seq 2 "$N"); do printf 'INSERT INTO c%s VALUES (1,
1);\n\n' "$i"; done
printf 'TRUNCATE c1 CASCADE;\n\nSELECT %s AS left_rows;\n\n' \
"$(seq -s+ -f '(SELECT count(*) FROM c%g)' 1 "$N")"; } |
sb "$B" "$D" 2>/dev/null | sed -n 's/.*left_rows =
"\([0-9]*\)".*/\1/p')
echo "cascade $S N=$N run=$R x3 TRUNCATE c1 CASCADE (chain of $N tables):" \
"instructions=$1 seq_scan=$2 seq_tup_read=$3 confrelid_idx_tup_read=$4;"
\
"per TRUNCATE=$(( $1 / 3 )); rows left after one more=$left"
;;
wide-setup)
C=${2:?}; N=${3:?}; B=$(bin "$C"); D=$WORK/wide-$(cat_of "$C")-$N
newdb "$D" "$B"
{ printf 'CREATE TABLE w0 (id int PRIMARY KEY);\n\n'
for i in $(seq "$N"); do
printf 'CREATE TABLE w%s (id int PRIMARY KEY, p int REFERENCES w0);\n\n'
"$i"
done; } | sb "$B" "$D" >/dev/null
echo "wide-setup $(cat_of "$C") N=$N pg_constraint rows/fk=$(fkrows "$B"
"$D")"
;;
wide)
S=${2:?}; N=${3:?}; R=${4:?}; B=$(bin "$S"); D=$WORK/wide-$(cat_of "$S")-$N;
O=$WORK/wi-$S-$N-$R
set -- $(measure "$B" "$D" "$O" 3 'TRUNCATE w0 CASCADE;')
left=$({ printf 'INSERT INTO w0 VALUES (1);\n\n'
for i in $(seq "$N"); do printf 'INSERT INTO w%s VALUES (1, 1);\n\n'
"$i"; done
printf 'TRUNCATE w0 CASCADE;\n\nSELECT %s AS left_rows;\n\n' \
"$(seq -s+ -f '(SELECT count(*) FROM w%g)' 0 "$N")"; } |
sb "$B" "$D" 2>/dev/null | sed -n 's/.*left_rows =
"\([0-9]*\)".*/\1/p')
echo "wide $S N=$N run=$R x3 TRUNCATE w0 CASCADE ($N tables reference w0):" \
"instructions=$1 seq_scan=$2 seq_tup_read=$3 confrelid_idx_tup_read=$4;"
\
"per TRUNCATE=$(( $1 / 3 )); rows left after one more=$left"
;;
part-setup)
# pp partitioned in P partitions, referenced by Q tables: every FK has a
# row per partition with conparentid set, which is what makes
# heap_truncate_find_FKs() climb to the parent and restart
C=${2:?}; P=${3:?}; Q=${4:?}; B=$(bin "$C"); D=$WORK/part-$(cat_of "$C")-$P-$Q
newdb "$D" "$B"
{ printf 'CREATE TABLE pp (id int PRIMARY KEY) PARTITION BY RANGE (id);\n\n'
for i in $(seq "$P"); do
printf 'CREATE TABLE pp_%s PARTITION OF pp FOR VALUES FROM (%s) TO
(%s);\n\n' \
"$i" $(( i * 10 )) $(( i * 10 + 10 ))
done
for j in $(seq "$Q"); do
printf 'CREATE TABLE pr_%s (id int, p int REFERENCES pp);\n\n' "$j"
done; } | sb "$B" "$D" >/dev/null
echo "part-setup $(cat_of "$C") P=$P Q=$Q pg_constraint rows/fk=$(fkrows "$B"
"$D")"
;;
part)
S=${2:?}; P=${3:?}; Q=${4:?}; R=${5:?}; B=$(bin "$S"); D=$WORK/part-$(cat_of
"$S")-$P-$Q
O=$WORK/pa-$S-$P-$Q-$R
set -- $(measure "$B" "$D" "$O-all" 3 'TRUNCATE pp CASCADE;') \
$(measure "$B" "$D" "$O-one" 3 'TRUNCATE pp_1 CASCADE;')
left=$({ printf 'INSERT INTO pp VALUES (10);\n\n'
for j in $(seq "$Q"); do printf 'INSERT INTO pr_%s VALUES (1,
10);\n\n' "$j"; done
printf 'TRUNCATE pp_1 CASCADE;\n\nSELECT %s AS left_rows;\n\n' \
"(SELECT count(*) FROM pp)+$(seq -s+ -f '(SELECT count(*) FROM
pr_%g)' 1 "$Q")"; } |
sb "$B" "$D" 2>/dev/null | sed -n 's/.*left_rows =
"\([0-9]*\)".*/\1/p')
echo "part $S P=$P Q=$Q run=$R x3 TRUNCATE pp CASCADE: instructions=$1
seq_scan=$2" \
"seq_tup_read=$3 confrelid_idx_tup_read=$4; x3 TRUNCATE pp_1 CASCADE:" \
"instructions=$5 seq_scan=$6 seq_tup_read=$7 confrelid_idx_tup_read=$8;"
\
"rows left after one more=$left"
;;
oncommit-setup)
C=${2:?}; B=$(bin "$C"); D=$WORK/oc-$(cat_of "$C")
newdb "$D" "$B"
sb "$B" "$D" >/dev/null <<'SQL'
CREATE FUNCTION f() RETURNS trigger LANGUAGE plpgsql AS $$BEGIN RETURN NEW;
END$$;
SQL
echo "oncommit-setup $(cat_of "$C") pg_constraint rows/fk=$(fkrows "$B" "$D")"
;;
oncommit)
# N ON COMMIT DELETE ROWS temp tables with a trigger, all truncated at
# every commit that touches temp tables; per commit = (100 - 10) commits
S=${2:?}; N=${3:?}; R=${4:?}; K=${5:-}; B=$(bin "$S")
if [ -n "$K" ]; then D=$WORK/spread-$(cat_of "$S"); else D=$WORK/oc-$(cat_of
"$S"); fi
O=$WORK/oc-$S-$N-$R-${K:-0}
mk=$( [ -n "$K" ] && printf 'CREATE OR REPLACE FUNCTION f() RETURNS trigger
LANGUAGE plpgsql AS $$BEGIN RETURN NEW; END$$;\n\n'
for i in $(seq "$N"); do
printf 'CREATE TEMP TABLE tt%s (a int) ON COMMIT DELETE ROWS;\n\n' "$i"
printf 'CREATE TRIGGER g BEFORE INSERT ON tt%s FOR EACH ROW EXECUTE
FUNCTION f();\n\n' "$i"
done)
set -- $(measure "$B" "$D" "$O-100" 100 'INSERT INTO tt1 VALUES (1);' "$mk") \
$(measure "$B" "$D" "$O-10" 10 'INSERT INTO tt1 VALUES (1);' "$mk")
echo "oncommit $S N=$N K=${K:-0} run=$R: x100 instructions=$1 seq_scan=$2
seq_tup_read=$3" \
"confrelid_idx_tup_read=$4; x10 instructions=$5;" \
"per commit=$(( ($1 - $5) / 90 ))"
;;
errpath-setup)
# e1..eN with a trigger each; eN referenced by ex from outside the list
C=${2:?}; N=${3:?}; B=$(bin "$C"); D=$WORK/err-$(cat_of "$C")-$N
newdb "$D" "$B"
{ printf 'CREATE FUNCTION f() RETURNS trigger LANGUAGE plpgsql AS $$BEGIN
RETURN NEW; END$$;\n\n'
for i in $(seq "$N"); do
printf 'CREATE TABLE e%s (id int PRIMARY KEY);\n\nCREATE TRIGGER g BEFORE
INSERT ON e%s FOR EACH ROW EXECUTE FUNCTION f();\n\n' "$i" "$i"
done
printf 'CREATE TABLE ex (id int REFERENCES e%s);\n\n' "$N"; } | sb "$B"
"$D" >/dev/null
echo "errpath-setup $(cat_of "$C") N=$N pg_constraint rows/fk=$(fkrows "$B"
"$D")"
;;
errpath)
# TRUNCATE e1..eN fails on eN, the last one heap_truncate_check_FKs() tries
S=${2:?}; N=${3:?}; R=${4:?}; B=$(bin "$S"); D=$WORK/err-$(cat_of "$S")-$N;
O=$WORK/er-$S-$N-$R
set -- $(MEASURE_ERRORS=10 measure "$B" "$D" "$O" 10 \
"TRUNCATE $(seq -s, -f 'e%g' 1 "$N" | sed 's/,$//');")
echo "errpath $S N=$N run=$R x10 failing TRUNCATE: instructions=$1
seq_scan=$2" \
"seq_tup_read=$3 confrelid_idx_tup_read=$4; per TRUNCATE=$(( $1 / 10 ))"
;;
*) echo "stage?"; exit 2;;
esac