# 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.