cockroachdb / cockroachdb/cockroach

sql: support ALTER TABLE ... SPLIT PARTITION and MERGE PARTITION

Open
#175,637 0 comments 0 reactions 0 assignees View on GitHub
A-partitioning C-enhancement T-sql-foundations
Dominant language
Go
Stars
32.5k
Forks
4.1k
PR merge metrics
PR metrics pending

Description

Track support for splitting an existing partition into two (or more) adjacent partitions, and the inverse merge:

```sql
ALTER TABLE t SPLIT PARTITION p_rest INTO (
PARTITION p_2026_09 VALUES FROM ('2026-09-01') TO ('2026-10-01'),
PARTITION p_rest VALUES FROM ('2026-10-01') TO (MAXVALUE)
);

ALTER TABLE t MERGE PARTITIONS (p_2026_09, p_2026_10) INTO p_2026_q3;
```

This is **not urgent** — see "Priority" below — but it is the missing piece for one specific schema shape, and it is worth tracking rather than rediscovering later.

## Why this should be cheap in CockroachDB

Unlike PostgreSQL, splitting a partition here moves no data.

In CRDB a partition is a name attached to a key span of an existing index. `PartitioningDescriptor` hangs off the index descriptor (`pkg/sql/catalog/descpb/structured.proto`, field 15), documented as the index being "partitioned into spans of keys each addressable by zone configs". Range partitions store `from_inclusive` / `to_exclusive` as encoded key bytes and are required to be sorted by upper bound (`pkg/sql/catalog/catpb/catalog.proto`). A row's partition membership is *derived* from its key prefix and never stored.

KV never learns about partitions at all. The only path down is partition name -> zone config subzone -> `spanconfig.Translate` (which "accounts for and negotiates subzone spans") -> `SpanConfig` entries. KV sees spans and configs.

So splitting `[A, C)` into `[A, B)` + `[B, C)` replaces one descriptor entry with two covering the same interval. Every key stays byte-for-byte where it was.

This is worth stating explicitly because PG's cost model does not transfer: there, partitions are separate relations and `SPLIT PARTITION` physically relocates tuples under an `ACCESS EXCLUSIVE` lock. PG merged their support recently (postgres/postgres@4b3d173629f4cd7ab6cd700d1053af5d5c7c9e37, noted in #173875). Nobody should size this work against that implementation.

## Priority: low, and why

`ALTER TABLE ... ADD PARTITION` (being built as part of #175486) covers schemas whose partitions are an enumerated set with no catch-all, which is the dominant shape in CRDB today — regional-by-row is `PARTITION ALL BY LIST (crdb_region)` over a fixed region set, and the `DROP PARTITION` prototype schema in #173875 uses discrete monthly `VALUES IN` buckets.

Catch-all partitions (`TO (MAXVALUE)` for RANGE, `VALUES IN (DEFAULT)` for LIST) are supported but should be rare, because CRDB gives users no reason to add one defensively. Rows matching no partition are legal here — they stay in the index, unpartitioned. In PG an unroutable insert fails with `no partition of relation ... found for row`, so `DEFAULT` partitions are insurance against a production error. CRDB has no equivalent failure mode.

## When it becomes necessary

For a table that *does* have a catch-all tail, `ADD PARTITION` is not merely awkward, it is structurally unusable: the new partition would have to overlap the tail, and the design in #173875 requires that new partitions not overlap existing ones. Those users are fully unserved, not partially. `SPLIT` is the only way to carve a new period off the tail.

The plausible reason to maintain such a tail is zone configs — a catch-all is how you attach placement or GC settings to future rows, since unpartitioned rows fall back to the index-level config.

Until this is implemented, that limitation should be documented alongside `ADD`/`DROP PARTITION`.

## Implementation notes

The hard parts are bookkeeping, not data handling:

1. **Subzone rewriting.** Zone configs key subzones by partition name, so one subzone must fan out to N (presumably inheriting the parent's config), and merge must reconcile N subzones into one — including deciding what happens when they disagree. This has historically been fiddly: `pkg/sql/partitioning/drop_test.go` carries a regression test for a bug caused by not using hydrated descriptors when rewriting zone config subzone spans in the index GC job.
2. **Tiling validation.** The resulting partitions must exactly cover the original — no gaps, no overlap — or rows are silently orphaned into the unpartitioned state. Range partitions must also remain sorted by upper bound.
3. **Declarative schema changer modeling.** #175486 introduces a `TablePartition` element in `scpb` shaped around 0->1 transitions. `SPLIT` is an atomic 1->N retire-and-publish and `MERGE` is N->1; the `opgen` lifecycle needs to express those.

Note that the genuinely expensive operation in this area is changing the partitioning *key* columns — under `PARTITION ALL BY` those are physically prepended to the index key, making it an index rewrite across every secondary index (#58736). `SPLIT`/`MERGE` do not touch key columns and should not be sized against that either.

## Related

- #175486 — scaffolding for `ALTER TABLE ... DROP PARTITION` (adds the `TablePartition` element and `ADD PARTITION` parser support this would build on)
- #173875 — automatically increment partitions for `PARTITION BY ALL` tables (design discussion for the ADD-then-DROP workflow; cites the PG `SPLIT PARTITION` work)
- #51295 — `ALTER TABLE DROP PARTITION`
- #58736 — `ALTER TABLE PARTITION ALL BY` transitions

Filed out of an internal discussion on partition maintenance DDL following #175486. Routing to T-sql-foundations as the owner of partitioning/schema-change surface area, though note the prerequisite work in #175486 and #173875 is currently T-sql-queries.

Jira issue: CRDB-68509

Contributor guide

Open the contributing guide

Research direction

Start with the partition descriptor definitions in pkg/sql/catalog/descpb/structured.proto and pkg/sql/catalog/catpb/catalog.proto, then read the partitioning zone-config regression in pkg/sql/partitioning/drop_test.go. Review #175486's TablePartition element and the related declarative schema changer and opgen work. Done means split and merge model the partition lifecycle, preserve valid tiling, and define subzone behavior for matching and conflicting configurations.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, sql
Domain
databases
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.