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