ClickHouse 사례 · 통신사 OSS

통신 단말의 데이터를 사람, 서비스, AI 에이전트가 함께 쓰는 OSS 레이크

통신망에 붙은 모든 단말(UE: 휴대폰, IoT 모듈, 차량, 공유기 등)과 장비는 로그, 지표, 이벤트를 끊임없이 냅니다. 이 페이지는 실제 통신사 OSS 플랫폼에서 일반화한 구조를 바탕으로, 그 데이터를 한곳에 모아 누가 어떻게 쓰는지 유즈케이스로 보여 줍니다.

구조는 실제 사례를 일반화한 것이고, 통신사와 도시, 데이터는 가상입니다. 일부 유즈케이스는 이 실습에서 실제로 실행한 결과를 함께 보여 줍니다.

이 레이크의 자리

통신사 시스템은 크게 둘로 나뉩니다. BSS는 고객과 돈(가입, 요금, 청구)을, OSS는 네트워크 자체(장비, 장애, 품질)를 다룹니다. 이 레이크는 OSS 쪽입니다.

BSS 가입 · 요금 · 청구 누가 얼마를 내는가 OSS 장비 · 장애 · 품질 단말 · 장비 데이터 수집 OSS 데이터 레이크 사람 · 서비스 · AI 에이전트

실제 사례에서 일반화한 구조

여러 원천의 데이터를 한 플랫폼에 모아, 사람과 AI 에이전트가 믿고 쓸 수 있는 단일 기준(SSOT)을 만드는 것이 목표입니다. 데이터는 멈추지 않고 흐르며, 단계마다 조금씩 다듬어집니다.

  1. 원천단말, 기지국, 서버, 애플리케이션의 로그 · 지표 · 이벤트
  2. 수집노드마다 둔 경량 에이전트가 모으고, 집계기가 한데 보냅니다
  3. 원천 메시지메시지 버스(Kafka)에 받은 그대로 쌓아 둡니다
  4. 표준화스트림 처리로 업계 표준 정보 모델에 맞추고 부가 정보를 붙입니다
  5. 저장이력은 오픈 테이블 포맷 레이크에, 바로 쓰는 요약은 ClickHouse에
  6. 서빙즉시 조회(REST), 대량 추출, 구독의 세 가지 방식으로 내보냅니다
  7. 활용관제 화면, 대시보드, 분석가, 업무 앱, AI 에이전트

스키마 계약 (Git): 컬럼 이름, 설명, 타입을 Git의 계약 파일 한 벌에 정의하고, 각 단계의 스키마와 DDL을 거기서 자동으로 만듭니다.

컨트롤 플레인 API: 토픽, 스트림 잡, 커넥터, 스키마, 테이블을 사람 손 대신 API로 선언하고 배포하고 관측합니다. 사람과 AI 에이전트가 같은 API를 씁니다.

ClickHouse의 자리: 표준화된 데이터를 받아 바로 쓰는 요약(골드)을 실시간으로 내주는 저장소입니다. 예측할 수 없는 즉석 질문도 받아 주고, 레이크의 이력과 함께 조회할 수 있습니다.

누가 무엇에 쓰는가

같은 데이터를 여러 팀이 각자의 방식으로 씁니다. 크게 보기, 받아 가기, 운영하기, AI의 네 묶음입니다.

보기: 화면과 분석

관제센터 (NOC) 실습에서 실행

지금 어디가 붐비고, 어디가 약하고, 어디서 장애가 났나

지난 1시간 동안 사용자가 가장 많은 셀(파랑), 신호가 가장 약한 셀(빨강), 방금 1분 사이 신호가 갑자기 떨어진 지역(노랑)을 한 화면에 그립니다.

그림은 JavaScript로 그립니다.

  • 사용자 많은 셀
  • 신호 약한 셀
  • 방금 신호가 떨어진 지역
  • 측정 건수

어떻게: 화면에 필요한 답만 1분마다 미리 계산해 두므로 화면은 바로 뜹니다. 셀 경계는 ClickHouse가 H3 함수로 돌려줍니다.

운영 대시보드 · BI 실습에서 실행

분마다 바뀌는 KPI 추이

대시보드 도구가 1분마다 같은 질의를 던져 선을 그립니다. 아래는 도시별 약한 신호 비율의 지난 1시간이며, 솟은 점은 그 분에 어느 지역에 장애가 있었다는 뜻입니다.

그림은 JavaScript로 그립니다.

어떻게: 대시보드 도구가 ClickHouse에 직접 붙습니다. 사용자 수처럼 중복을 빼야 하는 값도 1분 요약에 담긴 상태를 합쳐 정확히 셉니다.

SQL 보기
-- ---------------------------------------------------------------- 2 · dashboard
-- One point per city per minute for a time-series panel (Grafana, Superset, ...):
-- distinct users (uniq states merged across the city's cells) and weak-signal share.
CREATE OR REPLACE VIEW oss_lake.uc_dashboard AS
WITH (SELECT max(anchor_ts) FROM oss_lake.params) AS t
SELECT
    k.bucket                                                 AS minute,
    g.city                                                   AS city,
    uniqMerge(k.users_st)                                    AS users,
    sum(k.samples_n)                                         AS samples,
    round(100 * sum(k.poor_n) / sum(k.rsrp_n), 2)            AS poor_pct
FROM oss_lake.cell_kpi_1m AS k
INNER JOIN oss_lake.grid AS g ON g.h3 = k.cell
WHERE k.bucket >= t - INTERVAL 1 HOUR AND k.bucket < t
GROUP BY minute, city
ORDER BY minute, city;

품질 분석가 실습에서 실행

미리 준비하지 않은 질문에 바로 답하기

약한 신호는 도심에 많을까, 외곽에 많을까? 미리 만들어 두지 않은 질문을 그 자리에서 SQL로 묻습니다.

도시구역약한 신호 비율
A시도심0.3%
A시중간2.5%
A시외곽23.1%
B시도심0.1%
B시중간2.7%
B시외곽23.2%
C시도심0%
C시중간2.5%
C시외곽23.1%

어떻게: 1분 요약에 바로 묻고 지역 정보와 조인합니다. 파일 기반 레이크에서는 대화형 응답 시간을 맞추기 어려웠던 종류의 질문입니다.

SQL 보기
-- ---------------------------------------------------------------- 1 · ad-hoc analysis
-- A quality analyst's question: where is weak signal concentrated, downtown or at the edge?
CREATE OR REPLACE VIEW oss_lake.uc_analysis AS
WITH (SELECT max(anchor_ts) FROM oss_lake.params) AS t
SELECT
    g.city                                                   AS city,
    multiIf(g.ring <= 10, 'downtown', g.ring <= 30, 'middle', 'edge') AS zone,
    uniqExact(k.cell)                                        AS cells,
    sum(k.samples_n)                                         AS samples,
    round(sum(k.rsrp_sum) / sum(k.rsrp_n), 1)                AS avg_rsrp,
    round(100 * sum(k.poor_n) / sum(k.rsrp_n), 1)            AS poor_pct
FROM oss_lake.cell_kpi_1m AS k
INNER JOIN oss_lake.grid AS g ON g.h3 = k.cell
WHERE k.bucket >= t - INTERVAL 1 HOUR AND k.bucket < t
GROUP BY city, zone
ORDER BY city, indexOf(['downtown', 'middle', 'edge'], zone);

받아 가기: 서빙 방식

업무 앱 · 서비스 · AI 에이전트 실습에서 실행

즉시 조회: HTTP 한 번으로 받아 가기

소비자는 HTTP 요청 하나로 필요한 데이터를 받아 갑니다. 실제 사례에서는 API 서비스가 ClickHouse 앞에서 인증과 카탈로그를 맡고, 원래 형식과 업계 표준 모델 형식 중 하나를 골라 줍니다. 아래는 ClickHouse의 HTTP 인터페이스에 직접 보낸 요청과 응답의 앞부분입니다.

curl 'http://localhost:18123/?user=oss_reader' \
  --data-binary 'SELECT area, city, minute, cells, drop_db, rsrp_now FROM oss_lake.uc_alert_areas FORMAT JSONEachRow'
{"area":"8730c1646ffffff","city":1,"minute":"2026-10-18 09:17:00","cells":39,"drop_db":21.9,"rsrp_now":-119.7}

어떻게: ClickHouse는 HTTP로 질의를 받고 결과를 만드는 대로 한 줄에 한 행씩 흘려보냅니다. 드라이버가 없어도 됩니다.

온콜 담당자 · 다운스트림 시스템 실습에서 실행

구독: 일이 생기면 알려 주기

셀의 최근 1분 신호가 자기 지난 1시간보다 크게 떨어지면 알림이 됩니다. 셀을 지역 단위로 묶어 사람이 읽을 한 줄로 만듭니다. 아래는 이 실습에서 실제로 잡힌 알림입니다.

  • 09:17 · B시 지역 8730c1646ffffff: 셀 39개 신호 21.9 dB 하락

어떻게: 실제 사례에서는 새 데이터가 준비되면 메시지 큐로 '지금 가져가라'는 이벤트를 보내고, 구독자가 그때 받아 갑니다. Kafka 토픽으로 결과를 바로 밀어 줄 수도 있습니다.

예시: 알림을 Kafka 토픽으로 발행하기

예시이며 이 실습에서는 실행하지 않습니다(Kafka 브로커가 필요).

-- Example, not run in this lab (it needs a Kafka broker): publish each minute's alerts to a
-- topic that an on-call app or chat bot subscribes to. Inserting into a Kafka-engine table
-- produces messages; a refreshable MV with APPEND does that once a minute.
CREATE TABLE oss_lake.alerts_topic
(
    area String, city UInt8, minute DateTime, cells UInt64, drop_db Float64, rsrp_now Float64
)
ENGINE = Kafka
SETTINGS kafka_broker_list = 'broker:9092',
         kafka_topic_list  = 'noc-alerts',
         kafka_group_name  = 'oss-lake',
         kafka_format      = 'JSONEachRow';

CREATE MATERIALIZED VIEW oss_lake.alerts_publish
REFRESH EVERY 1 MINUTE APPEND TO oss_lake.alerts_topic AS
SELECT area, city, minute, cells, drop_db, rsrp_now FROM oss_lake.uc_alert_areas;

데이터 엔지니어 · 다른 분석 플랫폼 구성 예시

대량 추출: 한 번에 많이 가져가기

모델 학습이나 다른 시스템 적재처럼 큰 덩어리가 필요할 때는 기간 단위로 한꺼번에 내보냅니다.

어떻게: ClickHouse는 질의 결과를 오브젝트 스토리지에 파일로 바로 쓰거나, HTTP로 흘려보낼 수 있습니다. 실제 사례에서는 즉시 조회 다음 단계로 검토하는 방식입니다.

서빙 계층 · 업무 앱 (향후 확장) 구성 예시

Managed Postgres: 구독과 키, 업무 데이터는 Postgres에

서빙 계층에는 구독 목록, API 키, 어디까지 보냈는지(워터마크) 같은 작은 업무 데이터가 따릅니다. 실제 사례에서도 이것을 Postgres에 둡니다. ClickHouse의 Managed Postgres를 쓰면 이 업무 데이터와 레이크가 한 클라우드에 있고, pg_clickhouse 확장으로 Postgres SQL 그대로 레이크의 알림을 담당 팀, 열린 티켓과 조인할 수 있습니다.

근거: 같은 방식을 clickhouse-managed-postgres-hols 저장소의 pg_clickhouse 실습에서 검증했습니다(2026-10-01). 이 실습에는 포함하지 않았습니다.

예시: Postgres 쪽 SQL

확장 예시이며 이 실습에서는 실행하지 않습니다. 호스트와 비밀번호는 자리 표시입니다.

-- Expansion example, not run in this lab. Runs on (Managed) Postgres, where an operations app
-- keeps tickets and area owners; pg_clickhouse reads the lake's alert view in place.
-- Same statements as clickhouse-managed-postgres-hols extensions/pg-clickhouse-lab
-- (verified there 2026-10-01 on pg_clickhouse 0.11.0, PostgreSQL 18.6); host and password are placeholders.
CREATE EXTENSION IF NOT EXISTS pg_clickhouse;

CREATE SERVER lake FOREIGN DATA WRAPPER clickhouse_fdw
    OPTIONS (driver 'binary', host '<clickhouse host>', port '9000', dbname 'oss_lake');
CREATE USER MAPPING FOR CURRENT_USER SERVER lake
    OPTIONS (user 'oss_reader', password '<password>');

CREATE SCHEMA lake;
IMPORT FOREIGN SCHEMA oss_lake LIMIT TO (uc_alert_areas) FROM SERVER lake INTO lake;

-- who to call: this minute's alert areas with their owner team and open tickets
SELECT a.area, a.cells, a.drop_db, o.team, count(t.id) AS open_tickets
FROM lake.uc_alert_areas AS a
JOIN ops.area_owner AS o ON o.area = a.area
LEFT JOIN ops.ticket AS t ON t.area = a.area AND t.status = 'open'
GROUP BY a.area, a.cells, a.drop_db, o.team
ORDER BY a.cells DESC;

운영하기: 플랫폼 자체

데이터 플랫폼 팀 구성 예시

스키마를 한곳에서 바꾸고 바로 반영하기

스키마가 단계마다 따로 정의되면 이름과 뜻이 조금씩 어긋나고, 바꿀 때마다 여러 팀을 거쳐야 합니다. 실제 사례는 Git의 데이터 계약 파일 한 벌을 기준으로 메시지 스키마, 레이크 테이블 정의, ClickHouse DDL을 CI에서 자동으로 만듭니다.

어떻게: ClickHouse는 생성된 DDL을 그대로 받습니다. 테이블과 MV가 SQL로 정의되므로 리뷰와 배포를 코드처럼 합니다.

데이터 품질 담당 실습에서 실행

들어오는 데이터의 품질을 단계마다 확인하기

셀이나 가입자가 없는 측정은 다듬기 단계에서 버리고, 신호 값이 없는 측정은 0이 아니라 '없음'으로 남깁니다. 단계마다 무엇을 버리고 남겼는지 행 수로 확인할 수 있습니다.

어떻게: 정제 규칙이 Materialized View의 SQL 한 곳에 있으므로 규칙과 결과를 함께 검토합니다. 원본은 받은 그대로 남아 규칙이 바뀌면 다시 만들 수 있습니다.

플랫폼 운영 (SRE) 구성 예시

파이프라인 자체를 관측하기

수집기, 메시지 버스, 스트림 잡, 커넥터가 많아지면 어디서 밀리고 어디서 실패하는지 보는 것 자체가 일입니다. 이 구성 요소들의 로그, 지표, 트레이스도 같은 ClickHouse에 모아 볼 수 있습니다.

어떻게: ClickHouse 기반 관측 스택인 ClickStack으로 OpenTelemetry 데이터를 받아 검색과 대시보드를 씁니다.

분석가 · 데이터 과학자 구성 예시

오래된 이력과 최근 데이터를 함께 보기

긴 이력은 오픈 테이블 포맷 레이크(Iceberg 등)에 두고, 최근 데이터는 ClickHouse에서 빠르게 씁니다. 둘을 한 질의로 이어 보면 재처리나 장기 추세 분석이 쉬워집니다.

어떻게: ClickHouse는 오브젝트 스토리지의 Iceberg 테이블을 직접 읽을 수 있어, 레이크를 옮기지 않고 골드 테이블과 조인합니다.

AI: 에이전트가 쓰는 레이크

AI 에이전트 구성 예시

자연어 질문을 SQL로 바꿔 답하기

운영자가 '지난 30분 동안 B시에서 신호가 떨어진 지역은?'처럼 물으면, 에이전트가 카탈로그와 스키마 설명을 보고 SQL을 만들어 레이크에 묻습니다. 스키마 계약의 설명이 그대로 에이전트의 맥락이 됩니다.

어떻게: 에이전트는 MCP나 HTTP로 ClickHouse에 붙고, 읽기 전용 사용자로 조회합니다. 예측할 수 없는 질문이 많아도 요약 테이블 덕분에 빠르게 답합니다.

AI 개발 팀 구성 예시

에이전트가 맞게 답하는지 관측하기

에이전트가 만든 SQL과 답이 맞는지, 비용과 지연은 어떤지 기록하고 평가해야 믿고 쓸 수 있습니다.

어떻게: ClickHouse를 저장소로 쓰는 Langfuse로 에이전트의 트레이스, 평가, 비용을 같은 플랫폼에서 봅니다.

이 실습이 직접 돌리는 부분

실제 구조에서 원천 메시지와 표준화는 메시지 버스와 스트림 처리가 맡습니다. 이 실습은 흐름을 한눈에 보이도록 네 단계를 모두 ClickHouse 안에 두고, 단계 사이를 Materialized View로 잇습니다. 작은 글씨는 측정 한 건이 단계마다 보이는 모습입니다.

  1. 받은 그대로 보관

    고치지 않고 저장합니다. 규칙이 바뀌면 여기서 다시 만듭니다.

    h3 '8930e1d8c0bffff' · imsi 'DmddyT9J…'

  2. 다듬기

    형식을 맞추고 가입자 번호를 해시로 바꿉니다. 쓸 수 없는 행은 버립니다.

    h3 617853093946327039 · imsi → 346910…

  3. 1분 요약

    셀마다 1분에 한 행으로 접습니다. 어떤 기간이든 다시 합칠 수 있습니다.

    한 셀의 1분 412행 → 1행

  4. 바로 쓰는 답

    화면이 읽을 답만 1분마다 다시 계산해 둡니다.

    관제 지도에 필요한 20행

엔지니어용: 테이블, 변환, SQL, 측정값

네 단계는 데이터베이스 oss_lake의 ue_trace_raw, ue_trace, cell_kpi_1m, cell_rank_now 테이블입니다. 앞의 둘은 Materialized View로, 마지막은 1분마다 도는 Refreshable Materialized View로 이어집니다. 유즈케이스는 04-usecases.sql의 뷰입니다.

측정 한 건의 전과 후

받은 그대로다듬기바뀐 점
event_ts
2026-10-18 09:17:00.026
event_ts
2026-10-18 09:17:00.026
그대로
h3_res09
8930e1d8c0bffff
h3
617853093946327039
문자열 → 숫자 (stringToH3)
imsi
DmddyT9JovjFyxFw5+dsYiRd/DStC6O32IBmYSif74U=
imsi_hash
346910008698083475
암호문 → 해시 (cityHash64). 원래 번호는 레이크에 없습니다
rsrp
-71
rsrp
-71
Float32. 값이 없으면 NULL 유지
rsrq
-5
rsrq
-5
Float32
sinr
18.5
sinr
18.5
Float32
cqi
12
cqi
12
문자열 → 숫자
pci
365
—쓰지 않아 버림
probe
probe-a
—쓰지 않아 버림

412행이 접힌 1분 요약 한 행

bucket2026-10-18 09:17:00
cell617853093946327039
users_stuniqState(…) → 146
samples_n412
rsrp_n364
rsrp_sum-28464
poor_n0

관제 지도가 읽는 20행

사용자 많은 셀 10

#cellusers
18930e1d8c0bffff5,372
28930c1619d7ffff5,340
38930c1903cbffff5,304
48930c16198bffff2,577
58930e1d8c03ffff2,498
68930c190227ffff2,497
78930c16199bffff2,050
88930e1d8c0fffff1,975
98930c190237ffff1,916
108930c1619c3ffff1,784

신호 약한 셀 10

#cellavg_rsrp
18930c192257ffff-117.8 dBm
28930c192243ffff-117.7 dBm
38930c192247ffff-117.5 dBm
48930e1c339bffff-117.5 dBm
58930c16ad03ffff-117.4 dBm
68930c19220bffff-117.4 dBm
78930e1c3393ffff-117.3 dBm
88930c1922cfffff-117.3 dBm
98930e1c3397ffff-117.3 dBm
108930e1c3067ffff-117 dBm

만들며 알게 된 함정. 집계 상태 타입은 원본 컬럼의 Nullable 여부와 맞아야 합니다. 맞지 않는 MV는 CREATE에서 거부되지만, 나중에 ALTER로 원본 컬럼을 Nullable로 바꾸면 그 뒤 INSERT가 모두 실패하고 원본만 계속 쌓입니다(README에 재현 기록).

정의

-- ---------------------------------------------------------------- bronze
-- Exactly what the probes send: H3 as a string, the IMSI as the vendor's
-- ciphertext (44-char base64), every measurement Nullable, CQI as text.
CREATE TABLE oss_lake.ue_trace_raw
(
    event_ts   DateTime64(3),
    h3_res09   Nullable(String),
    imsi       Nullable(String),
    rsrp       Nullable(Float64),   -- dBm
    rsrq       Nullable(Float64),   -- dB
    sinr       Nullable(Float64),   -- dB
    cqi        Nullable(String),
    pci        Nullable(UInt16),
    probe      LowCardinality(String),
    ingest_ts  DateTime64(3) DEFAULT now64(3)
)
ENGINE = MergeTree
PARTITION BY toDate(event_ts)
ORDER BY event_ts
TTL toDateTime(event_ts) + INTERVAL 3 DAY;
-- Rows without a cell or a subscriber cannot be placed or counted: silver drops them.
CREATE MATERIALIZED VIEW oss_lake.ue_trace_mv TO oss_lake.ue_trace AS
SELECT
    event_ts,
    stringToH3(assumeNotNull(h3_res09))       AS h3,
    cityHash64(assumeNotNull(imsi))           AS imsi_hash,
    CAST(rsrp AS Nullable(Float32))           AS rsrp,
    toFloat32(ifNull(rsrq, 0))                AS rsrq,
    toFloat32(ifNull(sinr, 0))                AS sinr,
    toUInt8OrZero(ifNull(cqi, ''))            AS cqi
FROM oss_lake.ue_trace_raw
WHERE h3_res09 IS NOT NULL AND imsi IS NOT NULL;
-- ---------------------------------------------------------------- gold
-- One row per (minute, cell). Users are a uniq state so any time range can be merged;
-- the rest are plain sums. Column names differ from the aliases queries use
-- (users_st vs users): an alias equal to the column inside its own aggregate is
-- ILLEGAL_AGGREGATION.
CREATE TABLE oss_lake.cell_kpi_1m
(
    bucket     DateTime,
    cell       UInt64,
    users_st   AggregateFunction(uniq, UInt64),
    samples_n  SimpleAggregateFunction(sum, UInt64),
    rsrp_n     SimpleAggregateFunction(sum, UInt64),   -- samples that carried rsrp
    rsrp_sum   SimpleAggregateFunction(sum, Float64),
    poor_n     SimpleAggregateFunction(sum, UInt64)    -- rsrp < -100 dBm
)
ENGINE = AggregatingMergeTree
ORDER BY (bucket, cell);
CREATE MATERIALIZED VIEW oss_lake.cell_kpi_1m_mv TO oss_lake.cell_kpi_1m AS
SELECT
    toStartOfMinute(event_ts)  AS bucket,
    h3                         AS cell,
    uniqState(imsi_hash)       AS users_st,
    count()                    AS samples_n,
    count(rsrp)                AS rsrp_n,
    sum(ifNull(rsrp, 0))       AS rsrp_sum,
    countIf(rsrp < -100)       AS poor_n
FROM oss_lake.ue_trace
GROUP BY bucket, cell;
-- No APPEND: every refresh replaces the 20 rows.
CREATE MATERIALIZED VIEW oss_lake.cell_rank_rmv
REFRESH EVERY 1 MINUTE TO oss_lake.cell_rank_now AS
WITH (SELECT max(anchor_ts) FROM oss_lake.params) AS t
SELECT
    lr.1 AS list, lr.2 AS rnk, cell, users, samples, avg_rsrp, poor_pct, t AS window_end
FROM
(
    SELECT
        cell,
        uniqMerge(users_st)                              AS users,
        sum(samples_n)                                   AS samples,
        round(sum(rsrp_sum) / sum(rsrp_n), 1)            AS avg_rsrp,
        round(100 * sum(poor_n) / sum(rsrp_n), 1)        AS poor_pct,
        sum(rsrp_n) >= 30                                AS eligible,
        row_number() OVER (ORDER BY users DESC, cell)    AS r_users,
        row_number() OVER (PARTITION BY eligible ORDER BY sum(rsrp_sum) / sum(rsrp_n), cell) AS r_signal
    FROM oss_lake.cell_kpi_1m
    WHERE bucket >= t - INTERVAL 1 HOUR AND bucket < t
    GROUP BY cell
)
ARRAY JOIN [('users', r_users), ('signal', if(eligible, r_signal, 999))] AS lr
WHERE lr.2 <= 10;
-- ---------------------------------------------------------------- 3 · alerts
-- Cells whose signal in the latest minute is at least 12 dB below their own last hour.
-- Subscribers (an on-call app, a chat bot) pick these up every minute; see 4 and
-- subscribe-kafka.sql for how they are delivered.
CREATE OR REPLACE VIEW oss_lake.uc_alert_cells AS
WITH (SELECT max(anchor_ts) FROM oss_lake.params) AS t
SELECT
    cell, city, area, minute, rsrp_now, rsrp_hour,
    round(rsrp_hour - rsrp_now, 1)                           AS drop_db,
    samples_now
FROM
(
    SELECT
        k.cell                                               AS cell,
        any(g.city)                                          AS city,
        h3ToParent(k.cell, 7)                                AS area,
        t - INTERVAL 1 MINUTE                                AS minute,
        sumIf(k.rsrp_sum, k.bucket = minute) / sumIf(k.rsrp_n, k.bucket = minute) AS rsrp_now,
        sum(k.rsrp_sum) / sum(k.rsrp_n)                      AS rsrp_hour,
        sumIf(k.rsrp_n, k.bucket = minute)                   AS samples_now
    FROM oss_lake.cell_kpi_1m AS k
    INNER JOIN oss_lake.grid AS g ON g.h3 = k.cell
    WHERE k.bucket >= t - INTERVAL 1 HOUR AND k.bucket < t
    GROUP BY cell
)
WHERE samples_now >= 3 AND rsrp_hour - rsrp_now >= 12;

어느 단계에 물어도 같은 답

같은 관제 질문을 네 단계에 각각 던지면 20행이 똑같이 나옵니다(03-queries.sql이 검사). 단계마다 행 수와 읽는 양이 줄어듭니다.

  1. 받은 그대로 브론즈21,600,000
  2. 다듬기 실버21,470,089
  3. 1분 요약 골드2,431,516
  4. 바로 쓰는 답 서빙20
  • 받은 그대로 1,004.6 ms · 10,807,707행 읽음
  • 다듬기 289.9 ms · 10,742,429행 읽음
  • 1분 요약 249.8 ms · 1,219,101행 읽음
  • 바로 쓰는 답 1.4 ms · 20행 읽음

측정 2026-10-18 09:19 UTC, ClickHouse 26.9.13.15, Apple M4 Pro (macOS arm64), 컨테이너 CPU 12개 · 메모리 7.75 GiB. 워밍업 1회 뒤 5회 실행의 중앙값, 서버 측 경과 시간, 쿼리 캐시 끔. 참고용입니다.

직접 흘려 보기

라이브 데모에서 ‘다음 1분 보내기’를 누를 때마다 1분치 측정값이 네 단계를 지나고, 관제 지도와 알림이 새로 고쳐집니다.

cd usecase/oss-network-quality-lake
ui/up.sh

라이브 데모 열기

로컬에서 돌리려면 Docker와 Python 3만 있으면 됩니다. 이 사이트의 데모는 로컬에서 녹화한 몇 분을 재생합니다.