Publication date: April 20, 2026
Description
A bug in CockroachDB can cause partial index corruption when a partial index is being backfilled on a table that uses multiple column families while the table is receiving concurrent updates. This can occur when a partial index is first created with CREATE INDEX, or when an existing partial index is recreated as part of another schema change (such as ALTER TABLE ... ALTER COLUMN). The corruption can result in incorrect NULL values in the partial index, phantom rows that do not satisfy the partial index predicate, or rows missing from the partial index.
This advisory applies to the following versions of CockroachDB:
- v24.1: All versions
- v24.2: All versions
- v24.3: v24.3.0 through v24.3.30
- v25.1: All versions
- v25.2: v25.2.0 through v25.2.16
- v25.3: All versions
- v25.4: v25.4.0 through v25.4.8
- v26.1: v26.1.0 through v26.1.2
- v26.2: v26.2.0-alpha.1, v26.2.0-alpha.2, v26.2.0-beta.1
All of the following conditions are required to trigger this bug:
- A table uses multiple column families.
- A partial index is being backfilled on the table (e.g., due to
CREATE INDEXor another schema change that triggers an index rebuild). - A concurrent
UPDATEmodifies a column that is in a different column family from the columns referenced by the partial index. - The partial index predicate references columns that are not being updated.
The root cause is in the SQL optimizer, which incorrectly applies two optimizations during schema change backfills. First, it may avoid fetching columns that are not being updated, causing the backfill to see NULL values instead of the actual data. Second, it may skip writing to the new index entirely if it determines (incorrectly, during a backfill) that the update does not affect the partial index. Both optimizations are correct during normal operations but should not be applied while a new index is being backfilled.
Statement
This is resolved in CockroachDB by PR #166123, which fixes the optimizer to always fetch the required columns and avoid simplifying predicate evaluation for mutation indexes.
A fix has been applied to the following maintenance releases of CockroachDB:
- v24.3.31
- v25.2.17
- v25.4.9
- v26.1.3
- Testing release v26.2.0-beta.2
This public issue is tracked by #166122.
Mitigation
Users of affected CockroachDB versions are encouraged to upgrade to v24.3.31, v25.2.17, v25.4.9, v26.1.3, v26.2.0-beta.2, or a later version.
Users who have created partial indexes on tables with multiple column families while concurrent updates were occurring may have been affected. To determine whether your cluster was affected:
- Identify any partial indexes on tables that use multiple column families.
- If such indexes exist and were created while the table was receiving concurrent updates, use the detection method described below to check for corruption.
- If corruption is detected, the affected index can be dropped and recreated on a version containing the fix. Alternatively, if an immediate upgrade is not possible, the index can be dropped and recreated while the table is not receiving concurrent updates.
Please reach out to our support team if more information or assistance is needed.
Detection
Partial indexes can be checked for corruption by comparing data read from the partial index against data read from the primary index. This can detect three forms of corruption caused by this issue: phantom rows in the partial index that do not satisfy the predicate, columns with incorrect NULL values, and rows missing from the partial index.
For example, consider the following table with multiple column families and a partial index. The payload column is stored in a separate column family from the columns referenced by the partial index:
CREATE TABLE my_table (
id INT PRIMARY KEY,
status STRING,
a INT,
b INT,
payload JSONB,
FAMILY f1 (id, status, a, b),
FAMILY f2 (payload),
INDEX my_partial_idx (a, b)
WHERE status = 'active'
);
In this example, the bug could be triggered by an UPDATE to the payload column while my_partial_idx is being created, because payload is in a different column family from the columns used by the partial index (status, a, b).
Query 1: Check for phantom rows and incorrect NULL values
The following query finds rows in the partial index that either do not satisfy the predicate according to the primary index, or have column values that differ from the primary index:
(SELECT id, a, b FROM my_table@my_partial_idx WHERE status = 'active'
EXCEPT ALL
SELECT id, a, b FROM my_table@my_table_pkey WHERE status = 'active')
ORDER BY id;
Any rows returned by this query indicate corruption in the partial index.
Query 2: Check for missing rows
The following query finds rows that satisfy the partial index predicate in the primary index but are missing from the partial index:
(SELECT id, a, b FROM my_table@my_table_pkey WHERE status = 'active'
EXCEPT ALL
SELECT id, a, b FROM my_table@my_partial_idx WHERE status = 'active')
ORDER BY id;
Any rows returned by this query are present in the primary index and satisfy the partial index predicate, but are missing from the partial index.
If any discrepancies are found, the affected index can be dropped and recreated on a version containing the fix.
Impact
Clusters running affected versions may experience the following issues when creating partial indexes on tables with multiple column families while concurrent updates are occurring:
- Data loss / Corruption: Incorrect
NULLvalues may be written into the resulting partial index. - Phantom rows: The partial index may contain rows that no longer satisfy the partial index predicate. This occurs because the optimizer skips index writes for concurrent updates during the backfill, so rows that should have been removed from the index remain.
- Missing rows: Rows that satisfy the partial index predicate may be missing from the partial index.
- Validation failures: The resulting index may fail validation after the backfill completes due to the corrupted state.
Please reach out to our support team if more information or assistance is needed.