Attaching Partitions Without Long Locks

A nightly job bulk-loads the previous day’s clickstream into a standalone table, builds its indexes, and then attaches it to the partitioned clicks table. It used to take seconds. Now the loaded table has 60 million rows and the attach takes four minutes, during which the new table is locked — fine — but lately queries on the whole clicks table have also started stalling. Two separate mechanisms are at work: ATTACH PARTITION scans the incoming table to prove every row fits the partition bounds, and, if the parent has a default partition, it also scans the default partition to prove none of its rows belong in the new range. Both scans are avoidable. This guide explains the locks and scans involved and shows how to make attaching a partition a catalog operation. It belongs to Partitioning Live Tables Without Downtime.

What ATTACH PARTITION Checks Sequence between the loader, PostgreSQL, the incoming table and the default partition. The loader issues ATTACH PARTITION. PostgreSQL takes SHARE UPDATE EXCLUSIVE on the parent and ACCESS EXCLUSIVE on the incoming table. It looks for a validated CHECK implying the bounds; if none, it scans the incoming table. If a default partition exists, it scans it for rows in the new range. Then it commits. What ATTACH PARTITION Checks Loader PostgreSQL clicks_2026_09_17 clicks_default ATTACH PARTITION clicks_2026_09_17 locks: parent SUE, incoming AE validated CHECK? else full scan scan for rows in new range attached
Two possible scans; a validated CHECK removes the first, and not having a default partition removes the second.

Symptom / Error Signatures

These signals point to an attach doing more work than it should:

  • ALTER TABLE clicks ATTACH PARTITION ... runs for minutes; pg_stat_activity shows it active with no lock wait, and pg_stat_user_tables.seq_scan on the incoming table increments.
  • Queries touching the default partition stall during the attach, because it is also being scanned under a lock.
  • The attach fails at the end:
ERROR:  partition constraint of relation "clicks_2026_09_17" is violated by some row
ERROR:  updated partition constraint for default partition "clicks_default" would be violated by some row
  • On PostgreSQL 11 and earlier, the attach takes ACCESS EXCLUSIVE on the parent, so every query on the partitioned table waits for its duration.

Root Cause Analysis

Since PostgreSQL 12, ATTACH PARTITION locks the parent with SHARE UPDATE EXCLUSIVE — reads and writes to other partitions continue — and the incoming table with ACCESS EXCLUSIVE. The expensive part is validation: PostgreSQL must be certain every row in the incoming table satisfies the partition bound. It skips the scan if the table already has a validated CHECK constraint that implies the bound (including NOT NULL on the key for range partitions). Otherwise it scans the table under that exclusive lock.

A default partition adds a second obligation: after attaching, rows belonging to the new range must not remain in the default partition, so PostgreSQL scans the default partition too, holding a lock on it. A validated CHECK on the default partition that excludes the new range skips that scan, but in practice the simplest fix is to not have a default partition on tables where partitions are attached routinely.

Condition Incoming table scanned? Default partition scanned?
no CHECK, default partition exists yes yes
no CHECK, no default partition yes
validated CHECK implying bounds, no default no
validated CHECK, default with excluding CHECK no no
Attach Duration for a 60M-Row Partition Bar chart of ATTACH PARTITION duration for a 60 million row incoming table. With no CHECK and a default partition: 240 seconds. With no CHECK and no default: 180 seconds. With a validated CHECK and no default: 0.02 seconds. Attach Duration for a 60M-Row Partition no CHECK, default partition 240 s no CHECK, no default 180 s validated CHECK, no default 0.02 s seconds holding locks during ATTACH (illustrative)
The validated CHECK is the difference between a four-minute lock and a catalog update.

Immediate Mitigation

1. Add the constraint while the table is still standalone. For a table that is being loaded, the cheapest moment is right after loading: the constraint can be added normally (validated immediately) because nobody else is using the table yet.

-- PostgreSQL 12+ · loader role · standalone table, nobody else reads it yet
ALTER TABLE clicks_2026_09_17 ALTER COLUMN clicked_at SET NOT NULL;
ALTER TABLE clicks_2026_09_17 ADD CONSTRAINT clicks_2026_09_17_bounds
  CHECK (clicked_at >= '2026-09-17' AND clicked_at < '2026-09-18');

For a table that is live (for example a legacy table being adopted), add the check NOT VALID and VALIDATE it online instead, as in Adding Constraints Without Downtime.

2. Attach with a lock timeout.

-- PostgreSQL 12+ · SHARE UPDATE EXCLUSIVE on parent, brief ACCESS EXCLUSIVE on the child
SET lock_timeout = '3s';
ALTER TABLE clicks ATTACH PARTITION clicks_2026_09_17
  FOR VALUES FROM ('2026-09-17') TO ('2026-09-18');
ALTER TABLE clicks_2026_09_17 DROP CONSTRAINT clicks_2026_09_17_bounds;   -- redundant after attach
-- ROLLBACK PATH: ALTER TABLE clicks DETACH PARTITION clicks_2026_09_17;

3. Deal with the default partition. If one exists and may contain rows for the new range, move those rows out first; if it should be empty, add a validated CHECK to it that excludes the ranges you attach, or remove the default partition altogether.

4. Build indexes before attaching. If the incoming table lacks indexes the parent defines, the attach creates them — under the lock. Create matching indexes on the standalone table first (non-concurrently is fine while nobody uses it) so the attach can adopt them.

Permanent Fix / Long-Term Pattern

Make “load standalone, constrain, index, attach” the standard pipeline for bulk-loaded partitions. The standalone table is private until the attach, so every heavy step — loading, indexing, validating — runs with no contention, and the attach becomes a catalog update. For partitions created empty ahead of time (the usual pattern for continuously written tables), create them directly with CREATE TABLE ... PARTITION OF, which needs no validation at all, well before data arrives — see automating partition creation with pg_partman.

Avoid default partitions on tables that use routine attach or concurrent detach: they add scans to attaches and block DETACH ... CONCURRENTLY entirely. If you need protection against out-of-range inserts, monitor for them and pre-create partitions generously instead. The same constraint trick is at the heart of adopting an existing table, as in converting a Postgres table to declarative partitioning.

Bulk-Load Attach Pipeline Five steps for a bulk-loaded partition. Create a standalone table like the parent; load data with COPY; add NOT NULL and a bounds CHECK and build indexes matching the parent; attach with lock_timeout, which skips validation; drop the redundant CHECK. Bulk-Load Attach Pipeline STEP 1 Standalone table LIKE clicks STEP 2 COPY data no contention STEP 3 CHECK + indexes match bounds + parent STEP 4 ATTACH no scan STEP 5 Drop CHECK bound enforces it
All heavy work happens while the table is private; the attach itself only updates the catalog.

Verification Checklist

Frequently Asked Questions

Does ATTACH PARTITION block queries on the parent? Since PostgreSQL 12 it takes SHARE UPDATE EXCLUSIVE on the parent, so reads and writes to other partitions continue. On PostgreSQL 11 and earlier it takes ACCESS EXCLUSIVE on the parent, blocking everything.

What exactly must the CHECK constraint look like? It must imply the partition bound: for a range partition on clicked_at from A to B, clicked_at >= A AND clicked_at < B, together with NOT NULL on the column (or clicked_at IS NOT NULL in the check). It must be validated.

Why keep the CHECK after attaching? You do not need to. The partition bound enforces the same rule once attached, so the check is redundant and can be dropped to save a little work on each insert.

Should I use a default partition as a safety net? On tables that attach and detach partitions routinely, no. A default partition forces extra scans on attach and prevents concurrent detach. Pre-create partitions well ahead and alert if the horizon gets short.