Skip to main content

Overview

The StarTree Engine is a beta capability. Its opt-in mechanism may change between releases.
The StarTree Engine is an alternative, high-performance execution runtime for the Multi-Stage Query Engine (MSQE). It keeps everything you already know about MSQE — the SQL dialect, the query planner, and Pinot’s segment/index-powered table scans — but executes most of the query’s operators (joins, aggregations, sorts, window functions, set operations) in native code instead of Java. Under the hood it is built on Apache DataFusion, a vectorized query engine written in Rust, extended with a native Arrow-based data exchange layer for moving data between query stages.

What changes — and what doesn’t

A multi-stage query runs in three kinds of stages:
StageJava MSQEStarTree Engine
Leaf stages (segment scans, filter/index pushdown)Pinot Java executionPinot Java execution (unchanged)
Intermediate stages (joins, aggregations, sorts, windows)Java operatorsNative, vectorized operators
Root stage (final merge on the broker)Java operatorsNative operators
Data exchange between stagesJava mailboxNative Arrow-based exchange
Query planning is unchanged: the broker still parses, plans, and optimizes the query the same way, so EXPLAIN plans, query options, and the general query lifecycle look the same. Because leaf stages stay in Java, all of Pinot’s indexing (inverted, sorted, star-tree, JSON, etc.) keeps working exactly as before.

Enabling the StarTree Engine

Enablement has two layers: a cluster-level prerequisite and a per-query switch. The StarTree Engine is off by default — queries run on the Java MSQE runtime unless explicitly routed.

Cluster prerequisite

Every broker and server must start the native runtime. This is controlled by one configuration key, set in the broker and server configuration:
In StarTree Cloud these settings are managed as part of the deployment and are enabled by default. Contact StarTree support if the StarTree Engine is not available in your environment.
Optional tuning (defaults shown):
KeyDefaultPurpose
pinot.startree.rust.metrics.port8079Prometheus /metrics endpoint for the native runtime (<= 0 disables)
pinot.startree.datafusion.batch.size8192Rows per vectorized batch
Memory and spill-to-disk behavior has its own set of settings — see Spill to disk for large queries.

Per-query switch

Route an individual multi-stage query to the StarTree Engine with the workerRuntime query option. Using the SET command:
Or through the query options field of the REST APIs:
The StarTree Engine only applies to multi-stage queries. Single-stage queries are unaffected by the workerRuntime option.
If a query sets workerRuntime = 'startree' but the cluster prerequisite is missing, the query fails with a “runtime is not registered” error — the cluster configuration must be in place first.

When to expect benefits

The StarTree Engine accelerates the parts of the query that run above the table scan. It shines when intermediate stages do significant work:
  • Join-heavy queries, especially large hash joins between fact tables.
  • Large aggregations and group-bys over data shuffled between servers.
  • Sorts and window functions over sizeable intermediate results.
  • Queries that move a lot of data between stages — the native exchange layer transfers Arrow batches with far less serialization and garbage-collection overhead than the Java mailbox.
  • Memory-intensive queries: the native runtime uses a bounded off-heap memory pool with spill-to-disk for sorts and aggregations, so large queries degrade gracefully instead of pressuring the JVM heap.
In internal TPC-H benchmarks, most join- and aggregation-heavy queries ran 20–60% faster than Java MSQE, and performance continues to improve release over release.

When it doesn’t help

  • Leaf-dominated queries. If most of the time is spent scanning segments (highly selective filters, simple aggregations pushed down to the leaf), the leaf stage is identical in both runtimes and the StarTree Engine has little to accelerate.
  • Very fast, small queries. Crossing between the JVM and the native runtime has a small fixed cost per query and per batch. Queries that complete in a few tens of milliseconds on Java MSQE may see no gain, or a slight regression.
  • A few specific query shapes currently regress and are being actively closed (for example, some ASOF joins and certain UNION ALL patterns).
Switching runtimes is a one-line query option, so A/B comparison on your own workload is cheap. If a query matters to you, benchmark it on both runtimes.

Join strategies

Beyond the default hash join, two join strategies are available natively through the standard Calcite join hint:
StrategyHint valueNotes
Sort-merge joinsort_mergeINNER/LEFT joins with at least one equality key (including BIG_DECIMAL keys). Can spill to disk, so prefer it for very large joins. Falls back to hash join (same results) for unsupported shapes.
Native lookup joinlookupDimension-table joins (INNER/LEFT/SEMI/ANTI; non-equality conditions are allowed for INNER/LEFT only) run natively; the dimension-table lookup itself still happens in Java. Unsupported shapes are query errors, not silent fallbacks.

Spill to disk for large queries

Aggregations (GROUP BY/DISTINCT), sorts, and sort-merge joins can spill to local disk under memory pressure instead of failing with an out-of-memory error — the query gets slower, not killed. Spill is backed by a shared native memory pool on each broker and server, sized from the available off-heap memory (container memory minus the JVM max heap).
ConfigDefaultDescription
pinot.startree.datafusion.spill.enabledtrueMaster on/off switch. When off, the pool is unbounded.
pinot.startree.datafusion.spill.memory.size50%Percent of off-heap headroom, or an absolute size (4G, 512M).
pinot.startree.datafusion.spill.compressionuncompressedSpill file compression: uncompressed, lz4_frame, or zstd.
pinot.startree.datafusion.spill.max.disk.size100GMax spill data size before the query fails, or unlimited.
pinot.startree.datafusion.spill.dirOS temp dirAbsolute path override for spill files.
pinot.startree.datafusion.udaf.size.accounting.enabledfalseCharge Java-heap-backed UDAF state (e.g. DISTINCTCOUNT, percentiles, sketches) toward the spill trigger. Off by default since the estimate is heap-based, not off-heap.
pinot.startree.datafusion.udaf.groupby.max.capacity1,000,000Max simultaneously-live groups when UDAF size accounting is enabled.
Malformed or out-of-range values for these settings fail broker/server startup rather than silently falling back to a default.
What doesn’t spill: hash joins, nested-loop joins, cross joins, and window aggregates fail with a resources-exhausted error under memory pressure instead of spilling. If a large join hits this, use the sort_merge join hint above instead of the default hash join, or raise the memory pool size.

Unsupported cases

Not every query pattern runs natively yet. There are two very different behaviors, depending on when the engine detects the unsupported pattern.

Transparent fallback to Java MSQE (query still succeeds)

At planning time the broker checks the query plan for patterns the StarTree Engine does not support. If it finds one, it silently ignores the workerRuntime option and runs the whole query on the Java MSQE runtime. The query succeeds and returns the same results as if you had never set the option; the only trace is an INFO log line on the broker (Disabling DataFusion for this query: <reason> ...). Patterns that currently fall back at planning time:
  • INTERSECT ALL and EXCEPT ALL / MINUS ALL set operations.
  • Aggregations that return arrays or collections (e.g. ARRAY_AGG).
  • Certain optimizer-hint-driven aggregation paths that have no native equivalent, such as is_leaf_return_final_result combined with SUMPRECISION, funnel functions, or raw sketch aggregations.
  • UNNEST when the column-pruning optimization is enabled for it.
  • Unsupported column types, when they are visible in the logical plan (see below).
If the planner finds any of these patterns, the whole query falls back to the classic Java engine — not just the affected stage.

Query fails with an error (no fallback)

Some patterns can only be detected after planning, while the plan is being prepared for native execution on the servers. At that point there is no fallback: the query fails and the client receives an error. To run such a query, remove the workerRuntime option (or rewrite the query). Cases that fail at runtime:
  • Unsupported column types reaching an intermediate stage when not detectable at planning time: MAP, OBJECT, and exotic array shapes. Supported types are INT, LONG, FLOAT, DOUBLE, BOOLEAN, TIMESTAMP, STRING, JSON, BIG_DECIMAL, BYTES, and their common array variants.
  • Unsupported join sub-cases: LOOKUP joins outside INNER/LEFT/SEMI/ANTI, SEMI/ANTI lookup joins with non-equality conditions, and certain ASOF join match conditions.
  • Functions outside the supported envelope — see the function support section below for the specific lists of native, bridged, and unsupported functions.

Function support

Aggregation functions. The common aggregations run fully natively: COUNT, SUM, MIN, MAX, AVG, BOOL_AND, BOOL_OR, and the population statistics VAR_POP, STDDEV_POP, and COVAR_POP. Every other Pinot aggregation function — percentiles, DISTINCTCOUNT and its sketch variants, SUMPRECISION, funnel functions, and so on — executes through a built-in Java bridge: results are identical to Java MSQE, at some extra cost when aggregation state crosses the native/Java boundary. Known exceptions:
  • VAR_SAMP and STDDEV_SAMP currently fail at runtime with an Invalid arithmetic operation error.
  • Aggregations that return arrays or collections (e.g. ARRAY_AGG) fall back to Java MSQE at planning time, as noted above.
Scalar functions. Comparison, arithmetic, and logical operators, CASE, IN/NOT IN, and array construction run natively. All other registered Pinot scalar functions are automatically bridged to their Java implementation as long as their argument and return types are among INT, LONG, FLOAT, DOUBLE, BOOLEAN, STRING, BYTES, and TIMESTAMP (BIG_DECIMAL and JSON results are carried as strings). A scalar function outside that envelope — for example one that takes or returns arrays or Java objects (theta-sketch set operations being a supported exception) — fails at runtime with an Unsupported function error. Window functions. The standard window functions run natively: ROW_NUMBER, RANK, DENSE_RANK, PERCENT_RANK, CUME_DIST, NTILE, LAG, LEAD, FIRST_VALUE, LAST_VALUE, and NTH_VALUE, plus the native aggregations listed above used with an OVER clause. There is no Java bridge for window functions: a Pinot-specific window function outside this set fails at runtime.
BIG_DECIMAL precision. BIG_DECIMAL columns are fully supported in comparisons, sorting, joins, GROUP BY, DISTINCT, and MIN/MAX/SUM/AVG/COUNT, using an order-preserving binary encoding. However, arithmetic (+ - * / %) and aggregation over BIG_DECIMAL are computed in double-precision floating point — matching Java MSQE behavior, not arbitrary-precision decimal arithmetic. If your workload depends on exact decimal arithmetic beyond double precision, verify results before relying on it. Custom Java UDFs/UDAFs over BIG_DECIMAL are not yet supported on the StarTree Engine.

Behavioral differences to be aware of

A small number of cases run natively but intentionally behave differently from Java MSQE:
  • Integer division follows standard SQL. 1 / int_col truncates to an integer, as in PostgreSQL, whereas Java MSQE historically evaluated it as floating-point division. If a query relies on the legacy behavior, add an explicit CAST(... AS DOUBLE).
  • Some Java-MSQE-specific optimizer hints are not honored (e.g. group-trim hints), so queries using them may return results computed by a different — standard — strategy.
  • Detailed stats require the streaming stats channel. Per-stage and per-operator response stats for natively executed stages — including leaf counters such as numSegmentsQueried — are only delivered when MSQE’s streaming stats channel is active. Enable it per query with SET streamStats = true; or cluster-wide with the broker configuration pinot.broker.mse.stream.stats=true (all servers must run a version that supports it). Without it, StarTree Engine responses carry only broker-side stats. In addition, a few plan shapes (e.g. semi-join and dynamic-broadcast sub-plans) do not yet surface their leaf-stage stats. All of this affects observability, not results.

Monitoring

  • The broker logs when a query falls back to Java MSQE and why.
  • The native runtime exposes its own Prometheus metrics endpoint (pinot.startree.rust.metrics.port, default 8079). All metrics are prefixed rust_:
    MetricTypeWhat it tells you
    rust_spill_pool_reserved_bytes / rust_spill_pool_size_bytesgaugeNative memory pool usage vs. configured size (labelled by runtime)
    rust_operator_spills_total / rust_operator_spilled_bytes_totalcounterSpill events and bytes written by sorts/aggregations under memory pressure
    rust_operator_hash_build_seconds_totalcounterCumulative time building hash-join tables
    rust_mailbox_compress_* / rust_mailbox_decompress_*counterInter-stage exchange traffic: batches per codec, bytes before/after compression, and time spent
    rust_mailbox_fanout_payload_reuse_*counterBroadcast exchange payload reuse
    rust_mailbox_grpc_recv_tasks_active / _totalgauge / counterActive and total cross-server exchange receive tasks
    rust_tokio_workersgaugeNetwork runtime worker threads
    In StarTree Cloud these metrics are also stored in Prometheus and can be read using Grafana as usual.
  • In addition, brokers and servers export two engine-related meters through the standard Pinot metrics pipeline: datafusionOpchainAllocatorLeakCloseCount and datafusionOpchainAllocatorLeakedBytes. These are safety-net counters that track native memory reclaimed when a query’s allocator is closed while still holding buffers; they should normally stay at zero.
  • Standard Pinot query response metadata is returned for all queries, with the stats-coverage caveat noted above.