cockroachdb / cockroachdb/cockroach
tuple `(a, b) >= ANY (correlated subquery)` evaluated in a derived-table projection returns NULL instead of TRUE when the first tuple component decides the order, silently dropping rows
- Dominant language
- Go
- Stars
- 32.5k
- Forks
- 4.1k
- PR merge metrics
- PR metrics pending
Description
**Describe the problem**
A tuple quantified comparison `(a, b) >= ANY (SELECT x, y FROM WHERE )`
diverges between the WHERE path and the relocated projection path when the produced tuple has a NULL
second component.
The tuple comparison itself is correct in isolation — `(1, 5) >= (0, NULL)` evaluates to TRUE because
the first component already decides the lexicographic order — and the WHERE form also returns the row.
But the same predicate evaluated as a derived-table projection returns NULL instead of TRUE, so the
row is silently lost.
**To Reproduce**
1. Start a single-node CockroachDB (tested on v26.3.1, docker image `cockroachdb/cockroach:v26.3.1`,
`cockroach start-single-node --insecure`).
2. Create the schema and one row per table:
```sql
CREATE TABLE t29 (c0 INT8, rowid INT8);
CREATE TABLE t70 (c0 FLOAT8, rowid INT8);
CREATE TABLE t0 (rowid INT8);
INSERT INTO t29 VALUES (-471377708, 1209922362678116353);
INSERT INTO t70 VALUES (NULL, -471377708);
INSERT INTO t0 VALUES (-1664603939);
```
3. Run the two equivalent forms:
```sql
-- (a) predicate in WHERE
SELECT t29.c0, t29.rowid FROM t29
WHERE (t29.c0, t29.rowid) >= ANY (
SELECT t0.rowid, t70.c0 FROM t70, t0 WHERE (t70.rowid = t29.c0) AND (true));
-- (b) the same predicate relocated into a derived-table projection
SELECT ref0 FROM (
SELECT t29.c0 AS ref0, t29.rowid AS ref1,
((t29.c0, t29.rowid) >= ANY (
SELECT t0.rowid, t70.c0 FROM t70, t0 WHERE (t70.rowid = t29.c0) AND (true))) AS ref2
FROM t29
) AS s WHERE ref2;
```
4. See the disagreement:
```
(a) c0 = -471377708, rowid = 1209922362678116353 -- 1 row <-- correct
(b) (empty result) -- 0 rows <-- WRONG, row is lost
```
The subquery produces a single tuple `(-1664603939, NULL)`. The comparison
`(-471377708, 1209922362678116353) >= (-1664603939, NULL)` must be TRUE because the first components
already decide the order (`-471377708 > -1664603939`), exactly as the bare evaluation confirms:
```sql
SELECT (1, 5) >= (0, NULL::FLOAT8); -- true (first component short-circuits)
SELECT (1, 5) >= ANY (SELECT 0, NULL::FLOAT8); -- true
```
**Expected behavior**
Both equivalent forms must return the same row. The relocated projection form must evaluate the tuple
`>= ANY` comparison to TRUE whenever the first tuple component decides the order, regardless of a NULL
later component.
**Additional data / screenshots**
Schema and queries are included above (bare columns, one row per table — nothing else is needed).
Element analysis: both the correlated key and the two-table (cross join) subquery are required — an
uncorrelated two-table subquery and a single-table correlated subquery do NOT diverge with the same
data shape. Plan evidence: the WHERE form is rewritten to a `column20 <= (c0, rowid)` aggregate
comparison over a hash-joined subquery, while the relocated form keeps the correlated ANY as a
per-row evaluation whose tuple comparison is dragged to NULL by the NULL second component instead of
short-circuiting on the first component.
**Environment:**
- CockroachDB version: CCL v26.3.1 (docker image `cockroachdb/cockroach:v26.3.1`, built 2026/08/24)
- Server OS: official Docker image (Linux x86_64 host, Ubuntu 20.04)
- Client app: built-in `cockroach sql` CLI (also reproduced via JDBC)
**Additional context**
What was the impact?
Add any other context about the problem here.
Jira issue: CRDB-68373
Contributor guide
Research direction
Start by running the two SQL forms in the built-in cockroach sql CLI against the supplied single-node schema and compare their results. Then trace the WHERE rewrite to the aggregate comparison and the relocated projection's correlated ANY per-row evaluation, focusing on tuple NULL propagation and lexicographic short-circuiting. Done means both forms return the same row and the comparison remains TRUE when the first component decides the order.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- sql
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100