Why does a SELECT on sqlth_te return only one row?
Start where the query lands. A SELECT * FROM sqlth_te WHERE tagpath = '...' goes from the client, through the database connection, to one table: the tag entity registry. That table holds no samples. It maps each historized tag path to an integer ID plus metadata such as data type and when the entry was created or retired. One tag normally produces one row. A tag whose data type changed, or that was retired and re-added, produces a few more. One row back means the query worked, but it read the index instead of the data.
The historian writes samples into separate data tables that are partitioned by time. To reach those samples, a query must resolve the path to the integer ID, find which partition tables cover the time range, read those tables, and then apply the historian's storage rules. Each check below follows the query one hop further along that path.
| Table | What it holds | Rows per tag |
|---|---|---|
sqlth_te |
Tag path to ID mapping, data type, created/retired timestamps | Usually 1; more after type change or retirement |
sqlth_drv |
Historian driver/provider entries that the partitions belong to | n/a |
sqlth_partitions |
Partition table names with start and end time of each partition | n/a |
sqlt_data_* partitions |
The actual samples: tag ID, value columns, quality, timestamp | One per stored change |
sqlth_scinfo / sqlth_sce
|
Scan class info and execution windows, used to tell "unchanged" apart from "not collected" | n/a |
Check 1: Does the tag path resolve to the right ID?
Confirm the first hop before you open any partitions. Run the sqlth_te query and record the id, the data type, and the created and retired columns.
| Reading | Meaning | Next |
|---|---|---|
| Zero rows | The path string does not match what the historian stored | Fix the path (below), then re-run Check 1 |
| One row, retired column empty | Active tag with one ID | Check 2 with that ID |
| Several rows | Tag was retired or re-typed; history is split across IDs | Check 2 with every ID, split by created/retired time |
Path mismatches come from three places:
-
Binding substitution. The path in this case embeds
{Root Container.Text Field.text}twice. In a SQL Query property binding, those references are replaced by the Text Field's current value before the statement reaches the database. An empty field, or trailing whitespace in the field, sends a path that matches nothing. Read the Text Field value at runtime and paste the fully resolved string into a database query browser to test it. -
Provider prefix. The stored path does not include the tag provider in square brackets. The provider is tracked through
sqlth_drv. Remove any[provider]prefix from the WHERE clause. -
Case. The historian normalizes stored paths, so compare case-insensitively (
WHERE LOWER(tagpath) = LOWER('...')) or copy the exact string from the table.
Check 2: Which partition tables hold the samples?
Follow the ID into the data tables. The historian creates a new data table for each partition period. The default period is monthly, so a query across several months has to read several tables. Do not guess a table name. Read the list:
SELECT pname, drvid, start_time, end_time
FROM sqlth_partitions
ORDER BY start_time;
start_time and end_time are epoch milliseconds. Choose every partition whose range overlaps your query window, then join on the ID from Check 1:
-- replace sqlt_data_X with the pname values from sqlth_partitions
SELECT te.tagpath, d.t_stamp, d.intvalue, d.floatvalue,
d.stringvalue, d.datevalue, d.dataintegrity
FROM sqlth_te te
JOIN sqlt_data_X d ON d.tagid = te.id
WHERE te.id = <id from Check 1>
ORDER BY d.t_stamp;
| Reading | Meaning | Next |
|---|---|---|
| Rows returned | Storage path is intact | Check 3 to interpret them |
| No rows in any partition | Tag is registered but nothing was stored in the window | Check history is enabled on the tag, the scan class is running, and the historian store-and-forward queue is not backed up |
| Partition missing for the window | Data was pruned, or the window is before history was enabled | Compare against the historian's pruning setting and the created timestamp |
A UNION ALL across partitions is required for any multi-month window. This is the first reason hand-written SQL against the historian breaks down: the table list changes every partition period, so a query that is correct today returns nothing after the next rollover.
Check 3: Why do the raw rows look sparse or wrong?
Rows are present, but the trend looks wrong. That comes from how the historian stores data, not from a lost sample.
-
Value column by type. Only one of
intvalue,floatvalue,stringvalue,datevalueis populated, based on the data type insqlth_te. A humidity float reads NULL inintvalue. -
Deadband and on-change storage. The historian writes a row only when the value moves beyond the tag's deadband. A flat signal produces hours without rows. Seeing "no row" means "unchanged" only if the scan class was executing during that period. That execution record is in
sqlth_sce. -
Timestamps.
t_stampis epoch milliseconds in UTC. Convert it with in MySQL, and account for the server time-zone offset. -
Quality.
dataintegritycarries the quality code. Filter out bad-quality rows or display them separately. Do not average them in. - No seed value. A raw window query does not return the last value stored before the window start. A chart therefore starts blank until the first change inside the window.
Handling interpolation, seed values, scan-class gaps, quality, and partition joins in SQL is a lot of effort to reproduce behavior the historian already provides. That leads to the resolving branch.
How do I pull the tag's history through the historian instead?
Route the request through the historian's query engine. The engine resolves the path, walks the partitions, applies deadband-aware interpolation and seed values, and returns a dataset. There are two entry points.
| Method | Use when | Dynamic path from Text Field |
|---|---|---|
| Tag History property binding | A chart or table on a window needs a live dataset | Bind the tag path indirectly to the Text Field |
system.tag.queryTagHistory() |
Scripted exports, calculations, or custom aggregation | Build the path string in script |
| Raw SQL on partitions | External reporting tools with no gateway access | Requires Checks 1-3 and partition maintenance |
- Build the path string from the component value and remove whitespace.
- Call the historian:
sensor = event.source.parent.getComponent('Text Field').text.strip() path = 'a_name/goldtest7_7simulator/sensor ' + sensor + '/sensor ' + sensor + '/humidity' end = system.date.now() start = system.date.addHours(end, -24) ds = system.tag.queryTagHistory( paths=[path], startDate=start, endDate=end, returnSize=-1, # values as they changed aggregationMode='LastValue', returnFormat='Wide') event.source.parent.getComponent('Table').data = ds - For a binding, open the target's dataset property and select the Tag History binding type. Add the tag, then make the path indirect so the Text Field value supplies the sensor segment. Set the date range and the return size.
- Remove the SQL Query binding that targeted
sqlth_teso two bindings do not write to the same property.
How do I verify the history query is correct?
- Type a known sensor name into the Text Field. Confirm the dataset's column header matches the resolved tag path. A header with a blank sensor segment means the substitution failed before the request left the client.
- Pick a narrow window, such as one hour, and count the rows returned with
returnSize=-1. Run the Check 2 partition query for the same ID and window. The counts should match, apart from any seed row at the window start. - Spot-check three timestamps. Convert
t_stampwith and compare the value to the same instant in the dataset. A consistent whole-hour offset is a time-zone setting, not missing data. - Change the live tag value past its deadband. Wait one scan period, re-run the query, and confirm the new sample appears as the last row with good quality.
FAQ
How do I find which table stores Ignition tag history data?
Query sqlth_partitions and read the pname column. Each entry is a data table covering the start_time-end_time range in epoch milliseconds. Join those tables to sqlth_te on tagid = id.
How do I query Ignition tag history for a tag path taken from a Text Field?
Build the path string in script from the Text Field's text property, then pass it to system.tag.queryTagHistory(paths=[path], startDate=..., endDate=...). You can also use a Tag History binding with an indirect tag path.
Why does my tag history have gaps when the process was running?
The historian stores a row only when the value changes beyond the tag's deadband, so a steady value produces no new rows. Check sqlth_sce to confirm the scan class was executing during the gap. If it was, the value was unchanged. If it was not, collection stopped.
How do I convert the t_stamp column in Ignition history tables to a date in MySQL?
t_stamp is epoch milliseconds, so use . Adjust for the database server's time zone when you compare against client-side trends.