{"record":{"id":"688166968919ea48","repo":"apache/druid","slug":"cannot-operate-on-a-dimension-with-unknown-cardina-688166","errorCode":null,"errorMessage":"Cannot operate on a dimension with unknown cardinality","messagePattern":"Cannot operate on a dimension with unknown cardinality","errorType":"exception","errorClass":"UnsupportedOperationException","httpStatus":null,"severity":"error","filePath":"processing/src/main/java/org/apache/druid/query/topn/PooledTopNAlgorithm.java","lineNumber":498,"sourceCode":"   * See http://en.wikipedia.org/wiki/Duff's_device for more information on this kind of approach\n   *\n   * This allows out of order execution of the code. In local tests, the JVM inlines all the way to this function.\n   *\n   * If there are more than AGG_UNROLL_COUNT aggregates, then the remainder is calculated with the switch, and the\n   * blocks of AGG_UNROLL_COUNT are calculated in a partially unrolled for-loop.\n   *\n   * Putting the switch first allows for optimization for the common case (less than AGG_UNROLL_COUNT aggs) but\n   * still optimizes the high quantity of aggregate queries which benefit greatly from any speed improvements\n   * (they simply take longer to start with).\n   */\n  private static long scanAndAggregateDefault(\n      final PooledTopNParams params,\n      final int[] positions,\n      final BufferAggregator[] theAggregators\n  )\n  {\n    if (params.getCardinality() < 0) {\n      throw new UnsupportedOperationException(\"Cannot operate on a dimension with unknown cardinality\");\n    }\n\n    final ByteBuffer resultsBuf = params.getResultsBuf();\n    final int numBytesPerRecord = params.getNumBytesPerRecord();\n    final int[] aggregatorSizes = params.getAggregatorSizes();\n    final Cursor cursor = params.getCursor();\n    final CursorGranularizer granularizer = params.getGranularizer();\n    final DimensionSelector dimSelector = params.getDimSelector();\n\n    final int[] aggregatorOffsets = new int[aggregatorSizes.length];\n    for (int j = 0, offset = 0; j < aggregatorSizes.length; ++j) {\n      aggregatorOffsets[j] = offset;\n      offset += aggregatorSizes[j];\n    }\n\n    final int aggSize = theAggregators.length;\n    final int aggExtra = aggSize % AGG_UNROLL_COUNT;\n    int currentPosition = 0;","sourceCodeStart":480,"sourceCodeEnd":516,"githubUrl":"https://github.com/apache/druid/blob/9b90983fd291f26935af934383ce360473179e4d/processing/src/main/java/org/apache/druid/query/topn/PooledTopNAlgorithm.java#L480-L516","documentation":"In PooledTopNAlgorithm.scanAndAggregateDefault, params.getCardinality() must be non-negative because the default scan path indexes directly into a positions array sized by dimension cardinality. A negative cardinality (dimension with unknown/no dictionary) makes indexed aggregation impossible, so UnsupportedOperationException is thrown. This guards the aggregation loop against out-of-range indexing into per-value slots.","triggerScenarios":"TopN query falling back to scanAndAggregateDefault while the PooledTopNParams carries cardinality < 0 — a dimension selector with unknown cardinality, or params built without a valid cardinality (e.g. after init skipped validation or cardinality changed between init and scan).","commonSituations":"Querying segments where the chosen dimension lacks dictionary encoding; race between segment replacement/unmapping and query execution leaving stale params; using topN against data sources that don't dictionary-encode the dimension.","solutions":["Use a dictionary-encoded string dimension for topN","Convert the query to groupBy, which handles unknown cardinality","Verify the segment is not being swapped/unmapped mid-query; retry the query","Check ingestion config so the dimension column is dictionary-encoded"],"exampleFix":"// before\ntopN over non-dictionary dimension -> UnsupportedOperationException\n// after\nGroupByQuery.newBuilder().setDimension(new DefaultDimensionSpec(\"country\", \"country\"))...","handlingStrategy":"validation","validationCode":"if (params.getCardinality() < 0) { /* use groupBy fallback before scanAndAggregateDefault */ }","typeGuard":"boolean supportsPooledTopN(PooledTopNParams params) {\n  return params.getCardinality() >= 0;\n}","tryCatchPattern":"try {\n  result = algorithm.run(params);\n} catch (UnsupportedOperationException e) {\n  if (e.getMessage().contains(\"unknown cardinality\")) { /* rerun as groupBy */ }\n  throw e;\n}","preventionTips":["Ensure the topN dimension column is dictionary-encoded at ingestion","Fallback to groupBy when selector cardinality is unknown","Avoid topN over datasources whose segments can be swapped mid-query"],"tags":["query","topn","dictionary-encoding","druid"],"backgroundTag":"unsupported-operation","analyzedSha":"9b90983fd291f26935af934383ce360473179e4d","analyzedAt":"2026-09-07T13:32:30.957Z","contentChangedAt":"2026-09-07T13:32:30.957Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}