agora inbox for pgsql-hackers@postgresql.org  
help / color / mirror / Atom feed
[PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
169+ messages / 1 participants
[nested] [flat]

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--





^ permalink  raw  reply  [nested|flat] 169+ messages in thread

* [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats.
@ 2026-08-30 14:21 Nathan Bossart <nathan@postgresql.org>
  0 siblings, 0 replies; 169+ messages in thread

From: Nathan Bossart @ 2026-08-30 14:21 UTC (permalink / raw)

Commit 4b5ba0c4ca taught the attribute statistics query to join
pg_stats on the new tableid column, and it dropped the redundant
filter clause on s.tablename at the same time, on the theory that
the clause was only compensating for the name-based lookup.  That
isn't what the clause was doing.  pg_stats is a security barrier
view, so the planner will not push a join clause down into it,
whereas the redundant clause is a restriction clause on a
leakproof operator, which it will push.  Presently, we scan all of
pg_statistic once per batch of 64 relations, which makes dumping
statistics quadratic in the number of relations.  With 8000
tables, pg_dump --statistics-only takes 27.6s instead of 1.5s, and
pg_upgrade pays that cost during downtime.

To fix, put the filter clause back, now on s.tableid, and correct
the comment that claimed the OIDs had made it unnecessary.  I've
checked that the resulting plan holds up as a generic plan, which
matters here because pg_dump prepares this query once and executes
it once per batch.

Oversight in commit 4b5ba0c4ca.

Discussion: https://postgr.es/m/CADkLM%3DcoCVy92QkVUUTLdo5eO2bMDtwMrzRn_8miAhX%2BuPaqXg%40mail.gmail.com
Backpatch-through: 19
---
 src/bin/pg_dump/pg_dump.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db14834e430..b4bb0ce7b22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11150,17 +11150,19 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * The results must be in the order of the relations supplied in the
 		 * parameters to ensure we remain in sync as we walk through the TOC.
 		 *
-		 * For versions before 19, the redundant filter clause on s.tablename
-		 * = ANY(...) seems sufficient to convince the planner to use
-		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
-		 * In newer versions, pg_stats returns the table OIDs, eliminating the
-		 * need for that hack.
+		 * pg_stats is a security barrier view, so the planner will not push
+		 * the join clause down into it, and we would scan all of pg_statistic
+		 * once per batch.  The redundant filter clause is a restriction
+		 * clause on a leakproof operator, which the planner is willing to
+		 * push down, and that gets us an index scan.  This may not work for
+		 * all versions.
 		 */
 		if (fout->remoteVersion >= 190000)
 			appendPQExpBufferStr(query,
 								 "FROM pg_catalog.pg_stats s "
 								 "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) "
 								 "ON s.tableid = u.tableid "
+								 "WHERE s.tableid = ANY($1) "
 								 "ORDER BY u.ord, s.attname, s.inherited");
 		else
 			appendPQExpBufferStr(query,
-- 
2.55.0


--H90LzND/eZZd006U--






^ permalink  raw  reply  [nested|flat] 169+ messages in thread


end of thread, other threads:[~2026-08-30 14:21 UTC | newest]

Thread overview: 169+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>
2026-08-30 14:21 [PATCH v1 1/1] pg_dump: Avoid full scans of pg_stats. Nathan Bossart <nathan@postgresql.org>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox