Repository navigation
perf(tables): keep the batch unique check on per-value GIN probes - #8978
Conversation
The batch unique check ORed every value into one query under LIMIT n. The LIMIT made the planner expect an early exit and walk the table's rows through a btree with every value as a filter, so large batches read the whole table inside the write transaction while holding the unique and schema locks. Split the values into UNION ALL branches of at most 25 ORed predicates with no LIMIT, so each value plans as its own GIN probe. Cast-compared columns (ttl), which no index serves, stay one pass. The predicates, null and in-batch handling, error messages, result shape, and lock order are unchanged.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
There was a problem hiding this comment.
No issues found across 4 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Heads up: you’ve reached your flex budget. Increase your flex budget or wait for usage to reset.
Turn on auto-fix | Re-trigger cubic
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
|
There was a problem hiding this comment.
No issues found across 4 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Heads up: you’ve reached your flex budget. Increase your flex budget or wait for usage to reset.
Turn on auto-fix | Re-trigger cubic
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
No issues found across 4 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Heads up: you’ve reached your flex budget. Increase your flex budget or wait for usage to reset.
Turn on auto-fix | Re-trigger cubic
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
No issues found across 4 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Heads up: you’ve reached your flex budget. Increase your flex budget or wait for usage to reset.
Turn on auto-fix | Re-trigger cubic
Summary
checkBatchUniqueConstraintsDbruns inside the batch insert and import write transactions, after the unique locks are taken. It built a single query,table_id = $1 AND (data @> $a OR data @> $b …) LIMIT n, with up to 1000 ORed values per column. Because of theLIMIT, the planner assumed it could stop early. So instead of probing the tenant GIN index (user_table_rows_tenant_data_gin_idx), it walked the table's rows through a btree ontable_idand checked every value as a filter on each row. A slow check holds the unique-value and schema locks for its whole duration, so writers to the same table queue up behind it.This PR splits the values into
UNION ALLbranches. Each branch ORs at most 25 predicates and has noLIMIT. Each value now plans as its own GIN bitmap probe.uniqueValuePredicate, including the composite object/array equality.ttl) columns stay one pass. TheirCOALESCE(cast = …)predicate is a filter that no index serves, so splitting it into groups would mean one table walk per group.equalityCast(column)incolumn-types/comparison-sql.ts. It decides whether a column compares by containment or by cast, andfieldPredicate,columnTextForEqualityand the batch check now all use it.Plan evidence
Production, plain
EXPLAINwith synthetic values against a large tenant:LIMIT 200(before)Limit -> Index Scan using user_table_rows_workspace_table_idx, with the 200 values as aFilterLIMITtable_identryLIMITUNION ALLof single-value branchesLocal Postgres 17 replica of the table and its indexes, with a 20k-row tenant and 1000 values:
Failure modes covered
ttl(valueForEquality): the same instant written with a different offset conflicts, and the check makes exactly one scan of the table rather than one per group.Risks
LIMIT. The result is bounded by the rows that actually match. A unique column cannot hold duplicates, because enablinguniquerejects them, so this matches the old row bound. A legacy duplicate would now be reported for every matching row rather than an arbitrary subset.LIMITOR started adding the whole-tenanttable_idintersection (seen at 200, absent at 50).