Querying History
From a screen
| Component | Use |
|---|---|
| Historian | The full trend: live or a picked range, statistics, alarms, annotations, export |
| HmiTrendChart | A high-performance-HMI trend beside a controlled value |
| HmiSparkline | A small inline trend, light enough to repeat dozens of times |
Bind the tags and the chart does the rest. Window selection, bucketing and aggregation are handled server-side, and every chart reads through the designated historian, which reaches earlier stores through the epoch log. 1.0.8 Up to 1.0.7 a chart read through the tag's own storage connection first. When that connection had since been replaced and deleted, the chart stayed empty while its data was being recorded.
Retrieval modes Changed in 1.0.8
A chart asks for buckets, never for more points than it can draw. What it gets per bucket depends on the mode:
| Mode | Per bucket | Used by |
|---|---|---|
| M4 | First, minimum, maximum and last value | The Historian chart in Live (always), and in Range when Settings → Min / Max band → Peaks kept (min / max line) is chosen; HMI trends and sparklines, with one bucket per pixel column |
| Average | The mean | Average (single line) |
| Min / Max | The mean, plus a shaded band from minimum to maximum | Min / Max (with band) |
| Raw | Every stored sample, as stored | Export → CSV — every stored sample |
M4 keeps spikes. A 200 ms current spike averaged into a one-minute bucket disappears. Drawn through each bucket's first, minimum, maximum and last values, it stays visible at every zoom level. Each stroke is drawn at the samples' own times, so a smooth signal no longer looks like stairs. If the excursion is the reason you are looking, use M4 or Min / Max, never Average.
Raw means as stored: numbers, exact integers (not rounded through floating point), booleans,
strings and JSON, Bad rows included, each with its quality, status, ts_origin and microsecond
timestamp. It is the only way to read a string or JSON tag's history.
How buckets are chosen
- Sizes snap to a ladder: 1, 2, 5, 10, 15, 30 s · 1, 2, 5, 10, 15, 30 min · 1, 2, 3, 6, 12 h · 1 day, then whole days. The size is rounded up, so a chart never gets more points than it asked for. Every rung divides a day, so the history, the live edge and the rollups share one grid.
- Never finer than the tags are logged. When every tag on the chart has a store interval, the bucket is at least the finest of them. Finer buckets would be mostly empty, and the line would turn to dashes.
- Hours and days follow the time zone. A day bucket starts at local midnight in the project's time zone, or the browser's when the project sets none. The offset is the zone's at the window start, so a window that crosses a daylight-saving change is an hour off after it.
- Up to 32 tags per request, and at most 10,000 buckets.
What fills a bucket
| Situation | What the chart shows |
|---|---|
| The bucket has Good or Uncertain rows | Their value (Bad rows never count) |
| An on-change tag, holding steady | Its last value, carried across empty buckets, at most 8 hours, never across a Bad stretch → Carry |
| The first bucket of the window is empty | The bounding value: the last stored value before the window, held (step) or interpolated toward the next one (linear), but only if it is recent enough to be the current reading. Older is an outage and stays a gap. |
| A run of empty buckets shorter than a few logging intervals | Bridged with a solid line: that is the tag's cadence, not missing data |
| A longer run | A real hole: the line breaks, with a faint dashed bridge |
| Not Good | Shaded, with the reason under the crosshair |
Which table answers
Windows under 12 hours read the raw table. Longer windows read the coarsest rollup tier whose buckets fit exactly inside the chart's: the 1-minute, 1-hour or 1-day tier. A tier that is missing or more than 15 minutes behind is skipped, and the chart reads the next finer source. Rows replayed from a store-and-forward backlog older than a week are folded into the rollups too. 1.0.8 Up to 1.0.7 they reached the raw table but never the rollup, so every chart of 12 hours or more left them out.
Interpolation
How stored values connect follows the tag's Interpolation. Real numbers are linear; integers follow Settings → Historian (Linear unless changed there); booleans and text step. → Interpolation
Statistics
In Range mode, the series table shows per-tag statistics for the window. Pick the columns under Settings → Aggregations (range):
| Column | Notes |
|---|---|
| Average, Minimum, Maximum, Count, Sum | Over the stored rows, Bad rows left out |
| First, Last | By timestamp |
| Std Deviation (sample / population), Variance, Range | Rebuilt exactly from (Σx², Σx, n) across epochs |
| Time-weighted average 1.0.8 | Each value weighted by how long it held, from the value before the window to the end. Step or linear per the tag's interpolation. Bad time is left out, not counted as zero. |
| % Good 1.0.8 | Share of the window the tag was Good. Time with no known value is not Good. |
| Total (integral) 1.0.8 | A rate totalled over time: a flow in m³/h totals to m³. Set the unit with Settings → Total per (per second, minute, hour or day; default hour). Bad time and gaps add nothing. |
| Counter increase 1.0.8 | How much an ever-rising counter went up. Readings are chained across Bad gaps. A fall to half the previous value or less is a reset or wrap, and the new value counts. A smaller fall is a glitch and adds nothing, so the increase is never negative. |
| Time on, Time off 1.0.8 | Time the value was non-zero or zero, Bad time left out |
| Starts 1.0.8 | How many times a stored zero was followed by a stored non-zero |
The newer statistics are worked out inside QuestDB, not by pulling every row out of it. Tags with no
value in the window show no Total, Time on or Starts, rather than a misleading 0.
Long ranges are approximate. Time-weighted average, % Good and Total over a range longer than
7 days are computed from the 1-hour tier: each hour counts its own average. The table marks such
a value with ~, and its tooltip gives the resolution. That is exact for evenly sampled tags; a tag
stored on change can be off by up to one hour per change. In measurements on a real store it was
within 0.15 % for most tags. Counter increase, time on and off, and starts always read the raw rows.
The Quality column summarises each tag's window, for example Good, or Bad 12m · Uncertain 3m. The stats CSV adds Quality, Bad (s) and Uncertain (s).
The value at an instant New in 1.0.8
The tank level at shift change: system.hist.valueAt reads it from the rows on either side of
the instant, by the tag's interpolation. Step holds the value at or before the instant. Linear
follows the line to the next valued, non-Bad row. A row exactly at the instant is itself. A value
too old to say what the tag held then is reported as unknown, not as a stale number. A Bad row
before the instant gives Bad with no value. It reads the store that was the historian at that
instant.
system.hist.getStats returns the statistics above for a script.
→ System API
From a script
data = system.hist.getTrends(
["Line1/Filler/Motor1/Speed", "Line1/Filler/Motor1/Current"],
"2026-08-01T00:00:00Z",
"2026-08-02T00:00:00Z",
maxPoints=500,
agg="minmax",
)
agg is avg (the default) or minmax. Always pass maxPoints. A day of one-second data is 86,400
points per tag, and a chart 1,200 pixels wide can draw about 1,200.
→ System API
Live versus range mode
| Mode | Behaviour |
|---|---|
| Live | A rolling window that follows now. Always M4. The live tail buffer merges the newest in-memory samples, so the leading edge is genuinely live despite QuestDB's batched flush. A Bad reading breaks the live edge the same way it breaks the history. |
| Range | A fixed window, with statistics, zoom and pan, and the Min / Max band choice. |
Exporting history
The Historian chart's Export menu offers:
| Item | Contains |
|---|---|
| PNG image | The chart as drawn |
| CSV — stats table | One row per visible tag, one column per statistic |
| CSV — time series | The points the chart draws, one per bucket |
| CSV — every stored sample 1.0.8 | Every stored row of the visible window, at full resolution |
CSV — every stored sample is built by the server and streamed to the browser, so a large export
never sits in the page's memory. It has one row per stored sample (long format): Timestamp
(ISO 8601 in the project time zone, with its offset and microseconds), Tag (namespace path),
Value, Quality, Status and Origin. Each tag keeps its own timestamps; a wide table
would have to invent values where one tag has a sample and another does not. A value starting
=, +, - or @ that is not a number is neutralised against spreadsheet formula injection. The
file stops at 1,000,000 rows with a closing # truncated line. A failure part-way through is stated
at the end of the file. A live chart exports up to now.
1.0.8 The time-series CSV writes timestamps in the project
time zone with their offset (2026-10-09T13:00:00.123+07:00), matching the axis. Up to 1.0.7 it
wrote UTC.
Other routes out of QUBIQ:
- A Gateway script that calls
system.hist.getTrendsorgetStats, formats the result and writes it withsystem.files.save. - A pipeline on a schedule doing the same and pushing to an external system.
- Direct SQL against QuestDB through a named query, for tools that speak Postgres wire.
What keeps reads fast New in 1.0.8
- Abandoned reads are cancelled in QuestDB. Zoom away, close the screen or time out, and the query stops in QuestDB too. It no longer runs to completion holding one of the historian's query slots.
- Reads fail fast while a store is unreachable. After a failed connect, every read for that store fails at once with store unreachable, saying when it will retry: after 2 s, doubling to at most 30 s. Before, each chart, statistics or annotation read waited out its own 10-second connect timeout. A page with a few charts could hold every browser connection and freeze the interface for half a minute. A slow query on a live store does not trip this.
- One request per window for HMI trends. HMI trends and sparklines mounted together on one window are merged into a single historian request, up to 32 tags each.
- An oversized reply fails at once with too large… use fewer tags, a coarser resolution or a shorter time range. Up to 1.0.7 it was never sent, and the chart waited out its whole timeout before showing a generic failure.
| Symptom | Cause | Fix |
|---|---|---|
| Slow long-range query | A rollup tier is behind or invalid, so the chart reads raw | Look for Historian rollup needs a rebuild |
| too large error | Many tags at high resolution | Fewer tags, a coarser resolution, or a shorter range |
| store unreachable | QuestDB is down or unreachable | Fix the store; reads resume on their own |
| Trend has gaps | A real outage, a Bad stretch, or a deadband larger than the movement | Hover the shading for the reason |
| Trend flat-lines then jumps | An on-change tag held, then moved | Expected; set a store interval to prove it was alive |
| Trend stops at a date in the past | An epoch boundary with a retired store | Check History timeline in Settings → Historian |
Statistics marked ~ | A range over 7 days, read from the 1-hour tier | Expected; narrow the range for an exact value |
Interpreting a trend honestly
- A deadband means the stored trend is not the raw signal. That is intended, but know the band before drawing conclusions about noise.
- Average smooths. Switch to M4 or Min / Max before concluding an excursion did not happen.
- A shaded stretch is not zero. It is time the tag was Bad or Uncertain. Hover it for the reason.
- A carried line is a held value, not a measurement. It stops at Bad, at 8 hours, and at outage markers. Within those limits it is what the tag held.
Next
→ Annotations · Security