From 3506a67c28defa35b8d2b8997410ec996f65cddd Mon Sep 17 00:00:00 2001 From: windpacer Date: Mon, 15 Jun 2026 07:13:12 +0900 Subject: [PATCH] =?UTF-8?q?feat(report):=20P1b=20=EC=97=B0=EC=86=8D?= =?UTF-8?q?=EC=A7=91=EA=B3=84=20history=5F1min=20=E2=80=94=201=EC=B4=88=20?= =?UTF-8?q?=EB=B2=84=ED=8D=BC=2060=EC=B4=88=20=EB=A1=A4=EC=97=85(=EC=9E=A5?= =?UTF-8?q?=EA=B8=B0=20=EB=AC=B4=EC=86=90=EC=8B=A4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 온라인 히스토리안 2계층: history_1s(최근 14일, 고해상) + history_1min(장기, 60초). - history_1min 연속집계(timescaledb.continuous): time_bucket('1 min') + last(value)/last(controller_id). refresh 정책(start 3h, end 10min, 5분마다) → 집계 lag ≪ 보존윈도(14일)이라 1초 raw evict 전 materialize. - Hc900FastHistoryService.EnsureSchemaAsync에 cagg 생성+정책 멱등 추가. SQL 파일 동기화. 검증: 2계층 값 일치(1s last == 1min cagg), 정책 활성, 무손실 불변식 충족. Co-Authored-By: Claude Opus 4.8 --- scripts/sql/p1_historian.sql | 13 +++++++++++++ src/Infrastructure/Hc900/Hc900FastHistoryService.cs | 9 +++++++++ 2 files changed, 22 insertions(+) diff --git a/scripts/sql/p1_historian.sql b/scripts/sql/p1_historian.sql index 8a9a89b..70ac2fb 100644 --- a/scripts/sql/p1_historian.sql +++ b/scripts/sql/p1_historian.sql @@ -20,3 +20,16 @@ SELECT add_compression_policy('hc900.history_1s', INTERVAL '6 hours', if_not_exi SELECT add_retention_policy('hc900.history_1s', INTERVAL '14 days', if_not_exists => TRUE); CREATE INDEX IF NOT EXISTS ix_h1s_tag ON hc900.history_1s (tagname, recorded_at DESC); + +-- P1b: 연속집계 history_1min — 1초 버퍼 evict 전 60초 롤업(장기 무손실, 2계층) +CREATE MATERIALIZED VIEW IF NOT EXISTS hc900.history_1min +WITH (timescaledb.continuous) AS +SELECT time_bucket('1 minute', recorded_at) AS bucket, tagname, + last(value, recorded_at) AS value, last(controller_id, recorded_at) AS controller_id +FROM hc900.history_1s GROUP BY bucket, tagname +WITH NO DATA; + +-- 집계 lag(≤3h) ≪ 보존 윈도(14일) → raw 삭제 전 materialize 보장 +SELECT add_continuous_aggregate_policy('hc900.history_1min', + start_offset => INTERVAL '3 hours', end_offset => INTERVAL '10 minutes', + schedule_interval => INTERVAL '5 minutes', if_not_exists => TRUE); diff --git a/src/Infrastructure/Hc900/Hc900FastHistoryService.cs b/src/Infrastructure/Hc900/Hc900FastHistoryService.cs index d931560..72007bb 100644 --- a/src/Infrastructure/Hc900/Hc900FastHistoryService.cs +++ b/src/Infrastructure/Hc900/Hc900FastHistoryService.cs @@ -90,6 +90,15 @@ WHERE tagname = ANY(@tags)"; "SELECT add_compression_policy('hc900.history_1s', INTERVAL '6 hours', if_not_exists => TRUE)", $"SELECT add_retention_policy('hc900.history_1s', INTERVAL '{_retentionDays} days', if_not_exists => TRUE)", "CREATE INDEX IF NOT EXISTS ix_h1s_tag ON hc900.history_1s (tagname, recorded_at DESC)", + // P1b: 연속집계 — 1초 버퍼 evict 전 60초 롤업(장기 무손실) + @"CREATE MATERIALIZED VIEW IF NOT EXISTS hc900.history_1min + WITH (timescaledb.continuous) AS + SELECT time_bucket('1 minute', recorded_at) AS bucket, tagname, + last(value, recorded_at) AS value, last(controller_id, recorded_at) AS controller_id + FROM hc900.history_1s GROUP BY bucket, tagname WITH NO DATA", + @"SELECT add_continuous_aggregate_policy('hc900.history_1min', + start_offset => INTERVAL '3 hours', end_offset => INTERVAL '10 minutes', + schedule_interval => INTERVAL '5 minutes', if_not_exists => TRUE)", }; foreach (var s in stmts) {