fix(historian): per-sample Average/Min/Max/Range/ActualTime use Good samples only, Part 13 min/max status (#684)
Part 13 defines Average/Minimum/Maximum/Range/*ActualTime over Good raw values (independent of TreatUncertainAsBad) and the SDK's calculators skip Uncertain values under the server's TreatUncertainAsBad=false. SqlServer/QuestDB value columns now use the good flag (g) instead of usable (u); new UncMin/UncMax columns feed the Part 13 Minimum/Maximum/Range status rule (Uncertain on a Bad sample or an Uncertain value beyond the Good extreme -- the SDK applies only the Bad half). An Uncertain-only interval is BadNoData. Uncertain-bearing parity datasets (in-memory + real SQL) match the SDK unmasked; the beyond-extreme divergence is pinned explicitly. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -32,7 +32,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer;
|
||||
/// and bit 30): <i>usable</i> = severity Good or Uncertain (<c>StatusCode >= 0</c>) AND a stored value
|
||||
/// (<c>ValueKind <> 0</c>) — an <c>Empty</c> sample is an outage marker and counts as Bad whatever its
|
||||
/// code; <i>good</i> = usable with severity Good (<c>StatusCode < 0x40000000</c>); <i>bad</i> = not
|
||||
/// usable. Every value column below is computed over usable samples only.
|
||||
/// usable. Average/Minimum/Maximum/Range/*ActualTime value columns are computed over <i>good</i> samples only
|
||||
/// (#684); Start/End/Delta picks over usable samples (or every sample).
|
||||
/// </para>
|
||||
/// Common result columns (ordinals 0–5): <c>Bucket, TotalCount, UsableCount, GoodCount, BadCount,
|
||||
/// NonNumericCount</c> (usable String/DateTime samples). Family columns follow.
|
||||
@@ -47,8 +48,9 @@ internal static class SqlAggregateQueries
|
||||
|
||||
/// <summary>
|
||||
/// Average / Minimum / Maximum / Count / Range (PercentGood / PercentBad and Total are time-weighted, not here).
|
||||
/// Family columns: <c>6 AvgValue, 7 MinValue, 8 MaxValue, 9 SumValue</c> (float, NULL when no usable
|
||||
/// numeric sample).
|
||||
/// Family columns: <c>6 AvgValue, 7 MinValue, 8 MaxValue, 9 SumValue</c> over the <b>Good</b> numeric samples (float,
|
||||
/// NULL when none — #684: Part 13 and the SDK compute these over Good values only), then <c>10 UncMin, 11 UncMax</c>
|
||||
/// over the Uncertain ones (the Part 13 Minimum/Maximum/Range status rule, <see cref="AggregateEvaluator"/>).
|
||||
/// </summary>
|
||||
/// <param name="sample">Schema-qualified <c>Sample</c> table name.</param>
|
||||
/// <returns>The SQL text.</returns>
|
||||
@@ -56,10 +58,11 @@ internal static class SqlAggregateQueries
|
||||
public static string Simple(string sample, bool reverse = false) =>
|
||||
Classified(sample, reverse) +
|
||||
"SELECT b, " + CommonCounts() + ", " +
|
||||
"AVG(CASE WHEN u = 1 THEN NumValue END) AS AvgValue, " +
|
||||
"MIN(CASE WHEN u = 1 THEN NumValue END) AS MinValue, " +
|
||||
"MAX(CASE WHEN u = 1 THEN NumValue END) AS MaxValue, " +
|
||||
"SUM(CASE WHEN u = 1 THEN NumValue END) AS SumValue " +
|
||||
"AVG(CASE WHEN g = 1 THEN NumValue END) AS AvgValue, " +
|
||||
"MIN(CASE WHEN g = 1 THEN NumValue END) AS MinValue, " +
|
||||
"MAX(CASE WHEN g = 1 THEN NumValue END) AS MaxValue, " +
|
||||
"SUM(CASE WHEN g = 1 THEN NumValue END) AS SumValue, " +
|
||||
UncertainExtremes() + " " +
|
||||
"FROM s GROUP BY b ORDER BY b;";
|
||||
|
||||
/// <summary>Ordinal of the StartEnd family's first-sample-of-any-quality columns (<see cref="StartEnd"/>).</summary>
|
||||
@@ -107,9 +110,9 @@ internal static class SqlAggregateQueries
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MinimumActualTime / MaximumActualTime: the usable sample with the smallest (largest) <c>NumValue</c>,
|
||||
/// MinimumActualTime / MaximumActualTime: the <b>Good</b> sample (#684) with the smallest (largest) <c>NumValue</c>,
|
||||
/// ties broken by the earliest <c>SourceTimeUtc</c>. Family columns: <c>6 NumValue, 7 SourceTimeUtc</c>
|
||||
/// (NULL when the bucket has no usable numeric sample).
|
||||
/// (NULL when the bucket has no Good numeric sample), then <c>8 UncMin, 9 UncMax</c> as in <see cref="Simple"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Deliberately still two references to <c>s</c> (counts + the ranked extreme), unlike <see cref="StartEnd"/>:
|
||||
@@ -124,11 +127,12 @@ internal static class SqlAggregateQueries
|
||||
/// <param name="reverse">True for a reverse read (window <c>(@start, @end]</c>).</param>
|
||||
public static string ActualTime(string sample, bool maximum, bool reverse = false) =>
|
||||
Classified(sample, reverse) +
|
||||
", st AS (SELECT b, " + CommonCounts() + " FROM s GROUP BY b)" +
|
||||
", st AS (SELECT b, " + CommonCounts() + ", " + UncertainExtremes() + " FROM s GROUP BY b)" +
|
||||
", r AS (SELECT b, NumValue, SourceTimeUtc, " +
|
||||
"ROW_NUMBER() OVER (PARTITION BY b ORDER BY NumValue " + (maximum ? "DESC" : "ASC") + ", SourceTimeUtc ASC) AS rn " +
|
||||
"FROM s WHERE u = 1 AND NumValue IS NOT NULL) " +
|
||||
"SELECT st.*, r.NumValue, r.SourceTimeUtc " +
|
||||
"FROM s WHERE g = 1 AND NumValue IS NOT NULL) " +
|
||||
"SELECT st.b, st.TotalCount, st.UsableCount, st.GoodCount, st.BadCount, st.NonNumericCount, " +
|
||||
"r.NumValue, r.SourceTimeUtc, st.UncMin, st.UncMax " +
|
||||
"FROM st LEFT JOIN r ON r.b = st.b AND r.rn = 1 ORDER BY st.b;";
|
||||
|
||||
/// <summary>
|
||||
@@ -187,6 +191,13 @@ internal static class SqlAggregateQueries
|
||||
$"FROM {sample} WHERE TagId = @id AND " +
|
||||
(reverse ? "SourceTimeUtc > @start AND SourceTimeUtc <= @end) " : "SourceTimeUtc >= @start AND SourceTimeUtc < @end) ");
|
||||
|
||||
/// <summary>
|
||||
/// The minimum and maximum <b>Uncertain</b> (usable, not Good) numeric value of the bucket — <c>UncMin, UncMax</c>
|
||||
/// (#684). Not result values: Part 13 makes Minimum/Maximum/Range Uncertain when one lies beyond the Good extreme.
|
||||
/// </summary>
|
||||
private static string UncertainExtremes() =>
|
||||
"MIN(CASE WHEN u = 1 AND g = 0 THEN NumValue END) AS UncMin, MAX(CASE WHEN u = 1 AND g = 0 THEN NumValue END) AS UncMax";
|
||||
|
||||
/// <summary>The five common count columns (after <c>b</c>).</summary>
|
||||
private static string CommonCounts() =>
|
||||
"COUNT(*) AS TotalCount, SUM(u) AS UsableCount, SUM(g) AS GoodCount, COUNT(*) - SUM(u) AS BadCount, " +
|
||||
|
||||
Reference in New Issue
Block a user