PostgreSQL  

PostgreSQL 19 Partition Merge and Split: Safer Online Schema Changes

Partitioning is a common PostgreSQL strategy for managing large tables. Instead of storing all rows in one physical table, data is divided into smaller partitions based on a partition key such as date, tenant, or another logical boundary.

As data grows, partition layouts sometimes need to change. A team may need to split a large partition into smaller partitions or merge several partitions into a larger one.

Historically, these operations could require careful migration planning because changing the partition structure can involve significant data movement and locking considerations.

PostgreSQL 19 adds ATTACH PARTITION ... SPLIT INTO and ATTACH PARTITION ... MERGE, providing new declarative operations for changing partition layouts. These capabilities can make certain partition maintenance tasks easier to perform while keeping the partitioned table available for normal application operations.

For .NET applications using PostgreSQL, understanding these operations is useful when tables grow large enough that partition maintenance becomes part of regular database operations.

What Is PostgreSQL Table Partitioning?

Suppose an application stores orders in a large table:

CREATE TABLE orders
(
    id          bigint,
    customer_id bigint,
    order_date  date,
    total       numeric(12, 2)
)
PARTITION BY RANGE (order_date);

The data can then be divided by date:

CREATE TABLE orders_2026
PARTITION OF orders
FOR VALUES FROM ('2026-01-01')
             TO ('2027-01-01');

The application continues querying:

SELECT *
FROM orders
WHERE customer_id = 1001;

while PostgreSQL determines which partition or partitions need to be accessed.

Partitioning can help with data lifecycle management, partition pruning, and maintenance operations, but it also introduces an additional database structure that must be managed correctly.

Why Partition Layouts Need to Change

Partition boundaries that made sense when a system was small may become less appropriate as data volume changes.

For example:

orders
  |
  +-- 2026

might eventually become:

orders
  |
  +-- 2026-Q1
  +-- 2026-Q2
  +-- 2026-Q3
  +-- 2026-Q4

A different workload might require the opposite:

orders
  |
  +-- January
  +-- February
  +-- March

becoming:

orders
  |
  +-- Q1

PostgreSQL 19's partition merge and split operations address these types of structural changes.

Splitting a Partition

Imagine that a partition currently contains an entire year:

orders
    |
    +-- orders_2026

As data volume increases, the team may want quarterly partitions:

orders
    |
    +-- orders_2026_q1
    +-- orders_2026_q2
    +-- orders_2026_q3
    +-- orders_2026_q4

PostgreSQL 19 introduces the ability to split an existing partition into multiple partitions using ATTACH PARTITION ... SPLIT INTO.

The conceptual form is:

ALTER TABLE orders
ATTACH PARTITION orders_2026
SPLIT INTO (
    orders_2026_q1 FOR VALUES FROM ('2026-01-01') TO ('2026-04-01'),
    orders_2026_q2 FOR VALUES FROM ('2026-04-01') TO ('2026-07-01'),
    orders_2026_q3 FOR VALUES FROM ('2026-07-01') TO ('2026-10-01'),
    orders_2026_q4 FOR VALUES FROM ('2026-10-01') TO ('2027-01-01')
);

The exact syntax and restrictions should be checked against the PostgreSQL 19 documentation before using the operation in a production migration.

The important concept is that PostgreSQL can express the partition transformation as a database-level operation rather than requiring an application-managed copy-and-delete workflow.

Merging Partitions

The opposite situation can also occur.

Suppose an application has several smaller partitions:

orders
    |
    +-- orders_2026_q1
    +-- orders_2026_q2
    +-- orders_2026_q3
    +-- orders_2026_q4

The team may decide that fewer, larger partitions are easier to manage.

PostgreSQL 19 introduces MERGE support for combining partitioned data into a new partition structure.

Conceptually:

ALTER TABLE orders
ATTACH PARTITION orders_2026
MERGE (
    orders_2026_q1,
    orders_2026_q2,
    orders_2026_q3,
    orders_2026_q4
);

The exact statement should be validated against the PostgreSQL 19 ALTER TABLE syntax because partition bounds, indexes, constraints, and existing partition structure affect whether an operation is valid.

Why This Matters for .NET Applications

The biggest benefit is that the application does not need to understand the physical partition migration.

A typical .NET application still executes:

public async Task<Order?> GetOrderAsync(
    long orderId,
    CancellationToken cancellationToken)
{
    const string sql = """
        SELECT id, customer_id, order_date, total
        FROM orders
        WHERE id = @id;
        """;

    await using var connection =
        await _dataSource.OpenConnectionAsync(
            cancellationToken);

    await using var command =
        new NpgsqlCommand(sql, connection);

    command.Parameters.AddWithValue("id", orderId);

    await using var reader =
        await command.ExecuteReaderAsync(
            cancellationToken);

    if (!await reader.ReadAsync(cancellationToken))
        return null;

    return new Order(
        reader.GetInt64(0),
        reader.GetInt64(1),
        reader.GetFieldValue<DateOnly>(2),
        reader.GetDecimal(3));
}

The application queries the partitioned parent table:

.NET application
       |
       v
orders
       |
       +-- partition selection

The physical partition layout can change independently, provided the application's schema contract remains compatible.

Online Schema Changes and Availability

The term "online" needs to be used carefully.

An operation being designed to avoid unnecessary application downtime does not mean that it has no locking, resource, or performance impact.

Partition restructuring can involve:

  • Table locks

  • Data movement

  • Index maintenance

  • Constraint validation

  • Increased I/O

  • Additional CPU usage

  • Longer transactions

Before performing a production migration, teams should review the PostgreSQL 19 documentation for the exact locking and execution behavior of the specific partition operation.

This distinction is important for production planning.

Partitioning Example for a .NET SaaS Application

Consider a multi-tenant application:

CREATE TABLE audit_events
(
    id         bigint,
    tenant_id  bigint,
    created_at timestamptz NOT NULL,
    event_type text,
    payload    jsonb
)
PARTITION BY RANGE (created_at);

Initially, monthly partitions may be appropriate:

audit_events
    |
    +-- 2026_01
    +-- 2026_02
    +-- 2026_03

If one month becomes significantly larger than the others, the team might decide to split it:

2026_03
    |
    +-- 2026_03_01_to_15
    +-- 2026_03_16_to_31

The application continues to query:

SELECT id, tenant_id, created_at, event_type
FROM audit_events
WHERE tenant_id = @tenantId
  AND created_at >= @from
  AND created_at < @to;

This keeps the physical storage strategy separate from application-level query logic.

Partition Constraints Matter

Partition boundaries must correctly represent the rows that belong in each partition.

For a range-partitioned table:

Partition A
[2026-01-01, 2026-04-01)

Partition B
[2026-04-01, 2026-07-01)

The upper bound is exclusive.

Therefore:

2026-03-31 → Partition A
2026-04-01 → Partition B

Incorrect boundaries can cause a migration to fail or result in a partition structure that does not represent the intended data model.

Always validate partition definitions before executing a restructuring operation.

Migration Strategy for Production

A practical migration process can look like this:

Step 1: Inspect the Current Partition Layout

SELECT
    parent.relname AS parent_table,
    child.relname AS partition
FROM pg_inherits
JOIN pg_class parent
    ON pg_inherits.inhparent = parent.oid
JOIN pg_class child
    ON pg_inherits.inhrelid = child.oid
WHERE parent.relname = 'orders';

This provides a starting point for understanding the existing structure.

Step 2: Check Data Distribution

Determine how much data exists in the partitions that will be changed.

SELECT
    count(*)
FROM orders_2026;

For large systems, use the database's available statistics and monitoring rather than assuming row counts are evenly distributed.

Step 3: Test the Migration

Run the operation against a production-like environment first.

Test:

  • Application reads

  • Application writes

  • Concurrent queries

  • Lock behavior

  • Index availability

  • Rollback strategy

  • Monitoring alerts

Step 4: Execute During an Appropriate Window

Even when an operation is designed to reduce disruption, database resource consumption can change while the migration runs.

Step 5: Verify the New Layout

Inspect the resulting partitions:

SELECT
    parent.relname,
    child.relname
FROM pg_inherits
JOIN pg_class parent
    ON pg_inherits.inhparent = parent.oid
JOIN pg_class child
    ON pg_inherits.inhrelid = child.oid
WHERE parent.relname = 'orders';

Then run representative application queries.

Common Mistakes

Treating Partition Changes as Pure Metadata Operations

Partition restructuring can involve actual data movement and resource consumption. Do not assume every operation completes instantly.

Ignoring Indexes

Partitioned applications often have indexes on individual partitions. Review index requirements before and after restructuring.

Using Incorrect Bounds

Boundary errors are one of the easiest ways to create an invalid partition design.

Performing the Migration Without a Rollback Plan

Before changing a large production table, define how the team will recover if the operation cannot complete or creates unexpected application behavior.

Forgetting Monitoring

Monitor database locks, I/O, query latency, CPU, and application errors during the migration.

Best Practices

  1. Design partition boundaries around the actual workload.

  2. Test merge and split operations against production-like data.

  3. Verify partition constraints before migration.

  4. Review locking behavior before production execution.

  5. Monitor database and application performance during the operation.

  6. Keep application queries against the partitioned parent table where appropriate.

  7. Review indexes and constraints after restructuring.

  8. Have a rollback or recovery strategy.

  9. Schedule resource-intensive operations carefully.

  10. Document the resulting partition layout for future maintenance.

Advantages and Disadvantages

Advantages

  • Provides declarative partition restructuring.

  • Makes split and merge operations easier to express at the database level.

  • Can reduce the need for application-managed data-copy workflows.

  • Allows physical storage organization to evolve as workloads change.

  • Helps database teams manage growing partitioned tables more systematically.

Disadvantages

  • Requires PostgreSQL 19.

  • Partition operations can still consume significant database resources.

  • Locking and execution behavior must be understood before production use.

  • Existing indexes and constraints require careful review.

  • Incorrect partition boundaries can make migrations fail.

  • Large data movements can still take time.

Conclusion

PostgreSQL 19 adds new capabilities for changing partition layouts through partition split and merge operations. For teams managing large PostgreSQL tables, this provides another tool for adapting partition structures as data volume and workload patterns change.

For .NET developers, the main benefit is that application-level queries can remain focused on the partitioned parent table while database administrators evolve the physical partition layout.

The safest approach is not to treat partition restructuring as a routine metadata change. Test the exact operation, understand its locking and data-movement behavior, verify indexes and constraints, and monitor the database while the migration runs.

When these steps are followed, PostgreSQL 19's partition-management improvements can make long-term maintenance of large partitioned tables more manageable without forcing unnecessary changes into the application layer.