The publish_via_partition_root option decides whether a partitioned table’s changes replicate under the root relation or under each leaf partition, and that single boolean set on a publication determines the relation name every subscriber and Change Data Capture (CDC) consumer must resolve a row change against. Choose the wrong value and the mistake never shows up in the DDL — it surfaces on the subscriber as logical replication target relation "public.events_2026_01" does not exist, or in Kafka as one topic per monthly partition when downstream expected exactly one.
This page isolates the operational behavior of publish_via_partition_root (PostgreSQL 13+) for partitioned publishers: how the emitted relation identity changes, what shape the subscriber must present, how attach and detach interact with the published set, and why Debezium topic routing depends on it. The topology reasoning behind fan-out and partial datasets lives in publication and subscription models; this is the reference for the one option that reshapes the change event itself. All behavior is validated against PostgreSQL 13 through 17.
false) streams each partition under its own name; true collapses every partition into the root relation, which is what most subscribers and Debezium topic routing expect.Relation-Naming Semantics
publish_via_partition_root is a per-publication option, stored as the pubviaroot column in pg_publication. When a partitioned table is added to a publication its partitions are always included implicitly — you never enumerate leaves. What the option controls is the identity stamped on each row change the logical decoding subsystem emits: the leaf partition that physically holds the row, or the root partitioned table the reader thinks in terms of. That identity is also what the replica identity, row filter, and column list are taken from — with true, they are read off the root; with false, off each leaf.
| Option value / context | Change event’s relation is | Subscriber requirement |
|---|---|---|
false (default) |
the leaf partition, e.g. public.events_2026_01 |
Subscriber must own a relation of that exact name (a plain table or a matching leaf). Publisher-side tuple routing is not used; every partition is a distinct target. |
true (PG 13+) |
the root partitioned table, public.events |
Subscriber needs only one relation named events — a plain table, or a table partitioned on any scheme; the apply worker routes each row to the correct local partition. |
newly ATTACHed partition, pubviaroot = true |
still the root, public.events |
No subscriber change and no REFRESH — the new partition is invisible as a distinct relation, so its rows flow immediately under the root name. |
newly ATTACHed partition, pubviaroot = false |
the new leaf, e.g. public.events_2026_03 |
Subscriber must create that leaf and run ALTER SUBSCRIPTION … REFRESH PUBLICATION before its rows can apply; until then they error as a missing relation. |
Two interactions are specific to schema-scoped and whole-database publications. FOR ALL TABLES and FOR TABLES IN SCHEMA (PG 15+) both respect the publication’s publish_via_partition_root uniformly across every partitioned table they enroll, so a single WITH (publish_via_partition_root = true) normalizes the entire stream to root identities in one statement — the usual reason to reach for schema scope on a partition-heavy database. The second interaction is the trap: if the same partition is reachable through two publications a subscription names, and those publications disagree on pubviaroot, the emitted identity is not something to leave to chance. Keep the value identical across every publication that can touch a given partition; mixing them is a configuration smell that produces relation names your consumer was never built to resolve.
Detach is the mirror image of attach. DETACH PARTITION turns a leaf into a standalone table that is no longer a member of the partitioned root, so under pubviaroot = true its changes simply stop being published — the detached table must be added to a publication in its own right if you still want its rows. Under pubviaroot = false the leaf was already publishing under its own name, so detaching it leaves that stream intact but now decoupled from the root’s lifecycle.
Diagnostic Patterns
Confirm the emitted identity before a subscriber ever attaches; the catalog tells you exactly what relation name the decoder will stamp. The pubviaroot flag and the resolved published-table list are the two facts that decide whether the subscriber shape is correct.
-- Publisher. Is this publication root-publishing, and what relations does it expose?
SELECT p.pubname, p.pubviaroot,
pt.schemaname, pt.tablename
FROM pg_publication p
JOIN pg_publication_tables pt ON pt.pubname = p.pubname
WHERE p.pubname = 'events_pub'
ORDER BY pt.tablename;
Under pubviaroot = true the view reports the root (events) and not the individual partitions; under false it lists each leaf. If the output disagrees with what you expect the subscriber to hold, stop before creating the subscription. To see the actual relation identity in the decoded byte stream rather than trusting the catalog, peek at the slot without consuming it:
-- Publisher. Confirms the relation name each change carries (root vs leaf).
SELECT data
FROM pg_logical_slot_peek_changes('events_sub', NULL, 25,
'proto_version', '4', 'publication_names', 'events_pub');
Look at the table public.events … versus table public.events_2026_01 … prefix on each change — that prefix is the contract. On the subscriber, verify every relation the publisher will name actually exists with a usable replica identity, because a partition-root mismatch presents identically to a plain missing table:
-- Subscriber. Any row here is a relation the apply worker cannot resolve.
SELECT c.relname, c.relreplident
FROM pg_subscription_rel sr
JOIN pg_class c ON c.oid = sr.srrelid
JOIN pg_subscription s ON s.oid = sr.srsubid
WHERE s.subname = 'events_sub' AND c.relreplident = 'n';
Threshold guidance: treat any apply error containing does not exist immediately after an ATTACH PARTITION on the publisher as a pubviaroot = false shape gap, not a transient fault — the new leaf will never apply until the subscriber owns it. On PG 15+, set the replica identity on the partitioned root itself so every partition inherits it; a root at relreplident = 'n' breaks UPDATE/DELETE for the whole hierarchy.
Safe Deployment Sequence
Flipping publish_via_partition_root changes the relation identity mid-stream, so the subscriber’s shape must be correct before the change lands. Roll it out in five steps with the revert one statement away.
1. Decide the target identity and inventory the subscriber. If any consumer is Debezium or expects one relation per logical table, choose true. Confirm the subscriber owns whatever that implies — a single events table for true, or every current leaf name for false.
2. Prepare the subscriber relation and its replica identity. For true, create events on the subscriber (plain or partitioned) and set its identity; for false, ensure each leaf exists. Logical replication never ships DDL, so this is your responsibility, as covered in subscription sync procedures.
3. Set the option on the publication. New publications take it inline; existing ones can be altered without a recreate.
-- New publication of a partitioned root under the root identity.
CREATE PUBLICATION events_pub
FOR TABLE public.events
WITH (publish_via_partition_root = true);
-- Or flip an existing publication (PG 13+); verify with pg_publication.pubviaroot.
ALTER PUBLICATION events_pub SET (publish_via_partition_root = true);
4. Refresh the subscription and watch every relation reach ready. The change is inert on subscribers until they refresh; the per-table srsubstate must return to r for the newly identified relation.
-- Subscriber. Re-snapshots the relation set under its new identity.
ALTER SUBSCRIPTION events_sub REFRESH PUBLICATION WITH (copy_data = true);
5. Keep the revert ready. If apply errors on a relation name the subscriber cannot resolve, restore the previous identity and refresh; the reload is non-disruptive in both directions.
ALTER PUBLICATION events_pub SET (publish_via_partition_root = false);
-- then on the subscriber: ALTER SUBSCRIPTION events_sub REFRESH PUBLICATION;
Because the blast radius is bounded by how fast you notice a missing-relation error, wire an alert on apply failures before step 3, not after.
Pipeline Integration
For an event-streaming consumer the choice of identity is not cosmetic — it decides topic cardinality. The Debezium connector names topics <topic.prefix>.<schema>.<table> from the relation the change carries, so pubviaroot = false produces one topic per partition (inventory.public.events_2026_01, …_02, and a fresh topic every month as partitions roll), while true yields the single inventory.public.events that downstream stream processors and Kafka Connect sinks expect. Set publish.via.partition.root on the connector to match the publication, or collapse per-partition topics after the fact with a ByLogicalTableRouter transform — but changing the publication is cleaner than rewriting topic names in flight.
A Python consumer reading pgoutput directly must key its handler on the Relation message the decoder sends before each Insert/Update/Delete, since that is where root-versus-leaf identity lives.
# Route by the relation the decoder names; idempotent upsert absorbs an
# accidental root/leaf double-delivery during a pubviaroot flip.
def handle_change(relation_name: str, row: dict, conn) -> None:
# Under pubviaroot=true every partition arrives as 'events'; under false
# each leaf arrives under its own name. Normalize before writing.
target = "events" if relation_name.startswith("events") else relation_name
with conn.cursor() as cur:
cur.execute(
f"INSERT INTO {target} (id, ts, payload) VALUES (%(id)s, %(ts)s, %(payload)s) "
"ON CONFLICT (id) DO UPDATE SET ts = EXCLUDED.ts, payload = EXCLUDED.payload",
row,
)
Failover adds one partition-specific hazard. After a promotion, any partition ATTACHed on the old primary but not yet reflected on the promoted node changes the published set: under pubviaroot = false the new leaf is a new relation the subscriber has never seen, so reconcile the partition hierarchy on both sides before re-enabling the subscription. Under true the attach is transparent, which is one more operational reason to prefer root publishing for tables whose partitions are created on a schedule. Ship the count of unresolved relation errors to your metrics endpoint and alert on the first occurrence, as described in asynchronous monitoring integration.
Authoritative references
- PostgreSQL:
CREATE PUBLICATION— thepublish_via_partition_rootparameter, its effect on partition identity, row filters, and column lists. - PostgreSQL: Logical Replication — Partitioned Tables — the restrictions and tuple-routing behavior for partitioned publishers and subscribers.
- PostgreSQL:
ALTER PUBLICATION—SET (…)semantics for flipping the option on a live publication.
Related
- Creating publications — the parent workflow; the exposure boundary and
publishset this option reshapes the relation identity of. - Tuning synchronous_commit for logical replication — the durability boundary for the DML a partitioned publication emits.
- Publication and subscription models — the topology reasoning behind fan-out and partial datasets that partition-root publishing feeds.
- Subscription sync procedures — build the subscriber-side relation shape that a root-published or leaf-published stream requires.