본문으로 건너뛰기
clickhouse검색
SQL과 쿼리

날짜와 시간, 윈도우 함수 (toStartOf, 시간대, WITH FILL, 창 함수)

DateTime과 DateTime64의 시간대 동작, 시간 버킷 함수, 빈 구간을 채우는 WITH FILL, 그리고 윈도우 함수로 이동 평균과 세션화를 구현하는 방법을 정리합니다. 한국 시간(Asia/Seoul) 기준 보고서를 만들 때 틀리는 지점을 중심으로.

DateTime시간대toStartOfIntervalWITH FILL윈도우 함수시계열2026-10 기준, 약 11분상태 표시 설명

시간 타입 고르기

타입범위와 정밀도크기쓸 때
Date1970 ~ 2149, 일2바이트파티션 키, 일별 집계
Date321900 ~ 2299, 일4바이트과거 데이터
DateTime1970 ~ 2106, 초4바이트대부분의 이벤트 시각
DateTime64(3)1900 ~ 2299, 밀리초(정밀도 0~9)8바이트지연시간 분석, 순서가 중요한 로그
Time, Time64하루 안의 시각, 또는 길이영업 시간, 소요 시간 (최근 버전)
  • DateTime은 UTC 기준 유닉스 초를 저장하고, 시간대는 컬럼 메타데이터입니다. 저장 값은 시간대와 무관하게 같고, 보여 줄 때와 toDate 같은 분해 함수에서만 시간대가 쓰입니다.
  • 컬럼에 시간대를 적지 않으면 서버 시간대(timezone 설정)를 따릅니다. 서버는 보통 UTC이므로 한국 기준 날짜가 필요하면 컬럼에 시간대를 명시하는 편이 안전합니다.
  • DateTime64의 정밀도는 필요한 만큼만. 밀리초면 3, 마이크로초면 6. 정밀도가 높을수록 압축이 나빠집니다.
CREATE TABLE events
(
    ts         DateTime64(3, 'Asia/Seoul'),
    event_date Date DEFAULT toDate(ts),  -- 한국 기준 날짜
    user_id    UInt64,
    kind       LowCardinality(String)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (kind, ts);

시간대 다루기

SELECT
    now() AS server_now,                           -- 서버 시간대
    toTimeZone(now(), 'Asia/Seoul') AS kst,        -- 같은 순간, 표시만 KST
    toDateTime('2026-10-21 09:00:00', 'Asia/Seoul') AS parsed_kst,
    toDate(now(), 'Asia/Seoul') AS kst_date,       -- 분해 함수에 시간대 넘기기
    toUnixTimestamp(now()) AS epoch;
  • toTimeZone은 값을 바꾸지 않고 표시 시간대만 바꿉니다.
  • 문자열을 파싱할 때 시간대를 넘기지 않으면 서버 시간대로 해석됩니다. 클라이언트가 KST 문자열을 보내면 toDateTime(s, 'Asia/Seoul')로 받습니다.
  • 세션 단위로 바꾸려면 SET session_timezone = 'Asia/Seoul'. 시간대가 없는 리터럴과 분해 함수가 이 값을 따릅니다.
  • 날짜 경계 비교는 event_date 컬럼(파티션 키)과 ts 둘 다 거는 편이 파티션 가지치기와 정확성을 함께 얻습니다. 정렬 키, 기본 키, 파티션

주의 toDate(ts)의 결과는 컬럼 시간대에 따라 다릅니다. UTC 컬럼이면 한국 밤 9시 이후 이벤트가 다음 날로 넘어가지 않습니다. 보고서 날짜가 하루씩 틀리면 거의 이 문제입니다.

시간 버킷 함수

함수결과
toStartOfMinute, toStartOfHour, toStartOfDay그 단위의 시작
toStartOfFiveMinutes, toStartOfFifteenMinutes고정 간격
toStartOfInterval(ts, INTERVAL 10 MINUTE)임의 간격 (가장 범용)
toStartOfWeek(d, 1)월요일 시작 주 (두 번째 인자 1), toMonday도 같음
toStartOfMonth, toStartOfQuarter, toStartOfYear월, 분기, 연
toYYYYMM, toYYYYMMDD정수형 월, 일 (파티션 키에 흔함)
date_trunc('hour', ts)표준 SQL 스타일
toLastDayOfMonth, toDayOfWeek, toHour분해
SELECT
    toStartOfInterval(ts, INTERVAL 15 MINUTE) AS bucket,
    count() AS c,
    uniq(user_id) AS u
FROM events
WHERE ts >= now() - INTERVAL 6 HOUR
GROUP BY bucket
ORDER BY bucket;

날짜 연산과 변환

SELECT
    ts + INTERVAL 3 DAY AS plus3,
    addMonths(ts, 1) AS next_month,
    dateDiff('day', toDate('2026-01-01'), toDate(ts)) AS days_since_ny,
    age('month', toDate('2025-03-15'), toDate(ts)) AS full_months,
    formatDateTime(ts, '%Y-%m-%d %H:%i') AS pretty,
    parseDateTimeBestEffort('21 Oct 2026 09:00 +09:00') AS best_effort,
    parseDateTime('2026/10/21', '%Y/%m/%d') AS strict_parse
FROM events LIMIT 1;
  • dateDiff는 단위 경계를 넘은 횟수, age는 꽉 찬 단위 수입니다. 생일 계산에는 age.
  • parseDateTimeBestEffort는 다양한 모양을 받아 주지만 느립니다. 포맷이 정해져 있으면 parseDateTime(MySQL 스타일) 또는 parseDateTimeInJodaSyntax.
  • 밀리초 유닉스 시각은 fromUnixTimestamp64Milli(ms), 반대는 toUnixTimestamp64Milli(ts).
  • 26.9부터 DateTime에 Time 값을 더하고 빼는 연산이 가능해져 "영업 시작 시각 + 근무 길이" 같은 계산이 자연스러워졌습니다.

빈 구간 채우기: WITH FILL

시계열 그래프에서 이벤트가 없는 시각은 결과 행 자체가 없어 선이 끊깁니다. ORDER BY ... WITH FILL로 빈 버킷을 만들고 INTERPOLATE로 값을 정합니다.

SELECT
    toStartOfHour(ts) AS h,
    count() AS c
FROM events
WHERE ts >= toStartOfDay(now()) - INTERVAL 1 DAY
GROUP BY h
ORDER BY h WITH FILL
    FROM toStartOfDay(now()) - INTERVAL 1 DAY
    TO toStartOfHour(now()) + INTERVAL 1 HOUR
    STEP INTERVAL 1 HOUR;
  • 채워진 행의 집계값은 기본값(0)입니다. 이전 값으로 이어 가려면 INTERPOLATE (c AS c), 다른 식도 가능합니다.
  • 여러 정렬 키가 있으면 각각에 WITH FILL을 붙일 수 있고, 그룹마다 채우려면 WITH FILL ... STALENESS 같은 옵션을 최근 버전이 제공합니다.
  • 결과가 수만 행을 넘는 WITH FILL은 메모리를 많이 쓰지 않지만, 그래프 도구 쪽에서 "빈 구간을 0으로" 옵션이 있다면 그쪽이 더 단순합니다.

윈도우 함수

OVER (PARTITION BY ... ORDER BY ... ROWS BETWEEN ...) 문법을 지원합니다. 집계 함수 대부분과 전용 함수(row_number, rank, dense_rank, ntile, lagInFrame, leadInFrame, first_value, last_value, nth_value, percent_rank)를 쓸 수 있습니다.

-- 7일 이동 평균과 전일 대비
WITH daily AS
(
    SELECT toDate(ts) AS d, count() AS c
    FROM events GROUP BY d
)
SELECT
    d, c,
    avg(c) OVER (ORDER BY d ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS ma7,
    c - lagInFrame(c, 1) OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS diff_prev,
    sum(c) OVER (ORDER BY d) AS running_total
FROM daily
ORDER BY d;
  • lagInFrame과 leadInFrame은 프레임 안에서 동작합니다. 프레임을 적지 않으면 기본 프레임(RANGE UNBOUNDED PRECEDING)이라 lag 결과가 기대와 다릅니다. 위처럼 ROWS BETWEEN 1 PRECEDING AND CURRENT ROW를 꼭 적습니다.
  • 같은 창을 여러 번 쓰면 WINDOW w AS (PARTITION BY ... ORDER BY ...)로 이름을 붙여 재사용합니다.
  • RANGE BETWEEN INTERVAL 1 HOUR PRECEDING AND CURRENT ROW처럼 시간 범위 프레임도 됩니다.
  • 옛 함수 neighbor, runningDifference, runningAccumulate는 블록 단위로 동작해 결과가 불안정하므로 윈도우 함수로 바꿉니다.

세션화 예제

30분 이상 간격이 벌어지면 새 세션으로 보는 전형적인 패턴입니다.

WITH ordered AS
(
    SELECT user_id, ts,
        lagInFrame(ts, 1) OVER (PARTITION BY user_id ORDER BY ts
            ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_ts
    FROM events
    WHERE ts >= today() - 1
),
flagged AS
(
    SELECT *, if(prev_ts = toDateTime64(0, 3) OR ts - prev_ts > 1800, 1, 0) AS new_session
    FROM ordered
)
SELECT user_id, ts,
    sum(new_session) OVER (PARTITION BY user_id ORDER BY ts
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS session_no
FROM flagged
ORDER BY user_id, ts;

팁 사용자별 "첫 이벤트와 마지막 이벤트"는 윈도우 함수보다 min(ts), max(ts), argMin, argMax 집계가 훨씬 가볍습니다. 행 수를 유지해야 할 때만 윈도우 함수를 씁니다. 집계 요령은 집계 함수와 컴비네이터에서 이어집니다.

윈도우 함수 성능

  • 파티션 키가 정렬 키 접두어와 같으면 정렬 비용이 줄어듭니다. optimize_read_in_order가 켜져 있는지 확인합니다.
  • 큰 파티션(한 사용자에 수백만 행)은 메모리에 통째로 올라갑니다. 기간을 먼저 자르는 WHERE가 필수입니다.
  • 윈도우 함수 결과를 다시 GROUP BY 하는 쿼리는 서브쿼리나 CTE로 단계를 나눠 쓰면 읽기 쉽고 플랜도 단순해집니다. 쿼리 최적화

시계열 전용 기능 메모

  • TimeSeries 테이블 엔진과 PromQL 쿼리는 Prometheus 데이터를 ClickHouse에 직접 저장하고 조회하는 기능으로, 2026년 현재 Exp 단계이며 ClickHouse Cloud에서는 PrPr로 제공됩니다. 관측성 용도라면 ClickStack과 chDB의 ClickStack이 먼저입니다.
  • 다운샘플링은 toStartOfInterval + GROUP BY를 머티리얼라이즈드 뷰로 만들어 두는 방식이 표준입니다. 머티리얼라이즈드 뷰
  • 보존 기간은 TTL event_date + INTERVAL 90 DAY로 자동 삭제합니다. TTL, 디스크 티어링

공식 문서

이 문서에서