QuestDB 9.3.3: HORIZON JOIN, twap(), and JIT on ARM64
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:
SELECTh.offset / 1000000000 AS horizon_sec,t.symbol,avg((m.best_bid + m.best_ask) / 2) AS avg_midFROM fx_trades AS tHORIZON JOIN market_data AS m ON (symbol)RANGE FROM 1s TO 60s STEP 1s AS hORDER BY t.symbol, horizon_sec;
RANGE ... STEP generates uniform offsets. For non-uniform horizons - or to
look before the event - use LIST:
SELECTh.offset / 1000000000 AS horizon_sec,t.symbol,avg((m.best_bid + m.best_ask) / 2 - t.price) AS avg_markoutFROM fx_trades AS tHORIZON JOIN market_data AS m ON (symbol)LIST (-5s, -1s, 0, 1s, 5s, 30s, 1m) AS hORDER 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.
SELECT symbol, twap(price, timestamp) AS twap_priceFROM tradesWHERE 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.
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:
SELECT symbol, side, price,sum(price) OVER ws, avg(price) OVER ws,sum(amount) OVER ws, sum(price) OVER wtFROM tradesWINDOW 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:
SELECT avg(price) OVER w2 FROM tradesWINDOW 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 BYand 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/EXCEPTpushdown - timestamp filters now push into each branch, enabling per-branch partition pruningORDER BYmaterialization - sort keys on computed expressions pre-compute into off-heap buffers, reducing function evaluations from O(N log N) to O(N)CASE WHENon 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
_FILEsuffix 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 ALLcolumn 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/questdbdocker 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.