cockroachdb / cockroachdb/cockroach

optimizer: equality-equivalent BETWEEN and >=/<= join predicates produce cross joins and ~31x slowdown

Open
#172,411 2 comments 0 reactions 0 assignees View on GitHub
C-enhancement O-community T-sql-queries X-blathers-triaged
Dominant language
Go
Stars
32.5k
Forks
4.1k
PR merge metrics
PR metrics pending

Description

**Describe the problem**

Logically equivalent inner-join predicates over `INT8 NOT NULL` columns produce substantially different optimizer plans and execution times.

The following four predicate forms return the same 10 rows with the same checksum:

1. Explicit equality:

```sql
t3.c0 = t1.c1
AND t3.c1 = t2.c1
```

2. `BETWEEN` with identical lower and upper bounds:

```sql
t3.c0 BETWEEN t1.c1 AND t1.c1
AND t3.c1 BETWEEN t2.c1 AND t2.c1
```

3. Equivalent paired inequalities:

```sql
t3.c0 >= t1.c1 AND t3.c0 <= t1.c1
AND t3.c1 >= t2.c1 AND t3.c1 <= t2.c1
```

4. `NOT (<>)` control:

```sql
NOT (t3.c0 <> t1.c1)
AND NOT (t3.c1 <> t2.c1)
```

The explicit equality form and the `NOT (<>)` control are planned as normal equality joins:

```text
inner-join (hash)
inner-join (merge)
```

The optimizer records equality functional dependencies for both join predicates.

In contrast, both the `BETWEEN` and paired `>=` / `<=` forms are planned as two predicate-bearing cross joins:

```text
inner-join (cross)
inner-join (cross)
```

No equality join keys or equality functional dependencies are extracted for these forms.

The optimizer estimates approximately 2.67 billion rows at the top of the range-predicate plans, although the queries return only 10 rows.

All queries read the same 18,000 KV rows. The runtime difference is almost entirely SQL CPU time rather than KV or network time.

Results from three `EXPLAIN ANALYZE (VERBOSE)` runs per predicate form:

| Predicate form | Optimizer plan | Top-level estimated rows | Optimizer cost | Execution times | Median |
| ----------------- | -------------------------- | -----------------------: | -------------: | ------------------: | -----: |
| Explicit equality | Hash join over merge join | 6,001 | 19,245.8526 | 11ms, 11ms, 11ms | 11ms |
| `BETWEEN y AND y` | Cross join over cross join | 2.66667e+09 | 267,095,723 | 347ms, 347ms, 350ms | 347ms |
| `>= y AND <= y` | Cross join over cross join | 2.66667e+09 | 267,095,723 | 349ms, 344ms, 346ms | 346ms |
| `NOT (x <> y)` | Hash join over merge join | 6,001 | 19,245.8526 | 11ms, 11ms, 11ms | 11ms |

The median slowdown is approximately:

```text
BETWEEN form: 347ms / 11ms = 31.55x
>= / <= form: 346ms / 11ms = 31.45x
```

The SQL CPU time is approximately 4ms for the equality forms and 337–342ms for the range forms.

The runtime plan for the `BETWEEN` and `>=` / `<=` forms contains operators similar to:

```text
cross join
actual row count: 10
estimated row count: 2,666,666,667
pred: (c1 >= c1) AND (c1 <= c1)

cross join
actual row count: 6,000
estimated row count: 4,000,000
pred: (c0 >= c1) AND (c0 <= c1)
```

This suggests that equality-equivalent range predicates are not being represented as equality keys during join planning.

**To Reproduce**

1. Start a CockroachDB cluster.

2. Download the attached files:

between_join_predicate_equality_extraction_repro.sql

[between_join_predicate_equality_extraction_repro.sql](https://github.com/user-attachments/files/29873343/between_join_predicate_equality_extraction_repro.sql)

between_join_predicate_equality_extraction_repro_result.txt

[between_join_predicate_equality_extraction_repro_result.txt](https://github.com/user-attachments/files/29873362/between_join_predicate_equality_extraction_repro_result.txt)

3. Run the reproduction script:

```bash
cockroach sql \
--insecure \
--host=localhost:26257 \
--file=between_join_predicate_equality_extraction_repro.sql \
> between_join_predicate_equality_extraction_repro_result.txt 2>&1
```

4. Verify the result-equivalence section. All four predicate forms should return:

```text
result_rows = 10
checksum = 40000110
```

5. Compare the `EXPLAIN (OPT, VERBOSE)` sections:

* `equality` and `not_ne` use hash and merge equality joins.
* `between` and `ge_le` use two cross joins with residual predicates.
* The range forms estimate approximately 2.67 billion rows.

6. Compare the three `EXPLAIN ANALYZE (VERBOSE)` runs for each form and calculate the median execution time.

No optimizer hints or non-default optimizer settings are used.

**Expected behavior**

For the same-type `INT8 NOT NULL` columns in this testcase, predicates of the form:

```sql
x BETWEEN y AND y
```

and:

```sql
x >= y AND x <= y
```

are equality-equivalent to:

```sql
x = y
```

The optimizer should recognize or derive this equality for join planning when doing so is safe for the relevant data types and SQL null semantics.

The resulting plans should be able to use normal equality join keys, such as hash or merge join equality columns, rather than enumerating Cartesian candidate pairs through predicate-bearing cross joins.

Cardinality estimates for these forms should also be comparable to those of the explicit equality form rather than estimating billions of rows for a query that returns 10 rows.

**Additional data / screenshots**

The complete self-contained reproduction is attached as:

```text
between_join_predicate_equality_extraction_repro.sql
```

The complete output, including:

* CockroachDB version;
* result-equivalence checks;
* one `EXPLAIN (OPT, VERBOSE)` per predicate form;
* three `EXPLAIN ANALYZE (VERBOSE)` runs per predicate form;

is attached as:

```text
between_join_predicate_equality_extraction_repro_result.txt
```

No screenshots are required because the complete plans and runtime statistics are included in the result attachment.

**Environment:**

* CockroachDB version: CockroachDB CCL v26.2.2
* Architecture: x86_64
* Server OS: Ubuntu 22.04.4 LTS
* Deployment: local single-node cluster
* Client app: `cockroach sql`
* Query distribution: local
* Vectorized execution: enabled

**Additional context**

The impact is not limited to a small difference in scalar predicate evaluation.

Failure to extract equality keys causes:

* predicate-bearing cross joins instead of equality hash or merge joins;
* approximately 31.5x higher median execution time in this small testcase;
* approximately 85x higher SQL CPU time;
* a top-level estimate of approximately 2.67 billion rows for an actual 10-row result;
* potentially incorrect downstream join-order, distribution, and memory-costing decisions in larger queries.

The test uses only 6,000 rows per table. Since the cross-join plans must evaluate many candidate row pairs, the performance difference can grow rapidly as the input tables become larger.

The `NOT (x <> y)` control is simplified to the same equality-join plan as explicit `x = y`, which shows that the optimizer already recognizes at least one equality-equivalent predicate form. The issue appears specific to equality extraction from identical-bound `BETWEEN` predicates and paired `>=` / `<=` predicates.

Jira issue: CRDB-65647

Contributor guide

Open the contributing guide

Research direction

Start by running between_join_predicate_equality_extraction_repro.sql with the provided cockroach sql command and compare the EXPLAIN (OPT, VERBOSE) output for the four predicate forms. Trace the optimizer's equality extraction and join planning entry points from the CockroachDB source. Done means the identical-bound BETWEEN and paired >=/<= forms use equality join keys with cardinality estimates comparable to explicit equality, while preserving the reported results.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, sql
Domain
databases
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.