public inbox for [email protected]  
help / color / mirror / Atom feed
Initial COPY of Logical Replication is too slow
47+ messages / 8 participants
[nested] [flat]

* Initial COPY of Logical Replication is too slow
@ 2025-12-06 12:18 Marcos Pegoraro <[email protected]>
  2025-12-07 14:22 ` Re: Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  0 siblings, 2 replies; 47+ messages in thread

From: Marcos Pegoraro @ 2025-12-06 12:18 UTC (permalink / raw)
  To: PostgreSQL Hackers <[email protected]>

Subscriber needs to ask publisher about tables and fields to COPY and it
uses pg_get_publication_tables for that, and it is too slow when the number
of tables is high because on every table it's subscribed it has to run this
select.
We can get the same result with a join on pg_publication_rel.

regards
Marcos


Attachments:

  [application/octet-stream] V1-Initial COPY of Logical Replication.diff (2.6K, 3-V1-Initial%20COPY%20of%20Logical%20Replication.diff)
  download | inline diff:
From e2fdff4f13e05a5e911a2ab0ce5c386f795dace8 Mon Sep 17 00:00:00 2001
From: PegoraroF10 <[email protected]>
Date: Sat, 6 Dec 2025 08:54:05 -0300
Subject: [PATCH] Function pg_get_publication_tables is too slow, changed to
 pg_publication_rel and pg_publication

---
 src/backend/replication/logical/tablesync.c | 32 ++++++++++-----------
 1 file changed, 15 insertions(+), 17 deletions(-)

diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 6bb0cbeedad..1ba8261308c 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -799,16 +799,14 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		 */
 		resetStringInfo(&cmd);
 		appendStringInfo(&cmd,
-						 "SELECT DISTINCT"
-						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
-						 "   THEN NULL ELSE gpt.attrs END)"
-						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
-						 "  pg_class c"
-						 " WHERE gpt.relid = %u AND c.oid = gpt.relid"
-						 "   AND p.pubname IN ( %s )",
-						 lrel->remoteid,
-						 pub_names->data);
+                     	"SELECT CASE WHEN cardinality(r.prattrs) <> relnatts THEN "
+						"r.prattrs END FROM pg_class c "
+						"LEFT JOIN LATERAL (SELECT DISTINCT prattrs FROM "
+						"pg_publication_rel r INNER JOIN pg_publication p "
+						"ON p.oid = r.prpubid WHERE c.oid = r.prrelid AND "
+						"pubname in ( %s )) r ON TRUE WHERE c.oid = %u",
+						 pub_names->data,
+						 lrel->remoteid);
 
 		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
 							 lengthof(attrsRow), attrsRow);
@@ -983,13 +981,13 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		/* Check for row filters. */
 		resetStringInfo(&cmd);
 		appendStringInfo(&cmd,
-						 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
-						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt"
-						 " WHERE gpt.relid = %u"
-						 "   AND p.pubname IN ( %s )",
-						 lrel->remoteid,
-						 pub_names->data);
+                		"SELECT pg_get_expr(r.prqual, r.prrelid) FROM pg_class c "
+						"LEFT JOIN LATERAL (SELECT DISTINCT prqual, prrelid FROM "
+						"pg_publication_rel r INNER JOIN pg_publication p ON "
+						"p.oid = r.prpubid WHERE r.prrelid = c.oid AND "
+						"p.pubname IN ( %s )) r ON TRUE WHERE c.oid = %u",
+						 pub_names->data,
+						 lrel->remoteid);
 
 		res = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data, 1, qualRow);
 
-- 
2.51.0.windows.1



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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
@ 2025-12-07 14:22 ` Marcos Pegoraro <[email protected]>
  1 sibling, 0 replies; 47+ messages in thread

From: Marcos Pegoraro @ 2025-12-07 14:22 UTC (permalink / raw)
  To: PostgreSQL Hackers <[email protected]>

You can see how much time a subscriber will need to get all files which
were added with this.
Run first time and will create 10 thousand tables, publish them and measure
how much time to get all tables Ready on pg_subscription_rel.
Run again to add more 10 thousand tables and see that time will increase,
more tables and more time.

This is just to show that if you create a subscription with a high number
of tables it spends more time doing select on pg_get_publication_tables
than the time spent actually copying. My use case I have 50 thousand
tables, and it takes 5 seconds every time it needs to get next table to
copy.

--Create a empty publication
create publication my_pub;

--Run these 3 following anonymous blocks to create schemas, tables and add
them to publication.
--Need to have 3 blocks because I cannot create a table in a schema that is
not committed. And the same for a publication.
do $$ declare Schemas_Add integer = 100; Actual_Schema text; begin
  for Actual_Schema in select 'test_'||(select
to_char(coalesce(max(substring(nspname,'test_(\d+)')::integer),0)+g,'FM00000')
                                       from pg_namespace where nspname ~
'test_\d+') from generate_series(1,Schemas_Add) g loop
    execute format('create schema %s',Actual_Schema);
  end loop;
end;$$;

do $$ declare Tables_Add integer = 100; Actual_Schema text; begin
  for Actual_Schema in select nspname from pg_namespace where nspname  ~
'test_\d+' and
                      not exists(select from pg_class where relnamespace =
pg_namespace.oid) loop
    for j in 1..Tables_Add loop
      execute format('create table %s.test_%s as select
generate_series(1,random(0,10))::integer id;',
                     Actual_Schema,to_char(j,'FM00000'));
    end loop;
  end loop;
end;$$;

do $$ declare Schemas_To_Add text = (select string_agg(nspname,',') from
pg_namespace n where nspname ~ 'test_\d+' and
                              not exists(select from
pg_publication_namespace where pnnspid = n.oid)); begin
  execute format('alter publication my_pub add tables in schema
%s;',Schemas_To_Add);
end;$$;

--Then you can see what was generated and go to the subscriber side to
refresh the subscription and measure time spent to synchronize.
select * from pg_Namespace where nspname ~ 'test_\d+';
select pnnspid::regnamespace, * from pg_publication_namespace;
select oid::regclass, * from pg_Class where
relnamespace::regnamespace::text ~ 'test_\d+' and relkind = 'r';

--Later just clean what you do.
drop publication my_pub;

do $$ declare Schema_Drop text; begin
  for Schema_Drop in select nspname from pg_Namespace where nspname ~
'test_\d+' loop
    execute format ('drop schema %s cascade;',Schema_Drop);
  end loop;
end;$$;

regards
Marcos


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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
@ 2025-12-20 01:58 ` Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  1 sibling, 1 reply; 47+ messages in thread

From: Masahiko Sawada @ 2025-12-20 01:58 UTC (permalink / raw)
  To: Marcos Pegoraro <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>

Hi,

On Sat, Dec 6, 2025 at 4:19 AM Marcos Pegoraro <[email protected]> wrote:
>
> Subscriber needs to ask publisher about tables and fields to COPY and it uses pg_get_publication_tables for that, and it is too slow when the number of tables is high because on every table it's subscribed it has to run this select.

Yeah, if we pass a publication that a lot of tables belong to to
pg_get_publication_tables(), it could take a long time to return as it
needs to construct many entries.

> We can get the same result with a join on pg_publication_rel.

You changed the query not to use pg_get_publication_tables():

-                                                "SELECT DISTINCT"
-                                                "  (CASE WHEN
(array_length(gpt.attrs, 1) = c.relnatts)"
-                                                "   THEN NULL ELSE
gpt.attrs END)"
-                                                "  FROM pg_publication p,"
-                                                "  LATERAL
pg_get_publication_tables(p.pubname) gpt,"
-                                                "  pg_class c"
-                                                " WHERE gpt.relid =
%u AND c.oid = gpt.relid"
-                                                "   AND p.pubname IN ( %s )",
-                                                lrel->remoteid,
-                                                pub_names->data);
+                       "SELECT CASE WHEN cardinality(r.prattrs) <>
relnatts THEN "
+                                               "r.prattrs END FROM pg_class c "
+                                               "LEFT JOIN LATERAL
(SELECT DISTINCT prattrs FROM "
+                                               "pg_publication_rel r
INNER JOIN pg_publication p "
+                                               "ON p.oid = r.prpubid
WHERE c.oid = r.prrelid AND "
+                                               "pubname in ( %s )) r
ON TRUE WHERE c.oid = %u",
+                                                pub_names->data,
+                                                lrel->remoteid);

Simply replacing pg_get_publication_tables() with joining on
pg_publication_rel doesn't work since pg_get_publication_tables()
cares for several cases, for example where the specified columns are
generated columns and the specified table is a partitioned table etc.
Therefore the patch doesn't pass the regression tests.

I think it would make more sense to introduce a dedicated SQL function
that takes the reloid as well as the list of publications and returns
the relation's the column list and row filter expression while
filtering unnecessary rows inside the function.

Regards,

--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com





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

* RE: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-03-03 10:22   ` Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  0 siblings, 1 reply; 47+ messages in thread

From: Zhijie Hou (Fujitsu) @ 2026-03-03 10:22 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; Marcos Pegoraro <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>

On Saturday, February 28, 2026 7:48 AM Masahiko Sawada <[email protected]> wrote:
> To: Marcos Pegoraro <[email protected]>
> Cc: PostgreSQL Hackers <[email protected]>
> Subject: Re: Initial COPY of Logical Replication is too slow
> 
> Another variant of this approach is to extend
> pg_get_publication_table() so that it can accept a relid to get the publication
> information of the specific table. I've attached the patch for this idea. I'm
> going to add regression test cases.
> 
> pg_get_publication_table() is a VARIACID array function so the patch changes
> its signature to {text[] [, oid]}, breaking the tool compatibility. Given this
> function is mostly an internal-use function (we don't have the documentation
> for it), it would probably be okay with it. I find it's clearer than the other
> approach of introducing pg_get_publication_table_info(). Feedback is very
> welcome.

Thanks for updating the patch.

I have few comments for the function change:

1.

If we change the function signature, will it affect use cases where the
publisher version is newer and the subscriber version is older ? E.g., when
publisher is passing text style publication name to pg_get_publication_tables().

Besides, for upgrade scenarios where the publisher version is older, I think
the patch needs to add version checks to avoid passing the relid to
pg_get_publication_tables.

2.

In the following example, I expected it to output a table with valid row
filter, but it returns 0 row after applying the patch.

CREATE TABLE measurements (
    city_id         int not null,
    logdate         date not null,
    peaktemp        int,
    unitsales       int
) PARTITION BY RANGE (logdate);

-- Create partitions
CREATE TABLE measurements_2023_q1 PARTITION OF measurements
    FOR VALUES FROM ('2023-01-01') TO ('2023-04-01');

CREATE PUBLICATION pub FOR TABLE measurements_2023_q1 WHERE (city_id = 2);

select pg_get_publication_tables(ARRAY['pub2'], 'measurements_2023_q1'::regclass);
 pg_get_publication_tables
---------------------------
(0 rows)

Best Regards,
Hou zj


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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
@ 2026-03-09 22:09     ` Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  0 siblings, 1 reply; 47+ messages in thread

From: Masahiko Sawada @ 2026-03-09 22:09 UTC (permalink / raw)
  To: Zhijie Hou (Fujitsu) <[email protected]>; +Cc: Marcos Pegoraro <[email protected]>; PostgreSQL Hackers <[email protected]>

On Tue, Mar 3, 2026 at 2:22 AM Zhijie Hou (Fujitsu)
<[email protected]> wrote:
>
> On Saturday, February 28, 2026 7:48 AM Masahiko Sawada <[email protected]> wrote:
> > To: Marcos Pegoraro <[email protected]>
> > Cc: PostgreSQL Hackers <[email protected]>
> > Subject: Re: Initial COPY of Logical Replication is too slow
> >
> > Another variant of this approach is to extend
> > pg_get_publication_table() so that it can accept a relid to get the publication
> > information of the specific table. I've attached the patch for this idea. I'm
> > going to add regression test cases.
> >
> > pg_get_publication_table() is a VARIACID array function so the patch changes
> > its signature to {text[] [, oid]}, breaking the tool compatibility. Given this
> > function is mostly an internal-use function (we don't have the documentation
> > for it), it would probably be okay with it. I find it's clearer than the other
> > approach of introducing pg_get_publication_table_info(). Feedback is very
> > welcome.
>
> Thanks for updating the patch.
>
> I have few comments for the function change:
>
> 1.
>
> If we change the function signature, will it affect use cases where the
> publisher version is newer and the subscriber version is older ? E.g., when
> publisher is passing text style publication name to pg_get_publication_tables().

Good point.

I noticed that changing the function signature of
pg_get_publication_tables() breaks logical replication setups where
the subscriber is 18 or older. In the latest patch, I've switched the
approach back to the pg_get_publication_table_info() idea.

>
> 2.
>
> In the following example, I expected it to output a table with valid row
> filter, but it returns 0 row after applying the patch.
>
> CREATE TABLE measurements (
>     city_id         int not null,
>     logdate         date not null,
>     peaktemp        int,
>     unitsales       int
> ) PARTITION BY RANGE (logdate);
>
> -- Create partitions
> CREATE TABLE measurements_2023_q1 PARTITION OF measurements
>     FOR VALUES FROM ('2023-01-01') TO ('2023-04-01');
>
> CREATE PUBLICATION pub FOR TABLE measurements_2023_q1 WHERE (city_id = 2);
>
> select pg_get_publication_tables(ARRAY['pub2'], 'measurements_2023_q1'::regclass);
>  pg_get_publication_tables
> ---------------------------
> (0 rows)

Thank you for testing the patch. I've fixed it and added regression
tests in the latest patch.

I've attached the updated patch.


Regards,

--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com


Attachments:

  [application/x-patch] v2-0001-Avoid-full-table-scans-when-getting-publication-t.patch (26.5K, 2-v2-0001-Avoid-full-table-scans-when-getting-publication-t.patch)
  download | inline diff:
From 7ffa55e77413743b63092a824c1a70f74dd122f0 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
Date: Fri, 27 Feb 2026 15:42:38 -0800
Subject: [PATCH v2] Avoid full table scans when getting publication table
 information by tablesync workers.

Reported-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zhijie Hou (Fujitsu) <[email protected]>
Reviewed-by: Matheus Alcantara <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Discussion: https://postgr.es/m/CAB-JLwbBFNuASyEnZWP0Tck9uNkthBZqi6WoXNevUT6+mV8XmA@mail.gmail.com
---
 src/backend/catalog/pg_publication.c        | 382 +++++++++++++++-----
 src/backend/replication/logical/tablesync.c |  68 +++-
 src/include/catalog/pg_proc.dat             |   9 +
 src/test/regress/expected/publication.out   | 129 +++++++
 src/test/regress/sql/publication.sql        |  67 ++++
 5 files changed, 543 insertions(+), 112 deletions(-)

diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index aadc7c202c6..5213f1d0a23 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -1207,6 +1207,240 @@ GetPublicationByName(const char *pubname, bool missing_ok)
 	return OidIsValid(oid) ? GetPublication(oid) : NULL;
 }
 
+/*
+ * Returns true if the table of the given relid is published for the specified
+ * publication.
+ *
+ * This function evaluates the effective published OID based on the
+ * publish_via_partition_root setting, rather than just checking catalog entries
+ * (e.g., pg_publication_rel). For instance, when publish_via_partition_root is
+ * false, it returns false for a parent partitioned table and true for its leaf
+ * partitions, even if the parent is the one explicitly added to the publication.
+ *
+ * For performance reasons, this function avoids the overhead of constructing
+ * the complete list of published tables during the evaluation. It can execute
+ * quickly even when the publication contains a large number of relations.
+ */
+static bool
+is_table_publishable_in_publication(Oid relid, Publication *pub)
+{
+	if (pub->pubviaroot)
+	{
+		if (pub->alltables)
+		{
+			/*
+			 * ALL TABLE publications with pubviaroot=true include only tables
+			 * that are either regular tables or top-most partitioned tables.
+			 */
+			if (get_rel_relispartition(relid))
+				return false;
+
+			/*
+			 * Check if the table is specified in the EXCEPT clause in the
+			 * publication. ALL TABLE publications have pg_publication_rel
+			 * entries only for EXCEPT'ed tables, so it's sufficient to check
+			 * the existence of its entry.
+			 */
+			return !SearchSysCacheExists2(PUBLICATIONRELMAP,
+										  ObjectIdGetDatum(relid),
+										  ObjectIdGetDatum(pub->oid));
+		}
+
+		/*
+		 * Check if its corresponding entry exists either in
+		 * pg_publication_rel or pg_publication_namespace.
+		 */
+		return (SearchSysCacheExists2(PUBLICATIONRELMAP,
+									  ObjectIdGetDatum(relid),
+									  ObjectIdGetDatum(pub->oid)) ||
+				SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP,
+									  ObjectIdGetDatum(get_rel_namespace(relid)),
+									  ObjectIdGetDatum(pub->oid)));
+	}
+
+	/*
+	 * For non-pubviaroot publications, partitioned table's OID can never be a
+	 * published OID.
+	 */
+	if (get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE)
+		return false;
+
+	if (pub->alltables)
+	{
+		Oid			target_relid = relid;
+
+		if (get_rel_relispartition(relid))
+		{
+			List	   *ancestors = get_partition_ancestors(relid);
+
+			/*
+			 * Only the top-most ancestor can appear in the EXCEPT clause.
+			 * Therefore, for a partition, exclusion must be evaluated at the
+			 * top-most ancestor.
+			 */
+			target_relid = llast_oid(ancestors);
+
+			list_free(ancestors);
+		}
+
+		/*
+		 * The table is published unless it's specified in the EXCEPT clause.
+		 * ALL TABLE publications have pg_publication_rel entries only for
+		 * EXCEPT'ed tables, so it's sufficient to check the existence of its
+		 * entry.
+		 */
+		return !SearchSysCacheExists2(PUBLICATIONRELMAP,
+									  ObjectIdGetDatum(target_relid),
+									  ObjectIdGetDatum(pub->oid));
+	}
+
+	if (get_rel_relispartition(relid))
+	{
+		List	   *ancestors = get_partition_ancestors(relid);
+		Oid			topmost = GetTopMostAncestorInPublication(pub->oid, ancestors,
+															  NULL);
+
+		list_free(ancestors);
+
+		/* This table is published if its ancestor is published */
+		if (OidIsValid(topmost))
+			return true;
+
+		/* The partition itself might be published, so check below */
+	}
+
+	return (SearchSysCacheExists2(PUBLICATIONRELMAP,
+								  ObjectIdGetDatum(relid),
+								  ObjectIdGetDatum(pub->oid)) ||
+			SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP,
+								  ObjectIdGetDatum(get_rel_namespace(relid)),
+								  ObjectIdGetDatum(pub->oid)));
+}
+
+/*
+ * pg_get_publication_tables() and pg_get_publication_table_info() use
+ * the same record type.
+ */
+#define NUM_PUBLICATION_TABLES_ELEM 4
+
+/*
+ * Construct a tuple descriptor for both pg_get_publication_tales() and
+ * pg_get_publication_table_info() functions.
+ */
+static TupleDesc
+create_published_rel_tuple_desc(void)
+{
+	TupleDesc	tupdesc;
+
+	tupdesc = CreateTemplateTupleDesc(NUM_PUBLICATION_TABLES_ELEM);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "pubid",
+					   OIDOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "relid",
+					   OIDOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 3, "attrs",
+					   INT2VECTOROID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 4, "qual",
+					   PG_NODE_TREEOID, -1, 0);
+
+	return BlessTupleDesc(tupdesc);
+}
+
+/*
+ * Common routine for pg_get_publication_tables() and
+ * pg_get_publication_table_info() to construct the result tuple.
+ * tuple_desc should be the tuple description returned by
+ * create_published_rel_tuple_desc().
+ */
+static HeapTuple
+construct_published_rel_tuple(published_rel *table_info, TupleDesc tuple_desc)
+{
+	Publication *pub;
+	Oid			relid = table_info->relid;
+	Oid			schemaid = get_rel_namespace(relid);
+	HeapTuple	pubtuple = NULL;
+	Datum		values[NUM_PUBLICATION_TABLES_ELEM] = {0};
+	bool		nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
+
+	pub = GetPublication(table_info->pubid);
+
+	values[0] = ObjectIdGetDatum(pub->oid);
+	values[1] = ObjectIdGetDatum(relid);
+
+	/*
+	 * We don't consider row filters or column lists for FOR ALL TABLES or FOR
+	 * TABLES IN SCHEMA publications.
+	 */
+	if (!pub->alltables &&
+		!SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP,
+							   ObjectIdGetDatum(schemaid),
+							   ObjectIdGetDatum(pub->oid)))
+		pubtuple = SearchSysCacheCopy2(PUBLICATIONRELMAP,
+									   ObjectIdGetDatum(relid),
+									   ObjectIdGetDatum(pub->oid));
+
+	if (HeapTupleIsValid(pubtuple))
+	{
+		/* Lookup the column list attribute. */
+		values[2] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+									Anum_pg_publication_rel_prattrs,
+									&(nulls[2]));
+
+		/* Null indicates no filter. */
+		values[3] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+									Anum_pg_publication_rel_prqual,
+									&(nulls[3]));
+	}
+	else
+	{
+		nulls[2] = true;
+		nulls[3] = true;
+	}
+
+	/* Show all columns when the column list is not specified. */
+	if (nulls[2])
+	{
+		Relation	rel = table_open(relid, AccessShareLock);
+		int			nattnums = 0;
+		int16	   *attnums;
+		TupleDesc	desc = RelationGetDescr(rel);
+
+		attnums = palloc_array(int16, desc->natts);
+
+		for (int i = 0; i < desc->natts; i++)
+		{
+			Form_pg_attribute att = TupleDescAttr(desc, i);
+
+			if (att->attisdropped)
+				continue;
+
+			if (att->attgenerated)
+			{
+				/* We only support replication of STORED generated cols. */
+				if (att->attgenerated != ATTRIBUTE_GENERATED_STORED)
+					continue;
+
+				/*
+				 * User hasn't requested to replicate STORED generated cols.
+				 */
+				if (pub->pubgencols_type != PUBLISH_GENCOLS_STORED)
+					continue;
+			}
+
+			attnums[nattnums++] = att->attnum;
+		}
+
+		if (nattnums > 0)
+		{
+			values[2] = PointerGetDatum(buildint2vector(attnums, nattnums));
+			nulls[2] = false;
+		}
+
+		table_close(rel, AccessShareLock);
+	}
+
+	return heap_form_tuple(tuple_desc, values, nulls);
+}
+
 /*
  * Get information of the tables in the given publication array.
  *
@@ -1215,14 +1449,12 @@ GetPublicationByName(const char *pubname, bool missing_ok)
 Datum
 pg_get_publication_tables(PG_FUNCTION_ARGS)
 {
-#define NUM_PUBLICATION_TABLES_ELEM	4
 	FuncCallContext *funcctx;
 	List	   *table_infos = NIL;
 
 	/* stuff done only on the first call of the function */
 	if (SRF_IS_FIRSTCALL())
 	{
-		TupleDesc	tupdesc;
 		MemoryContext oldcontext;
 		ArrayType  *arr;
 		Datum	   *elems;
@@ -1311,18 +1543,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		if (viaroot)
 			filter_partitions(table_infos);
 
-		/* Construct a tuple descriptor for the result rows. */
-		tupdesc = CreateTemplateTupleDesc(NUM_PUBLICATION_TABLES_ELEM);
-		TupleDescInitEntry(tupdesc, (AttrNumber) 1, "pubid",
-						   OIDOID, -1, 0);
-		TupleDescInitEntry(tupdesc, (AttrNumber) 2, "relid",
-						   OIDOID, -1, 0);
-		TupleDescInitEntry(tupdesc, (AttrNumber) 3, "attrs",
-						   INT2VECTOROID, -1, 0);
-		TupleDescInitEntry(tupdesc, (AttrNumber) 4, "qual",
-						   PG_NODE_TREEOID, -1, 0);
-
-		funcctx->tuple_desc = BlessTupleDesc(tupdesc);
+		funcctx->tuple_desc = create_published_rel_tuple_desc();
 		funcctx->user_fctx = table_infos;
 
 		MemoryContextSwitchTo(oldcontext);
@@ -1334,99 +1555,74 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
 	if (funcctx->call_cntr < list_length(table_infos))
 	{
-		HeapTuple	pubtuple = NULL;
 		HeapTuple	rettuple;
-		Publication *pub;
 		published_rel *table_info = (published_rel *) list_nth(table_infos, funcctx->call_cntr);
-		Oid			relid = table_info->relid;
-		Oid			schemaid = get_rel_namespace(relid);
-		Datum		values[NUM_PUBLICATION_TABLES_ELEM] = {0};
-		bool		nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
-
-		/*
-		 * Form tuple with appropriate data.
-		 */
 
-		pub = GetPublication(table_info->pubid);
+		rettuple = construct_published_rel_tuple(table_info, funcctx->tuple_desc);
 
-		values[0] = ObjectIdGetDatum(pub->oid);
-		values[1] = ObjectIdGetDatum(relid);
+		SRF_RETURN_NEXT(funcctx, HeapTupleGetDatum(rettuple));
+	}
 
-		/*
-		 * We don't consider row filters or column lists for FOR ALL TABLES or
-		 * FOR TABLES IN SCHEMA publications.
-		 */
-		if (!pub->alltables &&
-			!SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP,
-								   ObjectIdGetDatum(schemaid),
-								   ObjectIdGetDatum(pub->oid)))
-			pubtuple = SearchSysCacheCopy2(PUBLICATIONRELMAP,
-										   ObjectIdGetDatum(relid),
-										   ObjectIdGetDatum(pub->oid));
-
-		if (HeapTupleIsValid(pubtuple))
-		{
-			/* Lookup the column list attribute. */
-			values[2] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
-										Anum_pg_publication_rel_prattrs,
-										&(nulls[2]));
-
-			/* Null indicates no filter. */
-			values[3] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
-										Anum_pg_publication_rel_prqual,
-										&(nulls[3]));
-		}
-		else
-		{
-			nulls[2] = true;
-			nulls[3] = true;
-		}
+	SRF_RETURN_DONE(funcctx);
+}
 
-		/* Show all columns when the column list is not specified. */
-		if (nulls[2])
-		{
-			Relation	rel = table_open(relid, AccessShareLock);
-			int			nattnums = 0;
-			int16	   *attnums;
-			TupleDesc	desc = RelationGetDescr(rel);
-			int			i;
+/*
+ * Similar to pg_get_publication_tables(), but retrieves publication
+ * information only for the specified table.
+ */
+Datum
+pg_get_publication_table_info(PG_FUNCTION_ARGS)
+{
+	FuncCallContext *funcctx;
+	published_rel *table_info = NULL;
 
-			attnums = palloc_array(int16, desc->natts);
+	if (SRF_IS_FIRSTCALL())
+	{
+		MemoryContext oldcontext;
+		Oid			relid;
+		Name		pubname;
+		Relation	rel;
+		Publication *pub;
+		published_rel *pubrel = NULL;
 
-			for (i = 0; i < desc->natts; i++)
-			{
-				Form_pg_attribute att = TupleDescAttr(desc, i);
+		/* create a function context for cross-call persistence */
+		funcctx = SRF_FIRSTCALL_INIT();
 
-				if (att->attisdropped)
-					continue;
+		/* switch to memory context appropriate for multiple function calls */
+		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
 
-				if (att->attgenerated)
-				{
-					/* We only support replication of STORED generated cols. */
-					if (att->attgenerated != ATTRIBUTE_GENERATED_STORED)
-						continue;
-
-					/*
-					 * User hasn't requested to replicate STORED generated
-					 * cols.
-					 */
-					if (pub->pubgencols_type != PUBLISH_GENCOLS_STORED)
-						continue;
-				}
-
-				attnums[nattnums++] = att->attnum;
-			}
+		relid = PG_GETARG_OID(0);
+		pubname = PG_GETARG_NAME(1);
 
-			if (nattnums > 0)
-			{
-				values[2] = PointerGetDatum(buildint2vector(attnums, nattnums));
-				nulls[2] = false;
-			}
+		rel = table_open(relid, AccessShareLock);
+		pub = GetPublicationByName(NameStr(*pubname), false);
 
-			table_close(rel, AccessShareLock);
+		if (is_table_publishable_in_publication(relid, pub))
+		{
+			pubrel = palloc_object(published_rel);
+			pubrel->relid = relid;
+			pubrel->pubid = pub->oid;
 		}
 
-		rettuple = heap_form_tuple(funcctx->tuple_desc, values, nulls);
+		table_close(rel, AccessShareLock);
+
+		/* Construct a tuple descriptor for the result rows. */
+		funcctx->tuple_desc = create_published_rel_tuple_desc();
+		funcctx->user_fctx = pubrel;
+
+		MemoryContextSwitchTo(oldcontext);
+	}
+
+	/* stuff done on every call of the function */
+	funcctx = SRF_PERCALL_SETUP();
+	table_info = (published_rel *) funcctx->user_fctx;
+
+	/* The function returns zero or one tuple */
+	if (table_info && funcctx->call_cntr == 0)
+	{
+		HeapTuple	rettuple;
+
+		rettuple = construct_published_rel_tuple(table_info, funcctx->tuple_desc);
 
 		SRF_RETURN_NEXT(funcctx, HeapTupleGetDatum(rettuple));
 	}
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index f49a4852ecb..ce7afd68533 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -798,17 +798,34 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		 * publications).
 		 */
 		resetStringInfo(&cmd);
-		appendStringInfo(&cmd,
-						 "SELECT DISTINCT"
-						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
-						 "   THEN NULL ELSE gpt.attrs END)"
-						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
-						 "  pg_class c"
-						 " WHERE gpt.relid = %u AND c.oid = gpt.relid"
-						 "   AND p.pubname IN ( %s )",
-						 lrel->remoteid,
-						 pub_names->data);
+
+		if (server_version >= 190000)
+		{
+			/* pg_get_publication_table_info() is available since vesion 19 */
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT"
+							 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
+							 "   THEN NULL ELSE gpt.attrs END)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_table_info(%u, p.pubname) gpt,"
+							 "  pg_class c"
+							 " WHERE c.oid = gpt.relid"
+							 "   AND p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
+		}
+		else
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT"
+							 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
+							 "   THEN NULL ELSE gpt.attrs END)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
+							 "  pg_class c"
+							 " WHERE gpt.relid = %u AND c.oid = gpt.relid"
+							 "   AND p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
 
 		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
 							 lengthof(attrsRow), attrsRow);
@@ -982,14 +999,27 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 
 		/* Check for row filters. */
 		resetStringInfo(&cmd);
-		appendStringInfo(&cmd,
-						 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
-						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt"
-						 " WHERE gpt.relid = %u"
-						 "   AND p.pubname IN ( %s )",
-						 lrel->remoteid,
-						 pub_names->data);
+
+		if (server_version >= 190000)
+		{
+			/* pg_get_publication_table_info() is available since version 19 */
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_table_info(%u, p.pubname) gpt"
+							 " WHERE  p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
+		}
+		else
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname) gpt"
+							 " WHERE gpt.relid = %u"
+							 "   AND p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
 
 		res = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data, 1, qualRow);
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 361e2cfffeb..b357a67ba7d 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12451,6 +12451,15 @@
   proargmodes => '{v,o,o,o,o}',
   proargnames => '{pubname,pubid,relid,attrs,qual}',
   prosrc => 'pg_get_publication_tables' },
+{ oid => '8060',
+  descr => 'get information of the table that is part of the specified publication',
+  proname => 'pg_get_publication_table_info', prorows => '1',
+  proretset => 't', provolatile => 's',
+  prorettype => 'record', proargtypes => 'oid name',
+  proallargtypes => '{oid,name,oid,oid,int2vector,pg_node_tree}',
+  proargmodes => '{i,i,o,o,o,o}',
+  proargnames => '{relid,pubname,pubid,relid,attrs,qual}',
+  prosrc => 'pg_get_publication_table_info' },
 { oid => '8052', descr => 'get OIDs of sequences in a publication',
   proname => 'pg_get_publication_sequences', prorows => '1000', proretset => 't',
   provolatile => 's', prorettype => 'oid', proargtypes => 'text',
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 681d2564ed5..e9914c147fa 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2182,6 +2182,135 @@ DROP TABLE testpub_merge_pk;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
+-- Test pg_get_publication_table_info() function
+CREATE SCHEMA gpt_test_sch;
+CREATE TABLE gpt_test_sch.tbl_sch (id int);
+CREATE TABLE tbl_normal (id int);
+CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1);
+CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10);
+CREATE PUBLICATION pub_all FOR ALL TABLES WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_all_novia_root FOR ALL TABLES WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch);
+CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch;
+CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10);
+CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_part_parent_novia_root FOR TABLE tbl_parent WITH (publish_via_partition_root = false);
+CREATE FUNCTION test_gpt(pubname text, relname text)
+RETURNS TABLE (
+  pubname text,
+  relname name,
+  attrs text,
+  qual text
+)
+BEGIN ATOMIC
+  SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid)
+    FROM pg_get_publication_table_info(relname::regclass::oid, pubname) gpt
+    JOIN pg_publication p ON p.oid = gpt.pubid
+    JOIN pg_class c ON c.oid = gpt.relid
+  ORDER BY p.pubname, c.relname;
+END;
+SELECT * FROM test_gpt('pub_normal', 'tbl_normal');
+  pubname   |  relname   | attrs |   qual    
+------------+------------+-------+-----------
+ pub_normal | tbl_normal | 1     | (id < 10)
+(1 row)
+
+SELECT * FROM test_gpt('pub_schema', 'tbl_normal'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_part_parent', 'tbl_parent');
+     pubname     |  relname   | attrs |    qual    
+-----------------+------------+-------+------------
+ pub_part_parent | tbl_parent | 1 2   | (id1 = 10)
+(1 row)
+
+SELECT * FROM test_gpt('pub_part_parent', 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_part1');
+          pubname           |  relname  | attrs | qual 
+----------------------------+-----------+-------+------
+ pub_part_parent_novia_root | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt('pub_part_leaf', 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_part_leaf', 'tbl_part1');
+    pubname    |  relname  | attrs | qual 
+---------------+-----------+-------+------
+ pub_part_leaf | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt('pub_all', 'tbl_parent');
+ pubname |  relname   | attrs | qual 
+---------+------------+-------+------
+ pub_all | tbl_parent | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt('pub_all', 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_all_except', 'tbl_normal');
+    pubname     |  relname   | attrs | qual 
+----------------+------------+-------+------
+ pub_all_except | tbl_normal | 1     | 
+(1 row)
+
+SELECT * FROM test_gpt('pub_all_except', 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_all_except', 'tbl_parent'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_all_except', 'tbl_part1'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_part1');
+      pubname       |  relname  | attrs | qual 
+--------------------+-----------+-------+------
+ pub_all_novia_root | tbl_part1 | 1 2 3 | 
+(1 row)
+
+-- Clean up
+DROP FUNCTION test_gpt(text[], relname);
+ERROR:  type "relname" does not exist
+DROP PUBLICATION pub_all;
+DROP PUBLICATION pub_all_novia_root;
+DROP PUBLICATION pub_all_except;
+DROP PUBLICATION pub_schema;
+DROP PUBLICATION pub_normal;
+DROP PUBLICATION pub_part_leaf;
+DROP PUBLICATION pub_part_parent;
+DROP PUBLICATION pub_part_parent_novia_root;
+DROP TABLE tbl_normal, tbl_parent, tbl_part1;
+DROP SCHEMA gpt_test_sch CASCADE;
+NOTICE:  drop cascades to table gpt_test_sch.tbl_sch
 -- stage objects for pg_dump tests
 CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int);
 CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int);
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 405579dad52..75f1bc2f2fc 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1378,6 +1378,73 @@ RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
 
+-- Test pg_get_publication_table_info() function
+CREATE SCHEMA gpt_test_sch;
+CREATE TABLE gpt_test_sch.tbl_sch (id int);
+CREATE TABLE tbl_normal (id int);
+CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1);
+CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10);
+
+CREATE PUBLICATION pub_all FOR ALL TABLES WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_all_novia_root FOR ALL TABLES WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch);
+CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch;
+CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10);
+CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_part_parent_novia_root FOR TABLE tbl_parent WITH (publish_via_partition_root = false);
+
+CREATE FUNCTION test_gpt(pubname text, relname text)
+RETURNS TABLE (
+  pubname text,
+  relname name,
+  attrs text,
+  qual text
+)
+BEGIN ATOMIC
+  SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid)
+    FROM pg_get_publication_table_info(relname::regclass::oid, pubname) gpt
+    JOIN pg_publication p ON p.oid = gpt.pubid
+    JOIN pg_class c ON c.oid = gpt.relid
+  ORDER BY p.pubname, c.relname;
+END;
+
+SELECT * FROM test_gpt('pub_normal', 'tbl_normal');
+SELECT * FROM test_gpt('pub_schema', 'tbl_normal'); -- no result
+
+SELECT * FROM test_gpt('pub_part_parent', 'tbl_parent');
+SELECT * FROM test_gpt('pub_part_parent', 'tbl_part1'); -- no result
+
+SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_parent'); -- no result
+SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_part1');
+
+SELECT * FROM test_gpt('pub_part_leaf', 'tbl_parent'); -- no result
+SELECT * FROM test_gpt('pub_part_leaf', 'tbl_part1');
+
+SELECT * FROM test_gpt('pub_all', 'tbl_parent');
+SELECT * FROM test_gpt('pub_all', 'tbl_part1'); -- no result
+
+SELECT * FROM test_gpt('pub_all_except', 'tbl_normal');
+SELECT * FROM test_gpt('pub_all_except', 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+SELECT * FROM test_gpt('pub_all_except', 'tbl_parent'); -- no result (excluded)
+SELECT * FROM test_gpt('pub_all_except', 'tbl_part1'); -- no result (excluded)
+
+SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_parent'); -- no result
+SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_part1');
+
+-- Clean up
+DROP FUNCTION test_gpt(text[], relname);
+DROP PUBLICATION pub_all;
+DROP PUBLICATION pub_all_novia_root;
+DROP PUBLICATION pub_all_except;
+DROP PUBLICATION pub_schema;
+DROP PUBLICATION pub_normal;
+DROP PUBLICATION pub_part_leaf;
+DROP PUBLICATION pub_part_parent;
+DROP PUBLICATION pub_part_parent_novia_root;
+DROP TABLE tbl_normal, tbl_parent, tbl_part1;
+DROP SCHEMA gpt_test_sch CASCADE;
+
 -- stage objects for pg_dump tests
 CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int);
 CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int);
-- 
2.53.0



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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-03-18 13:56       ` Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  0 siblings, 1 reply; 47+ messages in thread

From: Amit Kapila @ 2026-03-18 13:56 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Marcos Pegoraro <[email protected]>; PostgreSQL Hackers <[email protected]>

On Tue, Mar 10, 2026 at 3:40 AM Masahiko Sawada <[email protected]> wrote:
>
> On Tue, Mar 3, 2026 at 2:22 AM Zhijie Hou (Fujitsu)
> <[email protected]> wrote:
> >
> > On Saturday, February 28, 2026 7:48 AM Masahiko Sawada <[email protected]> wrote:
> > > To: Marcos Pegoraro <[email protected]>
> > > Cc: PostgreSQL Hackers <[email protected]>
> > > Subject: Re: Initial COPY of Logical Replication is too slow
> > >
> > > Another variant of this approach is to extend
> > > pg_get_publication_table() so that it can accept a relid to get the publication
> > > information of the specific table. I've attached the patch for this idea. I'm
> > > going to add regression test cases.
> > >
> > > pg_get_publication_table() is a VARIACID array function so the patch changes
> > > its signature to {text[] [, oid]}, breaking the tool compatibility. Given this
> > > function is mostly an internal-use function (we don't have the documentation
> > > for it), it would probably be okay with it. I find it's clearer than the other
> > > approach of introducing pg_get_publication_table_info(). Feedback is very
> > > welcome.
> >
> > Thanks for updating the patch.
> >
> > I have few comments for the function change:
> >
> > 1.
> >
> > If we change the function signature, will it affect use cases where the
> > publisher version is newer and the subscriber version is older ? E.g., when
> > publisher is passing text style publication name to pg_get_publication_tables().
>
> Good point.
>
> I noticed that changing the function signature of
> pg_get_publication_tables() breaks logical replication setups where
> the subscriber is 18 or older.
>

Why adding a new function with additional parameters (Oid relid)
couldn't help with such a case? I am asking because your previous
version code looks simpler as compared to the new patch version.

-- 
With Regards,
Amit Kapila.





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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
@ 2026-03-18 16:44         ` Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  0 siblings, 1 reply; 47+ messages in thread

From: Masahiko Sawada @ 2026-03-18 16:44 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Marcos Pegoraro <[email protected]>; PostgreSQL Hackers <[email protected]>

On Wed, Mar 18, 2026 at 6:56 AM Amit Kapila <[email protected]> wrote:
>
> On Tue, Mar 10, 2026 at 3:40 AM Masahiko Sawada <[email protected]> wrote:
> >
> > On Tue, Mar 3, 2026 at 2:22 AM Zhijie Hou (Fujitsu)
> > <[email protected]> wrote:
> > >
> > > On Saturday, February 28, 2026 7:48 AM Masahiko Sawada <[email protected]> wrote:
> > > > To: Marcos Pegoraro <[email protected]>
> > > > Cc: PostgreSQL Hackers <[email protected]>
> > > > Subject: Re: Initial COPY of Logical Replication is too slow
> > > >
> > > > Another variant of this approach is to extend
> > > > pg_get_publication_table() so that it can accept a relid to get the publication
> > > > information of the specific table. I've attached the patch for this idea. I'm
> > > > going to add regression test cases.
> > > >
> > > > pg_get_publication_table() is a VARIACID array function so the patch changes
> > > > its signature to {text[] [, oid]}, breaking the tool compatibility. Given this
> > > > function is mostly an internal-use function (we don't have the documentation
> > > > for it), it would probably be okay with it. I find it's clearer than the other
> > > > approach of introducing pg_get_publication_table_info(). Feedback is very
> > > > welcome.
> > >
> > > Thanks for updating the patch.
> > >
> > > I have few comments for the function change:
> > >
> > > 1.
> > >
> > > If we change the function signature, will it affect use cases where the
> > > publisher version is newer and the subscriber version is older ? E.g., when
> > > publisher is passing text style publication name to pg_get_publication_tables().
> >
> > Good point.
> >
> > I noticed that changing the function signature of
> > pg_get_publication_tables() breaks logical replication setups where
> > the subscriber is 18 or older.
> >
>
> Why adding a new function with additional parameters (Oid relid)
> couldn't help with such a case? I am asking because your previous
> version code looks simpler as compared to the new patch version.

I tried to pass a relid to pg_get_publication_tables() but we cannot
avoid changing its signature because it's a VARIADIC array function.
The previous patch changed pg_get_publication_tables(VARIADIC text[])
to pg_get_publication_tables(text[] {, relid}). However, changing the
function signature would break the logical replication from v19 to an
older version.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com





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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-03-18 22:31           ` Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  0 siblings, 1 reply; 47+ messages in thread

From: Masahiko Sawada @ 2026-03-18 22:31 UTC (permalink / raw)
  To: Jan Wieck <[email protected]>; +Cc: [email protected]

On Wed, Mar 18, 2026 at 1:11 PM Jan Wieck <[email protected]> wrote:
>
> On 3/18/26 12:44, Masahiko Sawada wrote:
> > On Wed, Mar 18, 2026 at 6:56 AM Amit Kapila <[email protected]> wrote:
> >>
> >> On Tue, Mar 10, 2026 at 3:40 AM Masahiko Sawada <[email protected]> wrote:
> >> >
> >> > On Tue, Mar 3, 2026 at 2:22 AM Zhijie Hou (Fujitsu)
> >> > <[email protected]> wrote:
> >> > >
> >> > > On Saturday, February 28, 2026 7:48 AM Masahiko Sawada <[email protected]> wrote:
> >> > > > To: Marcos Pegoraro <[email protected]>
> >> > > > Cc: PostgreSQL Hackers <[email protected]>
> >> > > > Subject: Re: Initial COPY of Logical Replication is too slow
> >> > > >
> >> > > > Another variant of this approach is to extend
> >> > > > pg_get_publication_table() so that it can accept a relid to get the publication
> >> > > > information of the specific table. I've attached the patch for this idea. I'm
> >> > > > going to add regression test cases.
> >> > > >
> >> > > > pg_get_publication_table() is a VARIACID array function so the patch changes
> >> > > > its signature to {text[] [, oid]}, breaking the tool compatibility. Given this
> >> > > > function is mostly an internal-use function (we don't have the documentation
> >> > > > for it), it would probably be okay with it. I find it's clearer than the other
> >> > > > approach of introducing pg_get_publication_table_info(). Feedback is very
> >> > > > welcome.
> >> > >
> >> > > Thanks for updating the patch.
> >> > >
> >> > > I have few comments for the function change:
> >> > >
> >> > > 1.
> >> > >
> >> > > If we change the function signature, will it affect use cases where the
> >> > > publisher version is newer and the subscriber version is older ? E.g., when
> >> > > publisher is passing text style publication name to pg_get_publication_tables().
> >> >
> >> > Good point.
> >> >
> >> > I noticed that changing the function signature of
> >> > pg_get_publication_tables() breaks logical replication setups where
> >> > the subscriber is 18 or older.
> >> >
> >>
> >> Why adding a new function with additional parameters (Oid relid)
> >> couldn't help with such a case? I am asking because your previous
> >> version code looks simpler as compared to the new patch version.
> >
> > I tried to pass a relid to pg_get_publication_tables() but we cannot
> > avoid changing its signature because it's a VARIADIC array function.
> > The previous patch changed pg_get_publication_tables(VARIADIC text[])
> > to pg_get_publication_tables(text[] {, relid}). However, changing the
> > function signature would break the logical replication from v19 to an
> > older version.
>
> Would it be possible to use function overloading to provide both
> signatures handled by different C functions internally?

Yes, we can define both pg_get_publication_tables(VARIADIC text[]) and
pg_get_publication_tables(text, oid), which seems like a less invasive
approach. I'll give this idea a shot and see how it goes.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com





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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-03-18 23:29             ` Masahiko Sawada <[email protected]>
  2026-03-24 00:53               ` Re: Initial COPY of Logical Replication is too slow Bharath Rupireddy <[email protected]>
  2026-03-24 06:54               ` Re: Initial COPY of Logical Replication is too slow Ajin Cherian <[email protected]>
  2026-03-24 06:59               ` Re: Initial COPY of Logical Replication is too slow Peter Smith <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  0 siblings, 4 replies; 47+ messages in thread

From: Masahiko Sawada @ 2026-03-18 23:29 UTC (permalink / raw)
  To: Jan Wieck <[email protected]>; +Cc: [email protected]

On Wed, Mar 18, 2026 at 3:31 PM Masahiko Sawada <[email protected]> wrote:
>
> On Wed, Mar 18, 2026 at 1:11 PM Jan Wieck <[email protected]> wrote:
> >
> > On 3/18/26 12:44, Masahiko Sawada wrote:
> > > On Wed, Mar 18, 2026 at 6:56 AM Amit Kapila <[email protected]> wrote:
> > >>
> > >> On Tue, Mar 10, 2026 at 3:40 AM Masahiko Sawada <[email protected]> wrote:
> > >> >
> > >> > On Tue, Mar 3, 2026 at 2:22 AM Zhijie Hou (Fujitsu)
> > >> > <[email protected]> wrote:
> > >> > >
> > >> > > On Saturday, February 28, 2026 7:48 AM Masahiko Sawada <[email protected]> wrote:
> > >> > > > To: Marcos Pegoraro <[email protected]>
> > >> > > > Cc: PostgreSQL Hackers <[email protected]>
> > >> > > > Subject: Re: Initial COPY of Logical Replication is too slow
> > >> > > >
> > >> > > > Another variant of this approach is to extend
> > >> > > > pg_get_publication_table() so that it can accept a relid to get the publication
> > >> > > > information of the specific table. I've attached the patch for this idea. I'm
> > >> > > > going to add regression test cases.
> > >> > > >
> > >> > > > pg_get_publication_table() is a VARIACID array function so the patch changes
> > >> > > > its signature to {text[] [, oid]}, breaking the tool compatibility. Given this
> > >> > > > function is mostly an internal-use function (we don't have the documentation
> > >> > > > for it), it would probably be okay with it. I find it's clearer than the other
> > >> > > > approach of introducing pg_get_publication_table_info(). Feedback is very
> > >> > > > welcome.
> > >> > >
> > >> > > Thanks for updating the patch.
> > >> > >
> > >> > > I have few comments for the function change:
> > >> > >
> > >> > > 1.
> > >> > >
> > >> > > If we change the function signature, will it affect use cases where the
> > >> > > publisher version is newer and the subscriber version is older ? E.g., when
> > >> > > publisher is passing text style publication name to pg_get_publication_tables().
> > >> >
> > >> > Good point.
> > >> >
> > >> > I noticed that changing the function signature of
> > >> > pg_get_publication_tables() breaks logical replication setups where
> > >> > the subscriber is 18 or older.
> > >> >
> > >>
> > >> Why adding a new function with additional parameters (Oid relid)
> > >> couldn't help with such a case? I am asking because your previous
> > >> version code looks simpler as compared to the new patch version.
> > >
> > > I tried to pass a relid to pg_get_publication_tables() but we cannot
> > > avoid changing its signature because it's a VARIADIC array function.
> > > The previous patch changed pg_get_publication_tables(VARIADIC text[])
> > > to pg_get_publication_tables(text[] {, relid}). However, changing the
> > > function signature would break the logical replication from v19 to an
> > > older version.
> >
> > Would it be possible to use function overloading to provide both
> > signatures handled by different C functions internally?
>
> Yes, we can define both pg_get_publication_tables(VARIADIC text[]) and
> pg_get_publication_tables(text, oid), which seems like a less invasive
> approach. I'll give this idea a shot and see how it goes.

I've attached the patch to implement this idea. The patch still
introduces a new function but it overloads
pg_get_publication_tables(). We might be able to handle different
input (array or text) in pg_get_publication_tables() better, but it's
enough for discussion at least.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com


Attachments:

  [text/x-patch] v3-0001-Avoid-full-table-scans-when-getting-publication-t.patch (25.5K, 2-v3-0001-Avoid-full-table-scans-when-getting-publication-t.patch)
  download | inline diff:
From c3647910dadb645895cea1ef15c11aaa6cd25d18 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
Date: Fri, 27 Feb 2026 15:42:38 -0800
Subject: [PATCH v3] Avoid full table scans when getting publication table
 information by tablesync workers.

Reported-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zhijie Hou (Fujitsu) <[email protected]>
Reviewed-by: Matheus Alcantara <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Discussion: https://postgr.es/m/CAB-JLwbBFNuASyEnZWP0Tck9uNkthBZqi6WoXNevUT6+mV8XmA@mail.gmail.com
---
 src/backend/catalog/pg_publication.c        | 294 +++++++++++++++-----
 src/backend/replication/logical/tablesync.c |  74 +++--
 src/include/catalog/pg_proc.dat             |  11 +-
 src/test/regress/expected/publication.out   | 131 +++++++++
 src/test/regress/sql/publication.sql        |  69 +++++
 5 files changed, 490 insertions(+), 89 deletions(-)

diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index a79157c43bf..5f687491a4e 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -1208,12 +1208,129 @@ GetPublicationByName(const char *pubname, bool missing_ok)
 }
 
 /*
- * Get information of the tables in the given publication array.
+ * Returns true if the table of the given relid is published for the specified
+ * publication.
+ *
+ * This function evaluates the effective published OID based on the
+ * publish_via_partition_root setting, rather than just checking catalog entries
+ * (e.g., pg_publication_rel). For instance, when publish_via_partition_root is
+ * false, it returns false for a parent partitioned table and true for its leaf
+ * partitions, even if the parent is the one explicitly added to the publication.
+ *
+ * For performance reasons, this function avoids the overhead of constructing
+ * the complete list of published tables during the evaluation. It can execute
+ * quickly even when the publication contains a large number of relations.
+ */
+static bool
+is_table_publishable_in_publication(Oid relid, Publication *pub)
+{
+	if (pub->pubviaroot)
+	{
+		if (pub->alltables)
+		{
+			/*
+			 * ALL TABLE publications with pubviaroot=true include only tables
+			 * that are either regular tables or top-most partitioned tables.
+			 */
+			if (get_rel_relispartition(relid))
+				return false;
+
+			/*
+			 * Check if the table is specified in the EXCEPT clause in the
+			 * publication. ALL TABLE publications have pg_publication_rel
+			 * entries only for EXCEPT'ed tables, so it's sufficient to check
+			 * the existence of its entry.
+			 */
+			return !SearchSysCacheExists2(PUBLICATIONRELMAP,
+										  ObjectIdGetDatum(relid),
+										  ObjectIdGetDatum(pub->oid));
+		}
+
+		/*
+		 * Check if its corresponding entry exists either in
+		 * pg_publication_rel or pg_publication_namespace.
+		 */
+		return (SearchSysCacheExists2(PUBLICATIONRELMAP,
+									  ObjectIdGetDatum(relid),
+									  ObjectIdGetDatum(pub->oid)) ||
+				SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP,
+									  ObjectIdGetDatum(get_rel_namespace(relid)),
+									  ObjectIdGetDatum(pub->oid)));
+	}
+
+	/*
+	 * For non-pubviaroot publications, partitioned table's OID can never be a
+	 * published OID.
+	 */
+	if (get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE)
+		return false;
+
+	if (pub->alltables)
+	{
+		Oid			target_relid = relid;
+
+		if (get_rel_relispartition(relid))
+		{
+			List	   *ancestors = get_partition_ancestors(relid);
+
+			/*
+			 * Only the top-most ancestor can appear in the EXCEPT clause.
+			 * Therefore, for a partition, exclusion must be evaluated at the
+			 * top-most ancestor.
+			 */
+			target_relid = llast_oid(ancestors);
+
+			list_free(ancestors);
+		}
+
+		/*
+		 * The table is published unless it's specified in the EXCEPT clause.
+		 * ALL TABLE publications have pg_publication_rel entries only for
+		 * EXCEPT'ed tables, so it's sufficient to check the existence of its
+		 * entry.
+		 */
+		return !SearchSysCacheExists2(PUBLICATIONRELMAP,
+									  ObjectIdGetDatum(target_relid),
+									  ObjectIdGetDatum(pub->oid));
+	}
+
+	if (get_rel_relispartition(relid))
+	{
+		List	   *ancestors = get_partition_ancestors(relid);
+		Oid			topmost = GetTopMostAncestorInPublication(pub->oid, ancestors,
+															  NULL);
+
+		list_free(ancestors);
+
+		/* This table is published if its ancestor is published */
+		if (OidIsValid(topmost))
+			return true;
+
+		/* The partition itself might be published, so check below */
+	}
+
+	return (SearchSysCacheExists2(PUBLICATIONRELMAP,
+								  ObjectIdGetDatum(relid),
+								  ObjectIdGetDatum(pub->oid)) ||
+			SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP,
+								  ObjectIdGetDatum(get_rel_namespace(relid)),
+								  ObjectIdGetDatum(pub->oid)));
+}
+
+/*
+ * Helper function to get information of the tables in the given
+ * publication(s).
+ *
+ * The parameters pubnames and {pubname, target_relid} are mutually exclusive.
+ * If target_relid is provided, the function returns information only for that
+ * specific table. Otherwise, if returns information for all tables within the
+ * specified publications.
  *
  * Returns pubid, relid, column list, row filter for each table.
  */
-Datum
-pg_get_publication_tables(PG_FUNCTION_ARGS)
+static Datum
+pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
+						  text *pubname, Oid target_relid)
 {
 #define NUM_PUBLICATION_TABLES_ELEM	4
 	FuncCallContext *funcctx;
@@ -1224,11 +1341,6 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 	{
 		TupleDesc	tupdesc;
 		MemoryContext oldcontext;
-		ArrayType  *arr;
-		Datum	   *elems;
-		int			nelems,
-					i;
-		bool		viaroot = false;
 
 		/* create a function context for cross-call persistence */
 		funcctx = SRF_FIRSTCALL_INIT();
@@ -1236,81 +1348,111 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		/* switch to memory context appropriate for multiple function calls */
 		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
 
-		/*
-		 * Deconstruct the parameter into elements where each element is a
-		 * publication name.
-		 */
-		arr = PG_GETARG_ARRAYTYPE_P(0);
-		deconstruct_array_builtin(arr, TEXTOID, &elems, NULL, &nelems);
-
-		/* Get Oids of tables from each publication. */
-		for (i = 0; i < nelems; i++)
+		if (pubname != NULL)
 		{
-			Publication *pub_elem;
-			List	   *pub_elem_tables = NIL;
-			ListCell   *lc;
+			Publication *pub;
 
-			pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]), false);
+			Assert(OidIsValid(target_relid));
+			pub = GetPublicationByName(text_to_cstring(pubname), false);
 
-			/*
-			 * Publications support partitioned tables. If
-			 * publish_via_partition_root is false, all changes are replicated
-			 * using leaf partition identity and schema, so we only need
-			 * those. Otherwise, get the partitioned table itself.
-			 */
-			if (pub_elem->alltables)
-				pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
-															 RELKIND_RELATION,
-															 pub_elem->pubviaroot);
-			else
+			if (is_table_publishable_in_publication(target_relid, pub))
 			{
-				List	   *relids,
-						   *schemarelids;
-
-				relids = GetIncludedPublicationRelations(pub_elem->oid,
-														 pub_elem->pubviaroot ?
-														 PUBLICATION_PART_ROOT :
-														 PUBLICATION_PART_LEAF);
-				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
-																pub_elem->pubviaroot ?
-																PUBLICATION_PART_ROOT :
-																PUBLICATION_PART_LEAF);
-				pub_elem_tables = list_concat_unique_oid(relids, schemarelids);
+				published_rel *table_info = palloc_object(published_rel);
+
+				table_info->relid = target_relid;
+				table_info->pubid = pub->oid;
+				table_infos = lappend(table_infos, table_info);
 			}
+		}
+		else
+		{
+			Datum	   *elems;
+			int			nelems,
+						i;
+			bool		viaroot = false;
+
+			Assert(pubnames != NULL);
 
 			/*
-			 * Record the published table and the corresponding publication so
-			 * that we can get row filters and column lists later.
-			 *
-			 * When a table is published by multiple publications, to obtain
-			 * all row filters and column lists, the structure related to this
-			 * table will be recorded multiple times.
+			 * Deconstruct the parameter into elements where each element is a
+			 * publication name.
 			 */
-			foreach(lc, pub_elem_tables)
+			deconstruct_array_builtin(pubnames, TEXTOID, &elems, NULL, &nelems);
+
+			/* Get Oids of tables from each publication. */
+			for (i = 0; i < nelems; i++)
 			{
-				published_rel *table_info = palloc_object(published_rel);
+				Publication *pub_elem;
+				List	   *pub_elem_tables = NIL;
+				ListCell   *lc;
+
+				pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]), false);
+
+				/*
+				 * Publications support partitioned tables. If
+				 * publish_via_partition_root is false, all changes are
+				 * replicated using leaf partition identity and schema, so we
+				 * only need those. Otherwise, get the partitioned table
+				 * itself.
+				 */
+				if (pub_elem->alltables)
+					pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
+																 RELKIND_RELATION,
+																 pub_elem->pubviaroot);
+				else
+				{
+					List	   *relids,
+							   *schemarelids;
+
+					relids = GetIncludedPublicationRelations(pub_elem->oid,
+															 pub_elem->pubviaroot ?
+															 PUBLICATION_PART_ROOT :
+															 PUBLICATION_PART_LEAF);
+					schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
+																	pub_elem->pubviaroot ?
+																	PUBLICATION_PART_ROOT :
+																	PUBLICATION_PART_LEAF);
+					pub_elem_tables = list_concat_unique_oid(relids, schemarelids);
+				}
 
-				table_info->relid = lfirst_oid(lc);
-				table_info->pubid = pub_elem->oid;
-				table_infos = lappend(table_infos, table_info);
+				/*
+				 * Record the published table and the corresponding
+				 * publication so that we can get row filters and column lists
+				 * later.
+				 *
+				 * When a table is published by multiple publications, to
+				 * obtain all row filters and column lists, the structure
+				 * related to this table will be recorded multiple times.
+				 */
+				foreach(lc, pub_elem_tables)
+				{
+					published_rel *table_info = palloc_object(published_rel);
+
+					table_info->relid = lfirst_oid(lc);
+					table_info->pubid = pub_elem->oid;
+					table_infos = lappend(table_infos, table_info);
+				}
+
+				/*
+				 * At least one publication is using
+				 * publish_via_partition_root.
+				 */
+				if (pub_elem->pubviaroot)
+					viaroot = true;
 			}
 
-			/* At least one publication is using publish_via_partition_root. */
-			if (pub_elem->pubviaroot)
-				viaroot = true;
+			/*
+			 * If the publication publishes partition changes via their
+			 * respective root partitioned tables, we must exclude partitions
+			 * in favor of including the root partitioned tables. Otherwise,
+			 * the function could return both the child and parent tables
+			 * which could cause data of the child table to be
+			 * double-published on the subscriber side.
+			 */
+			if (viaroot)
+				filter_partitions(table_infos);
 		}
 
-		/*
-		 * If the publication publishes partition changes via their respective
-		 * root partitioned tables, we must exclude partitions in favor of
-		 * including the root partitioned tables. Otherwise, the function
-		 * could return both the child and parent tables which could cause
-		 * data of the child table to be double-published on the subscriber
-		 * side.
-		 */
-		if (viaroot)
-			filter_partitions(table_infos);
-
 		/* Construct a tuple descriptor for the result rows. */
 		tupdesc = CreateTemplateTupleDesc(NUM_PUBLICATION_TABLES_ELEM);
 		TupleDescInitEntry(tupdesc, (AttrNumber) 1, "pubid",
@@ -1435,6 +1577,20 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 	SRF_RETURN_DONE(funcctx);
 }
 
+Datum
+pg_get_publication_tables_a(PG_FUNCTION_ARGS)
+{
+	/* Get the information of the tables in the given publications */
+	return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0), NULL, InvalidOid);
+}
+
+Datum
+pg_get_publication_tables_b(PG_FUNCTION_ARGS)
+{
+	/* Get the information of the specified table in the given publication */
+	return pg_get_publication_tables(fcinfo, NULL, PG_GETARG_TEXT_P(0), PG_GETARG_OID(1));
+}
+
 /*
  * Returns Oids of sequences in a publication.
  */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index f49a4852ecb..884b56bb26c 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -798,17 +798,37 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		 * publications).
 		 */
 		resetStringInfo(&cmd);
-		appendStringInfo(&cmd,
-						 "SELECT DISTINCT"
-						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
-						 "   THEN NULL ELSE gpt.attrs END)"
-						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
-						 "  pg_class c"
-						 " WHERE gpt.relid = %u AND c.oid = gpt.relid"
-						 "   AND p.pubname IN ( %s )",
-						 lrel->remoteid,
-						 pub_names->data);
+
+		if (server_version >= 190000)
+		{
+			/*
+			 * We can pass relid to pg_get_publication_table_info() since
+			 * version 19.
+			 */
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT"
+							 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
+							 "   THEN NULL ELSE gpt.attrs END)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname, %u) gpt,"
+							 "  pg_class c"
+							 " WHERE c.oid = gpt.relid"
+							 "   AND p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
+		}
+		else
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT"
+							 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
+							 "   THEN NULL ELSE gpt.attrs END)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
+							 "  pg_class c"
+							 " WHERE gpt.relid = %u AND c.oid = gpt.relid"
+							 "   AND p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
 
 		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
 							 lengthof(attrsRow), attrsRow);
@@ -982,14 +1002,30 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 
 		/* Check for row filters. */
 		resetStringInfo(&cmd);
-		appendStringInfo(&cmd,
-						 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
-						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt"
-						 " WHERE gpt.relid = %u"
-						 "   AND p.pubname IN ( %s )",
-						 lrel->remoteid,
-						 pub_names->data);
+
+		if (server_version >= 190000)
+		{
+			/*
+			 * We can pass relid to pg_get_publication_table_info() since
+			 * version 19.
+			 */
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname, %u) gpt"
+							 " WHERE p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
+		}
+		else
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname) gpt"
+							 " WHERE gpt.relid = %u"
+							 "   AND p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
 
 		res = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data, 1, qualRow);
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fc8d82665b8..294ee717a6d 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12453,7 +12453,16 @@
   proallargtypes => '{_text,oid,oid,int2vector,pg_node_tree}',
   proargmodes => '{v,o,o,o,o}',
   proargnames => '{pubname,pubid,relid,attrs,qual}',
-  prosrc => 'pg_get_publication_tables' },
+  prosrc => 'pg_get_publication_tables_a' },
+{ oid => '8060',
+  descr => 'get information of the specified table that is part of the specified publication',
+  proname => 'pg_get_publication_tables', prorows => '1',
+  proretset => 't', provolatile => 's',
+  prorettype => 'record', proargtypes => 'text oid',
+  proallargtypes => '{text,oid,oid,oid,int2vector,pg_node_tree}',
+  proargmodes => '{i,i,o,o,o,o}',
+  proargnames => '{pubname,relid,pubid,relid,attrs,qual}',
+  prosrc => 'pg_get_publication_tables_b' },
 { oid => '8052', descr => 'get OIDs of sequences in a publication',
   proname => 'pg_get_publication_sequences', prorows => '1000', proretset => 't',
   provolatile => 's', prorettype => 'oid', proargtypes => 'text',
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 681d2564ed5..91c339bd278 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2182,6 +2182,137 @@ DROP TABLE testpub_merge_pk;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
+-- Test pg_get_publication_tables(text, oid) function
+CREATE SCHEMA gpt_test_sch;
+CREATE TABLE gpt_test_sch.tbl_sch (id int);
+CREATE TABLE tbl_normal (id int);
+CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1);
+CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION pub_all FOR ALL TABLES WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_all_novia_root FOR ALL TABLES WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch);
+CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch;
+CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10);
+CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_part_parent_novia_root FOR TABLE tbl_parent WITH (publish_via_partition_root = false);
+RESET client_min_messages;
+CREATE FUNCTION test_gpt(pubname text, relname text)
+RETURNS TABLE (
+  pubname text,
+  relname name,
+  attrs text,
+  qual text
+)
+BEGIN ATOMIC
+  SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid)
+    FROM pg_get_publication_tables(pubname, relname::regclass::oid) gpt
+    JOIN pg_publication p ON p.oid = gpt.pubid
+    JOIN pg_class c ON c.oid = gpt.relid
+  ORDER BY p.pubname, c.relname;
+END;
+SELECT * FROM test_gpt('pub_normal', 'tbl_normal');
+  pubname   |  relname   | attrs |   qual    
+------------+------------+-------+-----------
+ pub_normal | tbl_normal | 1     | (id < 10)
+(1 row)
+
+SELECT * FROM test_gpt('pub_schema', 'tbl_normal'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_part_parent', 'tbl_parent');
+     pubname     |  relname   | attrs |    qual    
+-----------------+------------+-------+------------
+ pub_part_parent | tbl_parent | 1 2   | (id1 = 10)
+(1 row)
+
+SELECT * FROM test_gpt('pub_part_parent', 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_part1');
+          pubname           |  relname  | attrs | qual 
+----------------------------+-----------+-------+------
+ pub_part_parent_novia_root | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt('pub_part_leaf', 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_part_leaf', 'tbl_part1');
+    pubname    |  relname  | attrs | qual 
+---------------+-----------+-------+------
+ pub_part_leaf | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt('pub_all', 'tbl_parent');
+ pubname |  relname   | attrs | qual 
+---------+------------+-------+------
+ pub_all | tbl_parent | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt('pub_all', 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_all_except', 'tbl_normal');
+    pubname     |  relname   | attrs | qual 
+----------------+------------+-------+------
+ pub_all_except | tbl_normal | 1     | 
+(1 row)
+
+SELECT * FROM test_gpt('pub_all_except', 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_all_except', 'tbl_parent'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_all_except', 'tbl_part1'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_part1');
+      pubname       |  relname  | attrs | qual 
+--------------------+-----------+-------+------
+ pub_all_novia_root | tbl_part1 | 1 2 3 | 
+(1 row)
+
+-- Clean up
+DROP FUNCTION test_gpt(text[], relname);
+ERROR:  type "relname" does not exist
+DROP PUBLICATION pub_all;
+DROP PUBLICATION pub_all_novia_root;
+DROP PUBLICATION pub_all_except;
+DROP PUBLICATION pub_schema;
+DROP PUBLICATION pub_normal;
+DROP PUBLICATION pub_part_leaf;
+DROP PUBLICATION pub_part_parent;
+DROP PUBLICATION pub_part_parent_novia_root;
+DROP TABLE tbl_normal, tbl_parent, tbl_part1;
+DROP SCHEMA gpt_test_sch CASCADE;
+NOTICE:  drop cascades to table gpt_test_sch.tbl_sch
 -- stage objects for pg_dump tests
 CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int);
 CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int);
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 405579dad52..0f3f1400abe 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1378,6 +1378,75 @@ RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
 
+-- Test pg_get_publication_tables(text, oid) function
+CREATE SCHEMA gpt_test_sch;
+CREATE TABLE gpt_test_sch.tbl_sch (id int);
+CREATE TABLE tbl_normal (id int);
+CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1);
+CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10);
+
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION pub_all FOR ALL TABLES WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_all_novia_root FOR ALL TABLES WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch);
+CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch;
+CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10);
+CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_part_parent_novia_root FOR TABLE tbl_parent WITH (publish_via_partition_root = false);
+RESET client_min_messages;
+
+CREATE FUNCTION test_gpt(pubname text, relname text)
+RETURNS TABLE (
+  pubname text,
+  relname name,
+  attrs text,
+  qual text
+)
+BEGIN ATOMIC
+  SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid)
+    FROM pg_get_publication_tables(pubname, relname::regclass::oid) gpt
+    JOIN pg_publication p ON p.oid = gpt.pubid
+    JOIN pg_class c ON c.oid = gpt.relid
+  ORDER BY p.pubname, c.relname;
+END;
+
+SELECT * FROM test_gpt('pub_normal', 'tbl_normal');
+SELECT * FROM test_gpt('pub_schema', 'tbl_normal'); -- no result
+
+SELECT * FROM test_gpt('pub_part_parent', 'tbl_parent');
+SELECT * FROM test_gpt('pub_part_parent', 'tbl_part1'); -- no result
+
+SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_parent'); -- no result
+SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_part1');
+
+SELECT * FROM test_gpt('pub_part_leaf', 'tbl_parent'); -- no result
+SELECT * FROM test_gpt('pub_part_leaf', 'tbl_part1');
+
+SELECT * FROM test_gpt('pub_all', 'tbl_parent');
+SELECT * FROM test_gpt('pub_all', 'tbl_part1'); -- no result
+
+SELECT * FROM test_gpt('pub_all_except', 'tbl_normal');
+SELECT * FROM test_gpt('pub_all_except', 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+SELECT * FROM test_gpt('pub_all_except', 'tbl_parent'); -- no result (excluded)
+SELECT * FROM test_gpt('pub_all_except', 'tbl_part1'); -- no result (excluded)
+
+SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_parent'); -- no result
+SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_part1');
+
+-- Clean up
+DROP FUNCTION test_gpt(text[], relname);
+DROP PUBLICATION pub_all;
+DROP PUBLICATION pub_all_novia_root;
+DROP PUBLICATION pub_all_except;
+DROP PUBLICATION pub_schema;
+DROP PUBLICATION pub_normal;
+DROP PUBLICATION pub_part_leaf;
+DROP PUBLICATION pub_part_parent;
+DROP PUBLICATION pub_part_parent_novia_root;
+DROP TABLE tbl_normal, tbl_parent, tbl_part1;
+DROP SCHEMA gpt_test_sch CASCADE;
+
 -- stage objects for pg_dump tests
 CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int);
 CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int);
-- 
2.53.0



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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-03-24 00:53               ` Bharath Rupireddy <[email protected]>
  2026-03-24 18:41                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  3 siblings, 1 reply; 47+ messages in thread

From: Bharath Rupireddy @ 2026-03-24 00:53 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected]

Hi,

On Wed, Mar 18, 2026 at 4:29 PM Masahiko Sawada <[email protected]> wrote:
>
> I've attached the patch to implement this idea. The patch still
> introduces a new function but it overloads
> pg_get_publication_tables(). We might be able to handle different
> input (array or text) in pg_get_publication_tables() better, but it's
> enough for discussion at least.

Overall, the intent of this patch looks good to me. It avoids the cost
of the table sync worker querying all the pg_publication_rel tables to
filter them out later in the join.

I quickly reviewed the patch and here are some comments:

1/ Typo: s/pg_get_publication_table_info/pg_get_publication_tables

2/ I think it's good to have some quick numbers on how the query
latency looks for pre-V19 and the new one that the table sync worker
executes on the publisher, say, with 100, 1000, and 10000 tables at
least.

3/ + Assert(OidIsValid(target_relid));

Why not error out (by treating it as function input parameter
validation) when target_relid is invalid because asserts go unnoticed
on production systems?

--
Bharath Rupireddy
Amazon Web Services: https://aws.amazon.com





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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 00:53               ` Re: Initial COPY of Logical Replication is too slow Bharath Rupireddy <[email protected]>
@ 2026-03-24 18:41                 ` Masahiko Sawada <[email protected]>
  0 siblings, 0 replies; 47+ messages in thread

From: Masahiko Sawada @ 2026-03-24 18:41 UTC (permalink / raw)
  To: Bharath Rupireddy <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected]

On Mon, Mar 23, 2026 at 5:54 PM Bharath Rupireddy
<[email protected]> wrote:
>
> Hi,
>
> On Wed, Mar 18, 2026 at 4:29 PM Masahiko Sawada <[email protected]> wrote:
> >
> > I've attached the patch to implement this idea. The patch still
> > introduces a new function but it overloads
> > pg_get_publication_tables(). We might be able to handle different
> > input (array or text) in pg_get_publication_tables() better, but it's
> > enough for discussion at least.
>
> Overall, the intent of this patch looks good to me. It avoids the cost
> of the table sync worker querying all the pg_publication_rel tables to
> filter them out later in the join.
>
> I quickly reviewed the patch and here are some comments:

Thank you for reviewing the patch!

>
> 1/ Typo: s/pg_get_publication_table_info/pg_get_publication_tables

Fixed.

>
> 2/ I think it's good to have some quick numbers on how the query
> latency looks for pre-V19 and the new one that the table sync worker
> executes on the publisher, say, with 100, 1000, and 10000 tables at
> least.

You can refer to the performance test results that I previously
shared[1]. The patch I used was somewhat different from the current
patch but the performance trend should be similar as the both are
using the same approach.

>
> 3/ + Assert(OidIsValid(target_relid));
>
> Why not error out (by treating it as function input parameter
> validation) when target_relid is invalid because asserts go unnoticed
> on production systems?

Agreed. It would return no row if the specified relid is invalid or
there is no corresponding table.

I'll share the updated patch soon.

Regards,

[1] https://www.postgresql.org/message-id/CAD21AoDQM62GOtaTzD_CVMSsFhv6o9c0Au1dSM1QuxeKFkWAKw%40mail.gma...

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com





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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-03-24 06:54               ` Ajin Cherian <[email protected]>
  2026-03-24 18:42                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  3 siblings, 1 reply; 47+ messages in thread

From: Ajin Cherian @ 2026-03-24 06:54 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected]

On Thu, Mar 19, 2026 at 10:30 AM Masahiko Sawada <[email protected]> wrote:
>
> I've attached the patch to implement this idea. The patch still
> introduces a new function but it overloads
> pg_get_publication_tables(). We might be able to handle different
> input (array or text) in pg_get_publication_tables() better, but it's
> enough for discussion at least.
>

The patch looks like a good performance improvement. Some minor comments:

1. src/test/regress/expected/publication.out

+-- Clean up
+DROP FUNCTION test_gpt(text[], relname);
+ERROR:  type "relname" does not exist

Cleanup actually fails. Second parameter should be text, not relname.

2. src/include/catalog/pg_proc.dat

+  proallargtypes => '{text,oid,oid,oid,int2vector,pg_node_tree}',
+  proargmodes => '{i,i,o,o,o,o}',
+  proargnames => '{pubname,relid,pubid,relid,attrs,qual}',

Having two arguments with the same name "relid" seems odd, although
one is input and other is output parameter, how about calling input
parameter as target_relid?

3. src/backend/replication/logical/tablesync.c

+
+        if (server_version >= 190000)
+        {
+            /*
+             * We can pass relid to pg_get_publication_table_info() since
+             * version 19.
+             */
+            appendStringInfo(&cmd,
+                             "SELECT DISTINCT"

In multiple places in the code pg_get_publication_table_info() is
used, instead of pg_get_publication_tables()

thanks,
Ajin Cherian
Fujitsu Australia





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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 06:54               ` Re: Initial COPY of Logical Replication is too slow Ajin Cherian <[email protected]>
@ 2026-03-24 18:42                 ` Masahiko Sawada <[email protected]>
  0 siblings, 0 replies; 47+ messages in thread

From: Masahiko Sawada @ 2026-03-24 18:42 UTC (permalink / raw)
  To: Ajin Cherian <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected]

On Mon, Mar 23, 2026 at 11:54 PM Ajin Cherian <[email protected]> wrote:
>
> On Thu, Mar 19, 2026 at 10:30 AM Masahiko Sawada <[email protected]> wrote:
> >
> > I've attached the patch to implement this idea. The patch still
> > introduces a new function but it overloads
> > pg_get_publication_tables(). We might be able to handle different
> > input (array or text) in pg_get_publication_tables() better, but it's
> > enough for discussion at least.
> >
>
> The patch looks like a good performance improvement. Some minor comments:
>
> 1. src/test/regress/expected/publication.out
>
> +-- Clean up
> +DROP FUNCTION test_gpt(text[], relname);
> +ERROR:  type "relname" does not exist
>
> Cleanup actually fails. Second parameter should be text, not relname.
>
> 2. src/include/catalog/pg_proc.dat
>
> +  proallargtypes => '{text,oid,oid,oid,int2vector,pg_node_tree}',
> +  proargmodes => '{i,i,o,o,o,o}',
> +  proargnames => '{pubname,relid,pubid,relid,attrs,qual}',
>
> Having two arguments with the same name "relid" seems odd, although
> one is input and other is output parameter, how about calling input
> parameter as target_relid?
>
> 3. src/backend/replication/logical/tablesync.c
>
> +
> +        if (server_version >= 190000)
> +        {
> +            /*
> +             * We can pass relid to pg_get_publication_table_info() since
> +             * version 19.
> +             */
> +            appendStringInfo(&cmd,
> +                             "SELECT DISTINCT"
>
> In multiple places in the code pg_get_publication_table_info() is
> used, instead of pg_get_publication_tables()

Thank you for reviewing the patch! I agree with all the above points.
I'll share the updated patch soon.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com





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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-03-24 06:59               ` Peter Smith <[email protected]>
  2026-03-24 18:45                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  3 siblings, 1 reply; 47+ messages in thread

From: Peter Smith @ 2026-03-24 06:59 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected]

Hi Sawada-San.

Here are some review comments for the v3-0001 test code.

======
src/test/regress/expected/publication.out

1.
+-- Clean up
+DROP FUNCTION test_gpt(text[], relname);
+ERROR:  type "relname" does not exist

This seems a mistake. If the DROP FUNCTION was differently written
there there would be no error. PSA.

======
src/test/regress/sql/publication.sql

2.
+SELECT * FROM test_gpt('pub_normal', 'tbl_normal');
+SELECT * FROM test_gpt('pub_schema', 'tbl_normal'); -- no result

These tests seem strangely different from all the others because
everything else has both a "good result" test and a "no result" test
for every publication.

So I think there should be a "no result" test for 'pub_normal'

So I think there should be a "good result" test for 'pub_schema'

~~~

3.
Consider renaming that 'tbl_parent' to something like 'tbl_root'.
because 'parent' always makes me think of INHERITED parent/child
tables, rather than partitioned tables and their partitions. If you do
this, then you might also want to tweak several publication names --
e.g. 'pub_part_parent' -> 'pub_part_root'

~~~

PSA a diff file that makes those suggested changes #1 and #2.

======
Kind Regards,
Peter Smith.
Fujitsu Australia

diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 3dded67ab98..60033ea2fff 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2307,6 +2307,17 @@ SELECT * FROM test_gpt('pub_normal', 'tbl_normal');
  pub_normal | tbl_normal | 1     | (id < 10)
 (1 row)
 
+SELECT * FROM test_gpt('pub_normal', 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_schema', 'gpt_test_sch.tbl_sch');
+  pubname   | relname | attrs | qual 
+------------+---------+-------+------
+ pub_schema | tbl_sch | 1     | 
+(1 row)
+
 SELECT * FROM test_gpt('pub_schema', 'tbl_normal'); -- no result
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
@@ -2389,8 +2400,7 @@ SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_part1');
 (1 row)
 
 -- Clean up
-DROP FUNCTION test_gpt(text[], relname);
-ERROR:  type "relname" does not exist
+DROP FUNCTION test_gpt(pubname text, relname text);
 DROP PUBLICATION pub_all;
 DROP PUBLICATION pub_all_novia_root;
 DROP PUBLICATION pub_all_except;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 10fd97fe544..9801179e645 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1463,6 +1463,9 @@ BEGIN ATOMIC
 END;
 
 SELECT * FROM test_gpt('pub_normal', 'tbl_normal');
+SELECT * FROM test_gpt('pub_normal', 'tbl_parent'); -- no result
+
+SELECT * FROM test_gpt('pub_schema', 'gpt_test_sch.tbl_sch');
 SELECT * FROM test_gpt('pub_schema', 'tbl_normal'); -- no result
 
 SELECT * FROM test_gpt('pub_part_parent', 'tbl_parent');
@@ -1486,7 +1489,7 @@ SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_parent'); -- no result
 SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_part1');
 
 -- Clean up
-DROP FUNCTION test_gpt(text[], relname);
+DROP FUNCTION test_gpt(pubname text, relname text);
 DROP PUBLICATION pub_all;
 DROP PUBLICATION pub_all_novia_root;
 DROP PUBLICATION pub_all_except;


Attachments:

  [text/plain] PS_v3_testcode_topup.txt (2.1K, 2-PS_v3_testcode_topup.txt)
  download | inline diff:
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 3dded67ab98..60033ea2fff 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2307,6 +2307,17 @@ SELECT * FROM test_gpt('pub_normal', 'tbl_normal');
  pub_normal | tbl_normal | 1     | (id < 10)
 (1 row)
 
+SELECT * FROM test_gpt('pub_normal', 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_schema', 'gpt_test_sch.tbl_sch');
+  pubname   | relname | attrs | qual 
+------------+---------+-------+------
+ pub_schema | tbl_sch | 1     | 
+(1 row)
+
 SELECT * FROM test_gpt('pub_schema', 'tbl_normal'); -- no result
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
@@ -2389,8 +2400,7 @@ SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_part1');
 (1 row)
 
 -- Clean up
-DROP FUNCTION test_gpt(text[], relname);
-ERROR:  type "relname" does not exist
+DROP FUNCTION test_gpt(pubname text, relname text);
 DROP PUBLICATION pub_all;
 DROP PUBLICATION pub_all_novia_root;
 DROP PUBLICATION pub_all_except;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 10fd97fe544..9801179e645 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1463,6 +1463,9 @@ BEGIN ATOMIC
 END;
 
 SELECT * FROM test_gpt('pub_normal', 'tbl_normal');
+SELECT * FROM test_gpt('pub_normal', 'tbl_parent'); -- no result
+
+SELECT * FROM test_gpt('pub_schema', 'gpt_test_sch.tbl_sch');
 SELECT * FROM test_gpt('pub_schema', 'tbl_normal'); -- no result
 
 SELECT * FROM test_gpt('pub_part_parent', 'tbl_parent');
@@ -1486,7 +1489,7 @@ SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_parent'); -- no result
 SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_part1');
 
 -- Clean up
-DROP FUNCTION test_gpt(text[], relname);
+DROP FUNCTION test_gpt(pubname text, relname text);
 DROP PUBLICATION pub_all;
 DROP PUBLICATION pub_all_novia_root;
 DROP PUBLICATION pub_all_except;


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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 06:59               ` Re: Initial COPY of Logical Replication is too slow Peter Smith <[email protected]>
@ 2026-03-24 18:45                 ` Masahiko Sawada <[email protected]>
  0 siblings, 0 replies; 47+ messages in thread

From: Masahiko Sawada @ 2026-03-24 18:45 UTC (permalink / raw)
  To: Peter Smith <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected]

On Mon, Mar 23, 2026 at 11:59 PM Peter Smith <[email protected]> wrote:
>
> Hi Sawada-San.
>
> Here are some review comments for the v3-0001 test code.

Thank you for reviewing the patch!

>
> ======
> src/test/regress/expected/publication.out
>
> 1.
> +-- Clean up
> +DROP FUNCTION test_gpt(text[], relname);
> +ERROR:  type "relname" does not exist
>
> This seems a mistake. If the DROP FUNCTION was differently written
> there there would be no error. PSA.

Fixed.

>
> ======
> src/test/regress/sql/publication.sql
>
> 2.
> +SELECT * FROM test_gpt('pub_normal', 'tbl_normal');
> +SELECT * FROM test_gpt('pub_schema', 'tbl_normal'); -- no result
>
> These tests seem strangely different from all the others because
> everything else has both a "good result" test and a "no result" test
> for every publication.
>
> So I think there should be a "no result" test for 'pub_normal'
>
> So I think there should be a "good result" test for 'pub_schema'

Added.

>
> ~~~
>
> 3.
> Consider renaming that 'tbl_parent' to something like 'tbl_root'.
> because 'parent' always makes me think of INHERITED parent/child
> tables, rather than partitioned tables and their partitions. If you do
> this, then you might also want to tweak several publication names --
> e.g. 'pub_part_parent' -> 'pub_part_root'

Hmm, there are already some queries using 'parent' in the same .sql
file. I'll leave these names.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com





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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-03-24 10:47               ` Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  3 siblings, 1 reply; 47+ messages in thread

From: Amit Kapila @ 2026-03-24 10:47 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected]

On Thu, Mar 19, 2026 at 4:59 AM Masahiko Sawada <[email protected]> wrote:
>
> On Wed, Mar 18, 2026 at 3:31 PM Masahiko Sawada <[email protected]> wrote:
> >
>
> I've attached the patch to implement this idea. The patch still
> introduces a new function but it overloads
> pg_get_publication_tables(). We might be able to handle different
> input (array or text) in pg_get_publication_tables() better, but it's
> enough for discussion at least.
>

*
+ /*
+ * We can pass relid to pg_get_publication_table_info() since
+ * version 19.
+ */
+ appendStringInfo(&cmd,
+ "SELECT DISTINCT"
+ "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
+ "   THEN NULL ELSE gpt.attrs END)"
+ "  FROM pg_publication p,"
+ "  LATERAL pg_get_publication_tables(p.pubname, %u) gpt,"
+ "  pg_class c"
+ " WHERE c.oid = gpt.relid"
+ "   AND p.pubname IN ( %s )",
+ lrel->remoteid,
+ pub_names->data);

Why in the above query we need a join with pg_publication? Can't we
directly pass 'pub_names' and 'relid' to pg_get_publication_tables()
to get the required information?


--
With Regards,
Amit Kapila.





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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
@ 2026-03-24 18:57                 ` Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  0 siblings, 1 reply; 47+ messages in thread

From: Masahiko Sawada @ 2026-03-24 18:57 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected]

On Tue, Mar 24, 2026 at 3:47 AM Amit Kapila <[email protected]> wrote:
>
> On Thu, Mar 19, 2026 at 4:59 AM Masahiko Sawada <[email protected]> wrote:
> >
> > On Wed, Mar 18, 2026 at 3:31 PM Masahiko Sawada <[email protected]> wrote:
> > >
> >
> > I've attached the patch to implement this idea. The patch still
> > introduces a new function but it overloads
> > pg_get_publication_tables(). We might be able to handle different
> > input (array or text) in pg_get_publication_tables() better, but it's
> > enough for discussion at least.
> >
>
> *
> + /*
> + * We can pass relid to pg_get_publication_table_info() since
> + * version 19.
> + */
> + appendStringInfo(&cmd,
> + "SELECT DISTINCT"
> + "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
> + "   THEN NULL ELSE gpt.attrs END)"
> + "  FROM pg_publication p,"
> + "  LATERAL pg_get_publication_tables(p.pubname, %u) gpt,"
> + "  pg_class c"
> + " WHERE c.oid = gpt.relid"
> + "   AND p.pubname IN ( %s )",
> + lrel->remoteid,
> + pub_names->data);
>
> Why in the above query we need a join with pg_publication? Can't we
> directly pass 'pub_names' and 'relid' to pg_get_publication_tables()
> to get the required information?

Since the 'pub_names' is the list of publication names we cannot
directly pass it to the pg_get_publication_tables(). But if we make
pg_get_publication_tables() take {pubname text[], target_relid oid}
instead of {pubname text, target_relid oid}, yes. And it seems to help
somewhat simplify the patch. If having both
pg_get_publication_tables(VARIADIC text[]) and
pg_get_publication_tables(text[], oid) is not odd, it would be worth
trying it.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com





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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-03-25 05:06                   ` Masahiko Sawada <[email protected]>
  2026-03-25 08:48                     ` Re: Initial COPY of Logical Replication is too slow Peter Smith <[email protected]>
  2026-03-26 05:44                     ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-26 08:35                     ` RE: Initial COPY of Logical Replication is too slow Hayato Kuroda (Fujitsu) <[email protected]>
  2026-03-26 10:43                     ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  0 siblings, 4 replies; 47+ messages in thread

From: Masahiko Sawada @ 2026-03-25 05:06 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected]

On Tue, Mar 24, 2026 at 11:57 AM Masahiko Sawada <[email protected]> wrote:
>
> On Tue, Mar 24, 2026 at 3:47 AM Amit Kapila <[email protected]> wrote:
> >
> > On Thu, Mar 19, 2026 at 4:59 AM Masahiko Sawada <[email protected]> wrote:
> > >
> > > On Wed, Mar 18, 2026 at 3:31 PM Masahiko Sawada <[email protected]> wrote:
> > > >
> > >
> > > I've attached the patch to implement this idea. The patch still
> > > introduces a new function but it overloads
> > > pg_get_publication_tables(). We might be able to handle different
> > > input (array or text) in pg_get_publication_tables() better, but it's
> > > enough for discussion at least.
> > >
> >
> > *
> > + /*
> > + * We can pass relid to pg_get_publication_table_info() since
> > + * version 19.
> > + */
> > + appendStringInfo(&cmd,
> > + "SELECT DISTINCT"
> > + "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
> > + "   THEN NULL ELSE gpt.attrs END)"
> > + "  FROM pg_publication p,"
> > + "  LATERAL pg_get_publication_tables(p.pubname, %u) gpt,"
> > + "  pg_class c"
> > + " WHERE c.oid = gpt.relid"
> > + "   AND p.pubname IN ( %s )",
> > + lrel->remoteid,
> > + pub_names->data);
> >
> > Why in the above query we need a join with pg_publication? Can't we
> > directly pass 'pub_names' and 'relid' to pg_get_publication_tables()
> > to get the required information?
>
> Since the 'pub_names' is the list of publication names we cannot
> directly pass it to the pg_get_publication_tables(). But if we make
> pg_get_publication_tables() take {pubname text[], target_relid oid}
> instead of {pubname text, target_relid oid}, yes. And it seems to help
> somewhat simplify the patch. If having both
> pg_get_publication_tables(VARIADIC text[]) and
> pg_get_publication_tables(text[], oid) is not odd, it would be worth
> trying it.
>

I figured out that the join with pg_publication works as a filter;
non-existence publication names are not passed to the function. If we
pass the list of publication names to the new function signature,
while we can simplify the patch and avoid a join, we would change the
existing function behavior so that it ignores non-existence
publications.

I've attached the updated patch. The 0001 patch just incorporated the
review comments so far, and the 0002 patch is a draft change for the
above idea. Since pg_get_publication_tables(VARIADIC text) is not a
documented function, I think we can accept small behavior changes. So
I'm going to go with this direction. Feedback is very welcome.

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com


Attachments:

  [text/x-patch] v4-0002-POC-pass-the-list-of-publications-to-pg_get_publi.patch (21.1K, 2-v4-0002-POC-pass-the-list-of-publications-to-pg_get_publi.patch)
  download | inline diff:
From 2bcf744710589e88bfcdb370ba5c2b098cbdae9c Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
Date: Tue, 24 Mar 2026 20:59:26 -0700
Subject: [PATCH v4 2/2] POC: pass the list of publications to
 pg_get_publication_tables().

---
 src/backend/catalog/pg_publication.c        | 140 ++++++++++----------
 src/backend/replication/logical/tablesync.c |  26 ++--
 src/include/catalog/pg_proc.dat             |   6 +-
 src/test/regress/expected/publication.out   |  62 ++++++---
 src/test/regress/sql/publication.sql        |  49 ++++---
 5 files changed, 154 insertions(+), 129 deletions(-)

diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index f4649dbd8b9..181f999916c 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -1377,7 +1377,6 @@ is_table_publishable_in_publication(Oid relid, Publication *pub)
  * Helper function to get information of the tables in the given
  * publication(s).
  *
- * The parameters pubnames and {pubname, target_relid} are mutually exclusive.
  * If target_relid is provided, the function returns information only for that
  * specific table. Otherwise, if returns information for all tables within the
  * specified publications.
@@ -1386,7 +1385,7 @@ is_table_publishable_in_publication(Oid relid, Publication *pub)
  */
 static Datum
 pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
-						  text *pubname, Oid target_relid)
+						  Oid target_relid)
 {
 #define NUM_PUBLICATION_TABLES_ELEM	4
 	FuncCallContext *funcctx;
@@ -1397,6 +1396,10 @@ pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
 	{
 		TupleDesc	tupdesc;
 		MemoryContext oldcontext;
+		Datum	   *elems;
+		int			nelems,
+					i;
+		bool		viaroot = false;
 
 		/* create a function context for cross-call persistence */
 		funcctx = SRF_FIRSTCALL_INIT();
@@ -1404,49 +1407,37 @@ pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
 		/* switch to memory context appropriate for multiple function calls */
 		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
 
-		if (pubname != NULL)
-		{
-			/* Try to retrieve the specified table information */
-			if (SearchSysCacheExists1(RELOID, target_relid))
-			{
-				Publication *pub;
-
-				pub = GetPublicationByName(text_to_cstring(pubname), false);
+		Assert(pubnames != NULL);
 
-				if (is_table_publishable_in_publication(target_relid, pub))
-				{
-					published_rel *table_info = palloc_object(published_rel);
+		/*
+		 * Deconstruct the parameter into elements where each element is a
+		 * publication name.
+		 */
+		deconstruct_array_builtin(pubnames, TEXTOID, &elems, NULL, &nelems);
 
-					table_info->relid = target_relid;
-					table_info->pubid = pub->oid;
-					table_infos = lappend(table_infos, table_info);
-				}
-			}
-		}
-		else
+		/* Get Oids of tables from each publication. */
+		for (i = 0; i < nelems; i++)
 		{
-			Datum	   *elems;
-			int			nelems,
-						i;
-			bool		viaroot = false;
+			Publication *pub_elem;
+			List	   *pub_elem_tables = NIL;
+			ListCell   *lc;
 
-			Assert(pubnames != NULL);
+			pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]), true);
 
-			/*
-			 * Deconstruct the parameter into elements where each element is a
-			 * publication name.
-			 */
-			deconstruct_array_builtin(pubnames, TEXTOID, &elems, NULL, &nelems);
+			if (pub_elem == NULL)
+				continue;
 
-			/* Get Oids of tables from each publication. */
-			for (i = 0; i < nelems; i++)
+			if (OidIsValid(target_relid))
+			{
+				/* Try to retrieve the specified table information */
+				if (SearchSysCacheExists1(RELOID, target_relid) &&
+					is_table_publishable_in_publication(target_relid, pub_elem))
+				{
+					pub_elem_tables = list_make1_oid(target_relid);
+				}
+			}
+			else
 			{
-				Publication *pub_elem;
-				List	   *pub_elem_tables = NIL;
-				ListCell   *lc;
-
-				pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]), false);
-
 				/*
 				 * Publications support partitioned tables. If
 				 * publish_via_partition_root is false, all changes are
@@ -1473,45 +1464,45 @@ pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
 																	PUBLICATION_PART_LEAF);
 					pub_elem_tables = list_concat_unique_oid(relids, schemarelids);
 				}
+			}
 
-				/*
-				 * Record the published table and the corresponding
-				 * publication so that we can get row filters and column lists
-				 * later.
-				 *
-				 * When a table is published by multiple publications, to
-				 * obtain all row filters and column lists, the structure
-				 * related to this table will be recorded multiple times.
-				 */
-				foreach(lc, pub_elem_tables)
-				{
-					published_rel *table_info = palloc_object(published_rel);
-
-					table_info->relid = lfirst_oid(lc);
-					table_info->pubid = pub_elem->oid;
-					table_infos = lappend(table_infos, table_info);
-				}
+			/*
+			 * Record the published table and the corresponding
+			 * publication so that we can get row filters and column lists
+			 * later.
+			 *
+			 * When a table is published by multiple publications, to
+			 * obtain all row filters and column lists, the structure
+			 * related to this table will be recorded multiple times.
+			 */
+			foreach(lc, pub_elem_tables)
+			{
+				published_rel *table_info = palloc_object(published_rel);
 
-				/*
-				 * At least one publication is using
-				 * publish_via_partition_root.
-				 */
-				if (pub_elem->pubviaroot)
-					viaroot = true;
+				table_info->relid = lfirst_oid(lc);
+				table_info->pubid = pub_elem->oid;
+				table_infos = lappend(table_infos, table_info);
 			}
 
 			/*
-			 * If the publication publishes partition changes via their
-			 * respective root partitioned tables, we must exclude partitions
-			 * in favor of including the root partitioned tables. Otherwise,
-			 * the function could return both the child and parent tables
-			 * which could cause data of the child table to be
-			 * double-published on the subscriber side.
+			 * At least one publication is using
+			 * publish_via_partition_root.
 			 */
-			if (viaroot)
-				filter_partitions(table_infos);
+			if (pub_elem->pubviaroot)
+				viaroot = true;
 		}
 
+		/*
+		 * If the publication publishes partition changes via their
+		 * respective root partitioned tables, we must exclude partitions
+		 * in favor of including the root partitioned tables. Otherwise,
+		 * the function could return both the child and parent tables
+		 * which could cause data of the child table to be
+		 * double-published on the subscriber side.
+		 */
+		if (viaroot)
+			filter_partitions(table_infos);
+
 		/* Construct a tuple descriptor for the result rows. */
 		tupdesc = CreateTemplateTupleDesc(NUM_PUBLICATION_TABLES_ELEM);
 		TupleDescInitEntry(tupdesc, (AttrNumber) 1, "pubid",
@@ -1640,14 +1631,21 @@ Datum
 pg_get_publication_tables_a(PG_FUNCTION_ARGS)
 {
 	/* Get the information of the tables in the given publications */
-	return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0), NULL, InvalidOid);
+	return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0), InvalidOid);
 }
 
 Datum
 pg_get_publication_tables_b(PG_FUNCTION_ARGS)
 {
+	Oid relid = PG_GETARG_OID(1);
+
+	if (!OidIsValid(relid))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("invalid relation OID %u", relid)));
+
 	/* Get the information of the specified table in the given publication */
-	return pg_get_publication_tables(fcinfo, NULL, PG_GETARG_TEXT_P(0), PG_GETARG_OID(1));
+	return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0), relid);
 }
 
 /*
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index ec8840ebf42..d70c172e0f5 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -802,20 +802,18 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		if (server_version >= 190000)
 		{
 			/*
-			 * We can pass relid to pg_get_publication_table() since version
-			 * 19.
+			 * We can pass both publication names and relid to
+			 * pg_get_publication_table() since version 19.
 			 */
 			appendStringInfo(&cmd,
 							 "SELECT DISTINCT"
 							 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
 							 "   THEN NULL ELSE gpt.attrs END)"
-							 "  FROM pg_publication p,"
-							 "  LATERAL pg_get_publication_tables(p.pubname, %u) gpt,"
+							 "  FROM pg_get_publication_tables(ARRAY[%s], %u) gpt,"
 							 "  pg_class c"
-							 " WHERE c.oid = gpt.relid"
-							 "   AND p.pubname IN ( %s )",
-							 lrel->remoteid,
-							 pub_names->data);
+							 " WHERE c.oid = gpt.relid",
+							 pub_names->data,
+							 lrel->remoteid);
 		}
 		else
 			appendStringInfo(&cmd,
@@ -1006,16 +1004,14 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		if (server_version >= 190000)
 		{
 			/*
-			 * We can pass relid to pg_get_publication_table() since version
-			 * 19.
+			 * We can pass both publication names and relid to
+			 * pg_get_publication_table() since version 19.
 			 */
 			appendStringInfo(&cmd,
 							 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
-							 "  FROM pg_publication p,"
-							 "  LATERAL pg_get_publication_tables(p.pubname, %u) gpt"
-							 " WHERE p.pubname IN ( %s )",
-							 lrel->remoteid,
-							 pub_names->data);
+							 "  FROM pg_get_publication_tables(ARRAY[%s], %u) gpt",
+							 pub_names->data,
+							 lrel->remoteid);
 		}
 		else
 			appendStringInfo(&cmd,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 6c23f36495f..33729d9573a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12473,10 +12473,10 @@
   descr => 'get information of the specified table that is part of the specified publication',
   proname => 'pg_get_publication_tables', prorows => '1',
   proretset => 't', provolatile => 's',
-  prorettype => 'record', proargtypes => 'text oid',
-  proallargtypes => '{text,oid,oid,oid,int2vector,pg_node_tree}',
+  prorettype => 'record', proargtypes => '_text oid',
+  proallargtypes => '{_text,oid,oid,oid,int2vector,pg_node_tree}',
   proargmodes => '{i,i,o,o,o,o}',
-  proargnames => '{pubname,target_relid,pubid,relid,attrs,qual}',
+  proargnames => '{pubnames,target_relid,pubid,relid,attrs,qual}',
   prosrc => 'pg_get_publication_tables_b' },
 { oid => '8052', descr => 'get OIDs of sequences in a publication',
   proname => 'pg_get_publication_sequences', prorows => '1000', proretset => 't',
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 2c859de6c5e..c5f8e045307 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2287,7 +2287,7 @@ CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition
 CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true);
 CREATE PUBLICATION pub_part_parent_novia_root FOR TABLE tbl_parent WITH (publish_via_partition_root = false);
 RESET client_min_messages;
-CREATE FUNCTION test_gpt(pubname text, relname text)
+CREATE FUNCTION test_gpt(pubnames text[], relname text)
 RETURNS TABLE (
   pubname text,
   relname name,
@@ -2296,104 +2296,125 @@ RETURNS TABLE (
 )
 BEGIN ATOMIC
   SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid)
-    FROM pg_get_publication_tables(pubname, relname::regclass::oid) gpt
+    FROM pg_get_publication_tables(pubnames, relname::regclass::oid) gpt
     JOIN pg_publication p ON p.oid = gpt.pubid
     JOIN pg_class c ON c.oid = gpt.relid
   ORDER BY p.pubname, c.relname;
 END;
-SELECT * FROM test_gpt('pub_normal', 'tbl_normal');
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'tbl_normal');
   pubname   |  relname   | attrs |   qual    
 ------------+------------+-------+-----------
  pub_normal | tbl_normal | 1     | (id < 10)
 (1 row)
 
-SELECT * FROM test_gpt('pub_normal', 'gpt_test_sch.tbl_sch'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'gpt_test_sch.tbl_sch'); -- no result
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
 (0 rows)
 
-SELECT * FROM test_gpt('pub_schema', 'gpt_test_sch.tbl_sch');
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'gpt_test_sch.tbl_sch');
   pubname   | relname | attrs | qual 
 ------------+---------+-------+------
  pub_schema | tbl_sch | 1     | 
 (1 row)
 
-SELECT * FROM test_gpt('pub_schema', 'tbl_normal'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'tbl_normal'); -- no result
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
 (0 rows)
 
-SELECT * FROM test_gpt('pub_part_parent', 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_parent');
      pubname     |  relname   | attrs |    qual    
 -----------------+------------+-------+------------
  pub_part_parent | tbl_parent | 1 2   | (id1 = 10)
 (1 row)
 
-SELECT * FROM test_gpt('pub_part_parent', 'tbl_part1'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_part1'); -- no result
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
 (0 rows)
 
-SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_parent'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_novia_root'], 'tbl_parent'); -- no result
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
 (0 rows)
 
-SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_novia_root'], 'tbl_part1');
           pubname           |  relname  | attrs | qual 
 ----------------------------+-----------+-------+------
  pub_part_parent_novia_root | tbl_part1 | 1 2 3 | 
 (1 row)
 
-SELECT * FROM test_gpt('pub_part_leaf', 'tbl_parent'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_parent'); -- no result
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
 (0 rows)
 
-SELECT * FROM test_gpt('pub_part_leaf', 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_part1');
     pubname    |  relname  | attrs | qual 
 ---------------+-----------+-------+------
  pub_part_leaf | tbl_part1 | 1 2 3 | 
 (1 row)
 
-SELECT * FROM test_gpt('pub_all', 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_parent');
  pubname |  relname   | attrs | qual 
 ---------+------------+-------+------
  pub_all | tbl_parent | 1 2 3 | 
 (1 row)
 
-SELECT * FROM test_gpt('pub_all', 'tbl_part1'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_part1'); -- no result
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
 (0 rows)
 
-SELECT * FROM test_gpt('pub_all_except', 'tbl_normal');
+-- two rows with different row filter
+SELECT * FROM test_gpt(ARRAY['pub_all', 'pub_normal'], 'tbl_normal');
+  pubname   |  relname   | attrs |   qual    
+------------+------------+-------+-----------
+ pub_all    | tbl_normal | 1     | 
+ pub_normal | tbl_normal | 1     | (id < 10)
+(2 rows)
+
+-- one row with 'pub_part_parent'
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_part_parent_novia_root'], 'tbl_parent');
+     pubname     |  relname   | attrs |    qual    
+-----------------+------------+-------+------------
+ pub_part_parent | tbl_parent | 1 2   | (id1 = 10)
+(1 row)
+
+-- no result, partitions are excluded
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_all'], 'tbl_part1');
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_normal');
     pubname     |  relname   | attrs | qual 
 ----------------+------------+-------+------
  pub_all_except | tbl_normal | 1     | 
 (1 row)
 
-SELECT * FROM test_gpt('pub_all_except', 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
 (0 rows)
 
-SELECT * FROM test_gpt('pub_all_except', 'tbl_parent'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_parent'); -- no result (excluded)
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
 (0 rows)
 
-SELECT * FROM test_gpt('pub_all_except', 'tbl_part1'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_part1'); -- no result (excluded)
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
 (0 rows)
 
-SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_parent'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_all_novia_root'], 'tbl_parent'); -- no result
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
 (0 rows)
 
-SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_all_novia_root'], 'tbl_part1');
       pubname       |  relname  | attrs | qual 
 --------------------+-----------+-------+------
  pub_all_novia_root | tbl_part1 | 1 2 3 | 
@@ -2401,6 +2422,7 @@ SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_part1');
 
 -- Clean up
 DROP FUNCTION test_gpt(text, text);
+ERROR:  function test_gpt(text, text) does not exist
 DROP PUBLICATION pub_all;
 DROP PUBLICATION pub_all_novia_root;
 DROP PUBLICATION pub_all_except;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index c1c83f7d701..2016c0aac08 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1447,7 +1447,7 @@ CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 =
 CREATE PUBLICATION pub_part_parent_novia_root FOR TABLE tbl_parent WITH (publish_via_partition_root = false);
 RESET client_min_messages;
 
-CREATE FUNCTION test_gpt(pubname text, relname text)
+CREATE FUNCTION test_gpt(pubnames text[], relname text)
 RETURNS TABLE (
   pubname text,
   relname name,
@@ -1456,37 +1456,46 @@ RETURNS TABLE (
 )
 BEGIN ATOMIC
   SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid)
-    FROM pg_get_publication_tables(pubname, relname::regclass::oid) gpt
+    FROM pg_get_publication_tables(pubnames, relname::regclass::oid) gpt
     JOIN pg_publication p ON p.oid = gpt.pubid
     JOIN pg_class c ON c.oid = gpt.relid
   ORDER BY p.pubname, c.relname;
 END;
 
-SELECT * FROM test_gpt('pub_normal', 'tbl_normal');
-SELECT * FROM test_gpt('pub_normal', 'gpt_test_sch.tbl_sch'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'tbl_normal');
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'gpt_test_sch.tbl_sch'); -- no result
 
-SELECT * FROM test_gpt('pub_schema', 'gpt_test_sch.tbl_sch');
-SELECT * FROM test_gpt('pub_schema', 'tbl_normal'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'gpt_test_sch.tbl_sch');
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'tbl_normal'); -- no result
 
-SELECT * FROM test_gpt('pub_part_parent', 'tbl_parent');
-SELECT * FROM test_gpt('pub_part_parent', 'tbl_part1'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_part1'); -- no result
 
-SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_parent'); -- no result
-SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_novia_root'], 'tbl_parent'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_novia_root'], 'tbl_part1');
 
-SELECT * FROM test_gpt('pub_part_leaf', 'tbl_parent'); -- no result
-SELECT * FROM test_gpt('pub_part_leaf', 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_parent'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_part1');
 
-SELECT * FROM test_gpt('pub_all', 'tbl_parent');
-SELECT * FROM test_gpt('pub_all', 'tbl_part1'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_part1'); -- no result
 
-SELECT * FROM test_gpt('pub_all_except', 'tbl_normal');
-SELECT * FROM test_gpt('pub_all_except', 'gpt_test_sch.tbl_sch'); -- no result (excluded)
-SELECT * FROM test_gpt('pub_all_except', 'tbl_parent'); -- no result (excluded)
-SELECT * FROM test_gpt('pub_all_except', 'tbl_part1'); -- no result (excluded)
+-- two rows with different row filter
+SELECT * FROM test_gpt(ARRAY['pub_all', 'pub_normal'], 'tbl_normal');
 
-SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_parent'); -- no result
-SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_part1');
+-- one row with 'pub_part_parent'
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_part_parent_novia_root'], 'tbl_parent');
+
+-- no result, partitions are excluded
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_all'], 'tbl_part1');
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_normal');
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_parent'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_part1'); -- no result (excluded)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_novia_root'], 'tbl_parent'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_all_novia_root'], 'tbl_part1');
 
 -- Clean up
 DROP FUNCTION test_gpt(text, text);
-- 
2.53.0



  [text/x-patch] v4-0001-Avoid-full-table-scans-when-getting-publication-t.patch (26.0K, 3-v4-0001-Avoid-full-table-scans-when-getting-publication-t.patch)
  download | inline diff:
From adb8822ddd5fe1f5f616d31512930d62358a272c Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
Date: Fri, 27 Feb 2026 15:42:38 -0800
Subject: [PATCH v4 1/2] Avoid full table scans when getting publication table
 information by tablesync workers.

Reported-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zhijie Hou (Fujitsu) <[email protected]>
Reviewed-by: Matheus Alcantara <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Discussion: https://postgr.es/m/CAB-JLwbBFNuASyEnZWP0Tck9uNkthBZqi6WoXNevUT6+mV8XmA@mail.gmail.com
---
 src/backend/catalog/pg_publication.c        | 299 +++++++++++++++-----
 src/backend/replication/logical/tablesync.c |  74 +++--
 src/include/catalog/pg_proc.dat             |  11 +-
 src/test/regress/expected/publication.out   | 141 +++++++++
 src/test/regress/sql/publication.sql        |  72 +++++
 5 files changed, 507 insertions(+), 90 deletions(-)

diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index c92ff3f51c3..f4649dbd8b9 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -1264,12 +1264,129 @@ GetPublicationByName(const char *pubname, bool missing_ok)
 }
 
 /*
- * Get information of the tables in the given publication array.
+ * Returns true if the table of the given relid is published for the specified
+ * publication.
+ *
+ * This function evaluates the effective published OID based on the
+ * publish_via_partition_root setting, rather than just checking catalog entries
+ * (e.g., pg_publication_rel). For instance, when publish_via_partition_root is
+ * false, it returns false for a parent partitioned table and true for its leaf
+ * partitions, even if the parent is the one explicitly added to the publication.
+ *
+ * For performance reasons, this function avoids the overhead of constructing
+ * the complete list of published tables during the evaluation. It can execute
+ * quickly even when the publication contains a large number of relations.
+ */
+static bool
+is_table_publishable_in_publication(Oid relid, Publication *pub)
+{
+	if (pub->pubviaroot)
+	{
+		if (pub->alltables)
+		{
+			/*
+			 * ALL TABLE publications with pubviaroot=true include only tables
+			 * that are either regular tables or top-most partitioned tables.
+			 */
+			if (get_rel_relispartition(relid))
+				return false;
+
+			/*
+			 * Check if the table is specified in the EXCEPT clause in the
+			 * publication. ALL TABLE publications have pg_publication_rel
+			 * entries only for EXCEPT'ed tables, so it's sufficient to check
+			 * the existence of its entry.
+			 */
+			return !SearchSysCacheExists2(PUBLICATIONRELMAP,
+										  ObjectIdGetDatum(relid),
+										  ObjectIdGetDatum(pub->oid));
+		}
+
+		/*
+		 * Check if its corresponding entry exists either in
+		 * pg_publication_rel or pg_publication_namespace.
+		 */
+		return (SearchSysCacheExists2(PUBLICATIONRELMAP,
+									  ObjectIdGetDatum(relid),
+									  ObjectIdGetDatum(pub->oid)) ||
+				SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP,
+									  ObjectIdGetDatum(get_rel_namespace(relid)),
+									  ObjectIdGetDatum(pub->oid)));
+	}
+
+	/*
+	 * For non-pubviaroot publications, partitioned table's OID can never be a
+	 * published OID.
+	 */
+	if (get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE)
+		return false;
+
+	if (pub->alltables)
+	{
+		Oid			target_relid = relid;
+
+		if (get_rel_relispartition(relid))
+		{
+			List	   *ancestors = get_partition_ancestors(relid);
+
+			/*
+			 * Only the top-most ancestor can appear in the EXCEPT clause.
+			 * Therefore, for a partition, exclusion must be evaluated at the
+			 * top-most ancestor.
+			 */
+			target_relid = llast_oid(ancestors);
+
+			list_free(ancestors);
+		}
+
+		/*
+		 * The table is published unless it's specified in the EXCEPT clause.
+		 * ALL TABLE publications have pg_publication_rel entries only for
+		 * EXCEPT'ed tables, so it's sufficient to check the existence of its
+		 * entry.
+		 */
+		return !SearchSysCacheExists2(PUBLICATIONRELMAP,
+									  ObjectIdGetDatum(target_relid),
+									  ObjectIdGetDatum(pub->oid));
+	}
+
+	if (get_rel_relispartition(relid))
+	{
+		List	   *ancestors = get_partition_ancestors(relid);
+		Oid			topmost = GetTopMostAncestorInPublication(pub->oid, ancestors,
+															  NULL);
+
+		list_free(ancestors);
+
+		/* This table is published if its ancestor is published */
+		if (OidIsValid(topmost))
+			return true;
+
+		/* The partition itself might be published, so check below */
+	}
+
+	return (SearchSysCacheExists2(PUBLICATIONRELMAP,
+								  ObjectIdGetDatum(relid),
+								  ObjectIdGetDatum(pub->oid)) ||
+			SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP,
+								  ObjectIdGetDatum(get_rel_namespace(relid)),
+								  ObjectIdGetDatum(pub->oid)));
+}
+
+/*
+ * Helper function to get information of the tables in the given
+ * publication(s).
+ *
+ * The parameters pubnames and {pubname, target_relid} are mutually exclusive.
+ * If target_relid is provided, the function returns information only for that
+ * specific table. Otherwise, if returns information for all tables within the
+ * specified publications.
  *
  * Returns pubid, relid, column list, row filter for each table.
  */
-Datum
-pg_get_publication_tables(PG_FUNCTION_ARGS)
+static Datum
+pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
+						  text *pubname, Oid target_relid)
 {
 #define NUM_PUBLICATION_TABLES_ELEM	4
 	FuncCallContext *funcctx;
@@ -1280,11 +1397,6 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 	{
 		TupleDesc	tupdesc;
 		MemoryContext oldcontext;
-		ArrayType  *arr;
-		Datum	   *elems;
-		int			nelems,
-					i;
-		bool		viaroot = false;
 
 		/* create a function context for cross-call persistence */
 		funcctx = SRF_FIRSTCALL_INIT();
@@ -1292,81 +1404,114 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		/* switch to memory context appropriate for multiple function calls */
 		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
 
-		/*
-		 * Deconstruct the parameter into elements where each element is a
-		 * publication name.
-		 */
-		arr = PG_GETARG_ARRAYTYPE_P(0);
-		deconstruct_array_builtin(arr, TEXTOID, &elems, NULL, &nelems);
-
-		/* Get Oids of tables from each publication. */
-		for (i = 0; i < nelems; i++)
+		if (pubname != NULL)
 		{
-			Publication *pub_elem;
-			List	   *pub_elem_tables = NIL;
-			ListCell   *lc;
+			/* Try to retrieve the specified table information */
+			if (SearchSysCacheExists1(RELOID, target_relid))
+			{
+				Publication *pub;
 
-			pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]), false);
+				pub = GetPublicationByName(text_to_cstring(pubname), false);
 
-			/*
-			 * Publications support partitioned tables. If
-			 * publish_via_partition_root is false, all changes are replicated
-			 * using leaf partition identity and schema, so we only need
-			 * those. Otherwise, get the partitioned table itself.
-			 */
-			if (pub_elem->alltables)
-				pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
-															 RELKIND_RELATION,
-															 pub_elem->pubviaroot);
-			else
-			{
-				List	   *relids,
-						   *schemarelids;
-
-				relids = GetIncludedPublicationRelations(pub_elem->oid,
-														 pub_elem->pubviaroot ?
-														 PUBLICATION_PART_ROOT :
-														 PUBLICATION_PART_LEAF);
-				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
-																pub_elem->pubviaroot ?
-																PUBLICATION_PART_ROOT :
-																PUBLICATION_PART_LEAF);
-				pub_elem_tables = list_concat_unique_oid(relids, schemarelids);
+				if (is_table_publishable_in_publication(target_relid, pub))
+				{
+					published_rel *table_info = palloc_object(published_rel);
+
+					table_info->relid = target_relid;
+					table_info->pubid = pub->oid;
+					table_infos = lappend(table_infos, table_info);
+				}
 			}
+		}
+		else
+		{
+			Datum	   *elems;
+			int			nelems,
+						i;
+			bool		viaroot = false;
+
+			Assert(pubnames != NULL);
 
 			/*
-			 * Record the published table and the corresponding publication so
-			 * that we can get row filters and column lists later.
-			 *
-			 * When a table is published by multiple publications, to obtain
-			 * all row filters and column lists, the structure related to this
-			 * table will be recorded multiple times.
+			 * Deconstruct the parameter into elements where each element is a
+			 * publication name.
 			 */
-			foreach(lc, pub_elem_tables)
+			deconstruct_array_builtin(pubnames, TEXTOID, &elems, NULL, &nelems);
+
+			/* Get Oids of tables from each publication. */
+			for (i = 0; i < nelems; i++)
 			{
-				published_rel *table_info = palloc_object(published_rel);
+				Publication *pub_elem;
+				List	   *pub_elem_tables = NIL;
+				ListCell   *lc;
+
+				pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]), false);
+
+				/*
+				 * Publications support partitioned tables. If
+				 * publish_via_partition_root is false, all changes are
+				 * replicated using leaf partition identity and schema, so we
+				 * only need those. Otherwise, get the partitioned table
+				 * itself.
+				 */
+				if (pub_elem->alltables)
+					pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
+																 RELKIND_RELATION,
+																 pub_elem->pubviaroot);
+				else
+				{
+					List	   *relids,
+							   *schemarelids;
+
+					relids = GetIncludedPublicationRelations(pub_elem->oid,
+															 pub_elem->pubviaroot ?
+															 PUBLICATION_PART_ROOT :
+															 PUBLICATION_PART_LEAF);
+					schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
+																	pub_elem->pubviaroot ?
+																	PUBLICATION_PART_ROOT :
+																	PUBLICATION_PART_LEAF);
+					pub_elem_tables = list_concat_unique_oid(relids, schemarelids);
+				}
+
+				/*
+				 * Record the published table and the corresponding
+				 * publication so that we can get row filters and column lists
+				 * later.
+				 *
+				 * When a table is published by multiple publications, to
+				 * obtain all row filters and column lists, the structure
+				 * related to this table will be recorded multiple times.
+				 */
+				foreach(lc, pub_elem_tables)
+				{
+					published_rel *table_info = palloc_object(published_rel);
 
-				table_info->relid = lfirst_oid(lc);
-				table_info->pubid = pub_elem->oid;
-				table_infos = lappend(table_infos, table_info);
+					table_info->relid = lfirst_oid(lc);
+					table_info->pubid = pub_elem->oid;
+					table_infos = lappend(table_infos, table_info);
+				}
+
+				/*
+				 * At least one publication is using
+				 * publish_via_partition_root.
+				 */
+				if (pub_elem->pubviaroot)
+					viaroot = true;
 			}
 
-			/* At least one publication is using publish_via_partition_root. */
-			if (pub_elem->pubviaroot)
-				viaroot = true;
+			/*
+			 * If the publication publishes partition changes via their
+			 * respective root partitioned tables, we must exclude partitions
+			 * in favor of including the root partitioned tables. Otherwise,
+			 * the function could return both the child and parent tables
+			 * which could cause data of the child table to be
+			 * double-published on the subscriber side.
+			 */
+			if (viaroot)
+				filter_partitions(table_infos);
 		}
 
-		/*
-		 * If the publication publishes partition changes via their respective
-		 * root partitioned tables, we must exclude partitions in favor of
-		 * including the root partitioned tables. Otherwise, the function
-		 * could return both the child and parent tables which could cause
-		 * data of the child table to be double-published on the subscriber
-		 * side.
-		 */
-		if (viaroot)
-			filter_partitions(table_infos);
-
 		/* Construct a tuple descriptor for the result rows. */
 		tupdesc = CreateTemplateTupleDesc(NUM_PUBLICATION_TABLES_ELEM);
 		TupleDescInitEntry(tupdesc, (AttrNumber) 1, "pubid",
@@ -1491,6 +1636,20 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 	SRF_RETURN_DONE(funcctx);
 }
 
+Datum
+pg_get_publication_tables_a(PG_FUNCTION_ARGS)
+{
+	/* Get the information of the tables in the given publications */
+	return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0), NULL, InvalidOid);
+}
+
+Datum
+pg_get_publication_tables_b(PG_FUNCTION_ARGS)
+{
+	/* Get the information of the specified table in the given publication */
+	return pg_get_publication_tables(fcinfo, NULL, PG_GETARG_TEXT_P(0), PG_GETARG_OID(1));
+}
+
 /*
  * Returns Oids of sequences in a publication.
  */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index f49a4852ecb..ec8840ebf42 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -798,17 +798,37 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		 * publications).
 		 */
 		resetStringInfo(&cmd);
-		appendStringInfo(&cmd,
-						 "SELECT DISTINCT"
-						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
-						 "   THEN NULL ELSE gpt.attrs END)"
-						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
-						 "  pg_class c"
-						 " WHERE gpt.relid = %u AND c.oid = gpt.relid"
-						 "   AND p.pubname IN ( %s )",
-						 lrel->remoteid,
-						 pub_names->data);
+
+		if (server_version >= 190000)
+		{
+			/*
+			 * We can pass relid to pg_get_publication_table() since version
+			 * 19.
+			 */
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT"
+							 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
+							 "   THEN NULL ELSE gpt.attrs END)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname, %u) gpt,"
+							 "  pg_class c"
+							 " WHERE c.oid = gpt.relid"
+							 "   AND p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
+		}
+		else
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT"
+							 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
+							 "   THEN NULL ELSE gpt.attrs END)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
+							 "  pg_class c"
+							 " WHERE gpt.relid = %u AND c.oid = gpt.relid"
+							 "   AND p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
 
 		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
 							 lengthof(attrsRow), attrsRow);
@@ -982,14 +1002,30 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 
 		/* Check for row filters. */
 		resetStringInfo(&cmd);
-		appendStringInfo(&cmd,
-						 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
-						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt"
-						 " WHERE gpt.relid = %u"
-						 "   AND p.pubname IN ( %s )",
-						 lrel->remoteid,
-						 pub_names->data);
+
+		if (server_version >= 190000)
+		{
+			/*
+			 * We can pass relid to pg_get_publication_table() since version
+			 * 19.
+			 */
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname, %u) gpt"
+							 " WHERE p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
+		}
+		else
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname) gpt"
+							 " WHERE gpt.relid = %u"
+							 "   AND p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
 
 		res = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data, 1, qualRow);
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..6c23f36495f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12468,7 +12468,16 @@
   proallargtypes => '{_text,oid,oid,int2vector,pg_node_tree}',
   proargmodes => '{v,o,o,o,o}',
   proargnames => '{pubname,pubid,relid,attrs,qual}',
-  prosrc => 'pg_get_publication_tables' },
+  prosrc => 'pg_get_publication_tables_a' },
+{ oid => '8060',
+  descr => 'get information of the specified table that is part of the specified publication',
+  proname => 'pg_get_publication_tables', prorows => '1',
+  proretset => 't', provolatile => 's',
+  prorettype => 'record', proargtypes => 'text oid',
+  proallargtypes => '{text,oid,oid,oid,int2vector,pg_node_tree}',
+  proargmodes => '{i,i,o,o,o,o}',
+  proargnames => '{pubname,target_relid,pubid,relid,attrs,qual}',
+  prosrc => 'pg_get_publication_tables_b' },
 { oid => '8052', descr => 'get OIDs of sequences in a publication',
   proname => 'pg_get_publication_sequences', prorows => '1000', proretset => 't',
   provolatile => 's', prorettype => 'oid', proargtypes => 'text',
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index a220f48b285..2c859de6c5e 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2271,6 +2271,147 @@ DROP TABLE testpub_merge_pk;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
+-- Test pg_get_publication_tables(text, oid) function
+CREATE SCHEMA gpt_test_sch;
+CREATE TABLE gpt_test_sch.tbl_sch (id int);
+CREATE TABLE tbl_normal (id int);
+CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1);
+CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION pub_all FOR ALL TABLES WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_all_novia_root FOR ALL TABLES WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch);
+CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch;
+CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10);
+CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_part_parent_novia_root FOR TABLE tbl_parent WITH (publish_via_partition_root = false);
+RESET client_min_messages;
+CREATE FUNCTION test_gpt(pubname text, relname text)
+RETURNS TABLE (
+  pubname text,
+  relname name,
+  attrs text,
+  qual text
+)
+BEGIN ATOMIC
+  SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid)
+    FROM pg_get_publication_tables(pubname, relname::regclass::oid) gpt
+    JOIN pg_publication p ON p.oid = gpt.pubid
+    JOIN pg_class c ON c.oid = gpt.relid
+  ORDER BY p.pubname, c.relname;
+END;
+SELECT * FROM test_gpt('pub_normal', 'tbl_normal');
+  pubname   |  relname   | attrs |   qual    
+------------+------------+-------+-----------
+ pub_normal | tbl_normal | 1     | (id < 10)
+(1 row)
+
+SELECT * FROM test_gpt('pub_normal', 'gpt_test_sch.tbl_sch'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_schema', 'gpt_test_sch.tbl_sch');
+  pubname   | relname | attrs | qual 
+------------+---------+-------+------
+ pub_schema | tbl_sch | 1     | 
+(1 row)
+
+SELECT * FROM test_gpt('pub_schema', 'tbl_normal'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_part_parent', 'tbl_parent');
+     pubname     |  relname   | attrs |    qual    
+-----------------+------------+-------+------------
+ pub_part_parent | tbl_parent | 1 2   | (id1 = 10)
+(1 row)
+
+SELECT * FROM test_gpt('pub_part_parent', 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_part1');
+          pubname           |  relname  | attrs | qual 
+----------------------------+-----------+-------+------
+ pub_part_parent_novia_root | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt('pub_part_leaf', 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_part_leaf', 'tbl_part1');
+    pubname    |  relname  | attrs | qual 
+---------------+-----------+-------+------
+ pub_part_leaf | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt('pub_all', 'tbl_parent');
+ pubname |  relname   | attrs | qual 
+---------+------------+-------+------
+ pub_all | tbl_parent | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt('pub_all', 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_all_except', 'tbl_normal');
+    pubname     |  relname   | attrs | qual 
+----------------+------------+-------+------
+ pub_all_except | tbl_normal | 1     | 
+(1 row)
+
+SELECT * FROM test_gpt('pub_all_except', 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_all_except', 'tbl_parent'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_all_except', 'tbl_part1'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_part1');
+      pubname       |  relname  | attrs | qual 
+--------------------+-----------+-------+------
+ pub_all_novia_root | tbl_part1 | 1 2 3 | 
+(1 row)
+
+-- Clean up
+DROP FUNCTION test_gpt(text, text);
+DROP PUBLICATION pub_all;
+DROP PUBLICATION pub_all_novia_root;
+DROP PUBLICATION pub_all_except;
+DROP PUBLICATION pub_schema;
+DROP PUBLICATION pub_normal;
+DROP PUBLICATION pub_part_leaf;
+DROP PUBLICATION pub_part_parent;
+DROP PUBLICATION pub_part_parent_novia_root;
+DROP TABLE tbl_normal, tbl_parent, tbl_part1;
+DROP SCHEMA gpt_test_sch CASCADE;
+NOTICE:  drop cascades to table gpt_test_sch.tbl_sch
 -- stage objects for pg_dump tests
 CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int);
 CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int);
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 22e0a30b5c7..c1c83f7d701 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1429,6 +1429,78 @@ RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
 
+-- Test pg_get_publication_tables(text, oid) function
+CREATE SCHEMA gpt_test_sch;
+CREATE TABLE gpt_test_sch.tbl_sch (id int);
+CREATE TABLE tbl_normal (id int);
+CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1);
+CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10);
+
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION pub_all FOR ALL TABLES WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_all_novia_root FOR ALL TABLES WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch);
+CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch;
+CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10);
+CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_part_parent_novia_root FOR TABLE tbl_parent WITH (publish_via_partition_root = false);
+RESET client_min_messages;
+
+CREATE FUNCTION test_gpt(pubname text, relname text)
+RETURNS TABLE (
+  pubname text,
+  relname name,
+  attrs text,
+  qual text
+)
+BEGIN ATOMIC
+  SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid)
+    FROM pg_get_publication_tables(pubname, relname::regclass::oid) gpt
+    JOIN pg_publication p ON p.oid = gpt.pubid
+    JOIN pg_class c ON c.oid = gpt.relid
+  ORDER BY p.pubname, c.relname;
+END;
+
+SELECT * FROM test_gpt('pub_normal', 'tbl_normal');
+SELECT * FROM test_gpt('pub_normal', 'gpt_test_sch.tbl_sch'); -- no result
+
+SELECT * FROM test_gpt('pub_schema', 'gpt_test_sch.tbl_sch');
+SELECT * FROM test_gpt('pub_schema', 'tbl_normal'); -- no result
+
+SELECT * FROM test_gpt('pub_part_parent', 'tbl_parent');
+SELECT * FROM test_gpt('pub_part_parent', 'tbl_part1'); -- no result
+
+SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_parent'); -- no result
+SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_part1');
+
+SELECT * FROM test_gpt('pub_part_leaf', 'tbl_parent'); -- no result
+SELECT * FROM test_gpt('pub_part_leaf', 'tbl_part1');
+
+SELECT * FROM test_gpt('pub_all', 'tbl_parent');
+SELECT * FROM test_gpt('pub_all', 'tbl_part1'); -- no result
+
+SELECT * FROM test_gpt('pub_all_except', 'tbl_normal');
+SELECT * FROM test_gpt('pub_all_except', 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+SELECT * FROM test_gpt('pub_all_except', 'tbl_parent'); -- no result (excluded)
+SELECT * FROM test_gpt('pub_all_except', 'tbl_part1'); -- no result (excluded)
+
+SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_parent'); -- no result
+SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_part1');
+
+-- Clean up
+DROP FUNCTION test_gpt(text, text);
+DROP PUBLICATION pub_all;
+DROP PUBLICATION pub_all_novia_root;
+DROP PUBLICATION pub_all_except;
+DROP PUBLICATION pub_schema;
+DROP PUBLICATION pub_normal;
+DROP PUBLICATION pub_part_leaf;
+DROP PUBLICATION pub_part_parent;
+DROP PUBLICATION pub_part_parent_novia_root;
+DROP TABLE tbl_normal, tbl_parent, tbl_part1;
+DROP SCHEMA gpt_test_sch CASCADE;
+
 -- stage objects for pg_dump tests
 CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int);
 CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int);
-- 
2.53.0



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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-03-25 08:48                     ` Peter Smith <[email protected]>
  2026-03-31 09:36                       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  3 siblings, 1 reply; 47+ messages in thread

From: Peter Smith @ 2026-03-25 08:48 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; +Cc: Amit Kapila <[email protected]>; Jan Wieck <[email protected]>; [email protected]

Hi Swada-San. Here are some minor review comments for v4-0001/2 combined.

======
src/backend/catalog/pg_publication.c

is_table_publishable_in_publication:

1.
This function logic has a format like

if (cond)
{
 ...
 return;
}

if (cond2)
{
 ...
 return;
}

etc.

There are many return points, and most of those "if" blocks cannot
fall through (they return).

I found it slightly difficult to read the code because I kept having
to think, "OK, if we reached here, it means pubviaroot must be false,"
or "OK, if we reached this far, then puballtables must be false, and
pubviaroot must be false," etc.

Maybe a few more if/else, or a few Assert() can make it easier to
understand how we reached points deeper in this function.

~~~

pg_get_publication_tables:

2.
Several code comments appear to wrap prematurely. It looks as if
pg_indent was run when the code was different to what it is now.

~~~

pg_get_publication_tables_b:

3.
  /* Get the information of the specified table in the given publication */
- return pg_get_publication_tables(fcinfo, NULL, PG_GETARG_TEXT_P(0),
PG_GETARG_OID(1));
+ return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0), relid);

Should that comment be plural now, the same as the other one?
/publication/publications/

======
src/include/catalog/pg_proc.dat

4.
proargnames => '{pubname,pubid,relid,attrs,qual}',
prosrc => 'pg_get_publication_tables_a' },

Shouldn’t the original function proargnames here also be calling the
first arg 'pubnames' instead of 'pubname'

======
src/test/regress/sql/publication.sql

5.
-- Test pg_get_publication_tables(text, oid) function

Should that comment now say text[] instead of text?

~~~

6.
Many of those tests have a "good result" test and a "no result" test
for each publication. It might be better if they were consistently in
the same order (eg, good then none, or none then good).

~~~

7.
Only 3 of the tests are passing multiple publications. Maybe those can
be separated from the others (e.g. put last, just to keep all the ones
that look alike together).

======
Kind Regards,
Peter Smith.
Fujitsu Australia





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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 08:48                     ` Re: Initial COPY of Logical Replication is too slow Peter Smith <[email protected]>
@ 2026-03-31 09:36                       ` Amit Kapila <[email protected]>
  2026-03-31 12:07                         ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-31 17:28                         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  0 siblings, 2 replies; 47+ messages in thread

From: Amit Kapila @ 2026-03-31 09:36 UTC (permalink / raw)
  To: Peter Smith <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Jan Wieck <[email protected]>; [email protected]

On Wed, Mar 25, 2026 at 2:19 PM Peter Smith <[email protected]> wrote:
>
> Hi Swada-San. Here are some minor review comments for v4-0001/2 combined.
>
> ======
> src/backend/catalog/pg_publication.c
>
> is_table_publishable_in_publication:
>
> 1.
> This function logic has a format like
>
> if (cond)
> {
>  ...
>  return;
> }
>
> if (cond2)
> {
>  ...
>  return;
> }
>
> etc.
>
> There are many return points, and most of those "if" blocks cannot
> fall through (they return).
>
> I found it slightly difficult to read the code because I kept having
> to think, "OK, if we reached here, it means pubviaroot must be false,"
> or "OK, if we reached this far, then puballtables must be false, and
> pubviaroot must be false," etc.
>

I can't say exactly why, but I find it difficult to read this
function. So, I share your concerns about the code of this function.
Because of its complexity it is difficult to ascertain that the
functionality is correct or we missed something. Also, considering it
is correct today, in its current form, it may become difficult to
enhance it in future.

One more comment on latest patch:
*
+static Datum
+pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
+                          Oid target_relid, bool filter_by_relid,

Why do we need filter_by_relid as a separate parameter? Isn't the
valid value of target_relid the same? If so, can't we use target_relid
for the required checks?

--
With Regards,
Amit Kapila.





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

* RE: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 08:48                     ` Re: Initial COPY of Logical Replication is too slow Peter Smith <[email protected]>
  2026-03-31 09:36                       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
@ 2026-03-31 12:07                         ` Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-31 19:40                           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  1 sibling, 1 reply; 47+ messages in thread

From: Zhijie Hou (Fujitsu) @ 2026-03-31 12:07 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; Peter Smith <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>

On Tuesday, March 31, 2026 5:36 PM Amit Kapila <[email protected]> wrote:
> 
> On Wed, Mar 25, 2026 at 2:19 PM Peter Smith <[email protected]>
> wrote:
> >
> > There are many return points, and most of those "if" blocks cannot
> > fall through (they return).
> >
> > I found it slightly difficult to read the code because I kept having
> > to think, "OK, if we reached here, it means pubviaroot must be false,"
> > or "OK, if we reached this far, then puballtables must be false, and
> > pubviaroot must be false," etc.
> >
> 
> I can't say exactly why, but I find it difficult to read this function. So, I share
> your concerns about the code of this function.
> Because of its complexity it is difficult to ascertain that the functionality is
> correct or we missed something. Also, considering it is correct today, in its
> current form, it may become difficult to enhance it in future.
> 

I attempted to refactor the code a bit based on my preferred style, as shown in
the attachment. While the number of return points couldn't be reduced, I tried
to eliminate if-else branches where possible. Sharing this top-up patch as a
reference for an alternative style that reduces code size.

Best Regards,
Hou zj


Attachments:

  [application/octet-stream] v1-0001-refactor-the-function.patch (3.5K, 2-v1-0001-refactor-the-function.patch)
  download | inline diff:
From 706d7cb4b3ac7f30f131c64f859cabe00773b07d Mon Sep 17 00:00:00 2001
From: Zhijie Hou <[email protected]>
Date: Tue, 31 Mar 2026 19:35:44 +0800
Subject: [PATCH v1] refactor the function

---
 src/backend/catalog/pg_publication.c | 79 +++++++++-------------------
 1 file changed, 26 insertions(+), 53 deletions(-)

diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 14ae03fc0ff..a036cd8ff33 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -1312,75 +1312,48 @@ static bool
 is_table_publishable_in_publication(Oid relid, Publication *pub)
 {
 	bool		relispartition;
+	List	   *ancestors = NIL;
+	Oid			topmost = InvalidOid;
 
 	/*
 	 * For non-pubviaroot publications, a partitioned table is never the
 	 * effective published OID; only its leaf partitions can be.
 	 */
-	if (!pub->pubviaroot && get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE)
+	if (!pub->pubviaroot &&
+		get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE)
 		return false;
 
 	relispartition = get_rel_relispartition(relid);
 
-	if (pub->alltables)
-	{
-		Oid			target_relid = relid;
-
-		if (pub->pubviaroot)
-		{
-			/*
-			 * ALL TABLES with pubviaroot includes only regular tables or
-			 * top-most partitioned tables -- never child partitions.
-			 */
-			if (relispartition)
-				return false;
-		}
-		else if (relispartition)
-		{
-			List	   *ancestors = get_partition_ancestors(relid);
+	/*
+	 * ALL TABLES with pubviaroot includes only regular tables or
+	 * top-most partitioned tables -- never child partitions.
+	 */
+	if (pub->alltables && pub->pubviaroot && relispartition)
+		return false;
 
-			/*
-			 * Only the top-most ancestor can appear in the EXCEPT clause.
-			 * Therefore, for a partition, exclusion must be evaluated at the
-			 * top-most ancestor.
-			 */
-			target_relid = llast_oid(ancestors);
-			list_free(ancestors);
-		}
+	if (relispartition)
+		ancestors = get_partition_ancestors(relid);
 
-		/*
-		 * The table is published unless it appears in the EXCEPT clause. ALL
-		 * TABLES publications store only EXCEPT'ed tables in
-		 * pg_publication_rel, so checking existence is sufficient.
-		 */
+	/*
+	 * The table is published unless it appears in the EXCEPT clause. ALL
+	 * TABLES publications store only EXCEPT'ed tables in
+	 * pg_publication_rel, so checking existence is sufficient.
+	 */
+	if (pub->alltables)
 		return !SearchSysCacheExists2(PUBLICATIONRELMAP,
-									  ObjectIdGetDatum(target_relid),
+									  ObjectIdGetDatum(ancestors
+													   ? llast_oid(ancestors) : relid),
 									  ObjectIdGetDatum(pub->oid));
-	}
 
 	/*
-	 * Non-alltables
+	 * If pubviaroot is true, the ancestor is published instead of the
+	 * partition, so exclude it. Otherwise, the ancestor covers the partition,
+	 * so include it.
 	 */
-
-	if (relispartition)
-	{
-		List	   *ancestors = get_partition_ancestors(relid);
-		Oid			topmost = GetTopMostAncestorInPublication(pub->oid, ancestors, NULL);
-
-		list_free(ancestors);
-
-		if (OidIsValid(topmost))
-		{
-			/*
-			 * If pubviaroot is true, the ancestor is published instead of the
-			 * partition, so exclude it. Otherwise, the ancestor covers the
-			 * partition, so include it.
-			 */
-			return !pub->pubviaroot;
-		}
-
-		/* Ancestor not published; fall through to check the partition itself */
-	}
+	if (relispartition &&
+		OidIsValid(GetTopMostAncestorInPublication(pub->oid, ancestors, NULL)))
+		return !pub->pubviaroot;
 
 	/*
 	 * Check whether the table is explicitly published via pg_publication_rel
-- 
2.53.0.windows.2



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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 08:48                     ` Re: Initial COPY of Logical Replication is too slow Peter Smith <[email protected]>
  2026-03-31 09:36                       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-31 12:07                         ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
@ 2026-03-31 19:40                           ` Masahiko Sawada <[email protected]>
  2026-04-01 22:23                             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  0 siblings, 1 reply; 47+ messages in thread

From: Masahiko Sawada @ 2026-03-31 19:40 UTC (permalink / raw)
  To: Zhijie Hou (Fujitsu) <[email protected]>; +Cc: Amit Kapila <[email protected]>; Peter Smith <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>

On Tue, Mar 31, 2026 at 5:07 AM Zhijie Hou (Fujitsu)
<[email protected]> wrote:
>
> On Tuesday, March 31, 2026 5:36 PM Amit Kapila <[email protected]> wrote:
> >
> > On Wed, Mar 25, 2026 at 2:19 PM Peter Smith <[email protected]>
> > wrote:
> > >
> > > There are many return points, and most of those "if" blocks cannot
> > > fall through (they return).
> > >
> > > I found it slightly difficult to read the code because I kept having
> > > to think, "OK, if we reached here, it means pubviaroot must be false,"
> > > or "OK, if we reached this far, then puballtables must be false, and
> > > pubviaroot must be false," etc.
> > >
> >
> > I can't say exactly why, but I find it difficult to read this function. So, I share
> > your concerns about the code of this function.
> > Because of its complexity it is difficult to ascertain that the functionality is
> > correct or we missed something. Also, considering it is correct today, in its
> > current form, it may become difficult to enhance it in future.
> >
>
> I attempted to refactor the code a bit based on my preferred style, as shown in
> the attachment. While the number of return points couldn't be reduced, I tried
> to eliminate if-else branches where possible. Sharing this top-up patch as a
> reference for an alternative style that reduces code size.
>

Thanks. It looks like a good refactoring! I'd prefer to free the
ancestors list to avoid memory leak.

I've attached the patch that incorporated all comments I got so far.
Feedback is very welcome.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com


Attachments:

  [application/octet-stream] v7-0001-Add-target_relid-parameter-to-pg_get_publication_.patch (32.4K, 2-v7-0001-Add-target_relid-parameter-to-pg_get_publication_.patch)
  download | inline diff:
From f64b6d4611fa146433709d30b9c8288d15d2e240 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
Date: Fri, 27 Feb 2026 15:42:38 -0800
Subject: [PATCH v7] Add target_relid parameter to pg_get_publication_tables().

When a tablesync worker checks whether a specific table is published,
it previously called pg_get_publication_tables() and filtered the
result by relid on the subscriber side. This forced a full enumeration
of all tables in the publication before any filtering could occur. For
publications covering a large number of tables, this resulted in
expensive scans on the publisher and unnecessary overhead.

This commit adds a new overloaded form of pg_get_publication_tables()
that accepts an array of publication names and a target table
OID. Instead of enumerating all published tables, it evaluates
membership for the specified relation via syscache lookups, using the
new is_table_publishable_in_publication() helper. This helper
correctly accounts for publish_via_partition_root, ALL TABLES with
EXCEPT clauses, schema publications, and partition inheritance, while
avoiding the overhead of building the complete published table list.

The existing a VARIADIC array form of pg_get_publication_tables() is
preserved for backward compatibility. Tablesync workers use the new
two-argument form when connected to a publisher running PostgreSQL 19
or later.

Bump catalog version.

Reported-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zhijie Hou <[email protected]>
Reviewed-by: Matheus Alcantara <[email protected]>
Reviewed-by: Amit Kapila <[email protected]>
Reviewed-by: Peter Smith <[email protected]>
Reviewed-by: Hayato Kuroda <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Haoyan Wang <[email protected]>
Discussion: https://postgr.es/m/CAB-JLwbBFNuASyEnZWP0Tck9uNkthBZqi6WoXNevUT6+mV8XmA@mail.gmail.com
---
 src/backend/catalog/pg_publication.c        | 256 +++++++++++++++++---
 src/backend/replication/logical/tablesync.c |  70 ++++--
 src/backend/replication/pgoutput/pgoutput.c |   7 +-
 src/include/catalog/pg_proc.dat             |  11 +-
 src/include/catalog/pg_publication.h        |   2 +
 src/test/regress/expected/publication.out   | 225 +++++++++++++++++
 src/test/regress/sql/publication.sql        | 107 ++++++++
 7 files changed, 624 insertions(+), 54 deletions(-)

diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index a3192f19d35..b90e9794e7d 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -163,6 +163,37 @@ is_publishable_relation(Relation rel)
 	return is_publishable_class(RelationGetRelid(rel), rel->rd_rel);
 }
 
+/*
+ * Similar to is_publishable_class() but checks whether the given OID
+ * is a publishable "table" or not.
+ */
+static bool
+is_publishable_table(Oid tableoid)
+{
+	HeapTuple	tuple;
+	Form_pg_class relform;
+
+	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(tableoid));
+	if (!HeapTupleIsValid(tuple))
+		return false;
+
+	relform = (Form_pg_class) GETSTRUCT(tuple);
+
+	/*
+	 * Sequences are publishable according to is_publishable_class() so
+	 * explicitly exclude here.
+	 */
+	if (relform->relkind != RELKIND_SEQUENCE &&
+		is_publishable_class(tableoid, relform))
+	{
+		ReleaseSysCache(tuple);
+		return true;
+	}
+
+	ReleaseSysCache(tuple);
+	return false;
+}
+
 /*
  * SQL-callable variant of the above
  *
@@ -451,6 +482,26 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
 	return topmost_relid;
 }
 
+/*
+ * A variant of GetTopMostAncestorInPublication() returns the top most
+ * published ancestor of the given relid.
+ */
+Oid
+GetTopMostAncestorInPublicationRelid(Oid pubid, Oid relid,
+									 int *ancestor_level)
+{
+	List	   *ancestors = get_partition_ancestors(relid);
+	Oid			ancestor;
+
+	ancestor = GetTopMostAncestorInPublication(pubid, ancestors,
+											   ancestor_level);
+
+	if (ancestors)
+		list_free(ancestors);
+
+	return ancestor;
+}
+
 /*
  * attnumstoint2vector
  *		Convert a Bitmapset of AttrNumbers into an int2vector.
@@ -1264,12 +1315,111 @@ GetPublicationByName(const char *pubname, bool missing_ok)
 }
 
 /*
- * Get information of the tables in the given publication array.
+ * Returns true if the table of the given relid is published for the specified
+ * publication.
+ *
+ * This function evaluates the effective published OID based on the
+ * publish_via_partition_root setting, rather than just checking catalog entries
+ * (e.g., pg_publication_rel). For instance, when publish_via_partition_root is
+ * false, it returns false for a parent partitioned table and true for its leaf
+ * partitions, even if the parent is the one explicitly added to the publication.
  *
- * Returns pubid, relid, column list, row filter for each table.
+ * For performance reasons, this function avoids the overhead of constructing
+ * the complete list of published tables during the evaluation. It can execute
+ * quickly even when the publication contains a large number of relations.
  */
-Datum
-pg_get_publication_tables(PG_FUNCTION_ARGS)
+static bool
+is_table_publishable_in_publication(Oid relid, Publication *pub)
+{
+	bool		relispartition;
+
+	/*
+	 * For non-pubviaroot publications, a partitioned table is never the
+	 * effective published OID; only its leaf partitions can be.
+	 */
+	if (!pub->pubviaroot && get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE)
+		return false;
+
+	relispartition = get_rel_relispartition(relid);
+
+	if (pub->alltables)
+	{
+		Oid			target_relid = relid;
+
+		/*
+		 * ALL TABLES with pubviaroot includes only regular tables or top-most
+		 * partitioned tables -- never child partitions.
+		 */
+		if (pub->pubviaroot && relispartition)
+			return false;
+
+		if (relispartition)
+		{
+			List	   *ancestors = get_partition_ancestors(relid);
+
+			/*
+			 * Only the top-most ancestor can appear in the EXCEPT clause.
+			 * Therefore, for a partition, exclusion must be evaluated at the
+			 * top-most ancestor.
+			 */
+			target_relid = llast_oid(ancestors);
+			list_free(ancestors);
+		}
+
+		/*
+		 * The table is published unless it appears in the EXCEPT clause. ALL
+		 * TABLES publications store only EXCEPT'ed tables in
+		 * pg_publication_rel, so checking existence is sufficient.
+		 */
+		return !SearchSysCacheExists2(PUBLICATIONRELMAP,
+									  ObjectIdGetDatum(target_relid),
+									  ObjectIdGetDatum(pub->oid));
+	}
+
+	/*
+	 * Non-alltables publication.
+	 */
+
+	if (relispartition &&
+		OidIsValid(GetTopMostAncestorInPublicationRelid(pub->oid,
+														relid, NULL)))
+	{
+		/*
+		 * If pubviaroot is true, the ancestor is published instead of the
+		 * partition, so exclude it. Otherwise, the ancestor covers the
+		 * partition, so include it.
+		 */
+		return !pub->pubviaroot;
+	}
+
+	/*
+	 * Check whether the table is explicitly published via pg_publication_rel
+	 * or pg_publication_namespace.
+	 */
+	return (SearchSysCacheExists2(PUBLICATIONRELMAP,
+								  ObjectIdGetDatum(relid),
+								  ObjectIdGetDatum(pub->oid)) ||
+			SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP,
+								  ObjectIdGetDatum(get_rel_namespace(relid)),
+								  ObjectIdGetDatum(pub->oid)));
+}
+
+/*
+ * Helper function to get information of the tables in the given
+ * publication(s).
+ *
+ * If filter_by_relid is true, only the row for target_relid is returned;
+ * if target_relid does not exist or is not part of the publications, zero
+ * rows are returned.  If filter_by_relid is false, rows for all tables
+ * within the specified publications are returned and target_relid is
+ * ignored.
+ *
+ * Returns pubid, relid, column list, and row filter for each table.
+ */
+static Datum
+pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
+						  Oid target_relid, bool filter_by_relid,
+						  bool pub_missing_ok)
 {
 #define NUM_PUBLICATION_TABLES_ELEM	4
 	FuncCallContext *funcctx;
@@ -1280,7 +1430,6 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 	{
 		TupleDesc	tupdesc;
 		MemoryContext oldcontext;
-		ArrayType  *arr;
 		Datum	   *elems;
 		int			nelems,
 					i;
@@ -1289,6 +1438,14 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		/* create a function context for cross-call persistence */
 		funcctx = SRF_FIRSTCALL_INIT();
 
+		/*
+		 * Preliminary check if the specified table can be published in the
+		 * first place. If not, we can return early without checking the given
+		 * publications and the table.
+		 */
+		if (filter_by_relid && !is_publishable_table(target_relid))
+			SRF_RETURN_DONE(funcctx);
+
 		/* switch to memory context appropriate for multiple function calls */
 		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
 
@@ -1296,8 +1453,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		 * Deconstruct the parameter into elements where each element is a
 		 * publication name.
 		 */
-		arr = PG_GETARG_ARRAYTYPE_P(0);
-		deconstruct_array_builtin(arr, TEXTOID, &elems, NULL, &nelems);
+		deconstruct_array_builtin(pubnames, TEXTOID, &elems, NULL, &nelems);
 
 		/* Get Oids of tables from each publication. */
 		for (i = 0; i < nelems; i++)
@@ -1306,32 +1462,48 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			List	   *pub_elem_tables = NIL;
 			ListCell   *lc;
 
-			pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]), false);
+			pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]),
+											pub_missing_ok);
 
-			/*
-			 * Publications support partitioned tables. If
-			 * publish_via_partition_root is false, all changes are replicated
-			 * using leaf partition identity and schema, so we only need
-			 * those. Otherwise, get the partitioned table itself.
-			 */
-			if (pub_elem->alltables)
-				pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
-															 RELKIND_RELATION,
-															 pub_elem->pubviaroot);
+			if (pub_elem == NULL)
+				continue;
+
+			if (filter_by_relid)
+			{
+				/* Check if the given table is published for the publication */
+				if (is_table_publishable_in_publication(target_relid, pub_elem))
+				{
+					pub_elem_tables = list_make1_oid(target_relid);
+				}
+			}
 			else
 			{
-				List	   *relids,
-						   *schemarelids;
-
-				relids = GetIncludedPublicationRelations(pub_elem->oid,
-														 pub_elem->pubviaroot ?
-														 PUBLICATION_PART_ROOT :
-														 PUBLICATION_PART_LEAF);
-				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
-																pub_elem->pubviaroot ?
-																PUBLICATION_PART_ROOT :
-																PUBLICATION_PART_LEAF);
-				pub_elem_tables = list_concat_unique_oid(relids, schemarelids);
+				/*
+				 * Publications support partitioned tables. If
+				 * publish_via_partition_root is false, all changes are
+				 * replicated using leaf partition identity and schema, so we
+				 * only need those. Otherwise, get the partitioned table
+				 * itself.
+				 */
+				if (pub_elem->alltables)
+					pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
+																 RELKIND_RELATION,
+																 pub_elem->pubviaroot);
+				else
+				{
+					List	   *relids,
+							   *schemarelids;
+
+					relids = GetIncludedPublicationRelations(pub_elem->oid,
+															 pub_elem->pubviaroot ?
+															 PUBLICATION_PART_ROOT :
+															 PUBLICATION_PART_LEAF);
+					schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
+																	pub_elem->pubviaroot ?
+																	PUBLICATION_PART_ROOT :
+																	PUBLICATION_PART_LEAF);
+					pub_elem_tables = list_concat_unique_oid(relids, schemarelids);
+				}
 			}
 
 			/*
@@ -1491,6 +1663,30 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 	SRF_RETURN_DONE(funcctx);
 }
 
+Datum
+pg_get_publication_tables_a(PG_FUNCTION_ARGS)
+{
+	/*
+	 * Get information for all tables in the given publications.
+	 * filter_by_relid is false so all tables are returned; pub_missing_ok is
+	 * false for backward compatibility.
+	 */
+	return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0),
+									 InvalidOid, false, false);
+}
+
+Datum
+pg_get_publication_tables_b(PG_FUNCTION_ARGS)
+{
+	/*
+	 * Get information for the specified table in the given publications. The
+	 * SQL-level function is declared STRICT, so target_relid is guaranteed to
+	 * be non-NULL here.
+	 */
+	return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0),
+									 PG_GETARG_OID(1), true, true);
+}
+
 /*
  * Returns Oids of sequences in a publication.
  */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index f49a4852ecb..eb718114297 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -798,17 +798,35 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		 * publications).
 		 */
 		resetStringInfo(&cmd);
-		appendStringInfo(&cmd,
-						 "SELECT DISTINCT"
-						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
-						 "   THEN NULL ELSE gpt.attrs END)"
-						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
-						 "  pg_class c"
-						 " WHERE gpt.relid = %u AND c.oid = gpt.relid"
-						 "   AND p.pubname IN ( %s )",
-						 lrel->remoteid,
-						 pub_names->data);
+
+		if (server_version >= 190000)
+		{
+			/*
+			 * We can pass both publication names and relid to
+			 * pg_get_publication_tables() since version 19.
+			 */
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT"
+							 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
+							 "   THEN NULL ELSE gpt.attrs END)"
+							 "  FROM pg_get_publication_tables(ARRAY[%s], %u) gpt,"
+							 "  pg_class c"
+							 " WHERE c.oid = gpt.relid",
+							 pub_names->data,
+							 lrel->remoteid);
+		}
+		else
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT"
+							 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
+							 "   THEN NULL ELSE gpt.attrs END)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
+							 "  pg_class c"
+							 " WHERE gpt.relid = %u AND c.oid = gpt.relid"
+							 "   AND p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
 
 		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
 							 lengthof(attrsRow), attrsRow);
@@ -982,14 +1000,28 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 
 		/* Check for row filters. */
 		resetStringInfo(&cmd);
-		appendStringInfo(&cmd,
-						 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
-						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt"
-						 " WHERE gpt.relid = %u"
-						 "   AND p.pubname IN ( %s )",
-						 lrel->remoteid,
-						 pub_names->data);
+
+		if (server_version >= 190000)
+		{
+			/*
+			 * We can pass both publication names and relid to
+			 * pg_get_publication_tables() since version 19.
+			 */
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
+							 "  FROM pg_get_publication_tables(ARRAY[%s], %u) gpt",
+							 pub_names->data,
+							 lrel->remoteid);
+		}
+		else
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname) gpt"
+							 " WHERE gpt.relid = %u"
+							 "   AND p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
 
 		res = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data, 1, qualRow);
 
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 4ecfcbff7ab..f4531efe7ec 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2263,11 +2263,10 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
 				{
 					Oid			ancestor;
 					int			level;
-					List	   *ancestors = get_partition_ancestors(relid);
 
-					ancestor = GetTopMostAncestorInPublication(pub->oid,
-															   ancestors,
-															   &level);
+					ancestor = GetTopMostAncestorInPublicationRelid(pub->oid,
+																	relid,
+																	&level);
 
 					if (ancestor != InvalidOid)
 					{
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3579cec5744..afdcc915f08 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12468,7 +12468,16 @@
   proallargtypes => '{_text,oid,oid,int2vector,pg_node_tree}',
   proargmodes => '{v,o,o,o,o}',
   proargnames => '{pubname,pubid,relid,attrs,qual}',
-  prosrc => 'pg_get_publication_tables' },
+  prosrc => 'pg_get_publication_tables_a' },
+{ oid => '8060',
+  descr => 'get information of the specified table that is part of the specified publications',
+  proname => 'pg_get_publication_tables', prorows => '10',
+  proretset => 't', provolatile => 's',
+  prorettype => 'record', proargtypes => '_text oid',
+  proallargtypes => '{_text,oid,oid,oid,int2vector,pg_node_tree}',
+  proargmodes => '{i,i,o,o,o,o}',
+  proargnames => '{pubnames,target_relid,pubid,relid,attrs,qual}',
+  prosrc => 'pg_get_publication_tables_b' },
 { oid => '8052', descr => 'get OIDs of sequences in a publication',
   proname => 'pg_get_publication_sequences', prorows => '1000', proretset => 't',
   provolatile => 's', prorettype => 'oid', proargtypes => 'text',
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 89b4bb14f62..ad309e26e02 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -192,6 +192,8 @@ extern List *GetPubPartitionOptionRelations(List *result,
 											Oid relid);
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
 											int *ancestor_level);
+extern Oid	GetTopMostAncestorInPublicationRelid(Oid puboid, Oid relid,
+												 int *ancestor_level);
 
 extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 91332e75eeb..3b0eaec4f21 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2292,6 +2292,231 @@ DROP TABLE testpub_merge_pk;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
+-- Test pg_get_publication_tables(text[], oid) function
+CREATE SCHEMA gpt_test_sch;
+CREATE TABLE gpt_test_sch.tbl_sch (id int);
+CREATE TABLE tbl_normal (id int);
+CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1);
+CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10);
+CREATE VIEW gpt_test_view AS SELECT * FROM tbl_normal;
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION pub_all FOR ALL TABLES WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_all_no_viaroot FOR ALL TABLES WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except_no_viaroot FOR ALL TABLES EXCEPT (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch;
+CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10);
+CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_part_parent_no_viaroot FOR TABLE tbl_parent WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent_child FOR TABLE tbl_parent, tbl_part1 WITH (publish_via_partition_root = true);
+RESET client_min_messages;
+CREATE FUNCTION test_gpt(pubnames text[], relname text)
+RETURNS TABLE (
+  pubname text,
+  relname name,
+  attrs text,
+  qual text
+)
+BEGIN ATOMIC
+  SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid)
+    FROM pg_get_publication_tables(pubnames, relname::regclass::oid) gpt
+    JOIN pg_publication p ON p.oid = gpt.pubid
+    JOIN pg_class c ON c.oid = gpt.relid
+  ORDER BY p.pubname, c.relname;
+END;
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'tbl_normal');
+  pubname   |  relname   | attrs |   qual    
+------------+------------+-------+-----------
+ pub_normal | tbl_normal | 1     | (id < 10)
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'gpt_test_sch.tbl_sch'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'gpt_test_sch.tbl_sch');
+  pubname   | relname | attrs | qual 
+------------+---------+-------+------
+ pub_schema | tbl_sch | 1     | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'tbl_normal'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_parent');
+     pubname     |  relname   | attrs |    qual    
+-----------------+------------+-------+------------
+ pub_part_parent | tbl_parent | 1 2   | (id1 = 10)
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_part1');
+          pubname           |  relname  | attrs | qual 
+----------------------------+-----------+-------+------
+ pub_part_parent_no_viaroot | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_part1');
+    pubname    |  relname  | attrs | qual 
+---------------+-----------+-------+------
+ pub_part_leaf | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_parent');
+ pubname |  relname   | attrs | qual 
+---------+------------+-------+------
+ pub_all | tbl_parent | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_part1');
+      pubname       |  relname  | attrs | qual 
+--------------------+-----------+-------+------
+ pub_all_no_viaroot | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_parent');
+        pubname        |  relname   | attrs | qual 
+-----------------------+------------+-------+------
+ pub_part_parent_child | tbl_parent | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- test for the EXCLUDE clause
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_normal');
+    pubname     |  relname   | attrs | qual 
+----------------+------------+-------+------
+ pub_all_except | tbl_normal | 1     | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_parent'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_normal');
+          pubname          |  relname   | attrs | qual 
+---------------------------+------------+-------+------
+ pub_all_except_no_viaroot | tbl_normal | 1     | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_parent'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- two rows with different row filter
+SELECT * FROM test_gpt(ARRAY['pub_all', 'pub_normal'], 'tbl_normal');
+  pubname   |  relname   | attrs |   qual    
+------------+------------+-------+-----------
+ pub_all    | tbl_normal | 1     | 
+ pub_normal | tbl_normal | 1     | (id < 10)
+(2 rows)
+
+-- one row with 'pub_part_parent'
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_part_parent_no_viaroot'], 'tbl_parent');
+     pubname     |  relname   | attrs |    qual    
+-----------------+------------+-------+------------
+ pub_part_parent | tbl_parent | 1 2   | (id1 = 10)
+(1 row)
+
+-- no result, tbl_parent is the effective published OID due to pubviaroot
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_all'], 'tbl_part1');
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- no result, non-existent publication
+SELECT * FROM test_gpt(ARRAY['no_such_pub'], 'tbl_normal');
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- no result, non-table object
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'gpt_test_view');
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- no result, empty publication array
+SELECT * FROM test_gpt(ARRAY[]::text[], 'tbl_normal');
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- no result, OID 0 as target_relid
+SELECT * FROM pg_get_publication_tables(ARRAY['pub_normal'], 0::oid);
+ pubid | relid | attrs | qual 
+-------+-------+-------+------
+(0 rows)
+
+-- Clean up
+DROP FUNCTION test_gpt(text[], text);
+DROP PUBLICATION pub_all;
+DROP PUBLICATION pub_all_no_viaroot;
+DROP PUBLICATION pub_all_except;
+DROP PUBLICATION pub_all_except_no_viaroot;
+DROP PUBLICATION pub_schema;
+DROP PUBLICATION pub_normal;
+DROP PUBLICATION pub_part_leaf;
+DROP PUBLICATION pub_part_parent;
+DROP PUBLICATION pub_part_parent_no_viaroot;
+DROP PUBLICATION pub_part_parent_child;
+DROP VIEW gpt_test_view;
+DROP TABLE tbl_normal, tbl_parent, tbl_part1;
+DROP SCHEMA gpt_test_sch CASCADE;
+NOTICE:  drop cascades to table gpt_test_sch.tbl_sch
 -- stage objects for pg_dump tests
 CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int);
 CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int);
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 6bafad27571..94908e4f965 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1438,6 +1438,113 @@ RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
 
+-- Test pg_get_publication_tables(text[], oid) function
+CREATE SCHEMA gpt_test_sch;
+CREATE TABLE gpt_test_sch.tbl_sch (id int);
+CREATE TABLE tbl_normal (id int);
+CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1);
+CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10);
+CREATE VIEW gpt_test_view AS SELECT * FROM tbl_normal;
+
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION pub_all FOR ALL TABLES WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_all_no_viaroot FOR ALL TABLES WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except_no_viaroot FOR ALL TABLES EXCEPT (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch;
+CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10);
+CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_part_parent_no_viaroot FOR TABLE tbl_parent WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent_child FOR TABLE tbl_parent, tbl_part1 WITH (publish_via_partition_root = true);
+RESET client_min_messages;
+
+CREATE FUNCTION test_gpt(pubnames text[], relname text)
+RETURNS TABLE (
+  pubname text,
+  relname name,
+  attrs text,
+  qual text
+)
+BEGIN ATOMIC
+  SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid)
+    FROM pg_get_publication_tables(pubnames, relname::regclass::oid) gpt
+    JOIN pg_publication p ON p.oid = gpt.pubid
+    JOIN pg_class c ON c.oid = gpt.relid
+  ORDER BY p.pubname, c.relname;
+END;
+
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'tbl_normal');
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'gpt_test_sch.tbl_sch'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'gpt_test_sch.tbl_sch');
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'tbl_normal'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_part1'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_parent'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_parent'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_part1'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_parent'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_part1'); -- no result
+
+-- test for the EXCLUDE clause
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_normal');
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_parent'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_part1'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_normal');
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_parent'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_part1'); -- no result
+
+-- two rows with different row filter
+SELECT * FROM test_gpt(ARRAY['pub_all', 'pub_normal'], 'tbl_normal');
+
+-- one row with 'pub_part_parent'
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_part_parent_no_viaroot'], 'tbl_parent');
+
+-- no result, tbl_parent is the effective published OID due to pubviaroot
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_all'], 'tbl_part1');
+
+-- no result, non-existent publication
+SELECT * FROM test_gpt(ARRAY['no_such_pub'], 'tbl_normal');
+
+-- no result, non-table object
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'gpt_test_view');
+
+-- no result, empty publication array
+SELECT * FROM test_gpt(ARRAY[]::text[], 'tbl_normal');
+
+-- no result, OID 0 as target_relid
+SELECT * FROM pg_get_publication_tables(ARRAY['pub_normal'], 0::oid);
+
+-- Clean up
+DROP FUNCTION test_gpt(text[], text);
+DROP PUBLICATION pub_all;
+DROP PUBLICATION pub_all_no_viaroot;
+DROP PUBLICATION pub_all_except;
+DROP PUBLICATION pub_all_except_no_viaroot;
+DROP PUBLICATION pub_schema;
+DROP PUBLICATION pub_normal;
+DROP PUBLICATION pub_part_leaf;
+DROP PUBLICATION pub_part_parent;
+DROP PUBLICATION pub_part_parent_no_viaroot;
+DROP PUBLICATION pub_part_parent_child;
+DROP VIEW gpt_test_view;
+DROP TABLE tbl_normal, tbl_parent, tbl_part1;
+DROP SCHEMA gpt_test_sch CASCADE;
+
 -- stage objects for pg_dump tests
 CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int);
 CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int);
-- 
2.47.3



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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 08:48                     ` Re: Initial COPY of Logical Replication is too slow Peter Smith <[email protected]>
  2026-03-31 09:36                       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-31 12:07                         ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-31 19:40                           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-04-01 22:23                             ` Masahiko Sawada <[email protected]>
  2026-04-02 05:45                               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-04-02 06:12                               ` Re: Initial COPY of Logical Replication is too slow Peter Smith <[email protected]>
  0 siblings, 2 replies; 47+ messages in thread

From: Masahiko Sawada @ 2026-04-01 22:23 UTC (permalink / raw)
  To: Zhijie Hou (Fujitsu) <[email protected]>; +Cc: Amit Kapila <[email protected]>; Peter Smith <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>

On Tue, Mar 31, 2026 at 12:40 PM Masahiko Sawada <[email protected]> wrote:
>
> On Tue, Mar 31, 2026 at 5:07 AM Zhijie Hou (Fujitsu)
> <[email protected]> wrote:
> >
> > On Tuesday, March 31, 2026 5:36 PM Amit Kapila <[email protected]> wrote:
> > >
> > > On Wed, Mar 25, 2026 at 2:19 PM Peter Smith <[email protected]>
> > > wrote:
> > > >
> > > > There are many return points, and most of those "if" blocks cannot
> > > > fall through (they return).
> > > >
> > > > I found it slightly difficult to read the code because I kept having
> > > > to think, "OK, if we reached here, it means pubviaroot must be false,"
> > > > or "OK, if we reached this far, then puballtables must be false, and
> > > > pubviaroot must be false," etc.
> > > >
> > >
> > > I can't say exactly why, but I find it difficult to read this function. So, I share
> > > your concerns about the code of this function.
> > > Because of its complexity it is difficult to ascertain that the functionality is
> > > correct or we missed something. Also, considering it is correct today, in its
> > > current form, it may become difficult to enhance it in future.
> > >
> >
> > I attempted to refactor the code a bit based on my preferred style, as shown in
> > the attachment. While the number of return points couldn't be reduced, I tried
> > to eliminate if-else branches where possible. Sharing this top-up patch as a
> > reference for an alternative style that reduces code size.
> >
>
> Thanks. It looks like a good refactoring! I'd prefer to free the
> ancestors list to avoid memory leak.
>
> I've attached the patch that incorporated all comments I got so far.
> Feedback is very welcome.
>

I decided to simplify the code flow in the
is_table_publishable_in_publication() by taking more proposed changes
from Hou-san while accepting some memory leak. This function is
limited to be used only in per-call-memory context so we don't need to
worry about the actual memory leak. While we would need to change this
function in the futuer when we want to use it other places too, I
think it would be better to keep the function simple until then. I
hope the added new comment also help understand the code flow of this
function.

I think the patch is good shape, so planning to push it barring any objections.

Regards,

--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com


Attachments:

  [text/x-patch] v8-0001-Add-target_relid-parameter-to-pg_get_publication_.patch (31.0K, 2-v8-0001-Add-target_relid-parameter-to-pg_get_publication_.patch)
  download | inline diff:
From 66b52499f830572e28d57239eb4b397b4a6643aa Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
Date: Fri, 27 Feb 2026 15:42:38 -0800
Subject: [PATCH v8] Add target_relid parameter to pg_get_publication_tables().

When a tablesync worker checks whether a specific table is published,
it previously called pg_get_publication_tables() and filtered the
result by relid on the subscriber side. This forced a full enumeration
of all tables in the publication before any filtering could occur. For
publications covering a large number of tables, this resulted in
expensive scans on the publisher and unnecessary overhead.

This commit adds a new overloaded form of pg_get_publication_tables()
that accepts an array of publication names and a target table
OID. Instead of enumerating all published tables, it evaluates
membership for the specified relation via syscache lookups, using the
new is_table_publishable_in_publication() helper. This helper
correctly accounts for publish_via_partition_root, ALL TABLES with
EXCEPT clauses, schema publications, and partition inheritance, while
avoiding the overhead of building the complete published table list.

The existing a VARIADIC array form of pg_get_publication_tables() is
preserved for backward compatibility. Tablesync workers use the new
two-argument form when connected to a publisher running PostgreSQL 19
or later.

Bump catalog version.

Reported-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zhijie Hou <[email protected]>
Reviewed-by: Matheus Alcantara <[email protected]>
Reviewed-by: Amit Kapila <[email protected]>
Reviewed-by: Peter Smith <[email protected]>
Reviewed-by: Hayato Kuroda <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Haoyan Wang <[email protected]>
Discussion: https://postgr.es/m/CAB-JLwbBFNuASyEnZWP0Tck9uNkthBZqi6WoXNevUT6+mV8XmA@mail.gmail.com
---
 src/backend/catalog/pg_publication.c        | 241 +++++++++++++++++---
 src/backend/replication/logical/tablesync.c |  70 ++++--
 src/include/catalog/pg_proc.dat             |  11 +-
 src/test/regress/expected/publication.out   | 225 ++++++++++++++++++
 src/test/regress/sql/publication.sql        | 107 +++++++++
 5 files changed, 604 insertions(+), 50 deletions(-)

diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 82a22061d5b..a4c305f0695 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -163,6 +163,37 @@ is_publishable_relation(Relation rel)
 	return is_publishable_class(RelationGetRelid(rel), rel->rd_rel);
 }
 
+/*
+ * Similar to is_publishable_class() but checks whether the given OID
+ * is a publishable "table" or not.
+ */
+static bool
+is_publishable_table(Oid tableoid)
+{
+	HeapTuple	tuple;
+	Form_pg_class relform;
+
+	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(tableoid));
+	if (!HeapTupleIsValid(tuple))
+		return false;
+
+	relform = (Form_pg_class) GETSTRUCT(tuple);
+
+	/*
+	 * Sequences are publishable according to is_publishable_class() so
+	 * explicitly exclude here.
+	 */
+	if (relform->relkind != RELKIND_SEQUENCE &&
+		is_publishable_class(tableoid, relform))
+	{
+		ReleaseSysCache(tuple);
+		return true;
+	}
+
+	ReleaseSysCache(tuple);
+	return false;
+}
+
 /*
  * SQL-callable variant of the above
  *
@@ -1264,12 +1295,116 @@ GetPublicationByName(const char *pubname, bool missing_ok)
 }
 
 /*
- * Get information of the tables in the given publication array.
+ * A helper function for pg_get_publication_tables() to check whether the
+ * table of the given relid is published for the specified publication.
+ *
+ * This function evaluates the effective published OID based on the
+ * publish_via_partition_root setting, rather than just checking catalog entries
+ * (e.g., pg_publication_rel). For instance, when publish_via_partition_root is
+ * false, it returns false for a parent partitioned table and returns true
+ * for its leaf partitions, even if the parent is the one explicitly added
+ * to the publication.
+ *
+ * For performance reasons, this function avoids the overhead of constructing
+ * the complete list of published tables during the evaluation. It can execute
+ * quickly even when the publication contains a large number of relations.
  *
- * Returns pubid, relid, column list, row filter for each table.
+ * Note: this leaks memory for the ancestors list into the current memory
+ * context.
  */
-Datum
-pg_get_publication_tables(PG_FUNCTION_ARGS)
+static bool
+is_table_publishable_in_publication(Oid relid, Publication *pub)
+{
+	bool		relispartition;
+	List	   *ancestors = NIL;
+
+	/*
+	 * For non-pubviaroot publications, a partitioned table is never the
+	 * effective published OID; only its leaf partitions can be.
+	 */
+	if (!pub->pubviaroot && get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE)
+		return false;
+
+	relispartition = get_rel_relispartition(relid);
+
+	if (relispartition)
+		ancestors = get_partition_ancestors(relid);
+
+	if (pub->alltables)
+	{
+		/*
+		 * ALL TABLES with pubviaroot includes only regular tables or top-most
+		 * partitioned tables -- never child partitions.
+		 */
+		if (pub->pubviaroot && relispartition)
+			return false;
+
+		/*
+		 * For ALL TABLES publications, the table is published unless it
+		 * appears in the EXCEPT clause. Only the top-most can appear in the
+		 * EXCEPT clause, so exclusion must be evaluated at the top-most
+		 * ancestor if it has. These publications store only EXCEPT'ed tables
+		 * in pg_publication_rel, so checking existence is sufficient.
+		 *
+		 * Note that this existence check below would incorrectly return true
+		 * (published) for partitions when pubviaroot is enabled; however,
+		 * that case is already caught and returned false by the above check.
+		 */
+		return !SearchSysCacheExists2(PUBLICATIONRELMAP,
+									  ObjectIdGetDatum(ancestors
+													   ? llast_oid(ancestors) : relid),
+									  ObjectIdGetDatum(pub->oid));
+	}
+
+	/*
+	 * Non-ALL-TABLE publication cases.
+	 *
+	 * A table is published if it (or a containing schema) was explicitly
+	 * added, or if it is a partition whose ancestor was added.
+	 */
+
+	/*
+	 * If an ancestor is published, the partition's status depends on
+	 * publish_via_partition_root value.
+	 *
+	 * If it's true, the ancestor's relation OID is the effective published
+	 * OID, so the partition itself should be excluded (return false).
+	 *
+	 * If it's false, the partition is covered by its ancestor's presence in
+	 * the publication, it should be included (return true).
+	 */
+	if (relispartition &&
+		OidIsValid(GetTopMostAncestorInPublication(pub->oid, ancestors, NULL)))
+		return !pub->pubviaroot;
+
+	/*
+	 * Check whether the table is explicitly published via pg_publication_rel
+	 * or pg_publication_namespace.
+	 */
+	return (SearchSysCacheExists2(PUBLICATIONRELMAP,
+								  ObjectIdGetDatum(relid),
+								  ObjectIdGetDatum(pub->oid)) ||
+			SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP,
+								  ObjectIdGetDatum(get_rel_namespace(relid)),
+								  ObjectIdGetDatum(pub->oid)));
+}
+
+/*
+ * Helper function to get information of the tables in the given
+ * publication(s).
+ *
+ * If filter_by_relid is true, only the row for target_relid is returned;
+ * if target_relid does not exist or is not part of the publications, zero
+ * rows are returned.  If filter_by_relid is false, rows for all tables
+ * within the specified publications are returned and target_relid is
+ * ignored.
+ *
+ * Returns pubid, relid, column list, and row filter for each table.
+ */
+static Datum
+pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
+						  Oid target_relid, bool filter_by_relid,
+						  bool pub_missing_ok)
 {
 #define NUM_PUBLICATION_TABLES_ELEM	4
 	FuncCallContext *funcctx;
@@ -1280,7 +1415,6 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 	{
 		TupleDesc	tupdesc;
 		MemoryContext oldcontext;
-		ArrayType  *arr;
 		Datum	   *elems;
 		int			nelems,
 					i;
@@ -1289,6 +1423,14 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		/* create a function context for cross-call persistence */
 		funcctx = SRF_FIRSTCALL_INIT();
 
+		/*
+		 * Preliminary check if the specified table can be published in the
+		 * first place. If not, we can return early without checking the given
+		 * publications and the table.
+		 */
+		if (filter_by_relid && !is_publishable_table(target_relid))
+			SRF_RETURN_DONE(funcctx);
+
 		/* switch to memory context appropriate for multiple function calls */
 		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
 
@@ -1296,8 +1438,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		 * Deconstruct the parameter into elements where each element is a
 		 * publication name.
 		 */
-		arr = PG_GETARG_ARRAYTYPE_P(0);
-		deconstruct_array_builtin(arr, TEXTOID, &elems, NULL, &nelems);
+		deconstruct_array_builtin(pubnames, TEXTOID, &elems, NULL, &nelems);
 
 		/* Get Oids of tables from each publication. */
 		for (i = 0; i < nelems; i++)
@@ -1306,32 +1447,48 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			List	   *pub_elem_tables = NIL;
 			ListCell   *lc;
 
-			pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]), false);
+			pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]),
+											pub_missing_ok);
 
-			/*
-			 * Publications support partitioned tables. If
-			 * publish_via_partition_root is false, all changes are replicated
-			 * using leaf partition identity and schema, so we only need
-			 * those. Otherwise, get the partitioned table itself.
-			 */
-			if (pub_elem->alltables)
-				pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
-															 RELKIND_RELATION,
-															 pub_elem->pubviaroot);
+			if (pub_elem == NULL)
+				continue;
+
+			if (filter_by_relid)
+			{
+				/* Check if the given table is published for the publication */
+				if (is_table_publishable_in_publication(target_relid, pub_elem))
+				{
+					pub_elem_tables = list_make1_oid(target_relid);
+				}
+			}
 			else
 			{
-				List	   *relids,
-						   *schemarelids;
-
-				relids = GetIncludedPublicationRelations(pub_elem->oid,
-														 pub_elem->pubviaroot ?
-														 PUBLICATION_PART_ROOT :
-														 PUBLICATION_PART_LEAF);
-				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
-																pub_elem->pubviaroot ?
-																PUBLICATION_PART_ROOT :
-																PUBLICATION_PART_LEAF);
-				pub_elem_tables = list_concat_unique_oid(relids, schemarelids);
+				/*
+				 * Publications support partitioned tables. If
+				 * publish_via_partition_root is false, all changes are
+				 * replicated using leaf partition identity and schema, so we
+				 * only need those. Otherwise, get the partitioned table
+				 * itself.
+				 */
+				if (pub_elem->alltables)
+					pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
+																 RELKIND_RELATION,
+																 pub_elem->pubviaroot);
+				else
+				{
+					List	   *relids,
+							   *schemarelids;
+
+					relids = GetIncludedPublicationRelations(pub_elem->oid,
+															 pub_elem->pubviaroot ?
+															 PUBLICATION_PART_ROOT :
+															 PUBLICATION_PART_LEAF);
+					schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
+																	pub_elem->pubviaroot ?
+																	PUBLICATION_PART_ROOT :
+																	PUBLICATION_PART_LEAF);
+					pub_elem_tables = list_concat_unique_oid(relids, schemarelids);
+				}
 			}
 
 			/*
@@ -1491,6 +1648,30 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 	SRF_RETURN_DONE(funcctx);
 }
 
+Datum
+pg_get_publication_tables_a(PG_FUNCTION_ARGS)
+{
+	/*
+	 * Get information for all tables in the given publications.
+	 * filter_by_relid is false so all tables are returned; pub_missing_ok is
+	 * false for backward compatibility.
+	 */
+	return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0),
+									 InvalidOid, false, false);
+}
+
+Datum
+pg_get_publication_tables_b(PG_FUNCTION_ARGS)
+{
+	/*
+	 * Get information for the specified table in the given publications. The
+	 * SQL-level function is declared STRICT, so target_relid is guaranteed to
+	 * be non-NULL here.
+	 */
+	return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0),
+									 PG_GETARG_OID(1), true, true);
+}
+
 /*
  * Returns Oids of sequences in a publication.
  */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index f49a4852ecb..eb718114297 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -798,17 +798,35 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		 * publications).
 		 */
 		resetStringInfo(&cmd);
-		appendStringInfo(&cmd,
-						 "SELECT DISTINCT"
-						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
-						 "   THEN NULL ELSE gpt.attrs END)"
-						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
-						 "  pg_class c"
-						 " WHERE gpt.relid = %u AND c.oid = gpt.relid"
-						 "   AND p.pubname IN ( %s )",
-						 lrel->remoteid,
-						 pub_names->data);
+
+		if (server_version >= 190000)
+		{
+			/*
+			 * We can pass both publication names and relid to
+			 * pg_get_publication_tables() since version 19.
+			 */
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT"
+							 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
+							 "   THEN NULL ELSE gpt.attrs END)"
+							 "  FROM pg_get_publication_tables(ARRAY[%s], %u) gpt,"
+							 "  pg_class c"
+							 " WHERE c.oid = gpt.relid",
+							 pub_names->data,
+							 lrel->remoteid);
+		}
+		else
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT"
+							 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
+							 "   THEN NULL ELSE gpt.attrs END)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
+							 "  pg_class c"
+							 " WHERE gpt.relid = %u AND c.oid = gpt.relid"
+							 "   AND p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
 
 		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
 							 lengthof(attrsRow), attrsRow);
@@ -982,14 +1000,28 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 
 		/* Check for row filters. */
 		resetStringInfo(&cmd);
-		appendStringInfo(&cmd,
-						 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
-						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt"
-						 " WHERE gpt.relid = %u"
-						 "   AND p.pubname IN ( %s )",
-						 lrel->remoteid,
-						 pub_names->data);
+
+		if (server_version >= 190000)
+		{
+			/*
+			 * We can pass both publication names and relid to
+			 * pg_get_publication_tables() since version 19.
+			 */
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
+							 "  FROM pg_get_publication_tables(ARRAY[%s], %u) gpt",
+							 pub_names->data,
+							 lrel->remoteid);
+		}
+		else
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname) gpt"
+							 " WHERE gpt.relid = %u"
+							 "   AND p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
 
 		res = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data, 1, qualRow);
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3579cec5744..afdcc915f08 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12468,7 +12468,16 @@
   proallargtypes => '{_text,oid,oid,int2vector,pg_node_tree}',
   proargmodes => '{v,o,o,o,o}',
   proargnames => '{pubname,pubid,relid,attrs,qual}',
-  prosrc => 'pg_get_publication_tables' },
+  prosrc => 'pg_get_publication_tables_a' },
+{ oid => '8060',
+  descr => 'get information of the specified table that is part of the specified publications',
+  proname => 'pg_get_publication_tables', prorows => '10',
+  proretset => 't', provolatile => 's',
+  prorettype => 'record', proargtypes => '_text oid',
+  proallargtypes => '{_text,oid,oid,oid,int2vector,pg_node_tree}',
+  proargmodes => '{i,i,o,o,o,o}',
+  proargnames => '{pubnames,target_relid,pubid,relid,attrs,qual}',
+  prosrc => 'pg_get_publication_tables_b' },
 { oid => '8052', descr => 'get OIDs of sequences in a publication',
   proname => 'pg_get_publication_sequences', prorows => '1000', proretset => 't',
   provolatile => 's', prorettype => 'oid', proargtypes => 'text',
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index d2aa9d45e4a..6f55a394ce1 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2292,6 +2292,231 @@ DROP TABLE testpub_merge_pk;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
+-- Test pg_get_publication_tables(text[], oid) function
+CREATE SCHEMA gpt_test_sch;
+CREATE TABLE gpt_test_sch.tbl_sch (id int);
+CREATE TABLE tbl_normal (id int);
+CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1);
+CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10);
+CREATE VIEW gpt_test_view AS SELECT * FROM tbl_normal;
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION pub_all FOR ALL TABLES WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_all_no_viaroot FOR ALL TABLES WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except_no_viaroot FOR ALL TABLES EXCEPT (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch;
+CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10);
+CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_part_parent_no_viaroot FOR TABLE tbl_parent WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent_child FOR TABLE tbl_parent, tbl_part1 WITH (publish_via_partition_root = true);
+RESET client_min_messages;
+CREATE FUNCTION test_gpt(pubnames text[], relname text)
+RETURNS TABLE (
+  pubname text,
+  relname name,
+  attrs text,
+  qual text
+)
+BEGIN ATOMIC
+  SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid)
+    FROM pg_get_publication_tables(pubnames, relname::regclass::oid) gpt
+    JOIN pg_publication p ON p.oid = gpt.pubid
+    JOIN pg_class c ON c.oid = gpt.relid
+  ORDER BY p.pubname, c.relname;
+END;
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'tbl_normal');
+  pubname   |  relname   | attrs |   qual    
+------------+------------+-------+-----------
+ pub_normal | tbl_normal | 1     | (id < 10)
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'gpt_test_sch.tbl_sch'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'gpt_test_sch.tbl_sch');
+  pubname   | relname | attrs | qual 
+------------+---------+-------+------
+ pub_schema | tbl_sch | 1     | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'tbl_normal'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_parent');
+     pubname     |  relname   | attrs |    qual    
+-----------------+------------+-------+------------
+ pub_part_parent | tbl_parent | 1 2   | (id1 = 10)
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_part1');
+          pubname           |  relname  | attrs | qual 
+----------------------------+-----------+-------+------
+ pub_part_parent_no_viaroot | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_part1');
+    pubname    |  relname  | attrs | qual 
+---------------+-----------+-------+------
+ pub_part_leaf | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_parent');
+ pubname |  relname   | attrs | qual 
+---------+------------+-------+------
+ pub_all | tbl_parent | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_part1');
+      pubname       |  relname  | attrs | qual 
+--------------------+-----------+-------+------
+ pub_all_no_viaroot | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_parent');
+        pubname        |  relname   | attrs | qual 
+-----------------------+------------+-------+------
+ pub_part_parent_child | tbl_parent | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- test for the EXCLUDE clause
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_normal');
+    pubname     |  relname   | attrs | qual 
+----------------+------------+-------+------
+ pub_all_except | tbl_normal | 1     | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_parent'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_normal');
+          pubname          |  relname   | attrs | qual 
+---------------------------+------------+-------+------
+ pub_all_except_no_viaroot | tbl_normal | 1     | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_parent'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- two rows with different row filter
+SELECT * FROM test_gpt(ARRAY['pub_all', 'pub_normal'], 'tbl_normal');
+  pubname   |  relname   | attrs |   qual    
+------------+------------+-------+-----------
+ pub_all    | tbl_normal | 1     | 
+ pub_normal | tbl_normal | 1     | (id < 10)
+(2 rows)
+
+-- one row with 'pub_part_parent'
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_part_parent_no_viaroot'], 'tbl_parent');
+     pubname     |  relname   | attrs |    qual    
+-----------------+------------+-------+------------
+ pub_part_parent | tbl_parent | 1 2   | (id1 = 10)
+(1 row)
+
+-- no result, tbl_parent is the effective published OID due to pubviaroot
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_all'], 'tbl_part1');
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- no result, non-existent publication
+SELECT * FROM test_gpt(ARRAY['no_such_pub'], 'tbl_normal');
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- no result, non-table object
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'gpt_test_view');
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- no result, empty publication array
+SELECT * FROM test_gpt(ARRAY[]::text[], 'tbl_normal');
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- no result, OID 0 as target_relid
+SELECT * FROM pg_get_publication_tables(ARRAY['pub_normal'], 0::oid);
+ pubid | relid | attrs | qual 
+-------+-------+-------+------
+(0 rows)
+
+-- Clean up
+DROP FUNCTION test_gpt(text[], text);
+DROP PUBLICATION pub_all;
+DROP PUBLICATION pub_all_no_viaroot;
+DROP PUBLICATION pub_all_except;
+DROP PUBLICATION pub_all_except_no_viaroot;
+DROP PUBLICATION pub_schema;
+DROP PUBLICATION pub_normal;
+DROP PUBLICATION pub_part_leaf;
+DROP PUBLICATION pub_part_parent;
+DROP PUBLICATION pub_part_parent_no_viaroot;
+DROP PUBLICATION pub_part_parent_child;
+DROP VIEW gpt_test_view;
+DROP TABLE tbl_normal, tbl_parent, tbl_part1;
+DROP SCHEMA gpt_test_sch CASCADE;
+NOTICE:  drop cascades to table gpt_test_sch.tbl_sch
 -- stage objects for pg_dump tests
 CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int);
 CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int);
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 6bafad27571..94908e4f965 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1438,6 +1438,113 @@ RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
 
+-- Test pg_get_publication_tables(text[], oid) function
+CREATE SCHEMA gpt_test_sch;
+CREATE TABLE gpt_test_sch.tbl_sch (id int);
+CREATE TABLE tbl_normal (id int);
+CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1);
+CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10);
+CREATE VIEW gpt_test_view AS SELECT * FROM tbl_normal;
+
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION pub_all FOR ALL TABLES WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_all_no_viaroot FOR ALL TABLES WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except_no_viaroot FOR ALL TABLES EXCEPT (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch;
+CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10);
+CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_part_parent_no_viaroot FOR TABLE tbl_parent WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent_child FOR TABLE tbl_parent, tbl_part1 WITH (publish_via_partition_root = true);
+RESET client_min_messages;
+
+CREATE FUNCTION test_gpt(pubnames text[], relname text)
+RETURNS TABLE (
+  pubname text,
+  relname name,
+  attrs text,
+  qual text
+)
+BEGIN ATOMIC
+  SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid)
+    FROM pg_get_publication_tables(pubnames, relname::regclass::oid) gpt
+    JOIN pg_publication p ON p.oid = gpt.pubid
+    JOIN pg_class c ON c.oid = gpt.relid
+  ORDER BY p.pubname, c.relname;
+END;
+
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'tbl_normal');
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'gpt_test_sch.tbl_sch'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'gpt_test_sch.tbl_sch');
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'tbl_normal'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_part1'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_parent'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_parent'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_part1'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_parent'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_part1'); -- no result
+
+-- test for the EXCLUDE clause
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_normal');
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_parent'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_part1'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_normal');
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_parent'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_part1'); -- no result
+
+-- two rows with different row filter
+SELECT * FROM test_gpt(ARRAY['pub_all', 'pub_normal'], 'tbl_normal');
+
+-- one row with 'pub_part_parent'
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_part_parent_no_viaroot'], 'tbl_parent');
+
+-- no result, tbl_parent is the effective published OID due to pubviaroot
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_all'], 'tbl_part1');
+
+-- no result, non-existent publication
+SELECT * FROM test_gpt(ARRAY['no_such_pub'], 'tbl_normal');
+
+-- no result, non-table object
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'gpt_test_view');
+
+-- no result, empty publication array
+SELECT * FROM test_gpt(ARRAY[]::text[], 'tbl_normal');
+
+-- no result, OID 0 as target_relid
+SELECT * FROM pg_get_publication_tables(ARRAY['pub_normal'], 0::oid);
+
+-- Clean up
+DROP FUNCTION test_gpt(text[], text);
+DROP PUBLICATION pub_all;
+DROP PUBLICATION pub_all_no_viaroot;
+DROP PUBLICATION pub_all_except;
+DROP PUBLICATION pub_all_except_no_viaroot;
+DROP PUBLICATION pub_schema;
+DROP PUBLICATION pub_normal;
+DROP PUBLICATION pub_part_leaf;
+DROP PUBLICATION pub_part_parent;
+DROP PUBLICATION pub_part_parent_no_viaroot;
+DROP PUBLICATION pub_part_parent_child;
+DROP VIEW gpt_test_view;
+DROP TABLE tbl_normal, tbl_parent, tbl_part1;
+DROP SCHEMA gpt_test_sch CASCADE;
+
 -- stage objects for pg_dump tests
 CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int);
 CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int);
-- 
2.53.0



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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 08:48                     ` Re: Initial COPY of Logical Replication is too slow Peter Smith <[email protected]>
  2026-03-31 09:36                       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-31 12:07                         ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-31 19:40                           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-04-01 22:23                             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-04-02 05:45                               ` Amit Kapila <[email protected]>
  1 sibling, 0 replies; 47+ messages in thread

From: Amit Kapila @ 2026-04-02 05:45 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Peter Smith <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>

On Thu, Apr 2, 2026 at 3:53 AM Masahiko Sawada <[email protected]> wrote:
>
> On Tue, Mar 31, 2026 at 12:40 PM Masahiko Sawada <[email protected]> wrote:
> >
> > On Tue, Mar 31, 2026 at 5:07 AM Zhijie Hou (Fujitsu)
> > <[email protected]> wrote:
> > >
> > > On Tuesday, March 31, 2026 5:36 PM Amit Kapila <[email protected]> wrote:
> > > >
> > > > On Wed, Mar 25, 2026 at 2:19 PM Peter Smith <[email protected]>
> > > > wrote:
> > > > >
> > > > > There are many return points, and most of those "if" blocks cannot
> > > > > fall through (they return).
> > > > >
> > > > > I found it slightly difficult to read the code because I kept having
> > > > > to think, "OK, if we reached here, it means pubviaroot must be false,"
> > > > > or "OK, if we reached this far, then puballtables must be false, and
> > > > > pubviaroot must be false," etc.
> > > > >
> > > >
> > > > I can't say exactly why, but I find it difficult to read this function. So, I share
> > > > your concerns about the code of this function.
> > > > Because of its complexity it is difficult to ascertain that the functionality is
> > > > correct or we missed something. Also, considering it is correct today, in its
> > > > current form, it may become difficult to enhance it in future.
> > > >
> > >
> > > I attempted to refactor the code a bit based on my preferred style, as shown in
> > > the attachment. While the number of return points couldn't be reduced, I tried
> > > to eliminate if-else branches where possible. Sharing this top-up patch as a
> > > reference for an alternative style that reduces code size.
> > >
> >
> > Thanks. It looks like a good refactoring! I'd prefer to free the
> > ancestors list to avoid memory leak.
> >
> > I've attached the patch that incorporated all comments I got so far.
> > Feedback is very welcome.
> >
>
> I decided to simplify the code flow in the
> is_table_publishable_in_publication() by taking more proposed changes
> from Hou-san while accepting some memory leak. This function is
> limited to be used only in per-call-memory context so we don't need to
> worry about the actual memory leak. While we would need to change this
> function in the futuer when we want to use it other places too, I
> think it would be better to keep the function simple until then. I
> hope the added new comment also help understand the code flow of this
> function.
>

Yes, the function looks better and helps to understand the flow.
Thanks for improving it.

-- 
With Regards,
Amit Kapila.





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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 08:48                     ` Re: Initial COPY of Logical Replication is too slow Peter Smith <[email protected]>
  2026-03-31 09:36                       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-31 12:07                         ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-31 19:40                           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-04-01 22:23                             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-04-02 06:12                               ` Peter Smith <[email protected]>
  2026-04-02 22:13                                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  1 sibling, 1 reply; 47+ messages in thread

From: Peter Smith @ 2026-04-02 06:12 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Amit Kapila <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>

Hi Sawada-San

Some review comments for v8-0001.

======
Commit message

1.
The existing a VARIADIC array form of pg_get_publication_tables() is
preserved for backward compatibility. Tablesync workers use the new
two-argument form when connected to a publisher running PostgreSQL 19
or later.

~

Typo? "The existing a VARIADIC"

======
src/backend/catalog/pg_publication.c

is_publishable_table:

2.
+ /*
+ * Sequences are publishable according to is_publishable_class() so
+ * explicitly exclude here.
+ */
+ if (relform->relkind != RELKIND_SEQUENCE &&
+ is_publishable_class(tableoid, relform))
+ {
+ ReleaseSysCache(tuple);
+ return true;
+ }

It seemed strange to say that "sequences are publishable according to
is_publishable_class() so explicitly exclude", but then you proceed to
call is_publishable_class() anyway.

Maybe using a variable, and a different comment could be a better way
of expressing this?

SUGGESTION
bool ret;
...
/* Sequences are not tables, so this function returns false. */
if (relform->relkind == RELKIND_SEQUENCE)
    ret = false;
else
    ret = is_publishable_class(tableoid, relform);

ReleaseSysCache(tuple);
return ret;

~~~

is_table_publishable_in_publication:

3.
+ * A helper function for pg_get_publication_tables() to check whether the
+ * table of the given relid is published for the specified publication.

/table of the given relid/table with the given relid/

/is published for the/is published in the/

~~~

pg_get_publication_tables:

4.
+ * If filter_by_relid is true, only the row for target_relid is returned;
+ * if target_relid does not exist or is not part of the publications, zero
+ * rows are returned.  If filter_by_relid is false, rows for all tables
+ * within the specified publications are returned and target_relid is
+ * ignored.

Should that say "only the row(s) for target_relid", e.g. possibly
plural, because if same table is in multiple publications then there
are be multiple result rows, right?

======
src/include/catalog/pg_proc.dat

5.
Missed my previous [1] review comment #4?

First arg of pg_get_publication_tables_a should be plural 'pubnames',
same as first arg of pg_get_publication_tables_b.

======
src/test/regress/sql/publication.sql

6.
+CREATE PUBLICATION pub_all_except_no_viaroot FOR ALL TABLES EXCEPT
(TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH
(publish_via_partition_root = true);

Why is this publication called '...no_viaroot' when
publish_via_partition_root = true?

~~~

7.
+-- test for the EXCLUDE clause

Typo? /EXCLUDE clause/EXCEPT clause/

======
[1] https://www.postgresql.org/message-id/CAHut%2BPuSkabUB8H_hcwQz%3DBX5TWEj-8Ba%2BCP_PX78zN1fkhtKA%40ma...


Kind Regards,
Peter Smith.
Fujitsu Australia





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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 08:48                     ` Re: Initial COPY of Logical Replication is too slow Peter Smith <[email protected]>
  2026-03-31 09:36                       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-31 12:07                         ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-31 19:40                           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-04-01 22:23                             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-04-02 06:12                               ` Re: Initial COPY of Logical Replication is too slow Peter Smith <[email protected]>
@ 2026-04-02 22:13                                 ` Masahiko Sawada <[email protected]>
  0 siblings, 0 replies; 47+ messages in thread

From: Masahiko Sawada @ 2026-04-02 22:13 UTC (permalink / raw)
  To: Peter Smith <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Amit Kapila <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>

On Wed, Apr 1, 2026 at 11:12 PM Peter Smith <[email protected]> wrote:
>
> Hi Sawada-San
>
> Some review comments for v8-0001.
>
> ======
> Commit message
>
> 1.
> The existing a VARIADIC array form of pg_get_publication_tables() is
> preserved for backward compatibility. Tablesync workers use the new
> two-argument form when connected to a publisher running PostgreSQL 19
> or later.
>
> ~
>
> Typo? "The existing a VARIADIC"
>
> ======
> src/backend/catalog/pg_publication.c
>
> is_publishable_table:
>
> 2.
> + /*
> + * Sequences are publishable according to is_publishable_class() so
> + * explicitly exclude here.
> + */
> + if (relform->relkind != RELKIND_SEQUENCE &&
> + is_publishable_class(tableoid, relform))
> + {
> + ReleaseSysCache(tuple);
> + return true;
> + }
>
> It seemed strange to say that "sequences are publishable according to
> is_publishable_class() so explicitly exclude", but then you proceed to
> call is_publishable_class() anyway.
>
> Maybe using a variable, and a different comment could be a better way
> of expressing this?
>
> SUGGESTION
> bool ret;
> ...
> /* Sequences are not tables, so this function returns false. */
> if (relform->relkind == RELKIND_SEQUENCE)
>     ret = false;
> else
>     ret = is_publishable_class(tableoid, relform);
>
> ReleaseSysCache(tuple);
> return ret;
>
> ~~~
>
> is_table_publishable_in_publication:
>
> 3.
> + * A helper function for pg_get_publication_tables() to check whether the
> + * table of the given relid is published for the specified publication.
>
> /table of the given relid/table with the given relid/
>
> /is published for the/is published in the/
>
> ~~~
>
> pg_get_publication_tables:
>
> 4.
> + * If filter_by_relid is true, only the row for target_relid is returned;
> + * if target_relid does not exist or is not part of the publications, zero
> + * rows are returned.  If filter_by_relid is false, rows for all tables
> + * within the specified publications are returned and target_relid is
> + * ignored.
>
> Should that say "only the row(s) for target_relid", e.g. possibly
> plural, because if same table is in multiple publications then there
> are be multiple result rows, right?
>
> ======
> src/include/catalog/pg_proc.dat
>
> 5.
> Missed my previous [1] review comment #4?
>
> First arg of pg_get_publication_tables_a should be plural 'pubnames',
> same as first arg of pg_get_publication_tables_b.
>
> ======
> src/test/regress/sql/publication.sql
>
> 6.
> +CREATE PUBLICATION pub_all_except_no_viaroot FOR ALL TABLES EXCEPT
> (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH
> (publish_via_partition_root = true);
>
> Why is this publication called '...no_viaroot' when
> publish_via_partition_root = true?
>
> ~~~
>
> 7.
> +-- test for the EXCLUDE clause
>
> Typo? /EXCLUDE clause/EXCEPT clause/
>

Thank you for the comments!

I've pushed the patch after incorporating these points.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com





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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 08:48                     ` Re: Initial COPY of Logical Replication is too slow Peter Smith <[email protected]>
  2026-03-31 09:36                       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
@ 2026-03-31 17:28                         ` Masahiko Sawada <[email protected]>
  2026-04-01 05:04                           ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  1 sibling, 1 reply; 47+ messages in thread

From: Masahiko Sawada @ 2026-03-31 17:28 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: Peter Smith <[email protected]>; Jan Wieck <[email protected]>; [email protected]

On Tue, Mar 31, 2026 at 2:36 AM Amit Kapila <[email protected]> wrote:
>
> On Wed, Mar 25, 2026 at 2:19 PM Peter Smith <[email protected]> wrote:
> >
> > Hi Swada-San. Here are some minor review comments for v4-0001/2 combined.
> >
> > ======
> > src/backend/catalog/pg_publication.c
> >
> > is_table_publishable_in_publication:
> >
> > 1.
> > This function logic has a format like
> >
> > if (cond)
> > {
> >  ...
> >  return;
> > }
> >
> > if (cond2)
> > {
> >  ...
> >  return;
> > }
> >
> > etc.
> >
> > There are many return points, and most of those "if" blocks cannot
> > fall through (they return).
> >
> > I found it slightly difficult to read the code because I kept having
> > to think, "OK, if we reached here, it means pubviaroot must be false,"
> > or "OK, if we reached this far, then puballtables must be false, and
> > pubviaroot must be false," etc.
> >
>
> I can't say exactly why, but I find it difficult to read this
> function. So, I share your concerns about the code of this function.
> Because of its complexity it is difficult to ascertain that the
> functionality is correct or we missed something. Also, considering it
> is correct today, in its current form, it may become difficult to
> enhance it in future.

Okay, I'll refactor that function.

>
> One more comment on latest patch:
> *
> +static Datum
> +pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
> +                          Oid target_relid, bool filter_by_relid,
>
> Why do we need filter_by_relid as a separate parameter? Isn't the
> valid value of target_relid the same? If so, can't we use target_relid
> for the required checks?

If we don't have filter_by_relid, we would end up not filtering
anything if users pass 0 (InvalidOid) as the target_relid to the new
pg_get_publication_tables(). This is the same as the behavior of the
existing pg_get_publication_tables(), so I'm concerned that it could
be confusing that the function behaves the same even though passing
different arguments . We can check whether the given target_relid is
valid in pg_get_publication_b() but we would end up checking it
multiple times unnecessarily.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com





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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 08:48                     ` Re: Initial COPY of Logical Replication is too slow Peter Smith <[email protected]>
  2026-03-31 09:36                       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-31 17:28                         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-04-01 05:04                           ` Amit Kapila <[email protected]>
  2026-04-01 17:35                             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  0 siblings, 1 reply; 47+ messages in thread

From: Amit Kapila @ 2026-04-01 05:04 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; +Cc: Peter Smith <[email protected]>; Jan Wieck <[email protected]>; [email protected]

On Tue, Mar 31, 2026 at 10:58 PM Masahiko Sawada <[email protected]> wrote:
>
> On Tue, Mar 31, 2026 at 2:36 AM Amit Kapila <[email protected]> wrote:
> >
> > On Wed, Mar 25, 2026 at 2:19 PM Peter Smith <[email protected]> wrote:
> > >
> > > Hi Swada-San. Here are some minor review comments for v4-0001/2 combined.
> > >
> > > ======
> > > src/backend/catalog/pg_publication.c
> > >
> > > is_table_publishable_in_publication:
> > >
> > > 1.
> > > This function logic has a format like
> > >
> > > if (cond)
> > > {
> > >  ...
> > >  return;
> > > }
> > >
> > > if (cond2)
> > > {
> > >  ...
> > >  return;
> > > }
> > >
> > > etc.
> > >
> > > There are many return points, and most of those "if" blocks cannot
> > > fall through (they return).
> > >
> > > I found it slightly difficult to read the code because I kept having
> > > to think, "OK, if we reached here, it means pubviaroot must be false,"
> > > or "OK, if we reached this far, then puballtables must be false, and
> > > pubviaroot must be false," etc.
> > >
> >
> > I can't say exactly why, but I find it difficult to read this
> > function. So, I share your concerns about the code of this function.
> > Because of its complexity it is difficult to ascertain that the
> > functionality is correct or we missed something. Also, considering it
> > is correct today, in its current form, it may become difficult to
> > enhance it in future.
>
> Okay, I'll refactor that function.
>
> >
> > One more comment on latest patch:
> > *
> > +static Datum
> > +pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
> > +                          Oid target_relid, bool filter_by_relid,
> >
> > Why do we need filter_by_relid as a separate parameter? Isn't the
> > valid value of target_relid the same? If so, can't we use target_relid
> > for the required checks?
>
> If we don't have filter_by_relid, we would end up not filtering
> anything if users pass 0 (InvalidOid) as the target_relid to the new
> pg_get_publication_tables(). This is the same as the behavior of the
> existing pg_get_publication_tables(),
>

Isn't that what we want when a user passes InvalidOid? What is the
expected behavior in that case?

-- 
With Regards,
Amit Kapila.





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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 08:48                     ` Re: Initial COPY of Logical Replication is too slow Peter Smith <[email protected]>
  2026-03-31 09:36                       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-31 17:28                         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-04-01 05:04                           ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
@ 2026-04-01 17:35                             ` Masahiko Sawada <[email protected]>
  2026-04-02 05:28                               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  0 siblings, 1 reply; 47+ messages in thread

From: Masahiko Sawada @ 2026-04-01 17:35 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: Peter Smith <[email protected]>; Jan Wieck <[email protected]>; [email protected]

On Tue, Mar 31, 2026 at 10:04 PM Amit Kapila <[email protected]> wrote:
>
> On Tue, Mar 31, 2026 at 10:58 PM Masahiko Sawada <[email protected]> wrote:
> >
> > On Tue, Mar 31, 2026 at 2:36 AM Amit Kapila <[email protected]> wrote:
> > >
> > > On Wed, Mar 25, 2026 at 2:19 PM Peter Smith <[email protected]> wrote:
> > > >
> > > > Hi Swada-San. Here are some minor review comments for v4-0001/2 combined.
> > > >
> > > > ======
> > > > src/backend/catalog/pg_publication.c
> > > >
> > > > is_table_publishable_in_publication:
> > > >
> > > > 1.
> > > > This function logic has a format like
> > > >
> > > > if (cond)
> > > > {
> > > >  ...
> > > >  return;
> > > > }
> > > >
> > > > if (cond2)
> > > > {
> > > >  ...
> > > >  return;
> > > > }
> > > >
> > > > etc.
> > > >
> > > > There are many return points, and most of those "if" blocks cannot
> > > > fall through (they return).
> > > >
> > > > I found it slightly difficult to read the code because I kept having
> > > > to think, "OK, if we reached here, it means pubviaroot must be false,"
> > > > or "OK, if we reached this far, then puballtables must be false, and
> > > > pubviaroot must be false," etc.
> > > >
> > >
> > > I can't say exactly why, but I find it difficult to read this
> > > function. So, I share your concerns about the code of this function.
> > > Because of its complexity it is difficult to ascertain that the
> > > functionality is correct or we missed something. Also, considering it
> > > is correct today, in its current form, it may become difficult to
> > > enhance it in future.
> >
> > Okay, I'll refactor that function.
> >
> > >
> > > One more comment on latest patch:
> > > *
> > > +static Datum
> > > +pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
> > > +                          Oid target_relid, bool filter_by_relid,
> > >
> > > Why do we need filter_by_relid as a separate parameter? Isn't the
> > > valid value of target_relid the same? If so, can't we use target_relid
> > > for the required checks?
> >
> > If we don't have filter_by_relid, we would end up not filtering
> > anything if users pass 0 (InvalidOid) as the target_relid to the new
> > pg_get_publication_tables(). This is the same as the behavior of the
> > existing pg_get_publication_tables(),
> >
>
> Isn't that what we want when a user passes InvalidOid? What is the
> expected behavior in that case?
>

While it could be contrivarsial what we expect when "users wants to
filter the result by InvalidOid", I think the new
pg_get_publication_tables() should not return anything in this case
rather than return all table information. I think this behavior is
consistent with the case where users pass non-table OID to the
function. I don't want to make passing InvalidOid a special case in
the new function.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com





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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 08:48                     ` Re: Initial COPY of Logical Replication is too slow Peter Smith <[email protected]>
  2026-03-31 09:36                       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-31 17:28                         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-04-01 05:04                           ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-04-01 17:35                             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-04-02 05:28                               ` Amit Kapila <[email protected]>
  0 siblings, 0 replies; 47+ messages in thread

From: Amit Kapila @ 2026-04-02 05:28 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; +Cc: Peter Smith <[email protected]>; Jan Wieck <[email protected]>; [email protected]

On Wed, Apr 1, 2026 at 11:06 PM Masahiko Sawada <[email protected]> wrote:
>
> On Tue, Mar 31, 2026 at 10:04 PM Amit Kapila <[email protected]> wrote:
> >
> > On Tue, Mar 31, 2026 at 10:58 PM Masahiko Sawada <[email protected]> wrote:
> > >
> > > On Tue, Mar 31, 2026 at 2:36 AM Amit Kapila <[email protected]> wrote:
> > > >
> > > > On Wed, Mar 25, 2026 at 2:19 PM Peter Smith <[email protected]> wrote:
> > > > >
> > > > > Hi Swada-San. Here are some minor review comments for v4-0001/2 combined.
> > > > >
> > > > > ======
> > > > > src/backend/catalog/pg_publication.c
> > > > >
> > > > > is_table_publishable_in_publication:
> > > > >
> > > > > 1.
> > > > > This function logic has a format like
> > > > >
> > > > > if (cond)
> > > > > {
> > > > >  ...
> > > > >  return;
> > > > > }
> > > > >
> > > > > if (cond2)
> > > > > {
> > > > >  ...
> > > > >  return;
> > > > > }
> > > > >
> > > > > etc.
> > > > >
> > > > > There are many return points, and most of those "if" blocks cannot
> > > > > fall through (they return).
> > > > >
> > > > > I found it slightly difficult to read the code because I kept having
> > > > > to think, "OK, if we reached here, it means pubviaroot must be false,"
> > > > > or "OK, if we reached this far, then puballtables must be false, and
> > > > > pubviaroot must be false," etc.
> > > > >
> > > >
> > > > I can't say exactly why, but I find it difficult to read this
> > > > function. So, I share your concerns about the code of this function.
> > > > Because of its complexity it is difficult to ascertain that the
> > > > functionality is correct or we missed something. Also, considering it
> > > > is correct today, in its current form, it may become difficult to
> > > > enhance it in future.
> > >
> > > Okay, I'll refactor that function.
> > >
> > > >
> > > > One more comment on latest patch:
> > > > *
> > > > +static Datum
> > > > +pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
> > > > +                          Oid target_relid, bool filter_by_relid,
> > > >
> > > > Why do we need filter_by_relid as a separate parameter? Isn't the
> > > > valid value of target_relid the same? If so, can't we use target_relid
> > > > for the required checks?
> > >
> > > If we don't have filter_by_relid, we would end up not filtering
> > > anything if users pass 0 (InvalidOid) as the target_relid to the new
> > > pg_get_publication_tables(). This is the same as the behavior of the
> > > existing pg_get_publication_tables(),
> > >
> >
> > Isn't that what we want when a user passes InvalidOid? What is the
> > expected behavior in that case?
> >
>
> While it could be contrivarsial what we expect when "users wants to
> filter the result by InvalidOid", I think the new
> pg_get_publication_tables() should not return anything in this case
> rather than return all table information. I think this behavior is
> consistent with the case where users pass non-table OID to the
> function. I don't want to make passing InvalidOid a special case in
> the new function.
>

Fair enough. I am fine with this definition.

-- 
With Regards,
Amit Kapila.





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

* RE: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-03-26 05:44                     ` Zhijie Hou (Fujitsu) <[email protected]>
  3 siblings, 0 replies; 47+ messages in thread

From: Zhijie Hou (Fujitsu) @ 2026-03-26 05:44 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; Amit Kapila <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected] <[email protected]>

On Wednesday, March 25, 2026 2:07 PM Masahiko Sawada <[email protected]> wrote:
> On Tue, Mar 24, 2026 at 11:57 AM Masahiko Sawada
> 
> I figured out that the join with pg_publication works as a filter; non-existence
> publication names are not passed to the function. If we pass the list of
> publication names to the new function signature, while we can simplify the
> patch and avoid a join, we would change the existing function behavior so that
> it ignores non-existence publications.
> 
> I've attached the updated patch. The 0001 patch just incorporated the review
> comments so far, and the 0002 patch is a draft change for the above idea. Since
> pg_get_publication_tables(VARIADIC text) is not a documented function, I think
> we can accept small behavior changes. So I'm going to go with this direction.
> Feedback is very welcome.

Thanks for updating the patches. I have few comments for 0001:

1.

+ * specific table. Otherwise, if returns information for all tables within the
+ * specified publications.

if => it

2.

I think the function shall reject relids that do not reference a valid
publishable table (e.g., sequences, views, or materialized views).

3.

With publish_via_root = true, when both a partitioned table and its child are in
a publication, I expected passing the child's relid will return NULL (changes are
published via the root). Currently it returns the child's relid:

CREATE TABLE sales (
    id SERIAL,
    sale_date DATE NOT NULL
) PARTITION BY RANGE (sale_date);

CREATE TABLE sales_2024_q1 PARTITION OF sales
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');

CREATE publication pub for table sales, sales_2024_q1 with ( publish_via_partition_root );

select pg_get_publication_tables('pub', 'sales_2024_q1'::regclass);

Best Regards,
Hou zj


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

* RE: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-03-26 08:35                     ` Hayato Kuroda (Fujitsu) <[email protected]>
  2026-03-26 12:46                       ` RE: Initial COPY of Logical Replication is too slow Hayato Kuroda (Fujitsu) <[email protected]>
  2026-03-31 00:29                       ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  3 siblings, 2 replies; 47+ messages in thread

From: Hayato Kuroda (Fujitsu) @ 2026-03-26 08:35 UTC (permalink / raw)
  To: 'Masahiko Sawada' <[email protected]>; Amit Kapila <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected] <[email protected]>

Dear Sawada-san,
(Sending again because blocked by some rules)

I ran the performance testing independently for the 0001 patch. Overall performance looked
very nice, new function spent O(1) time based on the total number of tables.
It seems good enough.

Source code:
----------------
HEAD (4287c50f) + v4-0001 patch.

Setup:
---------
A database cluster was set up with shared_buffers=100GB. Several tables were
defined on the public schema, and same number of tables were on the sch1.
Total number of tables were {50, 500, 5000, 50000}.
A publication included a schema sch1 and all public tables individually.

Attached script setup the same. The suffix is changed to .txt to pass the rule.

Workload Run:
--------------------
I ran two types of SQLs and measured the execution time via \timing metacommand.
Cases were emulated which tablesync worker would do.

Case 1: old SQL
```
SELECT DISTINCT
  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)
   THEN NULL ELSE gpt.attrs END)
  FROM pg_publication p,
  LATERAL pg_get_publication_tables(p.pubname) gpt,
  pg_class c
 WHERE gpt.relid = 17885 AND c.oid = gpt.relid
   AND p.pubname IN ( 'pub' );
```

Case 2: new SQL
```
SELECT DISTINCT
  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)
   THEN NULL ELSE gpt.attrs END)
  FROM pg_publication p,
  LATERAL pg_get_publication_tables(p.pubname, 16535) gpt,
  pg_class c
 WHERE c.oid = gpt.relid
   AND p.pubname IN ( 'pub' );
```

Result Observations:
---------------
Attached bar graph shows the result. A logarithmic scale is used for the execution
time (y-axis) to see both small/large scale case. The spent time became approximately
10x longer for 500->5000, and 5000->50000, in case of old SQL is used.
Apart from that, the spent time for the new SQL is mostly the stable based on the
number of tables.

Detailed Result:
--------------
Each cell are the median of 10 runs.

Total tables	Execution time for the old SQL was done [ms]	Execution time for the old SQL was done [ms]
50		5.77						4.19
500		15.75						4.28
5000		120.39						4.22
50000		1741.89						4.60
500000		73287.16					4.95


Also, here is a small code comment. I think we can have an Assert at the
begining of the pg_get_publication_tables(), something like below.

```
@@ -1392,6 +1392,9 @@ pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
        FuncCallContext *funcctx;
        List       *table_infos = NIL;
 
+       Assert((pubnames && (!pubname && !OidIsValid(target_relid))) ||
+                  (!pubnames && (pubname && OidIsValid(target_relid))));
```

Best regards,
Hayato Kuroda
FUJITSU LIMITED


#!/bin/bash

####################
### Declarations ###
####################

## Publisher-related params
PORT_PUB=6633
DATA_PUB=data_pub
LOG_PUB=pub.log

## Number of runs
NUMRUN=1

## Measurement params
NUMTABLES=25000

# Setup an instance with above parameters.
function setup () {
    ################
    ### clean up ###
    ################
    pg_ctl stop -D $DATA_PUB -w
    rm -rf $DATA_PUB $LOG_PUB

    #######################
    ### setup publisher ###
    #######################
    initdb -D data_pub -U postgres
    cat << EOF >> data_pub/postgresql.conf
port=$PORT_PUB
autovacuum = false
shared_buffers = '100GB'
max_wal_size = 20GB
min_wal_size = 10GB
wal_level = logical
EOF
    pg_ctl -D $DATA_PUB start -w -l $LOG_PUB

    (
	echo "CREATE SCHEMA sch1;"
	echo "SELECT 'CREATE TABLE tab_' || generate_series(1, $NUMTABLES) || '(id int primary key)' ; \gexec"
	echo "SELECT 'CREATE TABLE sch1.tab_' || generate_series(1, $NUMTABLES) || '(id int primary key)' ; \gexec"
	echo "CREATE PUBLICATION pub FOR TABLES IN SCHEMA sch1;"
	echo "SELECT 'ALTER PUBLICATION pub ADD TABLE tab_' || generate_series(1, $NUMTABLES) ; \gexec"
    ) | psql -U postgres -p $PORT_PUB
}

setup


Attachments:

  [image/png] pg_get_publication_tables.png (30.3K, 2-pg_get_publication_tables.png)
  download | view image

  [text/plain] setup.txt (1.2K, 3-setup.txt)
  download | inline:
#!/bin/bash

####################
### Declarations ###
####################

## Publisher-related params
PORT_PUB=6633
DATA_PUB=data_pub
LOG_PUB=pub.log

## Number of runs
NUMRUN=1

## Measurement params
NUMTABLES=25000

# Setup an instance with above parameters.
function setup () {
    ################
    ### clean up ###
    ################
    pg_ctl stop -D $DATA_PUB -w
    rm -rf $DATA_PUB $LOG_PUB

    #######################
    ### setup publisher ###
    #######################
    initdb -D data_pub -U postgres
    cat << EOF >> data_pub/postgresql.conf
port=$PORT_PUB
autovacuum = false
shared_buffers = '100GB'
max_wal_size = 20GB
min_wal_size = 10GB
wal_level = logical
EOF
    pg_ctl -D $DATA_PUB start -w -l $LOG_PUB

    (
	echo "CREATE SCHEMA sch1;"
	echo "SELECT 'CREATE TABLE tab_' || generate_series(1, $NUMTABLES) || '(id int primary key)' ; \gexec"
	echo "SELECT 'CREATE TABLE sch1.tab_' || generate_series(1, $NUMTABLES) || '(id int primary key)' ; \gexec"
	echo "CREATE PUBLICATION pub FOR TABLES IN SCHEMA sch1;"
	echo "SELECT 'ALTER PUBLICATION pub ADD TABLE tab_' || generate_series(1, $NUMTABLES) ; \gexec"
    ) | psql -U postgres -p $PORT_PUB
}

setup

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

* RE: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-26 08:35                     ` RE: Initial COPY of Logical Replication is too slow Hayato Kuroda (Fujitsu) <[email protected]>
@ 2026-03-26 12:46                       ` Hayato Kuroda (Fujitsu) <[email protected]>
  1 sibling, 0 replies; 47+ messages in thread

From: Hayato Kuroda (Fujitsu) @ 2026-03-26 12:46 UTC (permalink / raw)
  To: 'Masahiko Sawada' <[email protected]>; Amit Kapila <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected] <[email protected]>

> I ran the performance testing independently for the 0001 patch. Overall
> performance looked
> very nice, new function spent O(1) time based on the total number of tables.
> It seems good enough.

...and I tested 0002 as well with the same settings, and the trend was the same as 0001.

Both 0001 and 0002 were applied and below SQL was run, which was same was what
tablesync worker would try:

```
SELECT DISTINCT
  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)
   THEN NULL ELSE gpt.attrs END)
  FROM pg_get_publication_tables(ARRAY['pub'], 16535) gpt,
  pg_class c
 WHERE c.oid = gpt.relid;
```

And below is the result. Each cell shows the execution time of the SQL. HEAD
column is the case when [1] was done. 0001 column is the case for [2].
Looks like the SQL used by 0002 looks slightly faster, which is same as the
expectation. JOIN was removed once.

Total tables	HEAD [ms]	0001 [ms]	0001 + 0002 [ms]
50		5.77		4.19		3.74
500		15.75		4.28		3.76
5000		120.39		4.22		3.79
50000		1741.89		4.60		4.11
500000		73287.16	4.95		4.38

Attached graph visualized the table.

[1]:
```
SELECT DISTINCT
  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)
   THEN NULL ELSE gpt.attrs END)
  FROM pg_publication p,
  LATERAL pg_get_publication_tables(p.pubname) gpt,
  pg_class c
 WHERE gpt.relid = 17885 AND c.oid = gpt.relid
   AND p.pubname IN ( 'pub' );
```

[2]:
```
SELECT DISTINCT
  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)
   THEN NULL ELSE gpt.attrs END)
  FROM pg_publication p,
  LATERAL pg_get_publication_tables(p.pubname, 16535) gpt,
  pg_class c
 WHERE c.oid = gpt.relid
   AND p.pubname IN ( 'pub' );
```

Best regards,
Hayato Kuroda
FUJITSU LIMITED



Attachments:

  [image/png] pg_get_publication_tables.png (30.4K, 2-pg_get_publication_tables.png)
  download | view image

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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-26 08:35                     ` RE: Initial COPY of Logical Replication is too slow Hayato Kuroda (Fujitsu) <[email protected]>
@ 2026-03-31 00:29                       ` Masahiko Sawada <[email protected]>
  1 sibling, 0 replies; 47+ messages in thread

From: Masahiko Sawada @ 2026-03-31 00:29 UTC (permalink / raw)
  To: Hayato Kuroda (Fujitsu) <[email protected]>; +Cc: Amit Kapila <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>

On Thu, Mar 26, 2026 at 1:35 AM Hayato Kuroda (Fujitsu)
<[email protected]> wrote:
>
> Dear Sawada-san,
> (Sending again because blocked by some rules)
>
> I ran the performance testing independently for the 0001 patch. Overall performance looked
> very nice, new function spent O(1) time based on the total number of tables.
> It seems good enough.
>
> Source code:
> ----------------
> HEAD (4287c50f) + v4-0001 patch.
>
> Setup:
> ---------
> A database cluster was set up with shared_buffers=100GB. Several tables were
> defined on the public schema, and same number of tables were on the sch1.
> Total number of tables were {50, 500, 5000, 50000}.
> A publication included a schema sch1 and all public tables individually.
>
> Attached script setup the same. The suffix is changed to .txt to pass the rule.
>
> Workload Run:
> --------------------
> I ran two types of SQLs and measured the execution time via \timing metacommand.
> Cases were emulated which tablesync worker would do.
>
> Case 1: old SQL
> ```
> SELECT DISTINCT
>   (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)
>    THEN NULL ELSE gpt.attrs END)
>   FROM pg_publication p,
>   LATERAL pg_get_publication_tables(p.pubname) gpt,
>   pg_class c
>  WHERE gpt.relid = 17885 AND c.oid = gpt.relid
>    AND p.pubname IN ( 'pub' );
> ```
>
> Case 2: new SQL
> ```
> SELECT DISTINCT
>   (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)
>    THEN NULL ELSE gpt.attrs END)
>   FROM pg_publication p,
>   LATERAL pg_get_publication_tables(p.pubname, 16535) gpt,
>   pg_class c
>  WHERE c.oid = gpt.relid
>    AND p.pubname IN ( 'pub' );
> ```
>
> Result Observations:
> ---------------
> Attached bar graph shows the result. A logarithmic scale is used for the execution
> time (y-axis) to see both small/large scale case. The spent time became approximately
> 10x longer for 500->5000, and 5000->50000, in case of old SQL is used.
> Apart from that, the spent time for the new SQL is mostly the stable based on the
> number of tables.
>
> Detailed Result:
> --------------
> Each cell are the median of 10 runs.
>
> Total tables    Execution time for the old SQL was done [ms]    Execution time for the old SQL was done [ms]
> 50              5.77                                            4.19
> 500             15.75                                           4.28
> 5000            120.39                                          4.22
> 50000           1741.89                                         4.60
> 500000          73287.16                                        4.95

Thank you for doing the performance tests! These observation match the
results of my local performance test.

BTW the new is_table_publishable_in_publication() can be useful other
places too where we check if the particular table is published by the
publication, for example get-rel_sync_entry(). It would be a separate
patch though.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com





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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-03-26 10:43                     ` Amit Kapila <[email protected]>
  2026-03-26 23:51                       ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  3 siblings, 1 reply; 47+ messages in thread

From: Amit Kapila @ 2026-03-26 10:43 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected]

On Wed, Mar 25, 2026 at 10:37 AM Masahiko Sawada <[email protected]> wrote:
>
> I figured out that the join with pg_publication works as a filter;
> non-existence publication names are not passed to the function. If we
> pass the list of publication names to the new function signature,
> while we can simplify the patch and avoid a join, we would change the
> existing function behavior so that it ignores non-existence
> publications.
>
> I've attached the updated patch. The 0001 patch just incorporated the
> review comments so far, and the 0002 patch is a draft change for the
> above idea. Since pg_get_publication_tables(VARIADIC text) is not a
> documented function, I think we can accept small behavior changes. So
> I'm going to go with this direction.
>

What behaviour change are you referring to? In general, the direction
appears right to me.

-- 
With Regards,
Amit Kapila.





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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-26 10:43                     ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
@ 2026-03-26 23:51                       ` Masahiko Sawada <[email protected]>
  2026-03-27 03:16                         ` RE: Initial COPY of Logical Replication is too slow Hayato Kuroda (Fujitsu) <[email protected]>
  0 siblings, 1 reply; 47+ messages in thread

From: Masahiko Sawada @ 2026-03-26 23:51 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected]

On Thu, Mar 26, 2026 at 3:44 AM Amit Kapila <[email protected]> wrote:
>
> On Wed, Mar 25, 2026 at 10:37 AM Masahiko Sawada <[email protected]> wrote:
> >
> > I figured out that the join with pg_publication works as a filter;
> > non-existence publication names are not passed to the function. If we
> > pass the list of publication names to the new function signature,
> > while we can simplify the patch and avoid a join, we would change the
> > existing function behavior so that it ignores non-existence
> > publications.
> >
> > I've attached the updated patch. The 0001 patch just incorporated the
> > review comments so far, and the 0002 patch is a draft change for the
> > above idea. Since pg_get_publication_tables(VARIADIC text) is not a
> > documented function, I think we can accept small behavior changes. So
> > I'm going to go with this direction.
> >
>
> What behaviour change are you referring to? In general, the direction
> appears right to me.

When passing a non-existent publication name, the current behavior
raises an error while the new behavior does nothing (i.e., the
difference is calling GetPublicationByName() with missing_ok = true or
false).

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com





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

* RE: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-26 10:43                     ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-26 23:51                       ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-03-27 03:16                         ` Hayato Kuroda (Fujitsu) <[email protected]>
  2026-03-27 03:51                           ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  0 siblings, 1 reply; 47+ messages in thread

From: Hayato Kuroda (Fujitsu) @ 2026-03-27 03:16 UTC (permalink / raw)
  To: 'Masahiko Sawada' <[email protected]>; Amit Kapila <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected] <[email protected]>

Dear Sawada-san,

> When passing a non-existent publication name, the current behavior
> raises an error while the new behavior does nothing (i.e., the
> difference is calling GetPublicationByName() with missing_ok = true or
> false).

To confirm; It's because in PG18-, p.pubname was chosen from the pg_publication
in the publisher, but this patch the name list is taken from the subscriber, right?
If some publications are dropped on the publisher, the ERROR could be raised.

For the backward compatibility I suggest switching the policy based on the API
version. E.g.,

```
 static Datum
 pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
-                                                 Oid target_relid)
+                                                 Oid target_relid, bool missing_ok)
...
@@ -1631,7 +1631,7 @@ Datum
 pg_get_publication_tables_a(PG_FUNCTION_ARGS)
 {
        /* Get the information of the tables in the given publications */
-       return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0), InvalidOid);
+       return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0), InvalidOid, false);
```

Another comment for publication.sql.

```
-- Clean up
DROP FUNCTION test_gpt(text, text);
```

It should be test_gpt(text[], text);

Best regards,
Hayato Kuroda
FUJITSU LIMITED



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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-26 10:43                     ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-26 23:51                       ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-27 03:16                         ` RE: Initial COPY of Logical Replication is too slow Hayato Kuroda (Fujitsu) <[email protected]>
@ 2026-03-27 03:51                           ` Amit Kapila <[email protected]>
  2026-03-27 06:20                             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  0 siblings, 1 reply; 47+ messages in thread

From: Amit Kapila @ 2026-03-27 03:51 UTC (permalink / raw)
  To: Hayato Kuroda (Fujitsu) <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>

On Fri, Mar 27, 2026 at 8:46 AM Hayato Kuroda (Fujitsu)
<[email protected]> wrote:
>
> Dear Sawada-san,
>
> > When passing a non-existent publication name, the current behavior
> > raises an error while the new behavior does nothing (i.e., the
> > difference is calling GetPublicationByName() with missing_ok = true or
> > false).
>
> To confirm; It's because in PG18-, p.pubname was chosen from the pg_publication
> in the publisher, but this patch the name list is taken from the subscriber, right?
> If some publications are dropped on the publisher, the ERROR could be raised.
>
> For the backward compatibility I suggest switching the policy based on the API
> version. E.g.,
>
> ```
>  static Datum
>  pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
> -                                                 Oid target_relid)
> +                                                 Oid target_relid, bool missing_ok)
> ...
> @@ -1631,7 +1631,7 @@ Datum
>  pg_get_publication_tables_a(PG_FUNCTION_ARGS)
>  {
>         /* Get the information of the tables in the given publications */
> -       return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0), InvalidOid);
> +       return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0), InvalidOid, false);
> ```
>

Sounds like a good idea for backward compatibility.

-- 
With Regards,
Amit Kapila.





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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-26 10:43                     ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-26 23:51                       ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-27 03:16                         ` RE: Initial COPY of Logical Replication is too slow Hayato Kuroda (Fujitsu) <[email protected]>
  2026-03-27 03:51                           ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
@ 2026-03-27 06:20                             ` Masahiko Sawada <[email protected]>
  2026-03-27 13:07                               ` Re: Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2026-03-30 07:16                               ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-30 07:42                               ` RE: Initial COPY of Logical Replication is too slow Hayato Kuroda (Fujitsu) <[email protected]>
  0 siblings, 3 replies; 47+ messages in thread

From: Masahiko Sawada @ 2026-03-27 06:20 UTC (permalink / raw)
  To: Amit Kapila <[email protected]>; +Cc: Hayato Kuroda (Fujitsu) <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>

On Thu, Mar 26, 2026 at 8:51 PM Amit Kapila <[email protected]> wrote:
>
> On Fri, Mar 27, 2026 at 8:46 AM Hayato Kuroda (Fujitsu)
> <[email protected]> wrote:
> >
> > Dear Sawada-san,
> >
> > > When passing a non-existent publication name, the current behavior
> > > raises an error while the new behavior does nothing (i.e., the
> > > difference is calling GetPublicationByName() with missing_ok = true or
> > > false).
> >
> > To confirm; It's because in PG18-, p.pubname was chosen from the pg_publication
> > in the publisher, but this patch the name list is taken from the subscriber, right?
> > If some publications are dropped on the publisher, the ERROR could be raised.
> >
> > For the backward compatibility I suggest switching the policy based on the API
> > version. E.g.,
> >
> > ```
> >  static Datum
> >  pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
> > -                                                 Oid target_relid)
> > +                                                 Oid target_relid, bool missing_ok)
> > ...
> > @@ -1631,7 +1631,7 @@ Datum
> >  pg_get_publication_tables_a(PG_FUNCTION_ARGS)
> >  {
> >         /* Get the information of the tables in the given publications */
> > -       return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0), InvalidOid);
> > +       return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0), InvalidOid, false);
> > ```
> >
>
> Sounds like a good idea for backward compatibility.

+1.

I've attached the updated patch. I believe I've addressed all comments
I got so far. In addition to that, I've refactored
is_table_publishable_in_publication() and added more regression tests.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com


Attachments:

  [text/x-patch] v5-0001-Avoid-full-table-scans-when-getting-publication-t.patch (28.4K, 2-v5-0001-Avoid-full-table-scans-when-getting-publication-t.patch)
  download | inline diff:
From 601c764b27df8994c801d3ced81c5821ba1400c6 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
Date: Fri, 27 Feb 2026 15:42:38 -0800
Subject: [PATCH v5] Avoid full table scans when getting publication table
 information by tablesync workers.

Reported-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zhijie Hou <[email protected]>
Reviewed-by: Matheus Alcantara <[email protected]>
Reviewed-by: Amit Kapila <[email protected]>
Reviewed-by: Peter Smith <[email protected]>
Reviewed-by: Hayato Kuroda <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Discussion: https://postgr.es/m/CAB-JLwbBFNuASyEnZWP0Tck9uNkthBZqi6WoXNevUT6+mV8XmA@mail.gmail.com
---
 src/backend/catalog/pg_publication.c        | 213 +++++++++++++++---
 src/backend/replication/logical/tablesync.c |  70 ++++--
 src/include/catalog/pg_proc.dat             |  11 +-
 src/test/regress/expected/publication.out   | 225 ++++++++++++++++++++
 src/test/regress/sql/publication.sql        | 107 ++++++++++
 5 files changed, 576 insertions(+), 50 deletions(-)

diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index c92ff3f51c3..b382e31f7c1 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -1264,12 +1264,120 @@ GetPublicationByName(const char *pubname, bool missing_ok)
 }
 
 /*
- * Get information of the tables in the given publication array.
+ * Returns true if the table of the given relid is published for the specified
+ * publication.
+ *
+ * This function evaluates the effective published OID based on the
+ * publish_via_partition_root setting, rather than just checking catalog entries
+ * (e.g., pg_publication_rel). For instance, when publish_via_partition_root is
+ * false, it returns false for a parent partitioned table and true for its leaf
+ * partitions, even if the parent is the one explicitly added to the publication.
  *
- * Returns pubid, relid, column list, row filter for each table.
+ * For performance reasons, this function avoids the overhead of constructing
+ * the complete list of published tables during the evaluation. It can execute
+ * quickly even when the publication contains a large number of relations.
  */
-Datum
-pg_get_publication_tables(PG_FUNCTION_ARGS)
+static bool
+is_table_publishable_in_publication(Oid relid, Publication *pub)
+{
+	bool		relispartition;
+
+	/*
+	 * For non-pubviaroot publications, a partitioned table is never the
+	 * effective published OID; only its leaf partitions can be.
+	 */
+	if (!pub->pubviaroot && get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE)
+		return false;
+
+	relispartition = get_rel_relispartition(relid);
+
+	if (pub->alltables)
+	{
+		Oid			target_relid = relid;
+
+		if (pub->pubviaroot)
+		{
+			/*
+			 * ALL TABLES with pubviaroot includes only regular tables or
+			 * top-most partitioned tables -- never child partitions.
+			 */
+			if (relispartition)
+				return false;
+		}
+		else if (relispartition)
+		{
+			List	   *ancestors = get_partition_ancestors(relid);
+
+			/*
+			 * Only the top-most ancestor can appear in the EXCEPT clause.
+			 * Therefore, for a partition, exclusion must be evaluated at the
+			 * top-most ancestor.
+			 */
+			target_relid = llast_oid(ancestors);
+			list_free(ancestors);
+		}
+
+		/*
+		 * The table is published unless it appears in the EXCEPT clause. ALL
+		 * TABLES publications store only EXCEPT'ed tables in
+		 * pg_publication_rel, so checking existence is sufficient.
+		 */
+		return !SearchSysCacheExists2(PUBLICATIONRELMAP,
+									  ObjectIdGetDatum(target_relid),
+									  ObjectIdGetDatum(pub->oid));
+	}
+
+	/*
+	 * Non-alltables
+	 */
+	if (relispartition)
+	{
+		List	   *ancestors = get_partition_ancestors(relid);
+		Oid			topmost = GetTopMostAncestorInPublication(pub->oid, ancestors, NULL);
+
+		list_free(ancestors);
+
+		if (OidIsValid(topmost))
+		{
+			/*
+			 * If pubviaroot is true, the ancestor is published instead of the
+			 * partition, so exclude it. Otherwise, the ancestor covers the
+			 * partition, so include it.
+			 */
+			return !pub->pubviaroot;
+		}
+
+		/* Ancestor not published; fall through to check the partition itself */
+	}
+
+	/*
+	 * Check whether the table is explicitly published via pg_publication_rel
+	 * or pg_publication_namespace.
+	 */
+	return (SearchSysCacheExists2(PUBLICATIONRELMAP,
+								  ObjectIdGetDatum(relid),
+								  ObjectIdGetDatum(pub->oid)) ||
+			SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP,
+								  ObjectIdGetDatum(get_rel_namespace(relid)),
+								  ObjectIdGetDatum(pub->oid)));
+}
+
+/*
+ * Helper function to get information of the tables in the given
+ * publication(s).
+ *
+ * If filter_by_relid is true, only the row for target_relid is returned;
+ * if target_relid does not exist or is not part of the publications, zero
+ * rows are returned.  If filter_by_relid is false, rows for all tables
+ * within the specified publications are returned and target_relid is
+ * ignored.
+ *
+ * Returns pubid, relid, column list, and row filter for each table.
+ */
+static Datum
+pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
+						  Oid target_relid, bool filter_by_relid,
+						  bool pub_missing_ok)
 {
 #define NUM_PUBLICATION_TABLES_ELEM	4
 	FuncCallContext *funcctx;
@@ -1280,11 +1388,11 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 	{
 		TupleDesc	tupdesc;
 		MemoryContext oldcontext;
-		ArrayType  *arr;
 		Datum	   *elems;
 		int			nelems,
 					i;
 		bool		viaroot = false;
+		char		relkind = '\0';
 
 		/* create a function context for cross-call persistence */
 		funcctx = SRF_FIRSTCALL_INIT();
@@ -1292,12 +1400,16 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		/* switch to memory context appropriate for multiple function calls */
 		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
 
+		Assert(pubnames != NULL);
+
 		/*
 		 * Deconstruct the parameter into elements where each element is a
 		 * publication name.
 		 */
-		arr = PG_GETARG_ARRAYTYPE_P(0);
-		deconstruct_array_builtin(arr, TEXTOID, &elems, NULL, &nelems);
+		deconstruct_array_builtin(pubnames, TEXTOID, &elems, NULL, &nelems);
+
+		if (filter_by_relid)
+			relkind = get_rel_relkind(target_relid);
 
 		/* Get Oids of tables from each publication. */
 		for (i = 0; i < nelems; i++)
@@ -1306,32 +1418,49 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			List	   *pub_elem_tables = NIL;
 			ListCell   *lc;
 
-			pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]), false);
+			pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]),
+											pub_missing_ok);
 
-			/*
-			 * Publications support partitioned tables. If
-			 * publish_via_partition_root is false, all changes are replicated
-			 * using leaf partition identity and schema, so we only need
-			 * those. Otherwise, get the partitioned table itself.
-			 */
-			if (pub_elem->alltables)
-				pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
-															 RELKIND_RELATION,
-															 pub_elem->pubviaroot);
+			if (pub_elem == NULL)
+				continue;
+
+			if (filter_by_relid)
+			{
+				/* Check if the given table is published for the publication */
+				if ((relkind == RELKIND_RELATION || relkind == RELKIND_PARTITIONED_TABLE) &&
+					is_table_publishable_in_publication(target_relid, pub_elem))
+				{
+					pub_elem_tables = list_make1_oid(target_relid);
+				}
+			}
 			else
 			{
-				List	   *relids,
-						   *schemarelids;
-
-				relids = GetIncludedPublicationRelations(pub_elem->oid,
-														 pub_elem->pubviaroot ?
-														 PUBLICATION_PART_ROOT :
-														 PUBLICATION_PART_LEAF);
-				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
-																pub_elem->pubviaroot ?
-																PUBLICATION_PART_ROOT :
-																PUBLICATION_PART_LEAF);
-				pub_elem_tables = list_concat_unique_oid(relids, schemarelids);
+				/*
+				 * Publications support partitioned tables. If
+				 * publish_via_partition_root is false, all changes are
+				 * replicated using leaf partition identity and schema, so we
+				 * only need those. Otherwise, get the partitioned table
+				 * itself.
+				 */
+				if (pub_elem->alltables)
+					pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
+																 RELKIND_RELATION,
+																 pub_elem->pubviaroot);
+				else
+				{
+					List	   *relids,
+							   *schemarelids;
+
+					relids = GetIncludedPublicationRelations(pub_elem->oid,
+															 pub_elem->pubviaroot ?
+															 PUBLICATION_PART_ROOT :
+															 PUBLICATION_PART_LEAF);
+					schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
+																	pub_elem->pubviaroot ?
+																	PUBLICATION_PART_ROOT :
+																	PUBLICATION_PART_LEAF);
+					pub_elem_tables = list_concat_unique_oid(relids, schemarelids);
+				}
 			}
 
 			/*
@@ -1491,6 +1620,30 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 	SRF_RETURN_DONE(funcctx);
 }
 
+Datum
+pg_get_publication_tables_a(PG_FUNCTION_ARGS)
+{
+	/*
+	 * Get information for all tables in the given publications.
+	 * filter_by_relid is false so all tables are returned; pub_missing_ok is
+	 * false for backward compatibility.
+	 */
+	return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0),
+									 InvalidOid, false, false);
+}
+
+Datum
+pg_get_publication_tables_b(PG_FUNCTION_ARGS)
+{
+	/*
+	 * Get information for the specified table in the given publications. The
+	 * SQL-level function is declared STRICT, so target_relid is guaranteed to
+	 * be non-NULL here.
+	 */
+	return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0),
+									 PG_GETARG_OID(1), true, true);
+}
+
 /*
  * Returns Oids of sequences in a publication.
  */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index f49a4852ecb..eb718114297 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -798,17 +798,35 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		 * publications).
 		 */
 		resetStringInfo(&cmd);
-		appendStringInfo(&cmd,
-						 "SELECT DISTINCT"
-						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
-						 "   THEN NULL ELSE gpt.attrs END)"
-						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
-						 "  pg_class c"
-						 " WHERE gpt.relid = %u AND c.oid = gpt.relid"
-						 "   AND p.pubname IN ( %s )",
-						 lrel->remoteid,
-						 pub_names->data);
+
+		if (server_version >= 190000)
+		{
+			/*
+			 * We can pass both publication names and relid to
+			 * pg_get_publication_tables() since version 19.
+			 */
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT"
+							 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
+							 "   THEN NULL ELSE gpt.attrs END)"
+							 "  FROM pg_get_publication_tables(ARRAY[%s], %u) gpt,"
+							 "  pg_class c"
+							 " WHERE c.oid = gpt.relid",
+							 pub_names->data,
+							 lrel->remoteid);
+		}
+		else
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT"
+							 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
+							 "   THEN NULL ELSE gpt.attrs END)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
+							 "  pg_class c"
+							 " WHERE gpt.relid = %u AND c.oid = gpt.relid"
+							 "   AND p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
 
 		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
 							 lengthof(attrsRow), attrsRow);
@@ -982,14 +1000,28 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 
 		/* Check for row filters. */
 		resetStringInfo(&cmd);
-		appendStringInfo(&cmd,
-						 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
-						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt"
-						 " WHERE gpt.relid = %u"
-						 "   AND p.pubname IN ( %s )",
-						 lrel->remoteid,
-						 pub_names->data);
+
+		if (server_version >= 190000)
+		{
+			/*
+			 * We can pass both publication names and relid to
+			 * pg_get_publication_tables() since version 19.
+			 */
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
+							 "  FROM pg_get_publication_tables(ARRAY[%s], %u) gpt",
+							 pub_names->data,
+							 lrel->remoteid);
+		}
+		else
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname) gpt"
+							 " WHERE gpt.relid = %u"
+							 "   AND p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
 
 		res = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data, 1, qualRow);
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..7ec43034b74 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12468,7 +12468,16 @@
   proallargtypes => '{_text,oid,oid,int2vector,pg_node_tree}',
   proargmodes => '{v,o,o,o,o}',
   proargnames => '{pubname,pubid,relid,attrs,qual}',
-  prosrc => 'pg_get_publication_tables' },
+  prosrc => 'pg_get_publication_tables_a' },
+{ oid => '8060',
+  descr => 'get information of the specified table that is part of the specified publications',
+  proname => 'pg_get_publication_tables', prorows => '10',
+  proretset => 't', provolatile => 's',
+  prorettype => 'record', proargtypes => '_text oid',
+  proallargtypes => '{_text,oid,oid,oid,int2vector,pg_node_tree}',
+  proargmodes => '{i,i,o,o,o,o}',
+  proargnames => '{pubnames,target_relid,pubid,relid,attrs,qual}',
+  prosrc => 'pg_get_publication_tables_b' },
 { oid => '8052', descr => 'get OIDs of sequences in a publication',
   proname => 'pg_get_publication_sequences', prorows => '1000', proretset => 't',
   provolatile => 's', prorettype => 'oid', proargtypes => 'text',
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index a220f48b285..df38d6d45db 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2271,6 +2271,231 @@ DROP TABLE testpub_merge_pk;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
+-- Test pg_get_publication_tables(text[], oid) function
+CREATE SCHEMA gpt_test_sch;
+CREATE TABLE gpt_test_sch.tbl_sch (id int);
+CREATE TABLE tbl_normal (id int);
+CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1);
+CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10);
+CREATE VIEW gpt_test_view AS SELECT * FROM tbl_normal;
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION pub_all FOR ALL TABLES WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_all_no_viaroot FOR ALL TABLES WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except_no_viaroot FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch;
+CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10);
+CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_part_parent_no_viaroot FOR TABLE tbl_parent WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent_child FOR TABLE tbl_parent, tbl_part1 WITH (publish_via_partition_root = true);
+RESET client_min_messages;
+CREATE FUNCTION test_gpt(pubnames text[], relname text)
+RETURNS TABLE (
+  pubname text,
+  relname name,
+  attrs text,
+  qual text
+)
+BEGIN ATOMIC
+  SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid)
+    FROM pg_get_publication_tables(pubnames, relname::regclass::oid) gpt
+    JOIN pg_publication p ON p.oid = gpt.pubid
+    JOIN pg_class c ON c.oid = gpt.relid
+  ORDER BY p.pubname, c.relname;
+END;
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'tbl_normal');
+  pubname   |  relname   | attrs |   qual    
+------------+------------+-------+-----------
+ pub_normal | tbl_normal | 1     | (id < 10)
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'gpt_test_sch.tbl_sch'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'gpt_test_sch.tbl_sch');
+  pubname   | relname | attrs | qual 
+------------+---------+-------+------
+ pub_schema | tbl_sch | 1     | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'tbl_normal'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_parent');
+     pubname     |  relname   | attrs |    qual    
+-----------------+------------+-------+------------
+ pub_part_parent | tbl_parent | 1 2   | (id1 = 10)
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_part1');
+          pubname           |  relname  | attrs | qual 
+----------------------------+-----------+-------+------
+ pub_part_parent_no_viaroot | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_part1');
+    pubname    |  relname  | attrs | qual 
+---------------+-----------+-------+------
+ pub_part_leaf | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_parent');
+ pubname |  relname   | attrs | qual 
+---------+------------+-------+------
+ pub_all | tbl_parent | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_part1');
+      pubname       |  relname  | attrs | qual 
+--------------------+-----------+-------+------
+ pub_all_no_viaroot | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_parent');
+        pubname        |  relname   | attrs | qual 
+-----------------------+------------+-------+------
+ pub_part_parent_child | tbl_parent | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- test for the EXCLUDE clause
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_normal');
+    pubname     |  relname   | attrs | qual 
+----------------+------------+-------+------
+ pub_all_except | tbl_normal | 1     | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_parent'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_normal');
+          pubname          |  relname   | attrs | qual 
+---------------------------+------------+-------+------
+ pub_all_except_no_viaroot | tbl_normal | 1     | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_parent'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- two rows with different row filter
+SELECT * FROM test_gpt(ARRAY['pub_all', 'pub_normal'], 'tbl_normal');
+  pubname   |  relname   | attrs |   qual    
+------------+------------+-------+-----------
+ pub_all    | tbl_normal | 1     | 
+ pub_normal | tbl_normal | 1     | (id < 10)
+(2 rows)
+
+-- one row with 'pub_part_parent'
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_part_parent_no_viaroot'], 'tbl_parent');
+     pubname     |  relname   | attrs |    qual    
+-----------------+------------+-------+------------
+ pub_part_parent | tbl_parent | 1 2   | (id1 = 10)
+(1 row)
+
+-- no result, tbl_parent is the effective published OID due to pubviaroot
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_all'], 'tbl_part1');
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- no result, non-existent publication
+SELECT * FROM test_gpt(ARRAY['no_such_pub'], 'tbl_normal');
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- no result, non-table object
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'gpt_test_view');
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- no result, empty publication array
+SELECT * FROM test_gpt(ARRAY[]::text[], 'tbl_normal');
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- no result, OID 0 as target_relid
+SELECT * FROM pg_get_publication_tables(ARRAY['pub_normal'], 0::oid);
+ pubid | relid | attrs | qual 
+-------+-------+-------+------
+(0 rows)
+
+-- Clean up
+DROP FUNCTION test_gpt(text[], text);
+DROP PUBLICATION pub_all;
+DROP PUBLICATION pub_all_no_viaroot;
+DROP PUBLICATION pub_all_except;
+DROP PUBLICATION pub_all_except_no_viaroot;
+DROP PUBLICATION pub_schema;
+DROP PUBLICATION pub_normal;
+DROP PUBLICATION pub_part_leaf;
+DROP PUBLICATION pub_part_parent;
+DROP PUBLICATION pub_part_parent_no_viaroot;
+DROP PUBLICATION pub_part_parent_child;
+DROP VIEW gpt_test_view;
+DROP TABLE tbl_normal, tbl_parent, tbl_part1;
+DROP SCHEMA gpt_test_sch CASCADE;
+NOTICE:  drop cascades to table gpt_test_sch.tbl_sch
 -- stage objects for pg_dump tests
 CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int);
 CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int);
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 22e0a30b5c7..057e364c753 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1429,6 +1429,113 @@ RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
 
+-- Test pg_get_publication_tables(text[], oid) function
+CREATE SCHEMA gpt_test_sch;
+CREATE TABLE gpt_test_sch.tbl_sch (id int);
+CREATE TABLE tbl_normal (id int);
+CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1);
+CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10);
+CREATE VIEW gpt_test_view AS SELECT * FROM tbl_normal;
+
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION pub_all FOR ALL TABLES WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_all_no_viaroot FOR ALL TABLES WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except_no_viaroot FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch;
+CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10);
+CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_part_parent_no_viaroot FOR TABLE tbl_parent WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent_child FOR TABLE tbl_parent, tbl_part1 WITH (publish_via_partition_root = true);
+RESET client_min_messages;
+
+CREATE FUNCTION test_gpt(pubnames text[], relname text)
+RETURNS TABLE (
+  pubname text,
+  relname name,
+  attrs text,
+  qual text
+)
+BEGIN ATOMIC
+  SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid)
+    FROM pg_get_publication_tables(pubnames, relname::regclass::oid) gpt
+    JOIN pg_publication p ON p.oid = gpt.pubid
+    JOIN pg_class c ON c.oid = gpt.relid
+  ORDER BY p.pubname, c.relname;
+END;
+
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'tbl_normal');
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'gpt_test_sch.tbl_sch'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'gpt_test_sch.tbl_sch');
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'tbl_normal'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_part1'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_parent'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_parent'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_part1'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_parent'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_part1'); -- no result
+
+-- test for the EXCLUDE clause
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_normal');
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_parent'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_part1'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_normal');
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_parent'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_part1'); -- no result
+
+-- two rows with different row filter
+SELECT * FROM test_gpt(ARRAY['pub_all', 'pub_normal'], 'tbl_normal');
+
+-- one row with 'pub_part_parent'
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_part_parent_no_viaroot'], 'tbl_parent');
+
+-- no result, tbl_parent is the effective published OID due to pubviaroot
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_all'], 'tbl_part1');
+
+-- no result, non-existent publication
+SELECT * FROM test_gpt(ARRAY['no_such_pub'], 'tbl_normal');
+
+-- no result, non-table object
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'gpt_test_view');
+
+-- no result, empty publication array
+SELECT * FROM test_gpt(ARRAY[]::text[], 'tbl_normal');
+
+-- no result, OID 0 as target_relid
+SELECT * FROM pg_get_publication_tables(ARRAY['pub_normal'], 0::oid);
+
+-- Clean up
+DROP FUNCTION test_gpt(text[], text);
+DROP PUBLICATION pub_all;
+DROP PUBLICATION pub_all_no_viaroot;
+DROP PUBLICATION pub_all_except;
+DROP PUBLICATION pub_all_except_no_viaroot;
+DROP PUBLICATION pub_schema;
+DROP PUBLICATION pub_normal;
+DROP PUBLICATION pub_part_leaf;
+DROP PUBLICATION pub_part_parent;
+DROP PUBLICATION pub_part_parent_no_viaroot;
+DROP PUBLICATION pub_part_parent_child;
+DROP VIEW gpt_test_view;
+DROP TABLE tbl_normal, tbl_parent, tbl_part1;
+DROP SCHEMA gpt_test_sch CASCADE;
+
 -- stage objects for pg_dump tests
 CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int);
 CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int);
-- 
2.53.0



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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-26 10:43                     ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-26 23:51                       ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-27 03:16                         ` RE: Initial COPY of Logical Replication is too slow Hayato Kuroda (Fujitsu) <[email protected]>
  2026-03-27 03:51                           ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-27 06:20                             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-03-27 13:07                               ` Marcos Pegoraro <[email protected]>
  2026-04-01 00:03                                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2 siblings, 1 reply; 47+ messages in thread

From: Marcos Pegoraro @ 2026-03-27 13:07 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; +Cc: Amit Kapila <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>

Em sex., 27 de mar. de 2026 às 03:20, Masahiko Sawada <[email protected]>
escreveu:

> I've attached the updated patch. I believe I've addressed all comments
> I got so far. In addition to that, I've refactored
> is_table_publishable_in_publication() and added more regression tests.
>

Today I had to create a few more schemas and see that problem again, how
the publisher is affected, almost crashing due to the overload.
That was because max_sync_workers_per_subscription was set to 10, which
caused 10 simultaneous connections to call this function immediately after
the refresh publication command.
Wouldn't it be good to document on this GUC that if your publisher server
is running version <= 18 then is it advisable to set this GUC to a really
low value ?
Because ok, version 19 is fine, will be covered, but all publisher servers
that are not updated will continue to have this trouble.
The publisher will be severely penalized when the subscription refreshes
its publication.

What do you think, change something on DOCs too ?

regards
Marcos


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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-26 10:43                     ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-26 23:51                       ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-27 03:16                         ` RE: Initial COPY of Logical Replication is too slow Hayato Kuroda (Fujitsu) <[email protected]>
  2026-03-27 03:51                           ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-27 06:20                             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-27 13:07                               ` Re: Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
@ 2026-04-01 00:03                                 ` Masahiko Sawada <[email protected]>
  0 siblings, 0 replies; 47+ messages in thread

From: Masahiko Sawada @ 2026-04-01 00:03 UTC (permalink / raw)
  To: Marcos Pegoraro <[email protected]>; +Cc: Amit Kapila <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>

On Fri, Mar 27, 2026 at 6:07 AM Marcos Pegoraro <[email protected]> wrote:
>
> Em sex., 27 de mar. de 2026 às 03:20, Masahiko Sawada <[email protected]> escreveu:
>>
>> I've attached the updated patch. I believe I've addressed all comments
>> I got so far. In addition to that, I've refactored
>> is_table_publishable_in_publication() and added more regression tests.
>
>
> Today I had to create a few more schemas and see that problem again, how the publisher is affected, almost crashing due to the overload.
> That was because max_sync_workers_per_subscription was set to 10, which caused 10 simultaneous connections to call this function immediately after the refresh publication command.
> Wouldn't it be good to document on this GUC that if your publisher server is running version <= 18 then is it advisable to set this GUC to a really low value ?
> Because ok, version 19 is fine, will be covered, but all publisher servers that are not updated will continue to have this trouble.
> The publisher will be severely penalized when the subscription refreshes its publication.
>
> What do you think, change something on DOCs too ?

I agree that the publisher overload is a serious issue that users
should be aware of. But I'm not sure it's a good idea to broadly
suggest lowing the GUC value as it ultimatly depends on multiple
factors. A value of 10 or more is perfectly fine depending on the
hardware and the number of tables etc. A definition of a large number
of tables also varies on systems. I guess the release note would be a
better place to mention this.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com





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

* RE: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-26 10:43                     ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-26 23:51                       ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-27 03:16                         ` RE: Initial COPY of Logical Replication is too slow Hayato Kuroda (Fujitsu) <[email protected]>
  2026-03-27 03:51                           ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-27 06:20                             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-03-30 07:16                               ` Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-31 04:08                                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2 siblings, 1 reply; 47+ messages in thread

From: Zhijie Hou (Fujitsu) @ 2026-03-30 07:16 UTC (permalink / raw)
  To: Masahiko Sawada <[email protected]>; Amit Kapila <[email protected]>; +Cc: Hayato Kuroda (Fujitsu) <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>

On Friday, March 27, 2026 2:20 PM Masahiko Sawada <[email protected]> wrote:
> I've attached the updated patch. I believe I've addressed all comments I got
> so far. In addition to that, I've refactored
> is_table_publishable_in_publication() and added more regression tests.

Thanks for updating the patch.

The latest patch looks mostly good to me. However, I noticed one issue: the
function returns table information even for unlogged or temporary tables. I
think we should return NULL for those cases instead.

BTW, I think we could use is_publishable_class() as a reference to check once
whether all unpublishable table types are properly ignored in this function.

Best Regards,
Hou zj


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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-26 10:43                     ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-26 23:51                       ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-27 03:16                         ` RE: Initial COPY of Logical Replication is too slow Hayato Kuroda (Fujitsu) <[email protected]>
  2026-03-27 03:51                           ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-27 06:20                             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-30 07:16                               ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
@ 2026-03-31 04:08                                 ` Masahiko Sawada <[email protected]>
  2026-03-31 11:39                                   ` RE: Initial COPY of Logical Replication is too slow Hayato Kuroda (Fujitsu) <[email protected]>
  0 siblings, 1 reply; 47+ messages in thread

From: Masahiko Sawada @ 2026-03-31 04:08 UTC (permalink / raw)
  To: Zhijie Hou (Fujitsu) <[email protected]>; +Cc: Amit Kapila <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>

On Mon, Mar 30, 2026 at 12:16 AM Zhijie Hou (Fujitsu)
<[email protected]> wrote:
>
> On Friday, March 27, 2026 2:20 PM Masahiko Sawada <[email protected]> wrote:
> > I've attached the updated patch. I believe I've addressed all comments I got
> > so far. In addition to that, I've refactored
> > is_table_publishable_in_publication() and added more regression tests.
>
> Thanks for updating the patch.
>
> The latest patch looks mostly good to me. However, I noticed one issue: the
> function returns table information even for unlogged or temporary tables. I
> think we should return NULL for those cases instead.

Indeed. Good catch!

>
> BTW, I think we could use is_publishable_class() as a reference to check once
> whether all unpublishable table types are properly ignored in this function.
>

+1. I've added is_publishable_class() check.

I've attached the updated patch.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com


Attachments:

  [text/x-patch] v6-0001-Add-target_relid-parameter-to-pg_get_publication_.patch (30.2K, 2-v6-0001-Add-target_relid-parameter-to-pg_get_publication_.patch)
  download | inline diff:
From b10a79d482e6fe0f178b0ff004767dcbc6bc86db Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
Date: Fri, 27 Feb 2026 15:42:38 -0800
Subject: [PATCH v6] Add target_relid parameter to pg_get_publication_tables().

When a tablesync worker checks whether a specific table is published,
it previously called pg_get_publication_tables() and filtered the
result by relid on the subscriber side. This forced a full enumeration
of all tables in the publication before any filtering could occur. For
publications covering a large number of tables, this resulted in
expensive scans on the publisher and unnecessary overhead.

This commit adds a new overloaded form of pg_get_publication_tables()
that accepts an array of publication names and a target table
OID. Instead of enumerating all published tables, it evaluates
membership for the specified relation via syscache lookups, using the
new is_table_publishable_in_publication() helper. This helper
correctly accounts for publish_via_partition_root, ALL TABLES with
EXCEPT clauses, schema publications, and partition inheritance, while
avoiding the overhead of building the complete published table list.

The existing a VARIADIC array form of pg_get_publication_tables() is
preserved for backward compatibility. Tablesync workers use the new
two-argument form when connected to a publisher running PostgreSQL 19
or later.

Bump catalog version.

Reported-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zhijie Hou <[email protected]>
Reviewed-by: Matheus Alcantara <[email protected]>
Reviewed-by: Amit Kapila <[email protected]>
Reviewed-by: Peter Smith <[email protected]>
Reviewed-by: Hayato Kuroda <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Discussion: https://postgr.es/m/CAB-JLwbBFNuASyEnZWP0Tck9uNkthBZqi6WoXNevUT6+mV8XmA@mail.gmail.com
---
 src/backend/catalog/pg_publication.c        | 246 +++++++++++++++++---
 src/backend/replication/logical/tablesync.c |  70 ++++--
 src/include/catalog/pg_proc.dat             |  11 +-
 src/test/regress/expected/publication.out   | 225 ++++++++++++++++++
 src/test/regress/sql/publication.sql        | 107 +++++++++
 5 files changed, 609 insertions(+), 50 deletions(-)

diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index c92ff3f51c3..0c49b1c69a6 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -163,6 +163,37 @@ is_publishable_relation(Relation rel)
 	return is_publishable_class(RelationGetRelid(rel), rel->rd_rel);
 }
 
+/*
+ * Similar to is_publishable_calss() but checks whether the given OID
+ * is a publishable "table" or not.
+ */
+static bool
+is_publishable_table(Oid tableoid)
+{
+	HeapTuple	tuple;
+	Form_pg_class relform;
+
+	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(tableoid));
+	if (!HeapTupleIsValid(tuple))
+		return false;
+
+	relform = (Form_pg_class) GETSTRUCT(tuple);
+
+	/*
+	 * Sequences are publishable according to is_publishable_class() so
+	 * explicitly exclude here.
+	 */
+	if (relform->relkind != RELKIND_SEQUENCE &&
+		is_publishable_class(tableoid, relform))
+	{
+		ReleaseSysCache(tuple);
+		return true;
+	}
+
+	ReleaseSysCache(tuple);
+	return true;
+}
+
 /*
  * SQL-callable variant of the above
  *
@@ -1264,12 +1295,121 @@ GetPublicationByName(const char *pubname, bool missing_ok)
 }
 
 /*
- * Get information of the tables in the given publication array.
+ * Returns true if the table of the given relid is published for the specified
+ * publication.
+ *
+ * This function evaluates the effective published OID based on the
+ * publish_via_partition_root setting, rather than just checking catalog entries
+ * (e.g., pg_publication_rel). For instance, when publish_via_partition_root is
+ * false, it returns false for a parent partitioned table and true for its leaf
+ * partitions, even if the parent is the one explicitly added to the publication.
  *
- * Returns pubid, relid, column list, row filter for each table.
+ * For performance reasons, this function avoids the overhead of constructing
+ * the complete list of published tables during the evaluation. It can execute
+ * quickly even when the publication contains a large number of relations.
  */
-Datum
-pg_get_publication_tables(PG_FUNCTION_ARGS)
+static bool
+is_table_publishable_in_publication(Oid relid, Publication *pub)
+{
+	bool		relispartition;
+
+	/*
+	 * For non-pubviaroot publications, a partitioned table is never the
+	 * effective published OID; only its leaf partitions can be.
+	 */
+	if (!pub->pubviaroot && get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE)
+		return false;
+
+	relispartition = get_rel_relispartition(relid);
+
+	if (pub->alltables)
+	{
+		Oid			target_relid = relid;
+
+		if (pub->pubviaroot)
+		{
+			/*
+			 * ALL TABLES with pubviaroot includes only regular tables or
+			 * top-most partitioned tables -- never child partitions.
+			 */
+			if (relispartition)
+				return false;
+		}
+		else if (relispartition)
+		{
+			List	   *ancestors = get_partition_ancestors(relid);
+
+			/*
+			 * Only the top-most ancestor can appear in the EXCEPT clause.
+			 * Therefore, for a partition, exclusion must be evaluated at the
+			 * top-most ancestor.
+			 */
+			target_relid = llast_oid(ancestors);
+			list_free(ancestors);
+		}
+
+		/*
+		 * The table is published unless it appears in the EXCEPT clause. ALL
+		 * TABLES publications store only EXCEPT'ed tables in
+		 * pg_publication_rel, so checking existence is sufficient.
+		 */
+		return !SearchSysCacheExists2(PUBLICATIONRELMAP,
+									  ObjectIdGetDatum(target_relid),
+									  ObjectIdGetDatum(pub->oid));
+	}
+
+	/*
+	 * Non-alltables
+	 */
+
+	if (relispartition)
+	{
+		List	   *ancestors = get_partition_ancestors(relid);
+		Oid			topmost = GetTopMostAncestorInPublication(pub->oid, ancestors, NULL);
+
+		list_free(ancestors);
+
+		if (OidIsValid(topmost))
+		{
+			/*
+			 * If pubviaroot is true, the ancestor is published instead of the
+			 * partition, so exclude it. Otherwise, the ancestor covers the
+			 * partition, so include it.
+			 */
+			return !pub->pubviaroot;
+		}
+
+		/* Ancestor not published; fall through to check the partition itself */
+	}
+
+	/*
+	 * Check whether the table is explicitly published via pg_publication_rel
+	 * or pg_publication_namespace.
+	 */
+	return (SearchSysCacheExists2(PUBLICATIONRELMAP,
+								  ObjectIdGetDatum(relid),
+								  ObjectIdGetDatum(pub->oid)) ||
+			SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP,
+								  ObjectIdGetDatum(get_rel_namespace(relid)),
+								  ObjectIdGetDatum(pub->oid)));
+}
+
+/*
+ * Helper function to get information of the tables in the given
+ * publication(s).
+ *
+ * If filter_by_relid is true, only the row for target_relid is returned;
+ * if target_relid does not exist or is not part of the publications, zero
+ * rows are returned.  If filter_by_relid is false, rows for all tables
+ * within the specified publications are returned and target_relid is
+ * ignored.
+ *
+ * Returns pubid, relid, column list, and row filter for each table.
+ */
+static Datum
+pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
+						  Oid target_relid, bool filter_by_relid,
+						  bool pub_missing_ok)
 {
 #define NUM_PUBLICATION_TABLES_ELEM	4
 	FuncCallContext *funcctx;
@@ -1280,7 +1420,6 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 	{
 		TupleDesc	tupdesc;
 		MemoryContext oldcontext;
-		ArrayType  *arr;
 		Datum	   *elems;
 		int			nelems,
 					i;
@@ -1296,8 +1435,15 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		 * Deconstruct the parameter into elements where each element is a
 		 * publication name.
 		 */
-		arr = PG_GETARG_ARRAYTYPE_P(0);
-		deconstruct_array_builtin(arr, TEXTOID, &elems, NULL, &nelems);
+		deconstruct_array_builtin(pubnames, TEXTOID, &elems, NULL, &nelems);
+
+		/*
+		 * Preliminary check if the specified table can be published in the
+		 * first place. If not, we can return early without checking the given
+		 * publications and the table.
+		 */
+		if (filter_by_relid && !is_publishable_table(target_relid))
+			SRF_RETURN_DONE(funcctx);
 
 		/* Get Oids of tables from each publication. */
 		for (i = 0; i < nelems; i++)
@@ -1306,32 +1452,48 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			List	   *pub_elem_tables = NIL;
 			ListCell   *lc;
 
-			pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]), false);
+			pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]),
+											pub_missing_ok);
 
-			/*
-			 * Publications support partitioned tables. If
-			 * publish_via_partition_root is false, all changes are replicated
-			 * using leaf partition identity and schema, so we only need
-			 * those. Otherwise, get the partitioned table itself.
-			 */
-			if (pub_elem->alltables)
-				pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
-															 RELKIND_RELATION,
-															 pub_elem->pubviaroot);
+			if (pub_elem == NULL)
+				continue;
+
+			if (filter_by_relid)
+			{
+				/* Check if the given table is published for the publication */
+				if (is_table_publishable_in_publication(target_relid, pub_elem))
+				{
+					pub_elem_tables = list_make1_oid(target_relid);
+				}
+			}
 			else
 			{
-				List	   *relids,
-						   *schemarelids;
-
-				relids = GetIncludedPublicationRelations(pub_elem->oid,
-														 pub_elem->pubviaroot ?
-														 PUBLICATION_PART_ROOT :
-														 PUBLICATION_PART_LEAF);
-				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
-																pub_elem->pubviaroot ?
-																PUBLICATION_PART_ROOT :
-																PUBLICATION_PART_LEAF);
-				pub_elem_tables = list_concat_unique_oid(relids, schemarelids);
+				/*
+				 * Publications support partitioned tables. If
+				 * publish_via_partition_root is false, all changes are
+				 * replicated using leaf partition identity and schema, so we
+				 * only need those. Otherwise, get the partitioned table
+				 * itself.
+				 */
+				if (pub_elem->alltables)
+					pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
+																 RELKIND_RELATION,
+																 pub_elem->pubviaroot);
+				else
+				{
+					List	   *relids,
+							   *schemarelids;
+
+					relids = GetIncludedPublicationRelations(pub_elem->oid,
+															 pub_elem->pubviaroot ?
+															 PUBLICATION_PART_ROOT :
+															 PUBLICATION_PART_LEAF);
+					schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
+																	pub_elem->pubviaroot ?
+																	PUBLICATION_PART_ROOT :
+																	PUBLICATION_PART_LEAF);
+					pub_elem_tables = list_concat_unique_oid(relids, schemarelids);
+				}
 			}
 
 			/*
@@ -1491,6 +1653,30 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 	SRF_RETURN_DONE(funcctx);
 }
 
+Datum
+pg_get_publication_tables_a(PG_FUNCTION_ARGS)
+{
+	/*
+	 * Get information for all tables in the given publications.
+	 * filter_by_relid is false so all tables are returned; pub_missing_ok is
+	 * false for backward compatibility.
+	 */
+	return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0),
+									 InvalidOid, false, false);
+}
+
+Datum
+pg_get_publication_tables_b(PG_FUNCTION_ARGS)
+{
+	/*
+	 * Get information for the specified table in the given publications. The
+	 * SQL-level function is declared STRICT, so target_relid is guaranteed to
+	 * be non-NULL here.
+	 */
+	return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0),
+									 PG_GETARG_OID(1), true, true);
+}
+
 /*
  * Returns Oids of sequences in a publication.
  */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index f49a4852ecb..eb718114297 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -798,17 +798,35 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		 * publications).
 		 */
 		resetStringInfo(&cmd);
-		appendStringInfo(&cmd,
-						 "SELECT DISTINCT"
-						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
-						 "   THEN NULL ELSE gpt.attrs END)"
-						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
-						 "  pg_class c"
-						 " WHERE gpt.relid = %u AND c.oid = gpt.relid"
-						 "   AND p.pubname IN ( %s )",
-						 lrel->remoteid,
-						 pub_names->data);
+
+		if (server_version >= 190000)
+		{
+			/*
+			 * We can pass both publication names and relid to
+			 * pg_get_publication_tables() since version 19.
+			 */
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT"
+							 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
+							 "   THEN NULL ELSE gpt.attrs END)"
+							 "  FROM pg_get_publication_tables(ARRAY[%s], %u) gpt,"
+							 "  pg_class c"
+							 " WHERE c.oid = gpt.relid",
+							 pub_names->data,
+							 lrel->remoteid);
+		}
+		else
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT"
+							 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
+							 "   THEN NULL ELSE gpt.attrs END)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
+							 "  pg_class c"
+							 " WHERE gpt.relid = %u AND c.oid = gpt.relid"
+							 "   AND p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
 
 		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
 							 lengthof(attrsRow), attrsRow);
@@ -982,14 +1000,28 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 
 		/* Check for row filters. */
 		resetStringInfo(&cmd);
-		appendStringInfo(&cmd,
-						 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
-						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt"
-						 " WHERE gpt.relid = %u"
-						 "   AND p.pubname IN ( %s )",
-						 lrel->remoteid,
-						 pub_names->data);
+
+		if (server_version >= 190000)
+		{
+			/*
+			 * We can pass both publication names and relid to
+			 * pg_get_publication_tables() since version 19.
+			 */
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
+							 "  FROM pg_get_publication_tables(ARRAY[%s], %u) gpt",
+							 pub_names->data,
+							 lrel->remoteid);
+		}
+		else
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname) gpt"
+							 " WHERE gpt.relid = %u"
+							 "   AND p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
 
 		res = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data, 1, qualRow);
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3579cec5744..afdcc915f08 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12468,7 +12468,16 @@
   proallargtypes => '{_text,oid,oid,int2vector,pg_node_tree}',
   proargmodes => '{v,o,o,o,o}',
   proargnames => '{pubname,pubid,relid,attrs,qual}',
-  prosrc => 'pg_get_publication_tables' },
+  prosrc => 'pg_get_publication_tables_a' },
+{ oid => '8060',
+  descr => 'get information of the specified table that is part of the specified publications',
+  proname => 'pg_get_publication_tables', prorows => '10',
+  proretset => 't', provolatile => 's',
+  prorettype => 'record', proargtypes => '_text oid',
+  proallargtypes => '{_text,oid,oid,oid,int2vector,pg_node_tree}',
+  proargmodes => '{i,i,o,o,o,o}',
+  proargnames => '{pubnames,target_relid,pubid,relid,attrs,qual}',
+  prosrc => 'pg_get_publication_tables_b' },
 { oid => '8052', descr => 'get OIDs of sequences in a publication',
   proname => 'pg_get_publication_sequences', prorows => '1000', proretset => 't',
   provolatile => 's', prorettype => 'oid', proargtypes => 'text',
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index a220f48b285..df38d6d45db 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2271,6 +2271,231 @@ DROP TABLE testpub_merge_pk;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
+-- Test pg_get_publication_tables(text[], oid) function
+CREATE SCHEMA gpt_test_sch;
+CREATE TABLE gpt_test_sch.tbl_sch (id int);
+CREATE TABLE tbl_normal (id int);
+CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1);
+CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10);
+CREATE VIEW gpt_test_view AS SELECT * FROM tbl_normal;
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION pub_all FOR ALL TABLES WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_all_no_viaroot FOR ALL TABLES WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except_no_viaroot FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch;
+CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10);
+CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_part_parent_no_viaroot FOR TABLE tbl_parent WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent_child FOR TABLE tbl_parent, tbl_part1 WITH (publish_via_partition_root = true);
+RESET client_min_messages;
+CREATE FUNCTION test_gpt(pubnames text[], relname text)
+RETURNS TABLE (
+  pubname text,
+  relname name,
+  attrs text,
+  qual text
+)
+BEGIN ATOMIC
+  SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid)
+    FROM pg_get_publication_tables(pubnames, relname::regclass::oid) gpt
+    JOIN pg_publication p ON p.oid = gpt.pubid
+    JOIN pg_class c ON c.oid = gpt.relid
+  ORDER BY p.pubname, c.relname;
+END;
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'tbl_normal');
+  pubname   |  relname   | attrs |   qual    
+------------+------------+-------+-----------
+ pub_normal | tbl_normal | 1     | (id < 10)
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'gpt_test_sch.tbl_sch'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'gpt_test_sch.tbl_sch');
+  pubname   | relname | attrs | qual 
+------------+---------+-------+------
+ pub_schema | tbl_sch | 1     | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'tbl_normal'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_parent');
+     pubname     |  relname   | attrs |    qual    
+-----------------+------------+-------+------------
+ pub_part_parent | tbl_parent | 1 2   | (id1 = 10)
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_part1');
+          pubname           |  relname  | attrs | qual 
+----------------------------+-----------+-------+------
+ pub_part_parent_no_viaroot | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_part1');
+    pubname    |  relname  | attrs | qual 
+---------------+-----------+-------+------
+ pub_part_leaf | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_parent');
+ pubname |  relname   | attrs | qual 
+---------+------------+-------+------
+ pub_all | tbl_parent | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_part1');
+      pubname       |  relname  | attrs | qual 
+--------------------+-----------+-------+------
+ pub_all_no_viaroot | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_parent');
+        pubname        |  relname   | attrs | qual 
+-----------------------+------------+-------+------
+ pub_part_parent_child | tbl_parent | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- test for the EXCLUDE clause
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_normal');
+    pubname     |  relname   | attrs | qual 
+----------------+------------+-------+------
+ pub_all_except | tbl_normal | 1     | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_parent'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_normal');
+          pubname          |  relname   | attrs | qual 
+---------------------------+------------+-------+------
+ pub_all_except_no_viaroot | tbl_normal | 1     | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_parent'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- two rows with different row filter
+SELECT * FROM test_gpt(ARRAY['pub_all', 'pub_normal'], 'tbl_normal');
+  pubname   |  relname   | attrs |   qual    
+------------+------------+-------+-----------
+ pub_all    | tbl_normal | 1     | 
+ pub_normal | tbl_normal | 1     | (id < 10)
+(2 rows)
+
+-- one row with 'pub_part_parent'
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_part_parent_no_viaroot'], 'tbl_parent');
+     pubname     |  relname   | attrs |    qual    
+-----------------+------------+-------+------------
+ pub_part_parent | tbl_parent | 1 2   | (id1 = 10)
+(1 row)
+
+-- no result, tbl_parent is the effective published OID due to pubviaroot
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_all'], 'tbl_part1');
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- no result, non-existent publication
+SELECT * FROM test_gpt(ARRAY['no_such_pub'], 'tbl_normal');
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- no result, non-table object
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'gpt_test_view');
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- no result, empty publication array
+SELECT * FROM test_gpt(ARRAY[]::text[], 'tbl_normal');
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- no result, OID 0 as target_relid
+SELECT * FROM pg_get_publication_tables(ARRAY['pub_normal'], 0::oid);
+ pubid | relid | attrs | qual 
+-------+-------+-------+------
+(0 rows)
+
+-- Clean up
+DROP FUNCTION test_gpt(text[], text);
+DROP PUBLICATION pub_all;
+DROP PUBLICATION pub_all_no_viaroot;
+DROP PUBLICATION pub_all_except;
+DROP PUBLICATION pub_all_except_no_viaroot;
+DROP PUBLICATION pub_schema;
+DROP PUBLICATION pub_normal;
+DROP PUBLICATION pub_part_leaf;
+DROP PUBLICATION pub_part_parent;
+DROP PUBLICATION pub_part_parent_no_viaroot;
+DROP PUBLICATION pub_part_parent_child;
+DROP VIEW gpt_test_view;
+DROP TABLE tbl_normal, tbl_parent, tbl_part1;
+DROP SCHEMA gpt_test_sch CASCADE;
+NOTICE:  drop cascades to table gpt_test_sch.tbl_sch
 -- stage objects for pg_dump tests
 CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int);
 CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int);
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 22e0a30b5c7..057e364c753 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1429,6 +1429,113 @@ RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
 
+-- Test pg_get_publication_tables(text[], oid) function
+CREATE SCHEMA gpt_test_sch;
+CREATE TABLE gpt_test_sch.tbl_sch (id int);
+CREATE TABLE tbl_normal (id int);
+CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1);
+CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10);
+CREATE VIEW gpt_test_view AS SELECT * FROM tbl_normal;
+
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION pub_all FOR ALL TABLES WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_all_no_viaroot FOR ALL TABLES WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except_no_viaroot FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch;
+CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10);
+CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_part_parent_no_viaroot FOR TABLE tbl_parent WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent_child FOR TABLE tbl_parent, tbl_part1 WITH (publish_via_partition_root = true);
+RESET client_min_messages;
+
+CREATE FUNCTION test_gpt(pubnames text[], relname text)
+RETURNS TABLE (
+  pubname text,
+  relname name,
+  attrs text,
+  qual text
+)
+BEGIN ATOMIC
+  SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid)
+    FROM pg_get_publication_tables(pubnames, relname::regclass::oid) gpt
+    JOIN pg_publication p ON p.oid = gpt.pubid
+    JOIN pg_class c ON c.oid = gpt.relid
+  ORDER BY p.pubname, c.relname;
+END;
+
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'tbl_normal');
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'gpt_test_sch.tbl_sch'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'gpt_test_sch.tbl_sch');
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'tbl_normal'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_part1'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_parent'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_parent'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_part1'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_parent'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_part1'); -- no result
+
+-- test for the EXCLUDE clause
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_normal');
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_parent'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_part1'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_normal');
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_parent'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_part1'); -- no result
+
+-- two rows with different row filter
+SELECT * FROM test_gpt(ARRAY['pub_all', 'pub_normal'], 'tbl_normal');
+
+-- one row with 'pub_part_parent'
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_part_parent_no_viaroot'], 'tbl_parent');
+
+-- no result, tbl_parent is the effective published OID due to pubviaroot
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_all'], 'tbl_part1');
+
+-- no result, non-existent publication
+SELECT * FROM test_gpt(ARRAY['no_such_pub'], 'tbl_normal');
+
+-- no result, non-table object
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'gpt_test_view');
+
+-- no result, empty publication array
+SELECT * FROM test_gpt(ARRAY[]::text[], 'tbl_normal');
+
+-- no result, OID 0 as target_relid
+SELECT * FROM pg_get_publication_tables(ARRAY['pub_normal'], 0::oid);
+
+-- Clean up
+DROP FUNCTION test_gpt(text[], text);
+DROP PUBLICATION pub_all;
+DROP PUBLICATION pub_all_no_viaroot;
+DROP PUBLICATION pub_all_except;
+DROP PUBLICATION pub_all_except_no_viaroot;
+DROP PUBLICATION pub_schema;
+DROP PUBLICATION pub_normal;
+DROP PUBLICATION pub_part_leaf;
+DROP PUBLICATION pub_part_parent;
+DROP PUBLICATION pub_part_parent_no_viaroot;
+DROP PUBLICATION pub_part_parent_child;
+DROP VIEW gpt_test_view;
+DROP TABLE tbl_normal, tbl_parent, tbl_part1;
+DROP SCHEMA gpt_test_sch CASCADE;
+
 -- stage objects for pg_dump tests
 CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int);
 CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int);
-- 
2.53.0



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

* RE: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-26 10:43                     ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-26 23:51                       ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-27 03:16                         ` RE: Initial COPY of Logical Replication is too slow Hayato Kuroda (Fujitsu) <[email protected]>
  2026-03-27 03:51                           ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-27 06:20                             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-30 07:16                               ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-31 04:08                                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-03-31 11:39                                   ` Hayato Kuroda (Fujitsu) <[email protected]>
  2026-03-31 19:29                                     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  0 siblings, 1 reply; 47+ messages in thread

From: Hayato Kuroda (Fujitsu) @ 2026-03-31 11:39 UTC (permalink / raw)
  To: 'Masahiko Sawada' <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; +Cc: Amit Kapila <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>

Dear Sawada-san,

Thanks for updating the patch! Few comments.

01.
```
+/*
+ * Similar to is_publishable_calss() but checks whether the given OID
+ * is a publishable "table" or not.
+ */
+static bool
+is_publishable_table(Oid tableoid)
```

s/is_publishable_calss/is_publishable_class/.

02.
```
+       ReleaseSysCache(tuple);
+       return true;
```

Is it correct? I expected to return false here.

03.
```
+               /*
+                * Preliminary check if the specified table can be published in the
+                * first place. If not, we can return early without checking the given
+                * publications and the table.
+                */
+               if (filter_by_relid && !is_publishable_table(target_relid))
+                       SRF_RETURN_DONE(funcctx);
```

I think we must switch to the old context.

04.
```
+CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except_no_viaroot FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = true);
```

It needs to be rebased due to 5984ea86.

05.
```
CREATE VIEW pg_publication_tables AS
    SELECT
        P.pubname AS pubname,
        N.nspname AS schemaname,
        C.relname AS tablename,
        ( SELECT array_agg(a.attname ORDER BY a.attnum)
          FROM pg_attribute a
          WHERE a.attrelid = GPT.relid AND
                a.attnum = ANY(GPT.attrs)
        ) AS attnames,
        pg_get_expr(GPT.qual, GPT.relid) AS rowfilter
     FROM pg_publication P,
          LATERAL pg_get_publication_tables(P.pubname) GPT,
          pg_class C JOIN pg_namespace N ON (N.oid = C.relnamespace)
     WHERE C.oid = GPT.relid;
```

Can we use the new API of pg_get_publication_tables() here? Below change can pass
tests on my env.

```
-         LATERAL pg_get_publication_tables(P.pubname) GPT,
-         pg_class C JOIN pg_namespace N ON (N.oid = C.relnamespace)
-    WHERE C.oid = GPT.relid;
+         pg_class C JOIN pg_namespace N ON (N.oid = C.relnamespace),
+         LATERAL pg_get_publication_tables(ARRAY[P.pubname], C.oid) GPT;
```

Best regards,
Hayato Kuroda
FUJITSU LIMITED



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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-26 10:43                     ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-26 23:51                       ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-27 03:16                         ` RE: Initial COPY of Logical Replication is too slow Hayato Kuroda (Fujitsu) <[email protected]>
  2026-03-27 03:51                           ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-27 06:20                             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-30 07:16                               ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-31 04:08                                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-31 11:39                                   ` RE: Initial COPY of Logical Replication is too slow Hayato Kuroda (Fujitsu) <[email protected]>
@ 2026-03-31 19:29                                     ` Masahiko Sawada <[email protected]>
  0 siblings, 0 replies; 47+ messages in thread

From: Masahiko Sawada @ 2026-03-31 19:29 UTC (permalink / raw)
  To: Hayato Kuroda (Fujitsu) <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Amit Kapila <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>

On Tue, Mar 31, 2026 at 4:39 AM Hayato Kuroda (Fujitsu)
<[email protected]> wrote:
>
> Dear Sawada-san,
>
> Thanks for updating the patch! Few comments.

Thank you for the comments!

>
> 01.
> ```
> +/*
> + * Similar to is_publishable_calss() but checks whether the given OID
> + * is a publishable "table" or not.
> + */
> +static bool
> +is_publishable_table(Oid tableoid)
> ```
>
> s/is_publishable_calss/is_publishable_class/.
>
> 02.
> ```
> +       ReleaseSysCache(tuple);
> +       return true;
> ```
>
> Is it correct? I expected to return false here.
>
> 03.
> ```
> +               /*
> +                * Preliminary check if the specified table can be published in the
> +                * first place. If not, we can return early without checking the given
> +                * publications and the table.
> +                */
> +               if (filter_by_relid && !is_publishable_table(target_relid))
> +                       SRF_RETURN_DONE(funcctx);
> ```
>
> I think we must switch to the old context.
>
> 04.
> ```
> +CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = false);
> +CREATE PUBLICATION pub_all_except_no_viaroot FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = true);
> ```
>
> It needs to be rebased due to 5984ea86.

Agreed with the all above points. I'll fix them in the next version patch.

>
> 05.
> ```
> CREATE VIEW pg_publication_tables AS
>     SELECT
>         P.pubname AS pubname,
>         N.nspname AS schemaname,
>         C.relname AS tablename,
>         ( SELECT array_agg(a.attname ORDER BY a.attnum)
>           FROM pg_attribute a
>           WHERE a.attrelid = GPT.relid AND
>                 a.attnum = ANY(GPT.attrs)
>         ) AS attnames,
>         pg_get_expr(GPT.qual, GPT.relid) AS rowfilter
>      FROM pg_publication P,
>           LATERAL pg_get_publication_tables(P.pubname) GPT,
>           pg_class C JOIN pg_namespace N ON (N.oid = C.relnamespace)
>      WHERE C.oid = GPT.relid;
> ```
>
> Can we use the new API of pg_get_publication_tables() here? Below change can pass
> tests on my env.
>
> ```
> -         LATERAL pg_get_publication_tables(P.pubname) GPT,
> -         pg_class C JOIN pg_namespace N ON (N.oid = C.relnamespace)
> -    WHERE C.oid = GPT.relid;
> +         pg_class C JOIN pg_namespace N ON (N.oid = C.relnamespace),
> +         LATERAL pg_get_publication_tables(ARRAY[P.pubname], C.oid) GPT;
> ```

I'm not sure the new API of pg_get_publication_tables() is better here
since this view is going to get the publication information of all
published tables, in which case the existing one might be faster.
Also, if a few tables among a huge number of tables (or whatever
relations) are published, checking all relations with the new API of
pg_get_publication_tables() would be quite slow.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com





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

* RE: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-26 10:43                     ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-26 23:51                       ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-27 03:16                         ` RE: Initial COPY of Logical Replication is too slow Hayato Kuroda (Fujitsu) <[email protected]>
  2026-03-27 03:51                           ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-27 06:20                             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
@ 2026-03-30 07:42                               ` Hayato Kuroda (Fujitsu) <[email protected]>
  2026-03-31 01:00                                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2 siblings, 1 reply; 47+ messages in thread

From: Hayato Kuroda (Fujitsu) @ 2026-03-30 07:42 UTC (permalink / raw)
  To: 'Masahiko Sawada' <[email protected]>; Amit Kapila <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected] <[email protected]>

Dear Sawada-san,

Thanks for updating the patch. I think the patch has a good shape.
Below contains minor comments.


```
+               if (filter_by_relid)
+                       relkind = get_rel_relkind(target_relid);
```

Can we return here if the relkind is not RELKIND_RELATION nor RELKIND_PARTITIONED_TABLE?
Key assumption here is that pg_get_publication_tables_b() returns at most one
tuple, thus this is would be called only once.

```
+       /*
+        * Non-alltables
+        */
+       if (relispartition)
```

else-if might be usalbe to clarify we're in the non-alltables case.

```
+               Assert(pubnames != NULL);
```

Personally I prefer to do Assert() before the SRF_FIRSTCALL_INIT(). Because it's
only related with argument and not related with other function calls.

```
+  proname => 'pg_get_publication_tables', prorows => '10',
```

Can prorows be 1? Because only a row would be returned here.

Best regards,
Hayato Kuroda
FUJITSU LIMITED



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

* Re: Initial COPY of Logical Replication is too slow
  2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
  2025-12-20 01:58 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-03 10:22   ` RE: Initial COPY of Logical Replication is too slow Zhijie Hou (Fujitsu) <[email protected]>
  2026-03-09 22:09     ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 13:56       ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-18 16:44         ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 22:31           ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-18 23:29             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-24 10:47               ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-24 18:57                 ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-25 05:06                   ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-26 10:43                     ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-26 23:51                       ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-27 03:16                         ` RE: Initial COPY of Logical Replication is too slow Hayato Kuroda (Fujitsu) <[email protected]>
  2026-03-27 03:51                           ` Re: Initial COPY of Logical Replication is too slow Amit Kapila <[email protected]>
  2026-03-27 06:20                             ` Re: Initial COPY of Logical Replication is too slow Masahiko Sawada <[email protected]>
  2026-03-30 07:42                               ` RE: Initial COPY of Logical Replication is too slow Hayato Kuroda (Fujitsu) <[email protected]>
@ 2026-03-31 01:00                                 ` Masahiko Sawada <[email protected]>
  0 siblings, 0 replies; 47+ messages in thread

From: Masahiko Sawada @ 2026-03-31 01:00 UTC (permalink / raw)
  To: Hayato Kuroda (Fujitsu) <[email protected]>; +Cc: Amit Kapila <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>

On Mon, Mar 30, 2026 at 12:42 AM Hayato Kuroda (Fujitsu)
<[email protected]> wrote:
>
> Dear Sawada-san,
>
> Thanks for updating the patch. I think the patch has a good shape.
> Below contains minor comments.

Thank you for the comments!

>
>
> ```
> +               if (filter_by_relid)
> +                       relkind = get_rel_relkind(target_relid);
> ```
>
> Can we return here if the relkind is not RELKIND_RELATION nor RELKIND_PARTITIONED_TABLE?
> Key assumption here is that pg_get_publication_tables_b() returns at most one
> tuple, thus this is would be called only once.

Yeah, I refactored these logic and do the preliminary check before
checking the publications.

>
> ```
> +       /*
> +        * Non-alltables
> +        */
> +       if (relispartition)
> ```
>
> else-if might be usalbe to clarify we're in the non-alltables case.

Hmm, we have the return statement at the end of the if branch so we
don't necessarily need else-if. Adding a new line after the comment
might help readability.

>
> ```
> +               Assert(pubnames != NULL);
> ```
>
> Personally I prefer to do Assert() before the SRF_FIRSTCALL_INIT(). Because it's
> only related with argument and not related with other function calls.

If we move it before the SRF_FIRSTCALL_INIT(), we would end up
executing the assertion every time we call
pg_get_publication_table_b() since it could return more than one
tuple, which seems unnecessary to me. I think we can remove this
assertion because both _a() and _b() are strict functions.

>
> ```
> +  proname => 'pg_get_publication_tables', prorows => '10',
> ```
>
> Can prorows be 1? Because only a row would be returned here.
>

If multiple publications are specified, it could return more than one tuples.

I'll submit the updated patch soon.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com





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


end of thread, other threads:[~2026-04-02 22:13 UTC | newest]

Thread overview: 47+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2025-12-06 12:18 Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
2025-12-07 14:22 ` Marcos Pegoraro <[email protected]>
2025-12-20 01:58 ` Masahiko Sawada <[email protected]>
2026-03-03 10:22   ` Zhijie Hou (Fujitsu) <[email protected]>
2026-03-09 22:09     ` Masahiko Sawada <[email protected]>
2026-03-18 13:56       ` Amit Kapila <[email protected]>
2026-03-18 16:44         ` Masahiko Sawada <[email protected]>
2026-03-18 22:31           ` Masahiko Sawada <[email protected]>
2026-03-18 23:29             ` Masahiko Sawada <[email protected]>
2026-03-24 00:53               ` Bharath Rupireddy <[email protected]>
2026-03-24 18:41                 ` Masahiko Sawada <[email protected]>
2026-03-24 06:54               ` Ajin Cherian <[email protected]>
2026-03-24 18:42                 ` Masahiko Sawada <[email protected]>
2026-03-24 06:59               ` Peter Smith <[email protected]>
2026-03-24 18:45                 ` Masahiko Sawada <[email protected]>
2026-03-24 10:47               ` Amit Kapila <[email protected]>
2026-03-24 18:57                 ` Masahiko Sawada <[email protected]>
2026-03-25 05:06                   ` Masahiko Sawada <[email protected]>
2026-03-25 08:48                     ` Peter Smith <[email protected]>
2026-03-31 09:36                       ` Amit Kapila <[email protected]>
2026-03-31 12:07                         ` Zhijie Hou (Fujitsu) <[email protected]>
2026-03-31 19:40                           ` Masahiko Sawada <[email protected]>
2026-04-01 22:23                             ` Masahiko Sawada <[email protected]>
2026-04-02 05:45                               ` Amit Kapila <[email protected]>
2026-04-02 06:12                               ` Peter Smith <[email protected]>
2026-04-02 22:13                                 ` Masahiko Sawada <[email protected]>
2026-03-31 17:28                         ` Masahiko Sawada <[email protected]>
2026-04-01 05:04                           ` Amit Kapila <[email protected]>
2026-04-01 17:35                             ` Masahiko Sawada <[email protected]>
2026-04-02 05:28                               ` Amit Kapila <[email protected]>
2026-03-26 05:44                     ` Zhijie Hou (Fujitsu) <[email protected]>
2026-03-26 08:35                     ` Hayato Kuroda (Fujitsu) <[email protected]>
2026-03-26 12:46                       ` Hayato Kuroda (Fujitsu) <[email protected]>
2026-03-31 00:29                       ` Masahiko Sawada <[email protected]>
2026-03-26 10:43                     ` Amit Kapila <[email protected]>
2026-03-26 23:51                       ` Masahiko Sawada <[email protected]>
2026-03-27 03:16                         ` Hayato Kuroda (Fujitsu) <[email protected]>
2026-03-27 03:51                           ` Amit Kapila <[email protected]>
2026-03-27 06:20                             ` Masahiko Sawada <[email protected]>
2026-03-27 13:07                               ` Marcos Pegoraro <[email protected]>
2026-04-01 00:03                                 ` Masahiko Sawada <[email protected]>
2026-03-30 07:16                               ` Zhijie Hou (Fujitsu) <[email protected]>
2026-03-31 04:08                                 ` Masahiko Sawada <[email protected]>
2026-03-31 11:39                                   ` Hayato Kuroda (Fujitsu) <[email protected]>
2026-03-31 19:29                                     ` Masahiko Sawada <[email protected]>
2026-03-30 07:42                               ` Hayato Kuroda (Fujitsu) <[email protected]>
2026-03-31 01:00                                 ` Masahiko Sawada <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox