apache/druid · error · IllegalArgumentException

Got a join, with a cartesian product that exceeds 1,000,000…

Error message

Got a join, with a cartesian product that exceeds 1,000,000 rows, cannot handle it

What it means

During a sorted inner join, when join key values are equal across all inputs but the combined cross-product of matching rows would exceed 1,000,000 rows, Druid refuses to materialize the join. The library enforces this hard cap because a huge cartesian product would exhaust memory and stall the query. The message deliberately omits the offending values to avoid leaking data.

Solutions

  1. Fix the join condition so keys are high-cardinality and actually correlate the two inputs
  2. Filter or pre-aggregate the inputs to reduce rows sharing a single key value
  3. Reformulate as a GROUP BY or subquery to avoid the cross product
  4. Increase limits by splitting the query into smaller partitions if the join is legitimately needed

Example fix

// before: join on constant/low-cardinality column
JOIN dim ON (fact.partition = dim.partition)  // both have millions of rows per value
// after: join on a selective key
JOIN dim ON (fact.user_id = dim.user_id)
Defensive patterns

Strategy: validation

Validate before calling

// before issuing the join, check key cardinality per value
long maxRowsPerKey = computeMaxRowsPerJoinKey(factRows, joinKey); // via GROUP BY key count()
if (maxRowsPerKey * matchingDimRows > 1_000_000L) {
  throw new IllegalArgumentException("join key too low-cardinality; refactor query");
}

Type guard

boolean isSafeJoin(DataSource left, DataSource right, JoinCondition c) {
  return !c.getCondition().isAlwaysTrue() && estimateJoinCardinality(left, right, c) <= 1_000_000L;
}

Try / catch

try {
  runJoin(query);
} catch (IllegalArgumentException e) {
  if (e.getMessage().contains("cartesian product that exceeds")) {
    // fall back to pre-aggregated or filtered join
    runJoin(refactorQuery(query));
  } else throw e;
}

Prevention

When it happens

Trigger: joinRows (also reached via process/alternativeRow) encounters rows on all inputs whose join keys match, and the product of the number of matching rows per join part (numRowsExpected) exceeds 1,000,000.

Common situations: Joining on a low-cardinality key (e.g. boolean, status flag, or constant column) so one key value matches millions of rows; accidental cartesian join caused by a wrong or constant join condition; skewed data where one tenant/ID dominates.

Understand the failure class

Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.

Related errors


AI-assisted analysis of apache/druid@9b90983fd2 (2026-09-07). Data as JSON: /api/errors/1f12c2d5b0479f83. Report an issue: GitHub.

Appendix: source

Thrown at processing/src/main/java/org/apache/druid/query/operator/join/SortedInnerJoinOperator.java:357

        if (joinPartIndex == 0) {
          // We have walked through all of the joinParts and have matches, so time to add to rowsToInclude

          // We have ranges in each of the parts, we will do the cartesian product, so we will produce the product
          // of the length of those ranges number of rows.  Let's compute it to see how many rows we will produce.
          int numRowsExpected = joinPart.scanToRowIndex - joinPart.currRowIndex;
          for (int i = 1; i < joinParts.size(); ++i) {
            final JoinPart subPart = joinParts.get(i);
            numRowsExpected *= subPart.scanToRowIndex - subPart.currRowIndex;
          }

          if (numRowsExpected == 1) {
            for (int i = 0; i < joinParts.size(); ++i) {
              rowsToInclude[i].add(joinParts.get(i).currRowIndex);
            }
          } else {
            if (numRowsExpected > 1_000_000) {
              // It would be helpful to serialize the actual value out with this error, but that risks leaking data
              throw new IAE("Got a join, with a cartesian product that exceeds 1,000,000 rows, cannot handle it");
            }

            // The rowIds that will be used in the result of the join will be a cartesian product, which means that
            // the "deepest" row ids will be repeated in-order over and over, then the next layer will have each
            // value repeated runSize times, forming a new run of length * runSize, and so on and so forth
            int partIndex = joinParts.size() - 1;
            int runSize = 1;
            // We are guaranteed that there is a runSize greater than 1 because otherwise numRowsExpected would be 1
            while (partIndex >= 0) {
              final JoinPart part = joinParts.get(partIndex);
              final int size = part.scanToRowIndex - part.currRowIndex;
              if (size == 1) {
                rowsToInclude[partIndex].fill(part.currRowIndex, numRowsExpected);
              } else {
                int[] vals = new int[size];
                for (int i = 0; i < vals.length; ++i) {
                  vals[i] = i + part.currRowIndex;
                }

View on GitHub (pinned to 9b90983fd2)