Interested in QuestDB use cases?

Learn more

QuestDB 9.3.3: HORIZON JOIN, twap(), and JIT on ARM64

Tags:
RedditHackerNewsX
QuestDB is the open-source time-series database for demanding workloads—from trading floors to mission control. It delivers ultra-low latency, high ingestion throughput, and a multi-tier storage engine. Native support for Parquet and SQL keeps your data portable, AI-ready—no vendor lock-in.

QuestDB 9.3.3 is here - and HORIZON JOIN is the headline. It is a new join type built for markout analysis: measure how prices evolve at specific time offsets after an event, in a single query. If you have been stitching this together with self-joins, window functions, and application logic, those days are over.

Alongside HORIZON JOIN, we are shipping twap() for time-weighted averages, SQL-standard WINDOW definitions, JIT-compiled filters on ARM64, and major performance gains across Parquet I/O, parallel GROUP BY, and UNION queries.


HORIZON JOIN for markout analysis

HORIZON JOIN lets you measure how a metric evolves at fixed time offsets relative to events - the core of markout analysis in trading. For each row on the left side, it computes left_timestamp + offset, performs an ASOF match against the right table, and aggregates the results.

Here is an example: measure the average mid-price at 1-second intervals up to 60 seconds after each trade:

Post-trade markout curve with RANGEDemo this query
SELECT
h.offset / 1000000000 AS horizon_sec,
t.symbol,
avg((m.best_bid + m.best_ask) / 2) AS avg_mid
FROM fx_trades AS t
HORIZON JOIN market_data AS m ON (symbol)
RANGE FROM 1s TO 60s STEP 1s AS h
ORDER BY t.symbol, horizon_sec;

RANGE ... STEP generates uniform offsets. For non-uniform horizons - or to look before the event - use LIST:

Pre- and post-trade markout with LISTDemo this query
SELECT
h.offset / 1000000000 AS horizon_sec,
t.symbol,
avg((m.best_bid + m.best_ask) / 2 - t.price) AS avg_markout
FROM fx_trades AS t
HORIZON JOIN market_data AS m ON (symbol)
LIST (-5s, -1s, 0, 1s, 5s, 30s, 1m) AS h
ORDER BY t.symbol, horizon_sec;

The horizon pseudo-table exposes h.offset (raw microsecond value) and h.timestamp (the computed timestamp), both usable in expressions and grouping.

No self-joins. No UNION ALL over multiple ASOF queries. One statement, any horizon shape.

Read the HORIZON JOIN documentation


Post-trade analysis cookbook

We have added a whole new cookbook section to make HORIZON JOIN and QuestDB's time-series joins more approachable for real-world trading workflows. It walks through five post-trade analysis patterns end to end:

  • Slippage analysis - compare execution prices to mid or top-of-book at fill time. Run on demo
  • Markout analysis - track post-fill price movement across multiple time horizons, down to millisecond granularity. Read the recipe
  • Implementation shortfall - decompose total costs into spread, permanent impact, and temporary impact using the Perold framework. Run on demo
  • Venue scoring - evaluate fill quality across trading venues to inform routing decisions. Read the recipe
  • Flow toxicity - detect informed trading with volume-synchronized metrics like VPIN. Run on demo

Each recipe uses real SQL patterns - ASOF JOIN, HORIZON JOIN, PIVOT, window functions - against demo tables you can load and query immediately.

Read the post-trade analysis cookbook


twap() - Time-Weighted Average Price

The new twap(price, timestamp) aggregate computes time-weighted averages using step-function integration: each price holds until the next observation, and the result is the area under the step function divided by the total time span.

Hourly TWAP for today's tradesDemo this query
SELECT symbol, twap(price, timestamp) AS twap_price
FROM trades
WHERE timestamp IN '$today'
SAMPLE BY 1h;

Unlike VWAP, which weights by volume, TWAP weights by duration. It answers the question "what was the average price over this period?" rather than "what price did most volume trade at?" Both matter. TWAP is commonly used as an execution benchmark for algorithmic orders.

twap() supports parallel GROUP BY and SAMPLE BY with all FILL modes. No client-side integration needed - it runs entirely inside the query engine.

Read the twap() documentation


WINDOW definition clause

QuestDB now supports the SQL-standard WINDOW clause. Define a window specification once, reference it by name across multiple window functions - no more repeating the same PARTITION BY and ORDER BY:

Named windows for cleaner multi-function queriesDemo this query
SELECT symbol, side, price,
sum(price) OVER ws, avg(price) OVER ws,
sum(amount) OVER ws, sum(price) OVER wt
FROM trades
WINDOW wt AS (ORDER BY timestamp),
ws AS (PARTITION BY symbol, side ORDER BY timestamp);

Window inheritance works too - define a base window and extend it with a frame clause:

Window inheritance with frame clausesDemo this query
SELECT avg(price) OVER w2 FROM trades
WINDOW w1 AS (PARTITION BY symbol ORDER BY timestamp),
w2 AS (w1 ROWS BETWEEN 1 PRECEDING AND CURRENT ROW);

Cleaner queries, less copy-paste, same execution performance.

Read the WINDOW clause documentation


JIT compilation on ARM64

QuestDB's JIT filter compiler now runs natively on ARM64 - Apple Silicon, AWS Graviton, and other aarch64 systems. Previously JIT was x86-only.

Benchmarks on Apple M-series show WHERE clause filters running up to 2x faster with JIT enabled, with OR-predicate filters seeing up to 5x improvement. If you are running QuestDB on Graviton instances or Apple hardware, filters just got significantly faster with zero configuration changes.


Performance highlights

Alongside new SQL features, QuestDB 9.3.3 delivers significant performance improvements:

  • Parquet late materialization - filter columns decode first, remaining columns decode only for matching rows. Up to 2.4x faster on aggregation queries with symbol filters
  • Faster parallel GROUP BY and Top K - unordered page frame collection eliminates head-of-line blocking. At concurrency 8, tail latency (p99) drops 30-44% and latency predictability improves from 2.5-3.6x down to 1.4-1.8x
  • UNION / INTERSECT / EXCEPT pushdown - timestamp filters now push into each branch, enabling per-branch partition pruning
  • ORDER BY materialization - sort keys on computed expressions pre-compute into off-heap buffers, reducing function evaluations from O(N log N) to O(N)
  • CASE WHEN on symbols - string constants resolve to integer keys at init time, comparisons run by integer at runtime

Also in this release

  • array_build() function for constructing arrays with controlled shape and fill
  • File-based secrets for Kubernetes: load passwords from files using the _FILE suffix convention, compatible with K8s Secrets, Docker Secrets, and Vault
  • Security fix: a crafted HTTP request with overflowing chunk size in chunked transfer encoding no longer crashes the server
  • Bugfixes for window joins, FILL(LINEAR), UNION ALL column mismatches, and Parquet export concurrency

That is QuestDB 9.3.3. HORIZON JOIN changes how you do post-trade analysis, twap() gives you a native time-weighted average, and the JIT compiler now covers ARM64. The engine keeps getting faster underneath.

Read the full 9.3.3 release notes

Ready to try it?

docker pull questdb/questdb
docker run -p 9000:9000 -p 8812:8812 questdb/questdb

Try a HORIZON JOIN query first. We think you will see the appeal.

Join our Slack or Discourse communities to share feedback and results.

Subscribe to our newsletters for the latest. Secure and never shared or sold.