BTP

SERIES_GENERATE vs 캘린더 테이블 — 3가지 차이 #shorts #SAP #HANA

▶ YouTube에서 보기

📖 개요 및 이 글에서 다루는 내용

매출 리포트를 만들 때 주문이 없는 날짜가 통째로 사라져 차트가 끊겨 보인 경험이 있다면, 이 글이 해결책이 됩니다. SAP HANA의 SERIES_GENERATE 계열 함수는 시작값·종료값·증가 간격만 지정하면 날짜, 타임스탬프, 숫자 시퀀스를 테이블 형태로 즉시 생성해 주는 내장 테이블 함수입니다. 별도의 캘린더 테이블을 만들어 관리할 필요가 없어, 시계열 분석의 기반 데이터를 SQL 한 줄로 확보할 수 있습니다.

  • SERIES_GENERATE 함수군의 동작 원리와 파라미터 구조 이해
  • 일별·주별·월별 날짜 시계열과 숫자 시계열 생성 패턴 3가지 습득
  • LEFT JOIN으로 판매 데이터의 빈 날짜 채우기(missing date fill) 구현
  • CTE와 결합한 생산 계획 캘린더 등 프로덕션 수준 활용법 정리

📚 사전에 알아두면 좋은 것

이 글은 advanced 난이도로, SQL의 JOIN, GROUP BY, CTE(WITH 절)에 익숙하다는 전제로 진행합니다. HANA의 날짜/시간 타입(DATE, TIMESTAMP, SECONDDATE) 간 차이와 TO_DATE, ADD_DAYS 같은 변환 함수를 사용해 본 경험이 있으면 예제를 훨씬 빠르게 따라올 수 있습니다.

🔧 환경 및 준비물

SERIES_GENERATE 함수군은 SAP HANA 1.0 SPS 09에서 시리즈 데이터(Series Data) 기능의 일부로 도입되었으며, SAP HANA 2.0(모든 SPS)과 SAP HANA Cloud에서 일반적으로 동일하게 사용할 수 있습니다. 별도 라이선스나 옵션 활성화 없이 표준 SQL 콘솔에서 바로 실행됩니다.

  • SAP HANA 2.0 SPS 05 이상 또는 SAP HANA Cloud (권장)
  • SQL 실행 도구: SAP HANA Database Explorer, DBeaver, hdbsql 중 택 1
  • 테스트 스키마에 대한 SELECT / CREATE TABLE 권한
  • 예제용 테이블: SALES_ORDER(주문), PRODUCTION_PLAN(생산 계획) — 본문에서 DDL 제공

💡 핵심 개념: 시계열의 "빈 칸"을 만들어 주는 함수

SERIES_GENERATE를 비유하자면 모눈종이를 인쇄해 주는 프린터입니다. 실제 데이터(그래프의 점)를 찍기 전에, 먼저 균일한 간격의 격자(날짜 축)를 깔아 주는 역할이죠. 격자가 없으면 데이터가 있는 지점만 연결되어 왜곡된 그림이 나오지만, 격자가 있으면 "데이터가 없는 날 = 0"이라는 사실까지 시각화할 수 있습니다.

이 함수는 단일 함수가 아니라 반환 타입별 함수군입니다. 대표적으로 다음이 있습니다.

함수용도파라미터 예
SERIES_GENERATE_TIMESTAMP날짜/시간 시계열('INTERVAL 1 DAY', 시작, 종료)
SERIES_GENERATE_DATE날짜 단위 시계열('INTERVAL 1 MONTH', 시작, 종료)
SERIES_GENERATE_INTEGER정수 시퀀스(증가값, 최소, 최대)
SERIES_GENERATE_DECIMAL소수 시퀀스(0.5, 0, 10)

공통 시그니처는 SERIES_GENERATE_<타입>(증가 간격, 시작값, 종료값)이며, 결과는 세 개의 컬럼을 가진 테이블입니다.

  • ELEMENT_NUMBER — 0부터 시작하는 요소 순번
  • GENERATED_PERIOD_START — 각 구간의 시작값 (실무에서 주로 사용)
  • GENERATED_PERIOD_END — 각 구간의 종료값 (다음 구간의 시작과 동일)

핵심 동작 원리: 종료값은 배타적(exclusive)입니다. '2026-07-01'부터 '2026-08-01'까지 1일 간격으로 생성하면 7월 1일~7월 31일, 총 31행이 나오고 8월 1일은 포함되지 않습니다. 이는 구간(period) 개념으로 설계되었기 때문이며, 월말 처리 버그를 예방하는 특성이기도 합니다.

날짜형 함수의 증가 간격은 'INTERVAL n SECOND | MINUTE | HOUR | DAY | MONTH | YEAR' 형식의 문자열로 지정합니다. 숫자형 함수는 숫자 리터럴을 그대로 넣습니다.

💻 실전 코드 3단계

1단계 — 기본 패턴 3가지: 일별·월별·숫자 시계열

먼저 함수의 출력을 눈으로 확인합니다. 패턴 1은 일별 날짜 축입니다.

-- 패턴 1: 2026년 7월 일별 시계열 (31행, 종료값 8/1은 미포함)
SELECT
    ELEMENT_NUMBER,
    TO_DATE(GENERATED_PERIOD_START) AS CALENDAR_DATE
FROM SERIES_GENERATE_TIMESTAMP('INTERVAL 1 DAY', '2026-07-01', '2026-08-01');

-- 패턴 2: 12개월 월별 시계열 (회계연도 리포트 축)
SELECT
    ELEMENT_NUMBER + 1              AS FISCAL_PERIOD,
    TO_DATE(GENERATED_PERIOD_START) AS PERIOD_START,
    ADD_DAYS(TO_DATE(GENERATED_PERIOD_END), -1) AS PERIOD_END
FROM SERIES_GENERATE_DATE('INTERVAL 1 MONTH', '2026-01-01', '2027-01-01');

-- 패턴 3: 정수 시퀀스 (0~95, 15분 슬롯 번호 96개)
SELECT ELEMENT_NUMBER, GENERATED_PERIOD_START AS SLOT_NO
FROM SERIES_GENERATE_INTEGER(1, 0, 96);

주별 시계열은 'INTERVAL 7 DAY'로 만들되, 시작값을 해당 주의 월요일로 맞추는 것이 일반적입니다. GENERATED_PERIOD_END에서 하루를 빼면 "구간 종료일"을 자연스럽게 얻을 수 있다는 점도 패턴 2에서 확인하세요.

2단계 — 실무 시나리오: 판매 데이터의 날짜 갭 채우기

주문이 없는 날을 0으로 채운 일별 매출 리포트를 만듭니다. 타입 불일치로 조인이 실패하는 흔한 에러까지 함께 처리합니다.

CREATE COLUMN TABLE SALES_ORDER (
    ORDER_ID    BIGINT PRIMARY KEY,
    ORDER_DATE  DATE NOT NULL,
    NET_AMOUNT  DECIMAL(15,2)
);

INSERT INTO SALES_ORDER VALUES (1001, '2026-07-01', 1200.00);
INSERT INTO SALES_ORDER VALUES (1002, '2026-07-01',  850.50);
INSERT INTO SALES_ORDER VALUES (1003, '2026-07-04', 3300.00);
INSERT INTO SALES_ORDER VALUES (1004, '2026-07-07',  410.00);
-- 7/2, 7/3, 7/5, 7/6은 주문 없음 → 리포트에서 사라지는 날짜들

WITH DATE_AXIS AS (
    -- 핵심: TIMESTAMP를 DATE로 캐스팅해야 ORDER_DATE와 정확히 매칭됨
    SELECT TO_DATE(GENERATED_PERIOD_START) AS CALENDAR_DATE
    FROM SERIES_GENERATE_TIMESTAMP('INTERVAL 1 DAY', '2026-07-01', '2026-07-08')
)
SELECT
    DA.CALENDAR_DATE,
    IFNULL(SUM(SO.NET_AMOUNT), 0)  AS DAILY_REVENUE,
    COUNT(SO.ORDER_ID)             AS ORDER_COUNT,
    CASE WHEN COUNT(SO.ORDER_ID) = 0
         THEN 'NO_SALES' ELSE 'OK' END AS DATA_QUALITY_FLAG
FROM DATE_AXIS DA
LEFT JOIN SALES_ORDER SO
    ON SO.ORDER_DATE = DA.CALENDAR_DATE
GROUP BY DA.CALENDAR_DATE
ORDER BY DA.CALENDAR_DATE;

포인트는 세 가지입니다. 첫째, 시계열 축이 왼쪽에 오는 LEFT JOIN이어야 빈 날짜가 보존됩니다. 둘째, TO_DATE() 캐스팅 없이 TIMESTAMPDATE를 조인하면 자정 시각(00:00:00)만 매칭되거나 암시적 변환 비용이 발생하므로 명시적으로 타입을 맞춥니다. 셋째, DATA_QUALITY_FLAG 같은 진단 컬럼을 두면 "0 매출"과 "데이터 누락"을 로그에서 구분할 수 있어 운영 모니터링에 유리합니다.

3단계 — 프로덕션 패턴: CTE 기반 생산 계획 캘린더

주말을 제외한 작업일 캘린더를 생성해 생산 계획 테이블과 매칭하고, 계획이 비어 있는 작업일을 탐지하는 예제입니다. CTE를 여러 단계로 쌓아 가독성과 재사용성을 확보합니다.

WITH RAW_CALENDAR AS (
    SELECT TO_DATE(GENERATED_PERIOD_START) AS PLAN_DATE
    FROM SERIES_GENERATE_TIMESTAMP('INTERVAL 1 DAY', '2026-08-01', '2026-09-01')
),
WORKDAY_CALENDAR AS (
    -- 주말 제외: WEEKDAY()는 월요일=0 ~ 일요일=6 반환
    SELECT PLAN_DATE,
           WEEKDAY(PLAN_DATE) AS DOW
    FROM RAW_CALENDAR
    WHERE WEEKDAY(PLAN_DATE) < 5
),
SHIFT_SLOTS AS (
    -- 작업일 × 2교대 = 크로스 조인으로 슬롯 확장
    SELECT WC.PLAN_DATE, SG.GENERATED_PERIOD_START AS SHIFT_NO
    FROM WORKDAY_CALENDAR WC
    CROSS JOIN SERIES_GENERATE_INTEGER(1, 1, 3) SG   -- 1, 2교대
)
SELECT
    SS.PLAN_DATE,
    SS.SHIFT_NO,
    IFNULL(PP.PLANNED_QTY, 0) AS PLANNED_QTY,
    CASE WHEN PP.PLAN_ID IS NULL
         THEN 'MISSING_PLAN' ELSE 'PLANNED' END AS PLAN_STATUS
FROM SHIFT_SLOTS SS
LEFT JOIN PRODUCTION_PLAN PP
    ON  PP.PLAN_DATE = SS.PLAN_DATE
    AND PP.SHIFT_NO  = SS.SHIFT_NO
ORDER BY SS.PLAN_DATE, SS.SHIFT_NO;

프로덕션 관점의 체크리스트입니다.

  • 성능: SERIES_GENERATE는 인메모리에서 즉석 생성되므로 수만 행 수준에서는 매우 가볍습니다. 다만 초 단위 간격으로 수년 치를 생성하면 수억 행이 되어 조인 비용이 급증하므로, 리포트 기간에 맞춰 시작/종료값을 파라미터로 제한하는 것이 권장됩니다.
  • 테스트: 경계값 검증이 필수입니다. 행 수 검증 쿼리(SELECT COUNT(*))로 "31일 = 31행", "종료일 미포함"을 단위 테스트로 고정하세요.
  • 보안: 시작/종료값을 애플리케이션에서 문자열로 조립하지 말고, 프로시저 파라미터나 바인드 변수로 전달해 SQL 인젝션 여지를 차단하는 것이 일반적입니다.

⚠️ 흔한 실수와 트러블슈팅

  • 실수 1 — 종료값 포함으로 착각: "7월 31일까지"를 원하면 종료값은 '2026-08-01'이어야 합니다. '2026-07-31'로 쓰면 30일까지만 생성되어 월말 데이터가 리포트에서 누락됩니다.
  • 실수 2 — INTERVAL 문자열 오타: '1 DAY'처럼 INTERVAL 키워드를 빼거나 DAYS(복수형)로 쓰면 파싱 오류가 발생합니다. 단수형 단위(DAY, MONTH, YEAR)를 지키세요.
  • 실수 3 — LEFT JOIN 후 WHERE로 필터: 조인 후 WHERE SO.STATUS = 'C'처럼 오른쪽 테이블 조건을 WHERE에 쓰면 NULL 행(빈 날짜)이 다시 탈락합니다. 오른쪽 테이블 조건은 반드시 ON 절로 옮기세요.

FAQ 1. 생성 결과를 물리 테이블로 저장해야 하나요? — 대부분 불필요합니다. 뷰나 CTE로 충분하며, 공휴일 같은 비즈니스 속성이 붙는 경우에만 별도 캘린더 테이블을 유지하는 것이 일반적입니다.

FAQ 2. 시작값을 다른 테이블의 MIN/MAX에서 가져올 수 있나요? — 함수 인자에는 상수 또는 파라미터가 필요하므로, SQLScript 프로시저에서 스칼라 변수에 MIN(ORDER_DATE)를 담아 전달하는 방식이 권장됩니다.

FAQ 3. HANA Cloud에서도 문법이 같나요? — 함수군과 반환 컬럼 구조는 일반적으로 동일합니다. 다만 세션 타임존 설정에 따라 TIMESTAMP 해석이 달라질 수 있어 DATE 캐스팅 후 조인하는 습관이 안전합니다.

🚀 이어서 살펴볼 주제

날짜 축을 만들었다면 다음 단계는 그 위에서의 분석입니다. SERIES_ROUND로 불규칙한 타임스탬프를 구간에 스냅하는 기법, 윈도우 함수(LEAD/LAG, 이동 평균)와 결합한 추세 분석, 그리고 HANA의 LINEAR_APPROX 같은 결측치 보간 함수가 자연스러운 다음 학습 지점입니다. CAP(CDS) 모델에서 캘린더 뷰를 노출해 SAC 대시보드와 연결하는 시나리오도 함께 검토해 보세요.

📚 참고 링크 모음

댓글 0

아직 댓글이 없습니다.