11  Time-Series Query Optimization

Predicate Design, Query Plans, Latest Values, Window Scans, Rollups, Counters, Gaps, and Dashboard Evidence

data-storage
time
series
queries

11.1 Start With the Question on the Screen

Query tuning starts with the sentence a user is trying to answer. “Show the latest room temperature for this floor” is a different storage problem from “replay the last outage at raw resolution.” Once the question names the time range, entity, metric, and quality rule, the query plan can be reviewed instead of guessed.

11.2 In 60 Seconds

Time-series query optimization is not a bag of product tricks. It is a review habit: name the question, bound the time range, filter by stable dimensions, choose the right data tier, prove the query plan, and preserve enough quality evidence for the result to be trusted.

The most common IoT query families are latest value, bounded window scan, dashboard rollup, rate or delta, gap and quality check, and incident replay. Each family has a different access path. A dashboard trend should not repeatedly scan raw history when a tested rollup exists. An incident replay should not read a coarse aggregate that hides spikes. A counter rate should not be treated like a gauge. The right query is the one that answers the product question with visible evidence.

Learning Objectives

After this chapter, you should be able to:

  • Classify IoT read requests into latest-value, window, rollup, rate, gap, and incident-replay query families.
  • Write bounded SQL query patterns that expose time, entity, metric, quality, and dimension predicates.
  • Use query-plan evidence to distinguish pruning, index use, filtering, sorting, and raw table scans.
  • Decide when a dashboard should read raw data, a continuous aggregate, a materialized view, a recording rule, or a cache.
  • Preserve sample counts, min/max values, freshness timestamps, and gap flags so rollups remain reviewable.
  • Build a query-review record that can be checked during release and incident investigations.

11.3 Query Optimization Starts With the Question

A time-series database can only optimize the query you actually send. Before touching indexes or dashboards, write the request in plain language:

For each device in building-1 / floor-3-east,
show the latest valid room_temperature value
observed in the last 2 hours,
and mark any stale device separately.

That sentence already contains the shape of the access path:

QuestionCurrent state, trend, alert, rate, gap, or replay?
Time rangeWhat start and end timestamps bound the read?
PredicatesWhich site, device, metric, tenant, quality, or asset filters are real?
GranularityRaw samples, bucketed summaries, or precomputed rollups?
EvidencePlan, rows, chunks, freshness, gaps, and result quality.
Never Optimize an Unbounded Question

“Show all temperature data” is not a dashboard query. It is an export or investigation request that needs paging, archive controls, and a clear time range. Every normal dashboard, alert, and report query should have a bounded time window or a documented reason for using a precomputed latest-state table.

11.4 Query Families

Latest

Current state

Find the newest valid reading per device, room, gateway, or metric and mark stale entities.

Window

Bounded scan

Read raw samples for one entity, site, or metric over a specific investigation interval.

Rollup

Dashboard trend

Read bucketed summaries with count, min, max, mean, and gap flags for repeat dashboards.

Rate

Counter change

Calculate increase, rate, or delta for counters without treating reset-prone counters as gauges.

Quality

Gaps and stale data

Find missing intervals, invalid samples, late arrivals, and devices whose current state is old.

Replay

Incident investigation

Recover the raw or highest-resolution interval around an event, then compare it with rollups.

Access path diagram showing a query question flowing through time range, dimension predicates, data tier choice, plan evidence, and result quality.
A time-series query should expose its question, predicates, data tier, plan evidence, and result-quality checks.

11.5 Predicates That Make Queries Reviewable

The strongest query predicates come from the data contract. They usually include:

  • observed_at: the event time used for analysis and dashboard windows.
  • received_at: the platform time used to inspect ingest lag and late arrivals.
  • tenant_id, site_id, zone_id, or asset_id: stable dimensions that users actually filter by.
  • device_id: the entity whose state, trend, or replay is being inspected.
  • metric_name: the measured signal, such as temperature, vibration, battery voltage, or queue depth.
  • quality_status: whether a reading is valid, late, estimated, rejected, or part of an incident window.
-- Reviewable bounded query pattern.
SELECT
  observed_at,
  device_id,
  metric_name,
  value,
  unit,
  quality_status
FROM readings
WHERE observed_at >= :start_time
  AND observed_at <  :end_time
  AND site_id = :site_id
  AND metric_name = :metric_name
  AND quality_status = 'valid'
ORDER BY observed_at ASC, device_id ASC
LIMIT 5000;
Predicate
Review question
Common failure
Time range
Does the query include both start and end bounds on the same time axis used for partitioning?
A query filters by display time, ingestion time, or no time at all, so the store scans too much history.
Entity or dimension
Does the filter match the dimensions users need and the schema made searchable?
Every payload field becomes an index, tag, or label even when it is not part of the access path.
Metric and unit
Does the query isolate the measured signal and unit policy before aggregating?
Different units or metric meanings are averaged together because the query only filters by device.
Quality state
Does the result include or intentionally exclude late, estimated, rejected, or duplicate readings?
Invalid samples are silently mixed into dashboards, hiding ingestion and device problems.

11.6 Plan Evidence

For SQL-backed time-series systems, plan evidence is usually the fastest way to catch a false optimization. PostgreSQL EXPLAIN exposes the plan tree, scan nodes, estimates, filters, sort operations, and index use. In a time-series store built on PostgreSQL, the same habit helps reviewers confirm that time and dimension predicates are being used as intended.

EXPLAIN (ANALYZE, BUFFERS)
SELECT
  time_bucket('15 minutes', observed_at) AS bucket_start,
  device_id,
  avg(value) AS avg_value,
  min(value) AS min_value,
  max(value) AS max_value,
  count(*) AS sample_count
FROM readings
WHERE observed_at >= TIMESTAMPTZ '2026-05-23 00:00:00+00'
  AND observed_at <  TIMESTAMPTZ '2026-05-24 00:00:00+00'
  AND site_id = 'building-1'
  AND metric_name = 'room_temperature'
  AND quality_status = 'valid'
GROUP BY bucket_start, device_id
ORDER BY bucket_start, device_id;

Reviewers should record the plan evidence in a compact table:

Evidence
Good sign
Warning sign
Next action
Time pruning
Only relevant chunks, partitions, or blocks are read.
The plan scans old history even though the query has a recent window.
Check the partitioning time column, predicate expression, and timestamp type.
Index use
Entity, site, metric, or quality filters reduce the candidate rows.
Filters appear after a broad scan because indexed columns are missing or wrapped in functions.
Adjust indexes, rewrite predicates, or revise the schema contract.
Sort and limit
The requested ordering is supported by the access path or a small result set.
A dashboard query sorts a large raw history before returning a small page.
Use latest-state tables, seek pagination, or an index ordered by entity and time.
Rows returned
Actual rows and buckets match the dashboard or API budget.
The query returns more points than the chart, network, or user can use.
Increase bucket size, page the result, or move to a rollup tier.
Capture Plans With Representative Data

A query that looks fine on a tiny development table may fail after realistic devices, dimensions, late arrivals, and retention tiers are present. Capture plans on a dataset that resembles the production access path, preferably while normal ingest is running.

11.7 Latest-Value Queries

Latest-value queries power current-state dashboards, status lists, maps, and device detail panels. They are often deceptively expensive because the UI asks for “one row per device” while the database may need to search a large recent window.

-- Latest valid temperature per device in one zone.
SELECT DISTINCT ON (device_id)
  device_id,
  observed_at,
  value,
  unit,
  quality_status
FROM readings
WHERE observed_at >= now() - interval '2 hours'
  AND site_id = 'building-1'
  AND zone_id = 'floor-3-east'
  AND metric_name = 'room_temperature'
  AND quality_status = 'valid'
ORDER BY device_id, observed_at DESC;
Freshness

Define stale

A current-state query should say how old a value may be before it is displayed as stale or missing.

Order

Entity then time

Latest-per-entity queries often benefit from an access path aligned to entity filters and descending time.

Cache

Latest-state table

When the same latest view is hit constantly, maintain a tested latest-state table or cache with freshness metadata.

Risk

Silent old values

Do not show the last known value as current unless the UI also exposes its timestamp and stale status.

11.8 Bounded Window Queries

Window queries are used for device detail charts, diagnostics, calibration checks, and incident triage. They should have a start time, end time, entity or dimension filters, and a result-size budget.

-- Raw samples for one device around an investigation window.
SELECT
  observed_at,
  received_at,
  value,
  unit,
  quality_status
FROM readings
WHERE observed_at >= TIMESTAMPTZ '2026-05-23 10:00:00+00'
  AND observed_at <  TIMESTAMPTZ '2026-05-23 10:30:00+00'
  AND device_id = 'sensor-a17'
  AND metric_name = 'motor_vibration_rms'
ORDER BY observed_at ASC;

For APIs, prefer seek pagination over growing offsets. The cursor should continue from the last (observed_at, device_id, sequence_id) tuple rather than asking the database to skip an increasing number of rows.

-- Seek-style next page.
SELECT observed_at, device_id, sequence_id, value, quality_status
FROM readings
WHERE observed_at >= :start_time
  AND observed_at <  :end_time
  AND site_id = :site_id
  AND (observed_at, device_id, sequence_id) > (:last_time, :last_device, :last_sequence)
ORDER BY observed_at, device_id, sequence_id
LIMIT 1000;

11.9 Rollups and Dashboard Queries

Repeated dashboards should usually read the coarsest data tier that can answer the question. A daily executive trend and a five-minute operations panel should not share the same raw scan.

Rollup evidence diagram showing raw readings, bucketed summaries, quality fields, refresh metadata, dashboard reads, and investigation fallback to raw data.
Rollup queries are trustworthy when they preserve quality fields and point back to raw data for incident replay.
-- Hourly dashboard trend with quality evidence.
SELECT
  bucket_start,
  zone_id,
  avg_value,
  min_value,
  max_value,
  sample_count,
  gap_count,
  refreshed_at
FROM room_temperature_hourly
WHERE bucket_start >= now() - interval '30 days'
  AND bucket_start <  now()
  AND site_id = 'building-1'
ORDER BY bucket_start ASC, zone_id ASC;

TimescaleDB users often use time_bucket for bucketed aggregation and continuous aggregates for repeated summaries. The durable lesson is broader than one product: a rollup must document bucket size, source query, refresh policy, late-arrival behavior, and quality fields.

Rollup field
Why it belongs
What breaks if omitted
sample_count
Shows whether a bucket has enough readings to trust the aggregate.
A bucket with one sample looks as strong as a complete bucket.
min_value and max_value
Preserves spikes and dips that an average can hide.
Short incidents disappear from historical dashboards.
gap_count
Marks missing intervals, late data, or device silence.
Connected chart lines imply continuity that did not exist.
refreshed_at
Proves how current the summary is.
Users cannot distinguish fresh trends from stale aggregate jobs.

11.10 Rollup Refresh and Invalidation Rules

A rollup is not only a smaller table. It is a promise that a dashboard can read fewer rows without losing the evidence needed to explain the result. TimescaleDB continuous aggregates, InfluxDB downsampling tasks, and Prometheus recording rules all make that trade: compute a cheaper series or bucketed view on a schedule, then route repeated dashboards to that precomputed tier.

The cost shift is easy to review. A fleet with 10,000 devices sampled once per minute creates 432,000,000 raw rows in 30 days for one metric. A device-level one-hour rollup for the same window has 7,200,000 rows, a 60x reduction. If most dashboards show site averages for 50 sites, the site-hour rollup is only 36,000 rows. That is small enough for repeated dashboard refreshes, provided the query is deliberately routed to the rollup tier and the UI labels the granularity honestly.

The hidden contract is refresh scope. A continuous aggregate policy might refresh buckets from three days ago up to five minutes ago, leaving the newest five minutes to raw real-time reads. Late readings from yesterday are corrected during the next refresh. Late readings from last month may require an explicit backfill job. Prometheus recording rules and InfluxDB tasks have the same ownership problem: they must name the source range, destination series or bucket, late-arrival behavior, and rerun policy.

Rule
What it proves
Failure if missing
Source query
Which raw rows, labels, quality states, and bucket size create the rollup.
The summary cannot be audited against the raw telemetry path.
Refresh window
Which recent and historical buckets are recomputed on the schedule.
Late arrivals silently diverge from the dashboard result.
Invalidation trigger
Which buckets are marked dirty when backfilled or corrected data arrives.
The system looks fast because it is reading stale summaries.
Raw fallback
Which incident or audit path can recover the highest-resolution window.
Operators cannot explain a short spike hidden by the aggregate.
Knowledge Check: Rollup Refresh Rules

11.11 Rates, Deltas, and Counters

Counters need different treatment from gauges. A temperature gauge can go up or down naturally. A message counter usually increases until the service restarts or the counter resets. Rate queries must account for that difference.

rate(iot_gateway_messages_total{site="plant-a",gateway="gw-17"}[5m])

Prometheus uses range selectors such as [5m] and functions such as rate() for counter changes. In SQL systems, the same design issue appears when calculating deltas between adjacent counter samples.

WITH ordered AS (
  SELECT
    observed_at,
    gateway_id,
    messages_total,
    lag(messages_total) OVER (
      PARTITION BY gateway_id
      ORDER BY observed_at
    ) AS previous_total,
    lag(observed_at) OVER (
      PARTITION BY gateway_id
      ORDER BY observed_at
    ) AS previous_at
  FROM gateway_counters
  WHERE observed_at >= now() - interval '30 minutes'
    AND site_id = 'plant-a'
)
SELECT
  observed_at,
  gateway_id,
  CASE
    WHEN previous_total IS NULL THEN NULL
    WHEN messages_total < previous_total THEN NULL
    ELSE (messages_total - previous_total)
         / EXTRACT(EPOCH FROM (observed_at - previous_at))
  END AS messages_per_second
FROM ordered
ORDER BY observed_at, gateway_id;
Review Counter Semantics

Record whether a signal is a gauge, counter, event count, cumulative energy value, or derived rate. Optimizing the query is not enough if the math treats a reset-prone counter as a regular measurement.

11.12 Gaps, Staleness, and Quality Checks

An optimized dashboard that hides missing data is not a good dashboard. Gap queries should be part of the release evidence for any telemetry path that supports monitoring, reports, or incident review.

-- Find expected 5-minute buckets with no valid sample for one device.
WITH expected AS (
  SELECT generate_series(
    TIMESTAMPTZ '2026-05-23 10:00:00+00',
    TIMESTAMPTZ '2026-05-23 11:00:00+00',
    INTERVAL '5 minutes'
  ) AS bucket_start
),
actual AS (
  SELECT
    time_bucket('5 minutes', observed_at) AS bucket_start,
    count(*) AS sample_count
  FROM readings
  WHERE observed_at >= TIMESTAMPTZ '2026-05-23 10:00:00+00'
    AND observed_at <  TIMESTAMPTZ '2026-05-23 11:05:00+00'
    AND device_id = 'sensor-a17'
    AND metric_name = 'room_temperature'
    AND quality_status = 'valid'
  GROUP BY bucket_start
)
SELECT
  expected.bucket_start,
  coalesce(actual.sample_count, 0) AS sample_count
FROM expected
LEFT JOIN actual USING (bucket_start)
WHERE coalesce(actual.sample_count, 0) = 0
ORDER BY expected.bucket_start;
Missing

No sample

The device did not report, the gateway dropped data, or the query filtered the sample out.

Late

Arrived after rollup

Late readings may require refreshing affected buckets and marking the aggregate as revised.

Stale

Old latest value

The latest value exists but is older than the freshness target for the UI or alert.

Estimated

Filled value

Interpolation or last-value carry-forward should be visible to users and excluded from some analyses.

11.13 Incident Replay Queries

Incident replay is the exception path that proves the telemetry system can still explain what happened. It should retrieve the highest-resolution data available around an event, include quality metadata, and compare the raw view with any dashboard rollup that users saw.

incident_window:
  event_id: overheat-2026-05-23-plant-a-line-2
  raw_start: 2026-05-23T09:58:00Z
  raw_end: 2026-05-23T10:12:00Z
  entities: motor-17, controller-17, gateway-4
  required_fields: observed_at, received_at, metric_name, value, unit, quality_status
  compare_with: five_minute_dashboard_rollup

The replay query is allowed to be more expensive than a dashboard query because it serves a different purpose. It still needs bounds, ownership, and a result-size expectation.

11.14 Label the Query Review

Label the Diagram

11.15 Code Challenge: Query Review Record

Code Challenge

11.16 Query Symptom Triage

11.17 Common Pitfalls

11.17.1 Optimizing a Demo Query Instead of the Product Query

A query that filters one device for five minutes may be easy. The real dashboard may filter a site, group by zone, compare device classes, read 30 days, and refresh repeatedly. Review the actual query mix.

11.17.2 Using Functions on the Time Column in the Predicate

Some expressions prevent the database from using the time axis efficiently. Prefer raw range predicates such as observed_at >= :start_time AND observed_at < :end_time, then bucket or format timestamps after the range has selected candidate rows.

11.17.3 Treating Rollups as Just Smaller Tables

Rollups are not only performance tools. They are evidence summaries. If they omit sample counts, min/max values, gap flags, freshness, or source-window metadata, they can make dashboards faster while making investigations weaker.

11.17.4 Letting a Cache Hide Freshness

A latest-value cache is useful only when it exposes when the cached value was observed, when it was refreshed, and how stale values are displayed. A cache with no timestamp can make bad data look current.

11.17.5 Mixing Monitoring Metrics and Business Telemetry

Prometheus-style metrics are excellent for infrastructure and service health: queue depth, request duration, scrape health, dropped messages, and ingest lag. Long-lived customer telemetry, regulated history, and replayable raw readings usually need a telemetry store or archive path as well.

11.18 Release Checklist

Question

Query family

Latest value, bounded window, rollup, rate, gap, or incident replay is named.

Predicate

Bounded access path

Time range, entity, metric, dimension, and quality predicates are explicit and tested.

Tier

Right data level

The query uses raw data, a latest-state table, a rollup, a monitoring rule, or a cache intentionally.

Plan

Evidence captured

Plans, scanned chunks or partitions, index use, rows returned, and runtime budget are recorded.

Quality

No hidden gaps

Sample counts, min/max values, freshness timestamps, quality status, and gap flags are visible.

Owner

Review packet

The dashboard owner, data owner, rollback limit, query budget, and next review date are documented.

11.19 Self-Assessment

11.20 Summary

Time-series query optimization is the discipline of making the access path visible. The strongest queries start from the user question, bind the time range, filter by meaningful dimensions, use the right data tier, and preserve result quality.

For IoT systems, query speed and trust are linked. A dashboard that scans too much raw history is slow. A dashboard that hides sample counts, min/max values, stale data, and gaps is misleading. The review habit in this chapter keeps both problems visible before they reach learners, operators, or customers.

11.21 Concept Relationships

11.22 What’s Next

If you need to… Read next
Design retention, rollups, archive, and deletion Data Retention and Downsampling
Practice query plans and release evidence Time-Series Practice and Labs
Revisit storage mechanics behind query paths Time-Series Database Fundamentals
Choose a platform role for the query workload Time-Series Database Platforms
Detect unusual telemetry behavior Anomaly Detection

11.23 Official References

11.24 Key Takeaway

Good time-series queries control time ranges, grouping, aggregation, and downsampling explicitly. Unbounded or poorly grouped queries become slow, expensive, and misleading at IoT scale.