관리자 30일 통계 쿼리 개선: 반복 스캔을 날짜별 집계로 줄이기

이 글의 목차 9개 항목

문제와 원인

관리자 30일 통계 쿼리는 날짜마다 DAU·MAU·SQL·SW 풀이의 상관 서브쿼리로 원시 테이블을 다시 읽었다. SQL 호출은 1회지만 날짜 변환과 세션 중복 제거가 30개 날짜에서 반복돼 데이터가 늘수록 처리 시간이 커졌다.

수정 전 코드

admin-read-use-cases.ts

SELECT
  day,
  (
    SELECT COUNT(DISTINCT e.anonymous_session_id)
    FROM analytics_events e
    WHERE e.event_type = 'page_view'
      AND date(datetime(e.occurred_at, '+9 hours')) = day
  ) AS dau,
  (
    SELECT COUNT(DISTINCT e.anonymous_session_id)
    FROM analytics_events e
    WHERE e.event_type = 'page_view'
      AND date(datetime(e.occurred_at, '+9 hours'))
        BETWEEN date(day, '-29 days') AND day
  ) AS mau
FROM days
ORDER BY day;

수정 후 코드

adminActivityTrendQuery

출력 30일과 첫 출력일의 MAU에 필요한 선행 29일을 합친 59일의 방문을 KST 날짜·세션별로 축약했다. visitor_days를 MATERIALIZED로 두어 DAU·MAU가 재사용하고, SQL·SW 풀이도 일별 집계 후 UNION ALL과 합계로 결합했다.

visitor_days AS MATERIALIZED (
  SELECT date(datetime(e.occurred_at, '+9 hours')) AS day,
    e.anonymous_session_id AS session_id
  FROM analytics_events e, bounds
  WHERE e.event_type = 'page_view'
    AND date(datetime(e.occurred_at, '+9 hours'))
      BETWEEN visitor_start AND end_day
  GROUP BY day, e.anonymous_session_id
),
daily_visitors AS MATERIALIZED (
  SELECT day, COUNT(DISTINCT session_id) AS dau
  FROM visitor_days GROUP BY day
)

MAU는 각 날짜의 30일 구간에서 DISTINCT 세션을 세어 여러 날 방문한 세션의 중복을 제거한다.

SELECT COUNT(DISTINCT visitor_days.session_id)
FROM visitor_days
WHERE visitor_days.day BETWEEN date(days.day, '-29 days') AND days.day

SQL 본문의 원시 테이블 참조는 4곳→3곳, 통계 조회는 SQL 1개를 유지했다. 날짜별 MAU 계산의 대상은 원시 이벤트에서 축약된 visitor_days로 바뀌었다.

검증 결과

로컬 SQL 실행 시간

Node.js·SQLite의 같은 합성 DB에서 원본 전후 SQL을 각각 15회 실행했다. 관리자 제외 조건에서 전체 결과 30행의 일치를 확인하고 중앙값을 비교했다.

규모데이터 구성수정 전 중앙값수정 후 중앙값감소량감소율
1배 · 총 4,200행페이지 조회 2,400행 + SQL 풀이 1,200행 + SW 풀이 600행29.836ms14.912ms14.924ms50.0%
5배 · 총 21,000행페이지 조회 12,000행 + SQL 풀이 6,000행 + SW 풀이 3,000행151.980ms82.120ms69.860ms46.0%

같은 입력의 SQL 재실행

Swagger UI에서 같은 원본 SQL 측정 명령을 다시 실행했다. 로컬 SQLite의 합성 DB에서 관리자 제외 조건으로 각 SQL을 15회 실행했고, 두 규모의 집계 결과는 각각 30행 모두 일치했다.

규모합성 데이터 행 수수정 전 중앙값수정 후 중앙값감소율
1배4,200행40.732ms19.277ms52.7%
5배21,000행314.694ms206.042ms34.5%
같은 SQL 재실행: 각 15회 중앙값과 30행 결과 일치원본 보기 (새 창)

같은 SQL 재실행: 각 15회 중앙값과 30행 결과 일치

100%원본 보기 (새 창)

확대한 이미지는 상하좌우로 이동할 수 있습니다. 키보드는 이미지 영역에 초점을 두고 방향키를 사용하세요. Esc로 닫습니다.

집계 결과 비교

조건결과
1배 합성 데이터전체 30행 동일 · 응답 JSON 1,890B 동일
5배 합성 데이터전체 30행 동일 · 응답 JSON 1,980B 동일
KST 자정·월 경계해당 날짜의 DAU·MAU·풀이 수 유지
관리자 포함·제외두 조건 모두 이전 결과와 동일
같은 세션의 같은 날 반복 방문세션 중복 제거 유지
서술형 SQL 풀이객관식 풀이 수에서 제외
늦게 도착한 새로운 세션 이벤트해당 날짜 DAU 1 증가
페이지 조회 실행계획기존 covering index 사용 가능