Initial commit: Wonderware / System Platform tools and reference
Five tools under one repo, all docs organized per DOCS-GUIDE.md: - aalogcli: .NET 4.8 / x86 CliFx CLI for reading System Platform binary logs (*.aaLGX) for LLM debugging, built on aaOpenSource/aaLog. Commands: last, tail, range, unread, fields. Stable JSON envelope under --llm-json. Build template under lib/build/ for rebuilding aaLogReader.dll. - aot: ArchestrA Object Toolkit 2014 v4.0 reference material. Dev guide (Markdown converted from CHM), API reference for the ArchestrA.Toolkit namespace, and the Monitor / Watchdog VS sample solutions. - graccesscli: .NET 4.8 / x86 CliFx CLI that automates Galaxy configuration via the ArchestrA GRAccess COM interop. Includes session daemon, IPC protocol, and llm-json envelope contract. - grdb: SQL/DDL exploration of the Galaxy Repository database. DDL captures, reusable queries, hierarchy / contained-name <-> tag-name translation notes. - histdb: LLM-oriented reference for AVEVA Historian retrieval. INSQL linked-server, extension tables, every wwXxx time-domain extension, every retrieval mode, alarm/event SQL recipes, REST API. Distilled from the 243-page Historian Retrieval Guide. Root contains: - CLAUDE.md: thin index pointing into each tool's README. - DOCS-GUIDE.md: doctrine for organizing docs for LLM consumption. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
# Overview — connecting and querying
|
||||
|
||||
## What's actually behind a Historian query
|
||||
|
||||
```
|
||||
client (SSMS / app / aalog-equivalent)
|
||||
│ T-SQL
|
||||
▼
|
||||
Microsoft SQL Server ◄── normal SQL Server tables (Tag, AnalogTag, EngineeringUnit, etc.)
|
||||
│ OLE DB
|
||||
▼
|
||||
INSQL linked server ◄── this is the AVEVA Historian OLE DB Provider, name "INSQL"
|
||||
│ HCAL
|
||||
▼
|
||||
Retrieval subsystem ◄── reads .aaLGX history blocks on disk, returns rowsets
|
||||
```
|
||||
|
||||
Configuration and event metadata live in real SQL Server tables. **Time-series ("history") data lives in binary history blocks on disk** and is presented through `INSQL` as if it were a SQL table. That's why every history query goes through the linked server.
|
||||
|
||||
## The extension tables
|
||||
|
||||
The "virtual" tables exposed by `INSQL`:
|
||||
|
||||
| Linked-server path | Local view | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `INSQL.Runtime.dbo.History` | `History` | Tall format. One row per (tag, timestamp). The default for everything historical. |
|
||||
| `INSQL.Runtime.dbo.Live` | `Live` | Most recent value per tag. Fast snapshot. |
|
||||
| `INSQL.Runtime.dbo.WideHistory` | `WideHistory` | Wide format — one row per timestamp, one column per tag. **Must be queried via `OPENQUERY`**. Up to 1024 columns. |
|
||||
| `INSQL.Runtime.dbo.AnalogSummaryHistory` | `AnalogSummaryHistory` | Pre-summarized analog data. |
|
||||
| `INSQL.Runtime.dbo.StateSummaryHistory` | `StateSummaryHistory` | Pre-summarized state-tag data. |
|
||||
| `INSQL.Runtime.dbo.HistoryBlock` | `HistoryBlock` | Metadata about the on-disk blocks (no `WHERE` required). |
|
||||
| `INSQL.Runtime.dbo.Events` | `Events` | Alarm/event records. See [`06-alarms-events.md`](06-alarms-events.md). |
|
||||
|
||||
**Legacy backward-compatibility tables** (still supported, not preferred):
|
||||
`AnalogHistory`, `DiscreteHistory`, `StringHistory`, `AnalogLive`, `DiscreteLive`, `StringLive`, plus `v_AlarmHistory`, `v_AlarmHistory2`, `v_EventHistory`, `v_AlarmEventHistoryInternal2`, `v_AlarmEventHistory2`. Use the unified `History` / `Live` / `Events` tables for new work.
|
||||
|
||||
## Linking `INSQL` to SQL Server
|
||||
|
||||
Done automatically by the Historian installer. If you ever need to redo it manually:
|
||||
|
||||
```sql
|
||||
EXEC sp_addlinkedserver @server='INSQL', @srvproduct='', @provider='INSQL';
|
||||
EXEC sp_serveroption 'INSQL', 'collation compatible', true;
|
||||
EXEC sp_addlinkedsrvlogin 'INSQL', 'TRUE', NULL, NULL, NULL;
|
||||
|
||||
-- Alias (used when joining the legacy analog and discrete tables in one query):
|
||||
EXEC sp_addlinkedserver @server='INSQLD', @srvproduct='', @provider='INSQL';
|
||||
EXEC sp_serveroption 'INSQLD', 'collation compatible', true;
|
||||
EXEC sp_addlinkedsrvlogin 'INSQLD', 'TRUE', NULL, NULL, NULL;
|
||||
```
|
||||
|
||||
The `INSQL` OLE DB provider cannot run standalone — it must be hosted by SQL Server.
|
||||
|
||||
## Four ways to issue a SELECT
|
||||
|
||||
You'll see all four in the wild; each has a niche.
|
||||
|
||||
### 1. Four-part naming (preferred for `History`, `Live`, `Events`)
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM INSQL.Runtime.dbo.History
|
||||
WHERE TagName = 'SysTimeSec'
|
||||
AND DateTime >= '2001-09-12 12:59:00'
|
||||
AND DateTime <= '2001-09-12 13:00:00';
|
||||
```
|
||||
|
||||
The four parts: `linked_server.catalog.schema.object_name`. For `INSQL` the catalog is **always `Runtime`** and the schema is **always `dbo`**.
|
||||
|
||||
### 2. Provider view (sugar over four-part)
|
||||
|
||||
Each extension table has a SQL Server view of the same name — `History`, `Live`, `Events`, etc.
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM History
|
||||
WHERE TagName = 'SysTimeSec'
|
||||
AND DateTime >= '2001-09-12 12:59:00'
|
||||
AND DateTime <= '2001-09-12 13:00:00';
|
||||
```
|
||||
|
||||
Backward-compatibility views use `v_<name>`.
|
||||
|
||||
### 3. `OPENQUERY` (required for `WideHistory`, useful for pass-through)
|
||||
|
||||
```sql
|
||||
SELECT * FROM OPENQUERY(INSQL, '
|
||||
SELECT DateTime, SysTimeSec, ReactTemp
|
||||
FROM WideHistory
|
||||
WHERE DateTime >= "2001-09-12 12:59:00"
|
||||
AND DateTime <= "2001-09-12 13:00:00"
|
||||
');
|
||||
```
|
||||
|
||||
The body is sent verbatim to `INSQL`. Quoting is **double quotes inside, single quote around the whole body.** Hard caps and quirks:
|
||||
|
||||
- 8000-character body limit.
|
||||
- DateTime format must be `yyyy-mm-dd hh:mm:ss.fff`.
|
||||
- `ORDER BY` and `GROUP BY` don't work *inside* the body (SQL Server doesn't see them) — apply them outside.
|
||||
- Joins inside the body are unsupported. Place them outside.
|
||||
- Variables (`@foo`) cannot appear inside the body.
|
||||
|
||||
### 4. `OPENROWSET` (one-off ad-hoc)
|
||||
|
||||
```sql
|
||||
SELECT * FROM OPENROWSET('INSQL', '', '
|
||||
SELECT DateTime, Quality, QualityDetail, Value
|
||||
FROM History
|
||||
WHERE TagName IN ("SysTimeSec")
|
||||
AND DateTime >= "2001-09-12 12:59:00"
|
||||
AND DateTime <= "2001-09-12 13:00:00"
|
||||
');
|
||||
```
|
||||
|
||||
Requires that ad-hoc distributed queries be enabled on the SQL Server (`sp_configure 'Ad Hoc Distributed Queries', 1`). Generally not recommended in production code.
|
||||
|
||||
## When to use which style
|
||||
|
||||
| Scenario | Style |
|
||||
| --- | --- |
|
||||
| Tall queries against `History`, `Live`, `Events` | **Four-part** or **view** |
|
||||
| `WideHistory` (one column per tag) | **OPENQUERY** (mandatory) |
|
||||
| Heavy filtering you want SQL Server to optimize | **Four-part** (the optimizer can see the predicates) |
|
||||
| Forcing a predicate down to `INSQL` that the optimizer is dropping | **OPENQUERY** (literal pass-through) |
|
||||
| Joining a SQL Server table against a Historian extension table | Four-part, with **`INNER REMOTE JOIN`** |
|
||||
| One-off scripts on a server with ad-hoc DQ enabled | **OPENROWSET** |
|
||||
|
||||
## Date format gotcha
|
||||
|
||||
For four-part queries on **non-English SQL Server installations** (French, German, etc.), the default datetime format may be `yyyy-dd-mm hh:mm:ss.fff` instead of `yyyy-mm-dd ...`. Use `CONVERT(...)` explicitly when in doubt — output formatting also depends on the OS regional settings.
|
||||
|
||||
For `OPENQUERY`, always use `yyyy-mm-dd hh:mm:ss.fff` regardless of locale.
|
||||
|
||||
## "No `WHERE` clause" error
|
||||
|
||||
If you get:
|
||||
|
||||
```
|
||||
Server: Msg 7320, Level 16, State 2, Line 1
|
||||
Could not execute query against OLE DB provider 'INSQL'.
|
||||
[OLE/DB provider returned message: InSQL did not receive a WHERE clause from SQL Server. ...]
|
||||
```
|
||||
|
||||
…the SQL Server query optimizer truncated the `WHERE` clause as "superfluous." Workaround: add a redundant condition such as `AND wwRetrievalMode = 'delta'`. See [`02-syntax-limits.md`](02-syntax-limits.md#where-clause-anomalies).
|
||||
|
||||
## Next
|
||||
|
||||
- [`02-syntax-limits.md`](02-syntax-limits.md) — what works and what doesn't, plus the `wwXxx` extensions overview.
|
||||
- [`03-retrieval-modes.md`](03-retrieval-modes.md) — pick a `wwRetrievalMode`.
|
||||
- [`05-query-recipes.md`](05-query-recipes.md) — practical patterns.
|
||||
@@ -0,0 +1,185 @@
|
||||
# SQL syntax — what works, what doesn't
|
||||
|
||||
The `INSQL` OLE DB provider supports a subset of T-SQL. Knowing the cliffs in advance saves a lot of "mysteriously empty rowset" debugging.
|
||||
|
||||
## Quick capability matrix
|
||||
|
||||
| Syntax element | Four-part / view | `OPENQUERY` |
|
||||
| --- | --- | --- |
|
||||
| `ORDER BY` | ✅ | ❌ inside the body. Wrap and order outside. |
|
||||
| `GROUP BY` | ✅ | ❌ |
|
||||
| `TagName IN (...)` | ✅ | ✅ |
|
||||
| `TagName LIKE '...'` | ✅ | ✅ |
|
||||
| Date/time functions (`DateAdd`, `GETDATE`, …) | ✅ | ✅ |
|
||||
| `MIN`, `MAX`, `AVG`, `SUM`, `STDEV` | All | `MIN`, `MAX`, `AVG`, `SUM` only — no `STDEV` |
|
||||
| Sub-`SELECT` (one normal SQL Server table + one extension table) | ✅ with restrictions | ❌ |
|
||||
| Sub-`SELECT` (two extension tables) | ❌ | ❌ |
|
||||
| Variables (`@foo`) | ✅ outside `OPENQUERY` | ❌ inside |
|
||||
|
||||
## Time-domain extensions (the `wwXxx` columns)
|
||||
|
||||
The Historian exposes "virtual columns" on every extension table that double as input parameters. They control retrieval semantics:
|
||||
|
||||
```text
|
||||
wwCycleCount wwEdgeDetection wwFilter wwInterpolationType
|
||||
wwOption wwQualityRule wwResolution wwRetrievalMode
|
||||
wwStateCalc wwTimeDeadband wwTimeStampRule wwTimeZone
|
||||
wwValueDeadband wwValueSelector wwVersion
|
||||
```
|
||||
|
||||
Reserved / deprecated: `wwParameters`, `wwMaxStates` (reserved), `wwRowCount` (deprecated — use `wwCycleCount`).
|
||||
|
||||
Set them in the `WHERE`:
|
||||
|
||||
```sql
|
||||
SELECT DateTime, Value
|
||||
FROM History
|
||||
WHERE TagName = 'SysTimeSec'
|
||||
AND DateTime >= '2001-12-02 10:00:00'
|
||||
AND DateTime <= '2001-12-02 10:02:00'
|
||||
AND wwResolution = 10
|
||||
AND wwRetrievalMode = 'cyclic';
|
||||
```
|
||||
|
||||
**Three rules that bite:**
|
||||
|
||||
1. The provider is stateless. **Every extension parameter must be set in every query** that needs it — there is no session memory.
|
||||
2. **`IN` and `OR` cannot multiplex an extension.** `wwVersion IN ('original','latest')` and `wwRetrievalMode = 'Delta' OR wwVersion = 'latest'` both fail.
|
||||
3. Values are **case-insensitive** even on a case-sensitive SQL Server collation.
|
||||
|
||||
Each individual extension is documented in [`04-retrieval-options.md`](04-retrieval-options.md).
|
||||
|
||||
## `LIKE` — only `TagName` and `Value`
|
||||
|
||||
`LIKE` only works on the `TagName` and `Value` columns (and `Value LIKE` only against a string tag).
|
||||
|
||||
```sql
|
||||
SELECT TagName, Value
|
||||
FROM History
|
||||
WHERE TagName LIKE 'Sys%'
|
||||
AND DateTime > '1999-05-24 14:30:00'
|
||||
AND DateTime < '1999-05-24 14:32:00';
|
||||
```
|
||||
|
||||
## `IN` against `AnalogTag` / `DiscreteTag` / `StringTag` with `LIKE`
|
||||
|
||||
If you push a `LIKE` *inside* an `IN`-driven sub-`SELECT` against the typed tag tables, the query fails **unless you select the `vValue` column**. An `INNER REMOTE JOIN` is more efficient anyway:
|
||||
|
||||
```sql
|
||||
-- Clunky (and only works if vValue is in the projection):
|
||||
SELECT DateTime, TagName, vValue, Quality, QualityDetail
|
||||
FROM History
|
||||
WHERE TagName IN (SELECT TagName FROM StringTag WHERE TagName LIKE 'SysString')
|
||||
AND DateTime BETWEEN '2001-06-21 16:00:00.000' AND '2001-06-21 16:40:00.000'
|
||||
AND wwRetrievalMode = 'Delta';
|
||||
|
||||
-- Preferred:
|
||||
SELECT h.TagName, DateTime, Value
|
||||
FROM SnapshotTag st
|
||||
INNER REMOTE JOIN INSQL.Runtime.dbo.History h
|
||||
ON st.TagName = h.TagName
|
||||
WHERE EventTagName = 'SysStatusEvent'
|
||||
AND DateTime = '2001-12-20 0:00';
|
||||
```
|
||||
|
||||
## Joins
|
||||
|
||||
- **Inside `OPENQUERY`: not supported.** Both implicit (`FROM Tag t, Live v`) and explicit (`JOIN`) joins fail. Always pull the join out:
|
||||
|
||||
```sql
|
||||
SELECT v.DateTime, v.TagName, v.Value, e.Unit
|
||||
FROM OPENQUERY(INSQL, '
|
||||
SELECT DateTime, TagName, Value FROM Live
|
||||
WHERE TagName LIKE "%Date%"
|
||||
') v
|
||||
JOIN AnalogTag t ON v.TagName = t.TagName
|
||||
JOIN EngineeringUnit e ON t.EUKey = e.EUKey
|
||||
ORDER BY t.TagName;
|
||||
```
|
||||
|
||||
- **Joining a SQL Server table against an extension table: use `INNER REMOTE JOIN`** with the SQL Server table on the left and the extension table on the right:
|
||||
|
||||
```text
|
||||
<SQLServerTable> INNER REMOTE JOIN <HistorianExtensionTable>
|
||||
```
|
||||
|
||||
This forces SQL Server to send the join predicate down to the OLE DB provider in one round trip rather than dragging the whole extension table back to the engine.
|
||||
|
||||
## `CONVERT` on `vValue` inside `OPENQUERY`
|
||||
|
||||
`CONVERT(...)` against the variant `vValue` column is **not supported inside the `OPENQUERY` body**. Filter outside:
|
||||
|
||||
```sql
|
||||
SELECT * FROM OPENQUERY(INSQL, '
|
||||
SELECT DateTime, Quality, OPCQuality, QualityDetail, Value, vValue, TagName
|
||||
FROM History
|
||||
WHERE TagName IN ("SysTimeMin", "SysPulse")
|
||||
AND DateTime >= "2001-12-30 04:00:00.000"
|
||||
AND DateTime <= "2001-12-30 09:00:00.000"
|
||||
AND wwRetrievalMode = "Delta"
|
||||
')
|
||||
WHERE CONVERT(float, vValue) = 20.0;
|
||||
```
|
||||
|
||||
## Sub-SELECTs against a SQL Server table + extension table
|
||||
|
||||
Permitted but inefficient — SQL Server fans the whole sub-query out badly. Always rewrite as `INNER REMOTE JOIN`. Two-extension-table sub-SELECTs are not supported at all.
|
||||
|
||||
## Optimizer dropping criteria
|
||||
|
||||
The SQL Server optimizer sometimes decides a `WHERE` clause is "redundant" and never sends it to `INSQL`, producing wrong-but-not-erroring results. Diagnose with the SSMS query plan; remediate by rewriting so the criteria flow through a more direct path, e.g. **put the small driver table on the left of `INNER REMOTE JOIN`** so its tagnames flow into the right side:
|
||||
|
||||
```sql
|
||||
DECLARE @TagList TABLE (TagName nvarchar(256));
|
||||
INSERT @TagList SELECT 'SysTimeSec' UNION SELECT 'SysPerfCPUTotal';
|
||||
|
||||
-- Wrong: TagName criteria gets optimized out
|
||||
SELECT DateTime, h.vValue, h.TagName
|
||||
FROM History h
|
||||
INNER REMOTE JOIN @TagList l ON h.TagName = l.TagName
|
||||
WHERE DateTime >= DATEADD(hour, -1, GETDATE()) AND DateTime < GETDATE()
|
||||
AND wwRetrievalMode = 'AVG' AND wwCycleCount = 1;
|
||||
|
||||
-- Right: driver table on the left, criteria reach the OLE DB provider
|
||||
SELECT DateTime, h.vValue, h.TagName
|
||||
FROM @TagList l
|
||||
INNER REMOTE JOIN History h ON h.TagName = l.TagName
|
||||
WHERE DateTime >= DATEADD(hour, -1, GETDATE()) AND DateTime < GETDATE()
|
||||
AND wwRetrievalMode = 'AVG' AND wwCycleCount = 1;
|
||||
```
|
||||
|
||||
## Variant columns + functions
|
||||
|
||||
If you wrap a variant column (`vValue`) with a function like `ROUND()`, SQL Server emits a syntax error that **doesn't propagate back through OLE DB** — you get silently wrong / empty rows. Cast first or filter on the typed `Value` column.
|
||||
|
||||
## `StartDateTime` cannot drive query criteria
|
||||
|
||||
Many retrieval modes return a `StartDateTime` for the cycle. You can `SELECT` it, but you **cannot use it in `WHERE`** — the predicate is silently ignored. Use `DateTime` only.
|
||||
|
||||
## Comparisons and `NULL`
|
||||
|
||||
A predicate like `Value > 0` will error when the provider returns a `NULL`. Always pair it with `AND Value IS NOT NULL`.
|
||||
|
||||
## `WHERE` clause anomalies
|
||||
|
||||
If the SQL Server optimizer decides your `WHERE` is "useless" (typically a `LIKE '%'` against `TagName`), it removes the clause entirely and you get the OLE DB error from [`01-overview.md`](01-overview.md). Add another harmless predicate to keep the clause "needed":
|
||||
|
||||
```sql
|
||||
SELECT DateTime, Value, QualityDetail
|
||||
FROM History
|
||||
WHERE TagName LIKE '%'
|
||||
AND wwRetrievalMode = 'delta'; -- forces the WHERE to survive optimization
|
||||
```
|
||||
|
||||
## Compatibility notes for the older `datetime` type
|
||||
|
||||
If your client uses the older SQL Server `datetime` type (instead of `datetime2`), expect rounding to ~3.3 ms vs. the 100 ns native `FILETIME` resolution.
|
||||
|
||||
## What `OPENQUERY` and Microsoft Query do *not* talk
|
||||
|
||||
Microsoft Query (the legacy Excel/Access tool) cannot parse `OPENQUERY` statements. Use one of the other connection paths from Excel.
|
||||
|
||||
## Next
|
||||
|
||||
- [`03-retrieval-modes.md`](03-retrieval-modes.md) — every `wwRetrievalMode` value.
|
||||
- [`04-retrieval-options.md`](04-retrieval-options.md) — every other `wwXxx` parameter.
|
||||
@@ -0,0 +1,183 @@
|
||||
# Retrieval modes (`wwRetrievalMode`)
|
||||
|
||||
Every history query has a retrieval mode. The default is **`Cyclic`** for analog tables (and analog/state summary tables) and **`Delta`** for `History`, discrete, and string tables. Set explicitly with `AND wwRetrievalMode = '<Mode>'` (case-insensitive).
|
||||
|
||||
## At-a-glance picker
|
||||
|
||||
| Want… | Use |
|
||||
| --- | --- |
|
||||
| Evenly spaced points across a window, fast | `Cyclic` |
|
||||
| Smooth curves (analog) over a window | `Interpolated` |
|
||||
| Only the *changes* — no duplicates | `Delta` |
|
||||
| Every stored point, including duplicates | `Full` |
|
||||
| A compromise: cyclic perf + delta accuracy | `BestFit` |
|
||||
| Time-weighted average per cycle | `Average` |
|
||||
| Min / max value per cycle | `Minimum` / `Maximum` |
|
||||
| Area under the curve per cycle | `Integral` |
|
||||
| Slope (units / time) per cycle | `Slope` |
|
||||
| Throughput / accumulator (handles wraps + resets) | `Counter` |
|
||||
| "How long was the tag in state X" | `ValueState` |
|
||||
| "How long between consecutive entries into state X" | `RoundTrip` |
|
||||
| Forecast points using simple linear regression | `Predictive` (a `wwFilter` mode, not a `wwRetrievalMode`) |
|
||||
| Retrieve bracketing values just outside the window | `BoundingValue` |
|
||||
|
||||
## Mode catalog
|
||||
|
||||
Each section below covers: how the mode picks rows, which `wwXxx` options apply to it, the typical query shape, and the gotchas.
|
||||
|
||||
### `Cyclic` — evenly spaced cycle boundaries
|
||||
|
||||
- One row per cycle boundary (last value at or before the boundary).
|
||||
- Number of cycles set by `wwCycleCount` *or* `wwResolution`. Default 100 cycles when neither is set.
|
||||
- Fast and cheap. May *miss* spikes/gaps that fall between boundaries — for that use `BestFit`.
|
||||
- Default mode for analog and analog-summary tables.
|
||||
- Applies to all tag types.
|
||||
- Options: `wwCycleCount`, `wwResolution`, `wwVersion`, `wwTimeStampRule`, `wwQualityRule`.
|
||||
|
||||
```sql
|
||||
SELECT DateTime, TagName, Value
|
||||
FROM History
|
||||
WHERE TagName = 'ReactLevel'
|
||||
AND DateTime >= '2001-03-13 13:15:00'
|
||||
AND DateTime <= '2001-03-13 14:15:00'
|
||||
AND wwRetrievalMode = 'Cyclic'
|
||||
AND wwCycleCount = 60;
|
||||
```
|
||||
|
||||
NULLs and initial values are returned as-is — no special handling.
|
||||
|
||||
### `Delta` — changes only
|
||||
|
||||
- Returns only physically stored points where the value (or quality) actually changed.
|
||||
- If no point sits exactly on the start time, the **last value before the start** is returned with timestamp shifted to the start time and `Quality = 133` (initial-value flag).
|
||||
- A NULL after a non-NULL is always returned; consecutive NULLs are suppressed.
|
||||
- Default for `History`, discrete, and string tables.
|
||||
- Applies to all tag types.
|
||||
- Options: `wwTimeDeadband`, `wwValueDeadband`, `wwVersion`, `wwQualityRule`.
|
||||
|
||||
```sql
|
||||
SELECT TagName, DateTime, Value, QualityDetail
|
||||
FROM History
|
||||
WHERE TagName = 'A001'
|
||||
AND DateTime >= '2009-09-12 00:20'
|
||||
AND DateTime <= '2009-09-12 00:40'
|
||||
AND wwRetrievalMode = 'Delta';
|
||||
```
|
||||
|
||||
`>=` on the start time pulls the initial value; `>` skips it. Same logic for `<` / `<=` on the end.
|
||||
|
||||
### `Full` — every stored point
|
||||
|
||||
- Returns all stored rows including duplicates.
|
||||
- Best fidelity to what was actually written by the I/O server.
|
||||
- Heavy — long windows × many tags = giant rowsets.
|
||||
- Initial-value handling matches `Delta`.
|
||||
- Applies to all tag types.
|
||||
- Options: `wwVersion`, `wwQualityRule`.
|
||||
|
||||
### `Interpolated` — `Cyclic`, but with interpolation between actual points
|
||||
|
||||
- Cycle boundaries identical to `Cyclic`. At each boundary, the value is **linearly interpolated** between the surrounding stored points (or stair-stepped, if that's the tag's configured interpolation type).
|
||||
- Per-tag interpolation comes from the `InterpolationType` column on the `AnalogTag` table, with system parameters `InterpolationTypeInteger` / `InterpolationTypeReal` filling in defaults. **`AND wwInterpolationType = 'Linear'`** in the query overrides everything for that query.
|
||||
- A NULL preceding a cycle boundary causes a NULL at the boundary (no interpolation across NULLs).
|
||||
- Useful for trend smoothness and for mixing slow + fast tags.
|
||||
- Only meaningful for analog tags — others fall back to stair-step (effectively `Cyclic`).
|
||||
- Options: `wwCycleCount`, `wwResolution`, `wwVersion`, `wwInterpolationType`, `wwTimeStampRule`, `wwQualityRule`.
|
||||
|
||||
### `BestFit` — five points per cycle, picked from real data
|
||||
|
||||
For each cycle returns up to **five** points: first, last, min (with its real timestamp), max (with its real timestamp), and the first non-Good-quality "exception." A point that fills two roles (e.g. last == max) is returned once. Cycles with no data return nothing. Initial and final values at the query boundaries are interpolated.
|
||||
|
||||
Combines cyclic-style cost with delta-style fidelity. **One week of 5-second data is ~120,960 rows in `Delta`, ~300 rows in `BestFit`.** Use it for long-window trends.
|
||||
|
||||
- Only applied to analog and analog-summary tags. Other tags get plain `Delta`.
|
||||
- Options: `wwCycleCount`, `wwResolution`, `wwVersion`, `wwInterpolationType`, `wwQualityRule`.
|
||||
- Quality detail of points returned from a cycle that contains a gap or an incomplete final cycle gets the `4096` "partial cycle" bit OR'd into `QualityDetail`.
|
||||
|
||||
### `Average` — time-weighted average per cycle
|
||||
|
||||
- One row per tag per cycle.
|
||||
- For tags with **linear interpolation**, midpoints between stored points are weighted by elapsed time.
|
||||
- For tags with **stair-step interpolation**, the earlier of two values carries the full interval.
|
||||
- Differs from SQL `AVG()`, which is unweighted; the Historian's time-weighted average is generally more accurate for sparse data.
|
||||
- Gaps (NULL ranges) are excluded from the calculation; the output column `TimeGood` reports total good time per cycle.
|
||||
- Initial / final values: with `wwTimeStampRule='END'` the initial value is the average over the cycle leading up to the query start; with `START`, the final value covers the cycle after the query end.
|
||||
- Only applies to analog and analog-summary tags.
|
||||
- Options: `wwCycleCount`, `wwResolution`, `wwVersion`, `wwInterpolationType`, `wwTimeStampRule`, `wwQualityRule`.
|
||||
|
||||
### `Minimum` / `Maximum` — per-cycle extremes
|
||||
|
||||
- Returns the minimum (or maximum) **stored** point inside each cycle, at its real timestamp.
|
||||
- Cycles with no data return nothing. Cycles containing a NULL return NULL.
|
||||
- If multiple points tie, the earliest is returned.
|
||||
- `QualityDetail` of returned cycle-extreme points has the `4096` "partial cycle" bit set when the cycle is incomplete.
|
||||
- Only applied to analog tags; others fall back to `Delta`.
|
||||
- Options: `wwCycleCount`, `wwResolution`, `wwVersion`, `wwTimeStampRule`, `wwQualityRule`.
|
||||
|
||||
### `Integral` — area under the curve
|
||||
|
||||
- Per-cycle integral of the tag value × time. Useful for things like "how many gallons" given a flow-rate tag.
|
||||
- The integration uses linear or stair-step depending on the tag's configured / overridden interpolation type.
|
||||
- Only applied to analog tags.
|
||||
- Options: `wwCycleCount`, `wwResolution`, `wwVersion`, `wwInterpolationType`, `wwTimeStampRule`, `wwQualityRule`.
|
||||
|
||||
### `Slope` — units per time per cycle
|
||||
|
||||
- Per-cycle slope (rate of change). Linear regression across the cycle's points.
|
||||
- Only analog tags.
|
||||
- Options: `wwCycleCount`, `wwResolution`, `wwVersion`, `wwTimeStampRule`, `wwQualityRule`.
|
||||
|
||||
### `Counter` — accumulators with wrap and reset support
|
||||
|
||||
- For tags that monotonically increase but periodically reset (totalizers, throughput counters, manual resets).
|
||||
- Returns the accumulated count per cycle, accounting for resets and configurable deadbands so brief drops aren't read as resets.
|
||||
- Supports manually-reset counters and counter deadbands.
|
||||
- Options: `wwCycleCount`, `wwResolution`, `wwVersion`, `wwTimeStampRule`, `wwQualityRule`.
|
||||
|
||||
### `ValueState` — time-in-state
|
||||
|
||||
- Calculates how long the tag spent in each value-state during each cycle.
|
||||
- The aggregation type comes from `wwStateCalc`: `Minimum`, `Maximum`, `Average`, `Total`, `Percent` (and `*Contained` variants).
|
||||
- If you specify a single value in the query (`AND Value = 'Open'`), one row per cycle. If you don't, one row per (state, cycle) — table-friendly but trend-unfriendly.
|
||||
- Works for integer, discrete, string, and state-summary tags. Other types return nothing.
|
||||
- NULLs are themselves a state.
|
||||
- Options: `wwCycleCount`, `wwResolution`, `wwVersion`, `wwStateCalc`, `wwTimeStampRule`, `wwQualityRule`.
|
||||
|
||||
### `RoundTrip` — time between consecutive entries to a state
|
||||
|
||||
- Like `ValueState` but measures the **time between successive leading edges** of the same state — useful for cycle-time analysis ("how long between successive 'Start' events").
|
||||
- `wwStateCalc` accepts `MinContained`, `MaxContained`, `AvgContained`, `TotalContained`, `PercentContained`.
|
||||
- Options: `wwCycleCount`, `wwResolution`, `wwVersion`, `wwStateCalc`, `wwTimeStampRule`, `wwQualityRule`, `wwEdgeDetection`.
|
||||
- Edge detection is configured via `wwEdgeDetection` (`Leading`, `Trailing`, `Both`) — see [`04-retrieval-options.md`](04-retrieval-options.md#wwedgedetection).
|
||||
|
||||
### `Predictive` — simple linear regression forecast
|
||||
|
||||
- Available from Historian 2014 R2 P01 onward.
|
||||
- Implemented as a `wwFilter` mode rather than a distinct `wwRetrievalMode`. Future Historian releases are expected to add other prediction algorithms.
|
||||
- Set `AND wwFilter = '<predictive expression>'` against an analog tag.
|
||||
|
||||
### `BoundingValue` — bracket the window
|
||||
|
||||
- Available from Historian 17.3.100 onward.
|
||||
- Returns the **two stored points immediately surrounding the query window**, in addition to whatever falls inside it.
|
||||
- Use when a chart renderer or trend client needs a value to anchor the line at the query edges without interpolation.
|
||||
|
||||
## Mixing modes per query
|
||||
|
||||
You can't. One `wwRetrievalMode` per query. To compare modes, run them as separate queries and `UNION ALL`.
|
||||
|
||||
## Initial-value semantics, in one place
|
||||
|
||||
| Mode | Initial value |
|
||||
| --- | --- |
|
||||
| `Cyclic`, `Full`, `Delta` | Last value before the start, timestamped at the query start. `Quality=133` if shifted; if no prior value exists, NULL with `Quality=1`, `QualityDetail=65536`. |
|
||||
| `Interpolated`, `BestFit` | Linearly interpolated between the surrounding points at the start time. |
|
||||
| `Average`, `Min`, `Max`, `Integral`, `Slope` | Calculated over the cycle adjacent to the boundary defined by `wwTimeStampRule`. |
|
||||
| `Counter`, `ValueState`, `RoundTrip` | Result of applying the algorithm to the cycle preceding the query start. |
|
||||
|
||||
`>=` on the start pulls an initial value; `>` does not. Same for `<=` / `<` on the end.
|
||||
|
||||
## Next
|
||||
|
||||
- [`04-retrieval-options.md`](04-retrieval-options.md) — `wwCycleCount`, `wwResolution`, `wwInterpolationType`, deadbands, quality, value selector, and more.
|
||||
- [`05-query-recipes.md`](05-query-recipes.md) — worked examples that combine modes and options.
|
||||
@@ -0,0 +1,193 @@
|
||||
# Retrieval options (the rest of the `wwXxx` parameters)
|
||||
|
||||
`wwRetrievalMode` picks the algorithm; everything below tunes it. Set in the `WHERE` clause exactly like `wwRetrievalMode`. Each is case-insensitive. None can be supplied via `IN` or `OR`.
|
||||
|
||||
## Which option applies to which mode
|
||||
|
||||
(Selecting the most-asked questions; consult the source PDF table on pp. 89-90 for the full grid.)
|
||||
|
||||
| Option | Cyclic | Delta | Full | Interpolated | BestFit | Average | Min | Max | Integral | Slope | Counter | ValueState | RoundTrip |
|
||||
| --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
|
||||
| `wwCycleCount` | ✅ | (limit) | – | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| `wwResolution` | ✅ | – | – | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| `wwTimeDeadband` | – | ✅ | – | – | – | – | – | – | – | – | – | – | – |
|
||||
| `wwValueDeadband` | – | ✅ | – | – | – | – | – | – | – | – | – | – | – |
|
||||
| `wwVersion` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| `wwInterpolationType` | – | – | – | ✅ | ✅ | ✅ | – | – | ✅ | ✅ | – | – | – |
|
||||
| `wwTimeStampRule` | ✅ | – | – | ✅ | – | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| `wwQualityRule` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| `wwStateCalc` | – | – | – | – | – | – | – | – | – | – | – | ✅ | ✅ |
|
||||
| `wwFilter` (analog filter) | ✅ where applicable | – | – | ✅ | ✅ | ✅ | – | – | – | – | – | – | – |
|
||||
| `wwValueSelector` (summary tags) | ✅ | – | – | – | – | – | – | – | – | – | – | – | – |
|
||||
| `wwTimeZone` | ✅ everywhere |
|
||||
| `wwEdgeDetection` | – | – | – | – | – | – | – | – | – | – | – | – | ✅ |
|
||||
|
||||
## `wwCycleCount` — number of cycles
|
||||
|
||||
```sql
|
||||
AND wwCycleCount = 60 -- give me 60 evenly-spaced cycles across the window
|
||||
```
|
||||
|
||||
For "truly cyclic" modes (`Cyclic`, `Interpolated`, `Average`, `Integral`) you get one row per tag per cycle. Other modes use cycles for grouping but may emit zero, one, or many rows per cycle.
|
||||
|
||||
For `Delta` / `Full`, `wwCycleCount` acts as a **limit on the first N matching rows** instead of a cycle count.
|
||||
|
||||
`wwCycleCount` and `wwResolution` are mutually exclusive intents — set one. If neither is set, the default cycle count is **100**.
|
||||
|
||||
## `wwResolution` — cycle length in milliseconds
|
||||
|
||||
```sql
|
||||
AND wwResolution = 60000 -- 60-second cycles
|
||||
```
|
||||
|
||||
`number_of_cycles = window_length / wwResolution`. Use this when "every minute" matters more than "100 evenly-spaced points." See "Phantom cycles" in the source PDF (p. 95) for what happens at the boundary edges.
|
||||
|
||||
## `wwTimeDeadband` — suppress fast changes (Delta only)
|
||||
|
||||
Milliseconds. Within `wwTimeDeadband` of the most recently returned point, further changes are dropped. The deadband resets every time a point is emitted.
|
||||
|
||||
```sql
|
||||
AND wwRetrievalMode = 'Delta' AND wwTimeDeadband = 10000 -- ≤1 sample / 10 s
|
||||
```
|
||||
|
||||
## `wwValueDeadband` — suppress small changes (Delta only)
|
||||
|
||||
Percent of the tag's engineering-unit full-scale. Drops changes smaller than the deadband from the previously returned point. Quality changes always force a row out, regardless of deadband.
|
||||
|
||||
```sql
|
||||
AND wwRetrievalMode = 'Delta' AND wwValueDeadband = 1.0 -- 1% of full scale
|
||||
```
|
||||
|
||||
## `wwVersion` — original vs. latest stored value
|
||||
|
||||
```sql
|
||||
AND wwVersion = 'Latest' -- default; honors any post-write corrections
|
||||
AND wwVersion = 'Original' -- as captured at write time
|
||||
```
|
||||
|
||||
Latest values that have been overwritten carry `QualityDetail = 202`. Applies to every retrieval mode.
|
||||
|
||||
## `wwInterpolationType`
|
||||
|
||||
```sql
|
||||
AND wwInterpolationType = 'Linear' -- override per-query
|
||||
AND wwInterpolationType = 'StairStep'
|
||||
```
|
||||
|
||||
Resolution order: query-level `wwInterpolationType` > per-tag `AnalogTag.InterpolationType` > system parameters `InterpolationTypeInteger` / `InterpolationTypeReal`. The output column also reports the type that was actually applied per row (`LINEAR` or `STAIRSTEP`).
|
||||
|
||||
## `wwTimeStampRule` — which end of the cycle owns the timestamp
|
||||
|
||||
```sql
|
||||
AND wwTimeStampRule = 'START' -- default; row stamped at cycle start
|
||||
AND wwTimeStampRule = 'END' -- row stamped at cycle end
|
||||
```
|
||||
|
||||
Affects the `DateTime` column in cyclic-flavored modes. Cycle-start time is also exposed via the read-only `StartDateTime` column for diagnostic purposes (cannot be used in `WHERE` — see [`02-syntax-limits.md`](02-syntax-limits.md)).
|
||||
|
||||
## `wwTimeZone` — return data in a different zone
|
||||
|
||||
Accepts any value from the `TimeZone.TimeZone` column. The `TimeZone` table is rebuilt at every server start from the OS's registered zones, so it picks up custom zones automatically. DST is corrected against the **server** OS; there is no client-side zone option.
|
||||
|
||||
```sql
|
||||
AND wwTimeZone = 'Eastern Standard Time'
|
||||
```
|
||||
|
||||
If unset, the server's local zone is used. For UTC-only output use `wwTimeZone = 'UTC'`.
|
||||
|
||||
## `wwQualityRule` — how to combine quality
|
||||
|
||||
When combining multiple stored points into one cycle output (any cyclic mode plus `Average`, `Min`, `Max`, etc.), this controls how individual qualities collapse into a single output quality:
|
||||
|
||||
- `Good` (default for most cyclic modes) — output is Good only if every contributing point was Good.
|
||||
- `Extended` — propagate worst quality with extra detail bits.
|
||||
- Other rule names per source PDF p. 111. Eight worked queries on pp. 113-118.
|
||||
|
||||
`Cyclic`, `Full`, `Delta`, `Min`, `Max`, `BestFit` do **not** combine qualities — `wwQualityRule` is silently ignored for those.
|
||||
|
||||
## `wwStateCalc` — `ValueState` / `RoundTrip` aggregation
|
||||
|
||||
For `ValueState`:
|
||||
|
||||
- `Minimum` — shortest time in each unique state.
|
||||
- `Maximum` — longest time in each unique state.
|
||||
- `Average` — average time in each unique state.
|
||||
- `Total` — total time in each unique state.
|
||||
- `Percent` — total time as a percentage of cycle length.
|
||||
|
||||
For `RoundTrip`, only the `*Contained` variants are valid: `MinContained`, `MaxContained`, `AvgContained`, `TotalContained`, `PercentContained`. The "contained" forms only count fully-contained intervals — partial intervals at cycle boundaries are excluded, which matters for slow-changing tags vs. long cycles. The non-contained calculations have arbitrary cycle-boundary effects that can produce non-intuitive results for slow tags over long cycles.
|
||||
|
||||
## `wwFilter` — analog value filtering
|
||||
|
||||
Three filter functions, plus the predictive variant.
|
||||
|
||||
- `SigmaLimit(<n>)` — drop outliers more than *n* standard deviations from the mean.
|
||||
- `ToDiscrete(<threshold>, <op>)` — convert analog to discrete; e.g. `ToDiscrete(5.0, >)` returns 1 above 5.0, 0 below. Supported ops: `>`, `>=`, `<`, `<=`, `=`, `<>`.
|
||||
- `SnapTo(<step>, <min>, <max>)` — quantize the value to the nearest `step`, clamped to `[min, max]`. Snapped values get the `0x2000` `QualityDetail` bit.
|
||||
- Predictive variant — see source PDF p. 86; SLR forecast over the cycle.
|
||||
|
||||
```sql
|
||||
AND wwFilter = 'SnapTo(0.01, 0, 1000)'
|
||||
AND wwFilter = 'ToDiscrete(5.0, >)'
|
||||
```
|
||||
|
||||
Bad filter syntax returns a syntax error and zero rows.
|
||||
|
||||
## `wwValueSelector` — analog summary value pickoff
|
||||
|
||||
When an analog summary tag has multiple summarized values stored for one period, this picks which one to surface:
|
||||
|
||||
| Setting | Meaning |
|
||||
| --- | --- |
|
||||
| `AUTO` (default) | Mode-dependent — see PDF p. 125-128. |
|
||||
| `FIRST` | First summarized value in the period. |
|
||||
| `LAST` | Last value in the period. |
|
||||
| `MIN` | Minimum value in the period (timestamp = where the min occurred). |
|
||||
| `MAX` | Maximum value in the period (timestamp = where the max occurred). |
|
||||
| `AVG` | Time-weighted average. |
|
||||
|
||||
## `wwEdgeDetection` (RoundTrip + alarm/event subqueries)
|
||||
|
||||
Picks which edges count for `RoundTrip`:
|
||||
|
||||
```sql
|
||||
AND wwEdgeDetection = 'Leading' -- transition into the state
|
||||
AND wwEdgeDetection = 'Trailing' -- transition out of the state
|
||||
AND wwEdgeDetection = 'Both' -- either
|
||||
```
|
||||
|
||||
Also used in event-pattern queries against the `WideHistory` table — see PDF pp. 78-85 for analog and discrete edge-detection examples.
|
||||
|
||||
## `wwOption` — reserved bit-bag
|
||||
|
||||
Catch-all for one-off feature flags AVEVA exposes per release. Almost never set in user queries; consult the source PDF if a feature note tells you to.
|
||||
|
||||
## Reserved / deprecated
|
||||
|
||||
| Name | Status |
|
||||
| --- | --- |
|
||||
| `wwParameters`, `wwMaxStates` | Reserved. Don't use. |
|
||||
| `wwRowCount` | Deprecated alias for `wwCycleCount`. Still works, but write new queries with `wwCycleCount`. |
|
||||
|
||||
## Combining options
|
||||
|
||||
Multiple `wwXxx` are fine as long as each appears in a single `AND` predicate (no `IN`, no `OR`). A representative dense query:
|
||||
|
||||
```sql
|
||||
SELECT TagName, DateTime, Value, StateTime, StartDateTime
|
||||
FROM History
|
||||
WHERE TagName IN ('Reactor1Level')
|
||||
AND DateTime >= DATEADD(hour, -8, GETDATE()) AND DateTime <= GETDATE()
|
||||
AND wwRetrievalMode = 'RoundTrip'
|
||||
AND wwStateCalc = 'AvgContained'
|
||||
AND vValue = CONVERT(SQL_VARIANT, '1')
|
||||
AND wwCycleCount = 1
|
||||
AND wwTimeStampRule = 'Start'
|
||||
AND wwQualityRule = 'Good'
|
||||
AND wwFilter = 'ToDiscrete(5.0,>)'
|
||||
AND wwVersion = 'Latest';
|
||||
```
|
||||
|
||||
## Next
|
||||
|
||||
- [`05-query-recipes.md`](05-query-recipes.md) — when these options come together to answer specific questions.
|
||||
@@ -0,0 +1,333 @@
|
||||
# Query recipes
|
||||
|
||||
Worked SQL patterns for the questions that come up most often. Each recipe links back to the deeper concept doc when more context is needed.
|
||||
|
||||
## Querying the `History` table — vanilla
|
||||
|
||||
```sql
|
||||
SELECT DateTime, Sec = DATEPART(ss, DateTime), TagName, Value
|
||||
FROM History
|
||||
WHERE TagName = 'ReactLevel'
|
||||
AND DateTime >= '2001-03-13 13:15:00'
|
||||
AND DateTime <= '2001-03-13 14:15:00'
|
||||
AND wwRetrievalMode = 'Cyclic'; -- explicit, even though Cyclic is the analog default
|
||||
```
|
||||
|
||||
Without `wwCycleCount` / `wwResolution`, default is **100 rows**. See [`03-retrieval-modes.md`](03-retrieval-modes.md#cyclic--evenly-spaced-cycle-boundaries).
|
||||
|
||||
## Latest snapshot per tag — `Live`
|
||||
|
||||
```sql
|
||||
SELECT TagName, Value
|
||||
FROM Live
|
||||
WHERE TagName = 'ReactLevel';
|
||||
```
|
||||
|
||||
`Live` is fed by streamed updates only. Non-streamed paths bypass it (CSV import, store-and-forward, post-shutdown writes, "Latest" revision data) — query `History` if `Live` shows stale results.
|
||||
|
||||
## Wide tables — `WideHistory` via `OPENQUERY`
|
||||
|
||||
```sql
|
||||
SELECT * FROM OPENQUERY(INSQL, '
|
||||
SELECT DateTime, ReactLevel, ReactTemp
|
||||
FROM WideHistory
|
||||
WHERE DateTime >= "2001-03-02 06:20:00"
|
||||
AND DateTime <= "2001-03-02 06:30:00"
|
||||
AND wwRetrievalMode = "Cyclic"
|
||||
AND wwResolution = 60000
|
||||
');
|
||||
```
|
||||
|
||||
Column types follow tag types. Up to 1024 columns. **Variables can't appear inside the body**; if you need parameterization, build the body string in T-SQL and `EXEC` it.
|
||||
|
||||
## Wide tables in `Delta` mode
|
||||
|
||||
`Delta` over a wide table is well-defined for a **single tag**. With multiple tags, what's a "delta" — the `OR` of all tags changed? — is ambiguous. Stick to `Cyclic` / `Interpolated` for multi-tag wide queries unless the source PDF section "Querying Wide Tables in Delta Retrieval Mode" (p. 131) covers your exact case.
|
||||
|
||||
## Joining a SQL Server table to an extension table — `INNER REMOTE JOIN`
|
||||
|
||||
```sql
|
||||
-- "Give me history for every string-tag with MaxLength = 64"
|
||||
SELECT DateTime, T.TagName, vValue, Quality, QualityDetail
|
||||
FROM StringTag T
|
||||
INNER REMOTE JOIN History H ON T.TagName = H.TagName
|
||||
WHERE T.MaxLength = 64
|
||||
AND DateTime >= '2002-03-10 12:00:00.000'
|
||||
AND DateTime <= '2002-03-10 16:40:00.000'
|
||||
AND wwRetrievalMode = 'Delta';
|
||||
```
|
||||
|
||||
Pattern: **driver SQL Server table on the left, extension table on the right.** Without `INNER REMOTE JOIN`, SQL Server pulls the entire extension table back into the engine before joining — order of magnitude slower.
|
||||
|
||||
## Aggregates inside `OPENQUERY`
|
||||
|
||||
`MIN`, `MAX`, `AVG`, `SUM` are supported inside the body; **`STDEV` is not**.
|
||||
|
||||
```sql
|
||||
SELECT * FROM OPENQUERY(INSQL, '
|
||||
SELECT
|
||||
"Minimum" = MIN(ReactLevel),
|
||||
"Maximum" = MAX(ReactLevel),
|
||||
"Average" = AVG(ReactLevel),
|
||||
"Sum" = SUM(ReactLevel)
|
||||
FROM WideHistory
|
||||
WHERE DateTime > "2001-02-28 18:55:00"
|
||||
AND DateTime < "2001-02-28 19:00:00"
|
||||
AND wwRetrievalMode = "Cyclic"
|
||||
');
|
||||
```
|
||||
|
||||
For `STDEV` or other unsupported aggregates, run the inner `OPENQUERY` to fetch raw rows and apply the aggregate outside.
|
||||
|
||||
## `COUNT(*)` quirks
|
||||
|
||||
`COUNT(*)` works directly in four-part queries:
|
||||
|
||||
```sql
|
||||
SELECT COUNT(*)
|
||||
FROM History
|
||||
WHERE TagName = 'SysTimeSec'
|
||||
AND DateTime >= '2001-12-20 00:00'
|
||||
AND DateTime <= '2001-12-20 00:05'
|
||||
AND wwRetrievalMode = 'delta'
|
||||
AND Value >= 30;
|
||||
```
|
||||
|
||||
Inside `OPENQUERY`, you cannot apply arithmetic to `COUNT(*)`. Push the count into the inner query and do math outside:
|
||||
|
||||
```sql
|
||||
SELECT COUNT(*), COUNT(*)/2 FROM OPENQUERY(INSQL, '
|
||||
SELECT DateTime, vValue, Quality, QualityDetail
|
||||
FROM History
|
||||
WHERE TagName IN ("SysTimeSec")
|
||||
AND DateTime >= "2002-04-16 03:00:00.000"
|
||||
AND DateTime <= "2002-04-16 06:00:00.000"
|
||||
AND wwRetrievalMode = "Delta"
|
||||
');
|
||||
```
|
||||
|
||||
## Arithmetic across columns
|
||||
|
||||
In `OPENQUERY`, math operators **need spaces around them**:
|
||||
|
||||
```sql
|
||||
SELECT * FROM OPENQUERY(INSQL, '
|
||||
SELECT DateTime, ReactLevel, Prodlevel, Sum = ReactLevel + Prodlevel
|
||||
FROM WideHistory
|
||||
WHERE DateTime > "2001-02-28 18:55:00"
|
||||
AND DateTime < "2001-02-28 19:00:00"
|
||||
AND wwRetrievalMode = "Cyclic"
|
||||
');
|
||||
```
|
||||
|
||||
`ReactLevel+Prodlevel` (no spaces) parses as a column-name suffix and breaks. `ReactLevel + Prodlevel` works.
|
||||
|
||||
## `GROUP BY` outside, raw rows inside
|
||||
|
||||
`GROUP BY` doesn't work inside `OPENQUERY`. Wrap and group outside:
|
||||
|
||||
```sql
|
||||
SELECT TagName,
|
||||
AvgValue = AVG(CAST(Value AS FLOAT))
|
||||
FROM OPENQUERY(INSQL, '
|
||||
SELECT TagName, Value FROM History
|
||||
WHERE TagName IN ("SysTimeSec","SysTimeMin")
|
||||
AND DateTime >= "2024-01-01 00:00"
|
||||
AND DateTime <= "2024-01-01 01:00"
|
||||
AND wwRetrievalMode = "Delta"
|
||||
') x
|
||||
GROUP BY TagName;
|
||||
```
|
||||
|
||||
## Variant `vValue` filtering
|
||||
|
||||
Always cast — see [`02-syntax-limits.md`](02-syntax-limits.md#convert-on-vvalue-inside-openquery):
|
||||
|
||||
```sql
|
||||
SELECT DateTime, vValue
|
||||
FROM OPENQUERY(INSQL, '
|
||||
SELECT DateTime, vValue
|
||||
FROM History
|
||||
WHERE TagName IN ("SomeFloatTag")
|
||||
AND DateTime >= "2024-01-01 00:00"
|
||||
AND DateTime <= "2024-01-01 01:00"
|
||||
AND wwRetrievalMode = "Delta"
|
||||
') x
|
||||
WHERE CONVERT(float, vValue) = 20.0;
|
||||
```
|
||||
|
||||
## Time-zone-aware queries
|
||||
|
||||
`wwTimeZone` re-projects timestamps into the named zone. SQL Server still does the **datetime math in server local time** before passing the predicates down — so `DATEADD(...)` works in server local, then results come back in the requested zone.
|
||||
|
||||
```sql
|
||||
SELECT DateTime, TagName, Value
|
||||
FROM History
|
||||
WHERE TagName IN ('SysTimeHour','SysTimeMin','SysTimeSec')
|
||||
AND DateTime > DATEADD(mi, -30, GETDATE())
|
||||
AND wwTimeZone = 'Eastern Daylight Time';
|
||||
```
|
||||
|
||||
## Time between value changes (`wwResolution` on output)
|
||||
|
||||
In `Cyclic`, `Delta`, `Full`, the `wwResolution` virtual column on output reports milliseconds since the previous row for the same tag. `-1` means the gap exceeded ~25 days (`> 2,147,000,000` ms). Doesn't apply to `AnalogSummaryHistory` / `StateSummaryHistory`.
|
||||
|
||||
```sql
|
||||
SELECT DateTime, Value, wwResolution
|
||||
FROM History
|
||||
WHERE TagName = 'PumpRunning'
|
||||
AND DateTime >= '2024-01-01 00:00'
|
||||
AND DateTime < '2024-01-02 00:00'
|
||||
AND wwRetrievalMode = 'Delta';
|
||||
```
|
||||
|
||||
Total time the value matched a condition — sum the resolutions:
|
||||
|
||||
```sql
|
||||
SELECT SUM(wwResolution) AS TotalMs
|
||||
FROM OPENQUERY(INSQL, '
|
||||
SELECT DateTime, Total = Pump1 + Pump2, wwResolution
|
||||
FROM WideHistory
|
||||
WHERE DateTime >= "2012-03-08 16:00"
|
||||
AND DateTime < "2012-03-08 17:00"
|
||||
AND wwRetrievalMode = "DELTA"
|
||||
') x
|
||||
WHERE Total = 0;
|
||||
```
|
||||
|
||||
## `SELECT INTO` from history
|
||||
|
||||
Materializing a slice into a normal SQL Server table is supported via `OPENQUERY`:
|
||||
|
||||
```sql
|
||||
DROP TABLE IF EXISTS dbo.MyTable;
|
||||
|
||||
SELECT DateTime,
|
||||
Sec = DATEPART(ss, DateTime),
|
||||
mS = DATEPART(ms, DateTime),
|
||||
ReactTemp, ReactLevel
|
||||
INTO dbo.MyTable
|
||||
FROM OPENQUERY(INSQL, '
|
||||
SELECT DateTime, ReactTemp, ReactLevel
|
||||
FROM WideHistory
|
||||
WHERE DateTime >= "2001-03-02 06:00"
|
||||
AND DateTime <= "2001-03-02 07:00"
|
||||
AND wwRetrievalMode = "Cyclic"
|
||||
AND wwResolution = 60000
|
||||
');
|
||||
```
|
||||
|
||||
## `SliceBy` (analog summary tables)
|
||||
|
||||
`AnalogSummaryHistory` supports a `SliceBy` column that buckets results by the value of *another* tag at the same time. Useful for "summarize flow when a valve was in each state":
|
||||
|
||||
```sql
|
||||
SELECT SliceByValue, TagName,
|
||||
StartDateTime = MIN(StartDateTime),
|
||||
EndDateTime = MAX(EndDateTime),
|
||||
TotalTime = SUM(wwResolution),
|
||||
Maximum = MAX(Maximum),
|
||||
Total = SUM(Integral),
|
||||
Average = SUM(Average*wwResolution)/SUM(wwResolution),
|
||||
AvgOfAvg = AVG(Average)
|
||||
FROM AnalogSummaryHistory
|
||||
WHERE TagName = 'M31.FlowIn'
|
||||
AND SliceBy = 'M31.ValveIn'
|
||||
AND EndDateTime >= '2018-11-27 00:00'
|
||||
AND StartDateTime <= '2018-11-28 00:00'
|
||||
GROUP BY TagName, SliceByValue;
|
||||
```
|
||||
|
||||
## Annotations
|
||||
|
||||
Free-text notes attached to a tag at a moment. Insert, then query:
|
||||
|
||||
```sql
|
||||
DECLARE @UserKey INT;
|
||||
SELECT @UserKey = UserKey FROM UserDetail WHERE UserName = 'wwAdmin';
|
||||
|
||||
INSERT INTO Annotation (TagName, UserKey, DateTime, Content)
|
||||
VALUES ('ReactLevel', @UserKey, GETDATE(), 'The pump is off');
|
||||
|
||||
SELECT DateTime, TagName, Content
|
||||
FROM Annotation
|
||||
WHERE TagName = 'ReactLevel'
|
||||
AND DateTime > '2024-02-27'
|
||||
AND DateTime <= GETDATE();
|
||||
```
|
||||
|
||||
## Bitwise extraction from a packed integer tag
|
||||
|
||||
When multiple discrete signals are packed into one PLC register and that register is mapped to a single Historian tag, use bitwise SQL:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
CONVERT(BIT, CAST(Value AS INT) & 1) AS Bit0,
|
||||
CONVERT(BIT, CAST(Value AS INT) & 2) AS Bit1,
|
||||
CONVERT(BIT, CAST(Value AS INT) & 4) AS Bit2,
|
||||
CONVERT(BIT, CAST(Value AS INT) & 8) AS Bit3
|
||||
FROM dbo.History
|
||||
WHERE TagName = 'PackedDigitals'
|
||||
AND DateTime >= '2024-01-01 00:00'
|
||||
AND DateTime < '2024-01-01 01:00'
|
||||
AND wwRetrievalMode = 'Delta';
|
||||
```
|
||||
|
||||
`CAST(... AS INT)` caps the addressable bits at 32. Add `2^(bit-1)` for each subsequent bit.
|
||||
|
||||
## Comparison operators with `Delta`
|
||||
|
||||
The boundary semantics matter. With `>=` on the start, an initial value is synthesized at the start time. With `>`, no initial value is returned. Same for `<` / `<=` on the end. See PDF pp. 149-153 for the full grid.
|
||||
|
||||
```sql
|
||||
-- "Anything strictly within the window, no boundary synthesis"
|
||||
WHERE DateTime > '2024-01-01 00:00'
|
||||
AND DateTime < '2024-01-02 00:00'
|
||||
AND wwRetrievalMode = 'Delta';
|
||||
```
|
||||
|
||||
## Retrieval across a data gap
|
||||
|
||||
When a tag has a gap (I/O server disconnect, undeployed object), retrieval inserts well-defined boundary points:
|
||||
|
||||
| Boundary | `Value` | `Quality` | `QualityDetail` |
|
||||
| --- | --- | --- | --- |
|
||||
| Last point before gap (closing point) | last good value | varies | varies |
|
||||
| First point after gap (initial of next block) | snapshot | 0 | 96 (0x60 = 150 decimal in some prints) |
|
||||
| Cyclic boundary inside the gap | `NULL` | 256 (0x100) | 0 |
|
||||
|
||||
Quality / value-deadband behavior across gaps: a value deadband is **not** applied across a NULL — every value immediately after a NULL is returned. A time deadband still suppresses values that fall within its window even across a gap.
|
||||
|
||||
## Mixing `wwCycleCount`, `wwResolution`, and `wwRetrievalMode`
|
||||
|
||||
Set behavior table (PDF p. 142):
|
||||
|
||||
| Mode | `wwResolution` | `wwCycleCount` | Result |
|
||||
| --- | --- | --- | --- |
|
||||
| `Cyclic` | N | 0 / unset | Apply N-ms resolution to all stored data in window. |
|
||||
| `Cyclic` | 0 / unset | 0 | Server returns **100,000** rows per tag. |
|
||||
| `Cyclic` | 0 / unset | N | N evenly-spaced rows. |
|
||||
| `Cyclic` | N | (any — ignored) | N-ms resolution wins; cycle count ignored. |
|
||||
| `Cyclic` | (none) | (none / negative) | **100** rows per tag (default). |
|
||||
| `Delta` / `Full` | (any — ignored) | N | First N matching rows. |
|
||||
| `Delta` / `Full` | (any — ignored) | (none) | All matching rows in the window. |
|
||||
|
||||
## Bounding-value behavior with stored proc / cursors
|
||||
|
||||
Any normal SQL Server stored procedure or server-side cursor can wrap an `INSQL` query — four-part, `OPENQUERY`, `OPENROWSET`, parameterized queries, views — to encapsulate complex retrieval. Useful for parameterizing what `OPENQUERY` itself can't.
|
||||
|
||||
## Aggregate same data four ways
|
||||
|
||||
The four documented ways to summarize:
|
||||
|
||||
1. SQL Server `AVG(Value)` over `Cyclic` rows — discrete sample average.
|
||||
2. `wwRetrievalMode = 'Average'` — time-weighted, the usual right answer for analog process data.
|
||||
3. `AnalogSummaryHistory` after configuring summary replication — pre-computed, cheapest at query time.
|
||||
4. Event-subsystem summary into the `SummaryData` table — driven by the Event subsystem (which uses simple `AVG`).
|
||||
|
||||
Pick replication when the query rate is high and the summary is stable. Pick the `Average` mode when ad-hoc.
|
||||
|
||||
## Next
|
||||
|
||||
- [`06-alarms-events.md`](06-alarms-events.md) — alarm/event-specific queries.
|
||||
- [`07-rest-api.md`](07-rest-api.md) — same data, no SQL needed.
|
||||
@@ -0,0 +1,214 @@
|
||||
# Alarms and events SQL
|
||||
|
||||
The Historian's `Events` table holds records emitted by Application Server's alarm/event subsystem — set, acknowledged, cleared, plus user actions (writes, secured/verified writes). This is **not** the legacy "Classic Event subsystem" — that's a separate data path documented elsewhere in the source PDF.
|
||||
|
||||
## Time columns and `wwTimeZone`
|
||||
|
||||
Two ways `Events` expresses time:
|
||||
|
||||
- **`EventTimeUtc`** — UTC. Not affected by `wwTimeZone`.
|
||||
- **`EventTime`** (and similar) — local time on the Historian server, projected through `wwTimeZone` if it's set.
|
||||
|
||||
Pick one and stick with it within a query, or you'll cross zones unexpectedly when DST flips.
|
||||
|
||||
## Schema essentials
|
||||
|
||||
The columns referenced in the recipes below — there are more, see PDF p. 175+:
|
||||
|
||||
| Column | Meaning |
|
||||
| --- | --- |
|
||||
| `EventTime`, `EventTimeUtc` | When the event occurred (local / UTC). |
|
||||
| `ReceivedTime` | When the Historian received it. |
|
||||
| `Type` | `Alarm.Set`, `Alarm.Acknowledge`, `Alarm.Clear`, `Alarm.Write`, `User.Write`, `User.Write.Secured`, `User.Write.Verified`, `Application.Write`, … |
|
||||
| `Alarm_ID` | GUID identifying the alarm instance — same across `Set` / `Ack` / `Clear` rows. |
|
||||
| `Alarm_State` | `UNACK_ALM`, `ACK_ALM`, `UNACK_RTN`, `ACK_RTN`. |
|
||||
| `Alarm_DurationMs` | Total alarm life. |
|
||||
| `Alarm_UnAckDurationMs` | Time spent unacknowledged. |
|
||||
| `Severity` | 1 = Critical, 2 = Major, 3 = Minor, 4 = Informational. |
|
||||
| `Priority` | 1-999, lower = higher priority. |
|
||||
| `Source_Area`, `Source_Object`, `Source_ConditionVariable` | Origin of the alarm. |
|
||||
| `User_Name` | Operator who performed an action (ack, write). |
|
||||
| `IsAlarm` | `true` for alarm-related events, `false` for plain events. |
|
||||
| `Namespace` | The event tag's namespace. |
|
||||
|
||||
Property values are **case-sensitive** — `"TRUE"`, `"True"`, and `"true"` are distinct strings as stored.
|
||||
|
||||
## Recipe 1 — list every event in a window
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM Events
|
||||
WHERE EventTime BETWEEN '2015-10-25 00:00' AND '2015-10-26 00:00';
|
||||
```
|
||||
|
||||
Use this as a sanity probe — if it's empty, your time-zone or window assumptions are off.
|
||||
|
||||
## Recipe 2 — average alarm rate per hour
|
||||
|
||||
```sql
|
||||
DECLARE @StartTime varchar(60) = '2015-10-25 12:00:00';
|
||||
DECLARE @EndTime varchar(60) = '2015-10-26 12:00:00';
|
||||
|
||||
DECLARE @AlarmRaise TABLE (
|
||||
EventTime nvarchar(60),
|
||||
ID nvarchar(50),
|
||||
AlarmState nvarchar(20),
|
||||
SourceArea nvarchar(20),
|
||||
SourceObject nvarchar(20)
|
||||
);
|
||||
INSERT @AlarmRaise
|
||||
SELECT EventTime, Alarm_ID, Alarm_State, Source_Area, Source_Object
|
||||
FROM Events
|
||||
WHERE EventTime > @StartTime AND EventTime < @EndTime
|
||||
AND Alarm_State = 'UNACK_ALM';
|
||||
|
||||
DECLARE @AlarmCounts TABLE (ForDate date, OnHour int, CountPerHour int);
|
||||
INSERT @AlarmCounts
|
||||
SELECT CAST(EventTime AS date), DATEPART(hour, EventTime), COUNT(*)
|
||||
FROM @AlarmRaise
|
||||
GROUP BY CAST(EventTime AS date), DATEPART(hour, EventTime);
|
||||
|
||||
SELECT AVG(CAST(CountPerHour AS INT)) AS [Average Alarm Rate on Hourly Basis]
|
||||
FROM @AlarmCounts;
|
||||
```
|
||||
|
||||
Pattern: stage matching alarm rows into a table variable, group, average. Filter on `Alarm_State = 'UNACK_ALM'` to count only fresh alarms (not re-emissions).
|
||||
|
||||
## Recipe 3 — most frequent alarms per hour
|
||||
|
||||
```sql
|
||||
DECLARE @StartTime varchar(60) = '2017-11-10 12:00:00';
|
||||
DECLARE @EndTime varchar(60) = '2017-11-10 12:10:00';
|
||||
|
||||
DECLARE @AlarmRaise TABLE (
|
||||
EventTime nvarchar(60),
|
||||
ID nvarchar(50),
|
||||
AlarmState nvarchar(20),
|
||||
SourceArea nvarchar(20),
|
||||
SourceObject nvarchar(20),
|
||||
SourceConditionVariable nvarchar(40)
|
||||
);
|
||||
INSERT @AlarmRaise
|
||||
SELECT EventTime, Alarm_ID, Alarm_State,
|
||||
Source_Area, Source_Object, Source_ConditionVariable
|
||||
FROM Events
|
||||
WHERE EventTime > @StartTime AND EventTime < @EndTime
|
||||
AND Alarm_State = 'UNACK_ALM';
|
||||
|
||||
SELECT CAST(EventTime AS date) AS ForDate,
|
||||
DATEPART(hour, EventTime) AS OnHour,
|
||||
COUNT(*) AS [Count per Hour],
|
||||
SourceObject + SourceConditionVariable AS [Alarm Attribute]
|
||||
FROM @AlarmRaise
|
||||
GROUP BY CAST(EventTime AS date),
|
||||
DATEPART(hour, EventTime),
|
||||
SourceObject, SourceConditionVariable
|
||||
ORDER BY ForDate ASC, OnHour, [Alarm Attribute];
|
||||
```
|
||||
|
||||
## Recipe 4 — alarms by area / object
|
||||
|
||||
```sql
|
||||
SELECT Source_Area AS [Source Area/Object], COUNT(*) AS [Total]
|
||||
FROM Events
|
||||
WHERE EventTime > '2015-10-25 12:00:00'
|
||||
AND EventTime < '2015-10-26 12:00:00'
|
||||
AND Alarm_State = 'UNACK_ALM'
|
||||
GROUP BY Source_Area
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT Source_Object, COUNT(*)
|
||||
FROM Events
|
||||
WHERE EventTime > '2015-10-25 12:00:00'
|
||||
AND EventTime < '2015-10-26 12:00:00'
|
||||
AND Alarm_State = 'UNACK_ALM'
|
||||
GROUP BY Source_Object;
|
||||
```
|
||||
|
||||
Useful for "where in the plant are alarms coming from."
|
||||
|
||||
## Recipe 5 — average time-to-acknowledge per hour and severity
|
||||
|
||||
```sql
|
||||
DECLARE @start datetime2 = '2017-12-11';
|
||||
DECLARE @end datetime2 = '2017-12-12';
|
||||
|
||||
SELECT DATEADD(hour, DATEDIFF(hour, 0, e.EventTime), 0) AS hour,
|
||||
e.Severity,
|
||||
AVG(Alarm_UnAckDurationMs) AS avg_unack,
|
||||
COUNT(*) AS count
|
||||
FROM Events e
|
||||
WHERE e.EventTime < @end
|
||||
AND e.EventTime >= @start
|
||||
AND e.Severity <= 3 -- 1=Critical, 2=High, 3=Medium, (4=Low)
|
||||
AND e.Type = 'Alarm.Acknowledged'
|
||||
GROUP BY DATEADD(hour, DATEDIFF(hour, 0, e.EventTime), 0), Severity
|
||||
ORDER BY hour, Severity;
|
||||
```
|
||||
|
||||
`Alarm_UnAckDurationMs` is set on the `Alarm.Acknowledged` row; that's why you filter on `Type = 'Alarm.Acknowledged'`.
|
||||
|
||||
To break it down by user instead:
|
||||
|
||||
```sql
|
||||
SELECT DATEADD(hour, DATEDIFF(hour, 0, e.EventTime), 0) AS hour,
|
||||
e.User_Name,
|
||||
AVG(Alarm_UnAckDurationMs) AS avg_unack,
|
||||
COUNT(*) AS count
|
||||
FROM Events e
|
||||
WHERE e.EventTime BETWEEN @start AND @end
|
||||
AND e.Type = 'Alarm.Acknowledged'
|
||||
GROUP BY DATEADD(hour, DATEDIFF(hour, 0, e.EventTime), 0), e.User_Name;
|
||||
```
|
||||
|
||||
## Recipe 6 — alarm life cycle (raise → ack → clear)
|
||||
|
||||
```sql
|
||||
DECLARE @StartTime varchar(60) = '2017-12-12 12:00:00';
|
||||
DECLARE @EndTime varchar(60) = '2017-12-12 12:02:00';
|
||||
|
||||
DECLARE @AlarmRaise TABLE (EventTime nvarchar(60), ID nvarchar(50), AlarmState nvarchar(20));
|
||||
INSERT @AlarmRaise
|
||||
SELECT EventTime, Alarm_ID, Alarm_State FROM Events
|
||||
WHERE EventTime > @StartTime AND EventTime < @EndTime
|
||||
AND Alarm_State = 'UNACK_ALM';
|
||||
|
||||
DECLARE @AlarmAck TABLE (EventTime nvarchar(60), ID nvarchar(50), UnAckDuration nvarchar(20));
|
||||
INSERT @AlarmAck
|
||||
SELECT EventTime, Alarm_ID, Alarm_UnAckDurationMs FROM Events
|
||||
WHERE EventTime > @StartTime AND EventTime < @EndTime
|
||||
AND Alarm_Acknowledged = 1;
|
||||
|
||||
DECLARE @AlarmClear TABLE (EventTime nvarchar(60), ID nvarchar(50), AlarmDuration nvarchar(20));
|
||||
INSERT @AlarmClear
|
||||
SELECT EventTime, Alarm_ID, Alarm_DurationMs FROM Events
|
||||
WHERE EventTime > @StartTime AND EventTime < @EndTime
|
||||
AND Type = 'Alarm.Clear';
|
||||
|
||||
SELECT 'Alarm Life - ' + s.ID AS Tag,
|
||||
CASE WHEN a.EventTime > c.EventTime THEN 'Cleared Before Ack'
|
||||
WHEN a.EventTime < c.EventTime THEN 'Acked Before Clear'
|
||||
ELSE '-' END AS Comment,
|
||||
s.EventTime AS AlarmRaised,
|
||||
a.EventTime AS AlarmAcked,
|
||||
c.EventTime AS AlarmClear,
|
||||
a.UnAckDuration AS UnAckDuration,
|
||||
c.AlarmDuration AS AlarmDuration
|
||||
FROM (@AlarmRaise s INNER JOIN @AlarmClear c ON c.ID = s.ID)
|
||||
LEFT JOIN @AlarmAck a ON a.ID = c.ID AND a.EventTime <> c.EventTime
|
||||
ORDER BY AlarmRaised ASC;
|
||||
```
|
||||
|
||||
The `LEFT JOIN` keeps unacknowledged-but-cleared alarms in the report. The `Cleared Before Ack` / `Acked Before Clear` heuristic is useful for diagnosing alarm-flood vs. proper-handling patterns.
|
||||
|
||||
## Notes
|
||||
|
||||
- **Alarm IDs are GUIDs and persist across the lifecycle.** Joining `Set` / `Ack` / `Clear` rows by `Alarm_ID` is the only safe way to reconstruct an alarm timeline.
|
||||
- **Severity values are inverted relative to priority** — 1 is highest severity, 4 is lowest. Don't confuse with `Priority` (1 = highest priority, 999 = lowest).
|
||||
- **`Alarm_State`** uses two-character suffixes: `_ALM` for active, `_RTN` for return-to-normal. Combine with `UNACK_` / `ACK_` for the four states.
|
||||
- The PDF chapter dedicates pp. 175-181 to these patterns. For full property catalog see PDF pp. 202-207.
|
||||
|
||||
## Next
|
||||
|
||||
- [`07-rest-api.md`](07-rest-api.md) — same data over HTTP.
|
||||
@@ -0,0 +1,172 @@
|
||||
# Historian Data REST API
|
||||
|
||||
For when the consumer can't (or shouldn't) speak SQL — browsers, Excel, Power BI, Tableau, web frontends. Same data as the SQL extension tables, exposed as OData over HTTPS.
|
||||
|
||||
## Versions
|
||||
|
||||
Two API versions are supported by AVEVA Insight:
|
||||
|
||||
| Version | Recommended? | Notes |
|
||||
| --- | --- | --- |
|
||||
| `v2` | **Yes** | `TagFilter` (POST/GET) on `ProcessValues`, `AnalogSummary`, `StateSummary`. Single quotes are NOT used around `DateTime` in `$filter` (except for events). Summary entities use `StartDateTime` / `EndDateTime`; raw / process value entities use `DateTime`. |
|
||||
| `v1` | Legacy | Still works. Uses `datetimeoffset'...'` quoting. Includes summary entities (`Summary`, `Daily`, `Hourly`, `Minutely`) that are v1-only. |
|
||||
|
||||
Pick **v2** for new code unless you need a v1-only summary entity.
|
||||
|
||||
## Authentication
|
||||
|
||||
Two flavors:
|
||||
|
||||
- **AVEVA Historian Insight (on-premises)** — Windows integrated security via Negotiate. The user must belong to `aaAdministrators`, `aaPowerUsers`, or `aaUsers`. Most modern browsers and Excel handle this transparently.
|
||||
- **AVEVA Insight (cloud)** — OpenID Connect bearer-token, or Basic auth (`username` is treated as the API key). Some clients (older Excel) lack OIDC support; Basic remains as a fallback.
|
||||
|
||||
There's also a "solution-specific retrieval token" path: include the token in the URL and skip the `s/<solution_id>` segment.
|
||||
|
||||
## Endpoint shape
|
||||
|
||||
```
|
||||
https://online.wonderware.com/s/<solution_id>/apis/historian/<api_version>/<resource>?<query_parameters>
|
||||
```
|
||||
|
||||
- `<solution_id>` — your Insight solution. Omitted when using a solution-specific retrieval token.
|
||||
- `<api_version>` — `v1` or `v2`.
|
||||
- `<resource>` — one of the resources below.
|
||||
- `<query_parameters>` — OData (`$filter`, `$top`, `$skip`, `$orderby`, `$select`, `$skiptoken`) or REST-style (`TagFilter=...&Resolution=...`).
|
||||
|
||||
In the URL: **`%20`** is space, **`%27`** is a single quote. The Chrome JSONView viewer also accepts `+` for space, which makes URLs more readable.
|
||||
|
||||
## OData operators
|
||||
|
||||
`eq`, `ne`, `gt`, `ge`, `lt`, `le`, `and`, `or`, `not`. Use parentheses for grouping. **A `$filter` may contain at most one time clause combined with one filter clause via `and`.** The filter clause itself may use any combination of operators internally.
|
||||
|
||||
If you need multiple values for the same field, use `or`:
|
||||
|
||||
```
|
||||
((Priority eq 100 or Priority eq 200 or Priority eq 500) and (Severity le 2))
|
||||
```
|
||||
|
||||
## `TagFilter` (REST-style)
|
||||
|
||||
Supported on `ProcessValues`, `AnalogSummary`, `StateSummary` (`v2`+). OData filter notation, with hard limits:
|
||||
|
||||
- Up to **20** `AND` clauses.
|
||||
- Up to **20** `OR` clauses.
|
||||
- Up to **2** UDF (user-defined function) clauses.
|
||||
- **Cannot mix `AND` and `OR`** in the same `TagFilter`.
|
||||
- Operators must be lowercase (`and`, not `And`).
|
||||
- Most attribute searches are case-insensitive; case-sensitive ones are: `InterpolationType`, `MessageOn`, `MessageOff`.
|
||||
|
||||
Valid:
|
||||
|
||||
```
|
||||
.../v2/ProcessValues?TagFilter=startswith(Source,'MVDS') and TagType eq 'string'
|
||||
```
|
||||
|
||||
Invalid (mixed-case operator):
|
||||
|
||||
```
|
||||
.../v2/ProcessValues?TagFilter=startswith(Source,'MVDS') And TagType eq 'string'
|
||||
```
|
||||
|
||||
## Retrieval resources (the catalog)
|
||||
|
||||
| Resource | Verb(s) | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `/ProcessValues` | GET | Raw value+time+quality (VTQ) records for one or more tags. |
|
||||
| `/AnalogSummary` | GET | Aggregated analog statistics (min/max/avg/integral/etc.) over a window. |
|
||||
| `/StateSummary` | GET | Time-in-state summaries for discrete/string/integer tags. |
|
||||
| `/Events` | GET | Events and alarms (same content as the SQL `Events` table). |
|
||||
| `/Tags` | GET / POST / DELETE | Tag metadata; v2 also returns extended properties. POST manages tags in bulk. |
|
||||
| `/TagProperties` | GET | List of property names available on tags. |
|
||||
| `/TagPropertyValues` | GET | Distinct values seen for a given tag property. |
|
||||
| `/TagGroups` | GET | Groups of tags. |
|
||||
| `/TagSuggest` | GET | Autocomplete-style tag-name suggestions. |
|
||||
| `/TagSearch` | GET | Search tags by partial name / property. |
|
||||
| `/TagExtendedProperties` | GET | Extended (custom) tag properties. |
|
||||
| `/Summary` | GET | **v1-only.** Generic summary. |
|
||||
| `/Daily`, `/Hourly`, `/Minutely` | GET | **v1-only.** Pre-bucketed summary periods. |
|
||||
| `/SystemParameters` | GET | **On-premises only.** Server config. |
|
||||
|
||||
## `RetrievalMode` and `Resolution` (REST-side names)
|
||||
|
||||
Same modes as the SQL `wwRetrievalMode`. The REST query parameter is named `RetrievalMode`. Supported values from the v2 spec: `BestFit`, `Counter`, `Delta`, `Full`, `Interpolated`, `Maximum`, `Slope` (and `Cyclic`, the default for analog).
|
||||
|
||||
`Resolution` is in milliseconds, applies to `Raw`, `ProcessValues`, and the summary entities — picks the granularity of the returned data.
|
||||
|
||||
## `ProcessValues` — VTQ records
|
||||
|
||||
```http
|
||||
GET /v2/ProcessValues?$filter=FQN+eq+'plant12.pump6'+and+DateTime+gt+2017-07-13T00:00:00Z
|
||||
```
|
||||
|
||||
Required: `FQN` (`datasource.tagname`), and a time predicate (`DateTime` for v2). Optional: `OPCQuality`, `Value`, `Text` (string tags / discrete-tag message text). Returns 200 with `{ fqn, datetime, ... }`. 404 / 401 on missing FQN / unauthorized.
|
||||
|
||||
## `AnalogSummary` — windowed analog stats
|
||||
|
||||
```http
|
||||
GET /v2/AnalogSummary
|
||||
?$filter=FQN+eq+'Depot.Train09'
|
||||
+and+StartDateTime+ge+2017-06-09T09:00:00-07:00
|
||||
+and+EndDateTime+le+2017-06-09T10:00:00-07:00
|
||||
&Resolution=3600000
|
||||
```
|
||||
|
||||
Required: `FQN`, `StartDateTime`, `EndDateTime` (RFC 3339 / ISO 8601, UTC `Z` or explicit offset). Optional: `RetrievalMode` (`Cyclic`/`Full`, default `Cyclic`), `Resolution`, `SliceBy` (split summary by another tag's value at the same time, up to 10 SliceBy tags), `SliceByValue` (filter criterion for SliceBy), `OPCQuality`. Quality logic mirrors the SQL `wwQualityRule = 'Good'`: all-Good → Good, full gap → Bad, mixed → Doubtful (`64`).
|
||||
|
||||
## `StateSummary` — windowed time-in-state
|
||||
|
||||
Same shape as `AnalogSummary` but for state-tag data. Use `RetrievalMode=Cyclic` and a `Resolution` to bucket by time.
|
||||
|
||||
## `Events` — alarms and events
|
||||
|
||||
```http
|
||||
GET /v2/Events?$filter=EventTime+gt+'2017-07-13T00:00:00'
|
||||
```
|
||||
|
||||
Note the **single quotes around the datetime** for events specifically — events break the v2 "no quotes" rule because `EventTime` is treated as a string-style column.
|
||||
|
||||
Filterable optional parameters include `ID` (alarm GUID), `EventTime`, `Type` (`Alarm.Set` / `Alarm.Acknowledge` / `Alarm.Clear` / `Alarm.Write` / `User.Write` / `User.Write.Secured` / `User.Write.Verified` / `Application.Write`), `Priority` (1-999, lower = higher), `Namespace`, `Severity` (1=Critical, 2=Major, 3=Minor, 4=Informational), `EventTimeUTCOffsetMins`, `ReceivedTime`, `IsAlarm`. **Property values are case-sensitive.**
|
||||
|
||||
## Pagination — `$skiptoken`
|
||||
|
||||
Responses larger than ~5000 records are paginated. The response body includes a `nextLink` containing `$skiptoken=...` — follow it to fetch the next page. You don't need to construct `$skiptoken` yourself.
|
||||
|
||||
## Worked browser query
|
||||
|
||||
```
|
||||
https://online.wonderware.com/s/ik97r5/apis/historian/v2/ProcessValues
|
||||
?$filter=FQN+eq+'Baytown.tank_level'
|
||||
+and+DateTime+gt+2017-07-13T00:00:00
|
||||
&$top=200
|
||||
```
|
||||
|
||||
Returns up to 200 raw VTQ records since the timestamp.
|
||||
|
||||
## Excel / Power BI hookups
|
||||
|
||||
The PDF chapter (pp. 232-240) walks through:
|
||||
|
||||
- **Excel** — `Data → Get Data → From Web`, paste the OData URL, sign in. Excel can refresh the connection on demand.
|
||||
- **Postman** — pick GET, paste URL, set Auth (Basic for cloud / Negotiate for on-prem), Send.
|
||||
- **Power BI** — `Get Data → OData feed`, paste the resource root (`.../v2/ProcessValues` or similar), authenticate, then build the report.
|
||||
|
||||
The shape of the data Power BI sees is identical to a JSON pull, so any aggregation you'd do in Power Query mirrors what `$filter` would have selected server-side.
|
||||
|
||||
## SSRS — Reporting Services extension
|
||||
|
||||
The **AVEVA Historian SQL Server Reporting Services Extension** lets SSRS report builders consume the same `INSQL` linked server queries documented in [`05-query-recipes.md`](05-query-recipes.md). PDF p. 241. Useful when the consumer is corporate reporting infrastructure rather than ad-hoc analysts.
|
||||
|
||||
## Errors
|
||||
|
||||
| Code | Meaning |
|
||||
| --- | --- |
|
||||
| 200 | OK with body. |
|
||||
| 401 Unauthorized | Token expired / missing membership in the `aa*` Windows groups. |
|
||||
| 404 Not Found | FQN doesn't exist, or solution / resource path wrong. |
|
||||
| 5xx | Historian / Insight server-side issue. Retry with backoff. |
|
||||
|
||||
## Cross-references
|
||||
|
||||
- For the full property catalog of each resource, see PDF chapter "Browser-Friendly Data Retrieval" pp. 183-240.
|
||||
- For SQL equivalents of every retrieval mode the REST API exposes, see [`03-retrieval-modes.md`](03-retrieval-modes.md).
|
||||
- For options that map across (`Resolution` ↔ `wwResolution`, `RetrievalMode` ↔ `wwRetrievalMode`), see [`04-retrieval-options.md`](04-retrieval-options.md).
|
||||
@@ -0,0 +1,51 @@
|
||||
# histdb
|
||||
|
||||
LLM-oriented reference for **AVEVA Historian** SQL retrieval — the Microsoft SQL Server linked-server (`INSQL`), the extension tables (`History`, `Live`, `WideHistory`, `AnalogSummaryHistory`, `StateSummaryHistory`, `Events`), the `wwXxx` time-domain extensions, every retrieval mode, and the Historian Data REST API. Distilled from the official *AVEVA Historian Retrieval Guide* (December 2020 edition, 243 pages).
|
||||
|
||||
## What this folder is for
|
||||
|
||||
Read-only reference. There's no code here — these files exist so an LLM can answer "how do I get data X out of Historian" without paging through the official PDF. For Galaxy / object configuration the right tool is [`graccesscli`](../graccesscli/README.md); for the System Platform Galaxy Repository (a different SQL database) it's [`grdb`](../grdb/README.md). This folder is specifically the **time-series / event store**.
|
||||
|
||||
## Hard constraints / things to know up front
|
||||
|
||||
- **Historian SQL access goes through a linked server named `INSQL`** that wraps the AVEVA OLE DB provider over the binary history blocks. The OLE DB provider is **stateless** — every query must repeat its `wwXxx` extension parameters.
|
||||
- **A `WHERE` clause is mandatory** on every extension table except `HistoryBlock`. Date ranges (`DateTime >= ... AND DateTime <= ...`) plus tag selection (`TagName = ...` / `TagName IN (...)` / `TagName LIKE ...`) is the minimum.
|
||||
- **All times are UTC internally; the wire format is local time** (or whatever `wwTimeZone` says). Time math uses Win32 `FILETIME` (100 ns resolution).
|
||||
- **Wide tables (`WideHistory`) require `OPENQUERY`** — the schema is built per query. Up to 1024 columns. Query bodies inside `OPENQUERY` are capped at 8000 characters.
|
||||
- **`IN` and `OR` cannot multiplex a `wwXxx` extension.** `wwVersion IN ('original','latest')` and `wwRetrievalMode = 'Delta' OR wwVersion = 'latest'` both fail. One value per extension per query.
|
||||
- **The full PDF is canonical** — these summaries reduce 243 pages to the essentials. When in doubt, check the source: <https://cdn.logic-control.com/docs/aveva/historian/HistorianRetrieval.pdf>.
|
||||
|
||||
## Layout
|
||||
|
||||
```text
|
||||
histdb/
|
||||
README.md this file
|
||||
01-overview.md OLE DB provider, linked server, extension tables, four-part / OPENQUERY / OPENROWSET
|
||||
02-syntax-limits.md Supported / unsupported SQL features, joins, sub-SELECTs, time-domain extensions overview
|
||||
03-retrieval-modes.md Every wwRetrievalMode value (Cyclic, Delta, Full, Interpolated, BestFit, Average, Min, Max, Integral, Slope, Counter, ValueState, RoundTrip, Predictive, BoundingValue)
|
||||
04-retrieval-options.md Every wwXxx parameter (CycleCount, Resolution, deadbands, Version, InterpolationType, TimeStampRule, TimeZone, QualityRule, StateCalc, Filter, ValueSelector)
|
||||
05-query-recipes.md Practical SQL recipes (wide-table joins, INNER REMOTE JOIN, aggregate patterns, time-between-changes, gaps)
|
||||
06-alarms-events.md Events table queries — listing events, alarm rates, time-to-clear, response-time analysis
|
||||
07-rest-api.md Historian Data REST API — endpoints, $filter / OData syntax, auth, examples
|
||||
```
|
||||
|
||||
## Resource index — by task
|
||||
|
||||
| Task | Go to |
|
||||
| --- | --- |
|
||||
| Connect to Historian via SQL Server, pick a query style (four-part / view / OPENQUERY / OPENROWSET) | [`01-overview.md`](01-overview.md) |
|
||||
| What SQL syntax works and what doesn't (LIKE, IN, OR, joins, CONVERT, sub-SELECTs) | [`02-syntax-limits.md`](02-syntax-limits.md) |
|
||||
| Pick the right retrieval mode for the question being asked | [`03-retrieval-modes.md`](03-retrieval-modes.md) |
|
||||
| Tune what the mode returns (cycle count, resolution, deadbands, quality rule, value selector, time zone) | [`04-retrieval-options.md`](04-retrieval-options.md) |
|
||||
| Worked SQL recipes for common questions (wide tables, aggregates, gaps, GROUP BY) | [`05-query-recipes.md`](05-query-recipes.md) |
|
||||
| List events / count alarms / measure time-to-acknowledge | [`06-alarms-events.md`](06-alarms-events.md) |
|
||||
| Pull data from a browser, Excel, Power BI, or any HTTP client (no SQL needed) | [`07-rest-api.md`](07-rest-api.md) |
|
||||
| Original PDF — definitive source for any uncovered detail | <https://cdn.logic-control.com/docs/aveva/historian/HistorianRetrieval.pdf> |
|
||||
|
||||
## Source
|
||||
|
||||
Source PDF: *AVEVA Historian Retrieval Guide* (formerly Wonderware), publication date 3 December 2020, downloaded from `cdn.logic-control.com/docs/aveva/historian/HistorianRetrieval.pdf`. The guide describes Historian retrieval; companion volumes (administration, database reference, glossary) are *not* covered here — see references inside each deep doc.
|
||||
|
||||
## Maintenance
|
||||
|
||||
This folder follows the doctrine in [`../DOCS-GUIDE.md`](../DOCS-GUIDE.md). When AVEVA ships a new Retrieval Guide that adds modes, options, or REST endpoints, update the affected deep doc(s) and bump any new entries into the Resource index above. The root [`../CLAUDE.md`](../CLAUDE.md) holds one row pointing here — don't fan out per-file links from the root.
|
||||
Reference in New Issue
Block a user