이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • 데이터 검증 SQL은 pipeline 결과에 어떤 자동 질문을 던지는가?
  • 좋은 quality rule은 SQL, threshold, owner, severity, 실행 주기, 실패 조치를 어떻게 함께 가진다?
  • source-target 검증 SQL을 작성할 때 grain, timezone, null, 중복을 어떻게 고려해야 하는가?
  • rule 실행 결과를 어떻게 저장해야 알림과 회고로 이어질 수 있는가?

개요

데이터 검증 SQL은 pipeline 결과가 믿을 수 있는지 확인하는 자동 질문이다.

SQL 자체보다 rule 계약이 중요하다.

어떤 dataset에 대해, 어떤 기준으로, 얼마나 자주, 실패하면 누구에게, 어떤 심각도로 알릴지 정해야 한다.

검증 SQL은 pipeline 코드와 독립적으로 실행될 수 있어야 한다.

pipeline이 성공했다고 rule까지 성공한 것은 아니기 때문이다.

Rule 구성 요소

좋은 rule은 다음 정보를 가진다.

rule_id: order_mart_amount_accuracy
dataset: mart_order_daily
dimension: accuracy
schedule: after daily_order_mart DAG
owner: data-platform
severity: critical
threshold: abs(amount_diff) <= 10000
action: block report and open incident

SQL만 있으면 운영자가 실패 의미를 다시 해석해야 한다.

metadata가 있어야 알림과 runbook으로 연결된다.

Freshness SQL

SELECT
  CASE
    WHEN max(event_time) >= now() - interval '30 minutes' THEN 'PASS'
    ELSE 'FAIL'
  END AS status,
  max(event_time) AS latest_event_time
FROM mart_order_events;

freshness rule은 timestamp column의 의미를 확인해야 한다.

event 발생 시각인지, 적재 시각인지, mart 갱신 시각인지에 따라 해석이 달라진다.

Completeness SQL

WITH source AS (
  SELECT count(*) AS cnt
  FROM orders
  WHERE created_at >= :from AND created_at < :to
),
target AS (
  SELECT count(*) AS cnt
  FROM mart_orders
  WHERE order_date >= :from_date AND order_date < :to_date
)
SELECT source.cnt, target.cnt, source.cnt - target.cnt AS diff
FROM source CROSS JOIN target;

completeness rule은 source와 target grain이 같을 때만 단순 count 비교가 가능하다.

grain이 다르면 distinct key coverage를 본다.

SELECT count(DISTINCT order_id)
FROM mart_order_items
WHERE order_date = :dt;

Accuracy SQL

WITH source AS (
  SELECT sum(paid_amount) AS amount
  FROM orders
  WHERE order_date = :dt
),
target AS (
  SELECT sum(paid_amount) AS amount
  FROM mart_order_daily
  WHERE order_date = :dt
)
SELECT source.amount, target.amount, source.amount - target.amount AS diff
FROM source CROSS JOIN target;

accuracy rule은 business 정의와 연결된다.

취소 주문 포함 여부, 환불 차감 시점, timezone 기준이 문서화되어야 한다.

Validity SQL

SELECT count(*) AS invalid_count
FROM payments
WHERE amount < 0
   OR status NOT IN ('READY', 'PAID', 'CANCELED')
   OR paid_at IS NULL AND status = 'PAID';

validity rule은 schema와 domain enum 변경에 민감하다.

백엔드가 새 status를 추가하면 rule도 함께 바뀌어야 한다.

Rule 실행 결과 저장

rule 결과는 table에 남긴다.

CREATE TABLE quality_rule_runs (
    run_id varchar(80) PRIMARY KEY,
    rule_id varchar(120) NOT NULL,
    dataset varchar(120) NOT NULL,
    dimension varchar(40) NOT NULL,
    status varchar(20) NOT NULL,
    measured_value numeric,
    threshold_text text NOT NULL,
    checked_at timestamp NOT NULL DEFAULT now()
);

결과를 저장해야 추세, 회고, 알림 중복 억제가 가능하다.

로그만 남기면 나중에 “언제부터 깨졌는가”를 찾기 어렵다.

Pipeline과 분리

검증 SQL은 pipeline 코드 내부 assert로만 두면 안 된다.

별도 task나 job으로 실행 가능해야 한다.

Airflow에서는 load 이후 validation task로 둔다.

Spring Batch에서는 job step으로 두더라도 rule 결과 table에 남긴다.

검증 실패는 report publish를 막거나 incident로 연결되어야 한다.

운영 지표

  • rule failure count by dimension
  • consecutive failure count
  • rule runtime
  • measured value vs threshold
  • datasets without rules
  • muted rule count

rule이 너무 느리면 pipeline을 막을 수 있다.

무거운 SQL은 summary table이나 sampling 전략을 검토한다.

개인 프로젝트 기준

개인 프로젝트에서는 세 개 rule이면 충분하다.

freshness rule 하나, completeness count rule 하나, accuracy sum rule 하나를 만든다.

각 rule 결과를 table에 저장한다.

하나를 실패시키고 알림 또는 실패 상태가 남는지 확인한다.

위험 신호!

  • 검증 SQL만 있고 threshold와 owner가 없다.
  • source와 target grain이 다른데 count를 비교한다.
  • rule 실패가 pipeline log에만 남는다.
  • 품질 실패인데 report publish가 계속된다.
  • 백엔드 enum 변경과 validity rule 변경이 연결되어 있지 않다.

확인 질문

확인 질문

  • 이 rule은 어떤 dimension을 검증하는가?
    • freshness, completeness, accuracy, validity, consistency 중 하나로 답해야 한다.
  • 실패하면 어떤 action이 실행되는가?
    • block, alert, warn, incident, backfill 중 하나가 명확해야 한다.
  • rule 결과는 어디에 저장되어 추세와 회고에 쓰이는가?
    • quality_rule_runs 같은 실행 이력 table이 필요하다.

참고 문서