QuestDB Time-Series SQL

QuestDB Time-Series SQL: Mastering SAMPLE BY, LATEST ON, and ASOF JOIN (Hands-On Guide)

Meta description (156 chars):
Hands-on QuestDB time-series SQL: use SAMPLE BY for rollups, LATEST ON for last-known values, and ASOF JOIN for nearest time merges - real patterns that ship.


Why this matters (a quick story)

You've got billions of events flowing in every day - prices, sensor readings, app metrics. Your team needs rollups, last-known values, and near-time joins to power dashboards and anomaly detectors. QuestDB's time-series SQL gives you all three with SAMPLE BY, LATEST ON, and ASOF JOIN - no custom pipeline glue or clunky windowing jobs required.

This guide shows how to use each feature correctly and fast, with production caveats mid-level data engineers actually run into.


The setup (minimal schema)

-- Partitioned table with a designated timestamp
CREATE TABLE trades (
  ts        TIMESTAMP,
  symbol    SYMBOL,         -- use SYMBOL for high-cardinality tags
  price     DOUBLE,
  size      INT
) TIMESTAMP(ts)
  PARTITION BY DAY;

TIMESTAMP(ts) elects the designated timestamp - the column QuestDB uses to time-order data and enable time-series features like SAMPLE BY, LATEST ON, and ASOF JOIN. (QuestDB)


SAMPLE BY: fast, calendar-aligned rollups

What it does

SAMPLE BY bins rows into fixed time buckets (seconds → years) and computes aggregates - ideal for dashboards and SLOs. Requires a designated timestamp. (QuestDB)

Common pattern

-- Per-minute rollups for today, with sensible gaps handling
SELECT
  ts,                     -- the bucket timestamp
  count()        AS trades,
  avg(price)     AS avg_px
FROM trades
WHERE ts IN today()
SAMPLE BY 1m
FILL(0, PREV);            -- fill count() with 0, avg() with previous value

Tips that save you time:

  • Fill gaps using FILL(NULL|PREV|LINEAR|<constant>). You can provide one fill per aggregate in SELECT order. (QuestDB)
  • Align to calendar by default; you can still control time zone and offsets when needed. (QuestDB)
  • Need rows outside your data's bounds (pre/post-fill)? Use FROM '...' TO '...' with SAMPLE BY to explicitly shape the output range. (QuestDB)

Small mental model:

raw data  ──●● ●  ●     ●───●   (irregular)
SAMPLE BY 1m → |●|●|●|●|●|●|●|    (regular bins + fill)

LATEST ON: last-known values per key

What it does

LATEST ON <timestamp> PARTITION BY <key(s)> returns the most recent row for each series (e.g., per symbol). This needs a designated timestamp (or a subquery that exposes one). (QuestDB)

Common patterns

Latest price per symbol:

SELECT symbol, price, ts
FROM trades
LATEST ON ts PARTITION BY symbol;

Latest in a time slice (note how parentheses change execution order):

-- WHERE first, then latest
SELECT * FROM trades
WHERE ts IN '2025-11-20'
LATEST ON ts PARTITION BY symbol;

-- LATEST first, then filter
(SELECT * FROM trades LATEST ON ts PARTITION BY symbol)
WHERE price > 100;

Execution order matters for result semantics and performance - use parentheses to make it explicit. (QuestDB)


ASOF JOIN: nearest-time merges

What it does

ASOF JOIN pairs each left-table row with the latest right-table row at the same or earlier timestamp (optionally within a tolerance). This is how you align trades to quotes, metrics to configs, or events to feature flags. (QuestDB)

Trades to quotes with a 2-second window:

SELECT
  t.ts, t.symbol, t.price,
  q.bid, q.ask
FROM trades t
ASOF JOIN quotes q ON (symbol)
TOLERANCE 2s;

Important details:

  • By default, QuestDB uses each table's designated timestamp for the join. (QuestDB)
  • If you must join on a different time column, sort a subquery (ORDER BY other_ts ASC) so that column becomes the subquery's effective timestamp for the join. The engine will use it automatically. (QuestDB)
  • Use TOLERANCE to bound the look-back window - it improves correctness and performance. (QuestDB)

Visual intuition (per key)

left (trades):   -----t1-----------t2----------t3----
right (quotes):  ---qA------qB-------------qC--------
ASOF result:     t1→qA     t2→qB           t3→qC

When to use which? (cheat sheet)

ClauseSolves forTypical SELECT patternGotchas / notes
SAMPLE BYRollups & resampling... SAMPLE BY 1m FILL(0, PREV)Needs designated timestamp; fill rules differ for keyed vs non-keyed queries; default aligns to calendar. (QuestDB)
LATEST ONLast-known value per series/key... LATEST ON ts PARTITION BY symbolExecution order with WHERE changes semantics; multi-column partitions may scan more. (QuestDB)
ASOF JOINNearest-time merge across tables... ASOF JOIN b ON (key) TOLERANCE 2sUses designated timestamp by default; for other ts, ORDER BY in a subquery; add TOLERANCE. (QuestDB)

Real example end-to-end

Goal: build a per-minute trade volume dashboard, overlay last price per symbol, and enrich with near-time quote spreads.

-- 1) Per-minute rollups (count + avg price)
WITH minute_rollups AS (
  SELECT ts, count() AS trades, avg(price) AS avg_px
  FROM trades
  WHERE ts IN today()
  SAMPLE BY 1m
  FILL(0, PREV)
),

-- 2) Latest price per symbol (time slice)
latest_px AS (
  (SELECT symbol, price, ts
   FROM trades
   WHERE ts IN today()
   LATEST ON ts PARTITION BY symbol)
),

-- 3) Enrich rollups with quotes near each minute mark
rollups_with_spread AS (
  SELECT
    m.ts,
    m.trades,
    m.avg_px,
    q.bid,
    q.ask,
    (q.ask - q.bid) AS spread
  FROM minute_rollups m
  ASOF JOIN quotes q TOLERANCE 2s ON ()   -- join by time only; or ON (symbol)
)

SELECT *
FROM rollups_with_spread
ORDER BY ts;

This pattern avoids custom stream jobs for last-value lookups and near-time alignment - pure SQL.


Best practices and common pitfalls

Schema & partitions

  • Always set a designated timestamp; it unlocks time-series features and optimal scans. (QuestDB)
  • Choose partitions that match retention and query windows (e.g., PARTITION BY DAY). You can add TTLs to drop old partitions automatically. (QuestDB)

SAMPLE BY

  • Use FILL(0, PREV, ...) to keep charts continuous; prefer constants for additive metrics (count()), PREV or LINEAR for ratios and averages. (QuestDB)
  • If you need rows before/after data exists (pre/post-fill ranges), use FROM ... TO .... (QuestDB)

LATEST ON

  • For unordered data or tables without a designated timestamp, wrap with a subquery; otherwise the engine can't derive "latest." (QuestDB)
  • Be explicit about execution order with parentheses when mixing WHERE and LATEST ON. (QuestDB)

ASOF JOIN

  • Start with the default: rely on each table's designated timestamp. If you must use another column, ORDER BY <col> ASC in a subquery so QuestDB picks it up as the subquery timestamp. (QuestDB)
  • Add a TOLERANCE window to avoid accidental joins to stale rows and to speed up lookups. (QuestDB)

Key references (official)

  • SAMPLE BY reference & fill behavior. (QuestDB)
  • LATEST ON reference (syntax, execution order). (QuestDB)
  • ASOF JOIN reference (semantics, tolerance). (QuestDB)
  • Designated timestamp concept. (QuestDB)
  • CREATE TABLE (partitions, TTL, WAL default). (QuestDB)

Internal link ideas (official pages to link from this article)

  • QuestDB SAMPLE BY SQL reference - full syntax, FILL, alignment, FROM...TO. (QuestDB)
  • LATEST ON reference - single vs multi-key, execution order examples. (QuestDB)
  • ASOF JOIN reference - default timestamp usage, TOLERANCE. (QuestDB)
  • Designated timestamp concept - why it matters, properties. (QuestDB)
  • JOIN overview - supported join types (ASOF, LT, SPLICE). (QuestDB)
  • CREATE TABLE - partitions, TTL, dedup, WAL defaults. (QuestDB)
  • Time-series optimizations - engine behaviors worth knowing. (QuestDB)
  • Materialized views - precompute rollups when queries get heavy. (QuestDB)

Summary & call-to-action

  • Use SAMPLE BY for rollups and resampling (remember FILL).
  • Use LATEST ON for last-known per-key values - watch execution order.
  • Use ASOF JOIN to align data across tables in time - add TOLERANCE.

Spin up a small dataset, run the snippets above, and then check the linked docs to deepen your understanding. Next step: add TTL and materialized views to keep query latency flat as data grows.


Image prompt (for your designer/AI tool)

"A clean, modern data architecture diagram showing QuestDB tables with a designated timestamp, a SAMPLE BY 1m rollup, a LATEST ON selection per symbol, and an ASOF JOIN into a quotes table - minimalistic, high contrast, 3D isometric style."

Tags

#NoSQL #QuestDB #TimeSeriesSQL #DataEngineering #DatabaseDesign #Scalability #SQL #RealTimeAnalytics #Architecture

Leave a Comment