일간과 주간과 월간 통계를 각각 만들었는데 셋이 각자 원본을 훑고 있었다. 원본이 커지면서 세 배치가 같은 구간을 반복해 읽는 것이 문제가 됐다.
Table of contents
Open Table of contents
구간이 겹쳐 있었다
각 배치의 조회 조건을 나란히 놨다.
일간 reg_date >= 오늘 00:00
주간 reg_date >= 7일 전
월간 reg_date >= 30일 전
어제 자료는 세 배치가 다 읽고 오늘 자료도 셋 다 읽는다. 30일치가 400만 행인데 월간 배치가 매번 그 400만 행을 전부 훑고 있었다.
겹치지 않게 나눴다
주간은 일간 결과를 쓰고 월간은 주간 결과를 쓰게 했다.
일간 원본 → stat_daily (오늘 것만)
주간 stat_daily → stat_weekly (7행)
월간 stat_daily → stat_monthly (30행)
주간이 원본을 안 보고 stat_daily 의 7행만 본다. 월간도 stat_daily 30행을 보는데 주간을 거치면 주 경계가 월 경계와 안 맞아서 일간에서 바로 갔다.
읽는 양이 400만 행에서 30행으로 줄면서 월간 배치가 도는 시간도 그만큼 짧아졌다.
합칠 수 있는 값과 없는 값
다만 모든 값이 이렇게 합쳐지는 것은 아니었다.
| 값 | 합칠 수 있나 |
|---|---|
| 건수 | SUM |
| 합계 | SUM |
| 최소·최대 | MIN·MAX |
| 평균 | 개수로 가중해야 함 |
| 순 방문자 수 | 불가 |
AVG 는 단순 평균이 안 되고 일별 건수가 다르면 틀린다.
SELECT SUM(sum_v) / SUM(cnt) AS avg_v FROM stat_daily WHERE ...;
합계와 개수를 저장해 두고 나중에 나눈다. 순 방문자 수는 아예 안 되는데 어제 온 사람이 오늘도 오면 이틀 합이 두 배가 아니다. 이것은 원본을 봐야 해서 따로 뒀다.
다시 계산할 수 있게 만들었다
stat_daily 가 틀리면 주간과 월간도 틀리므로 다시 계산하는 방법이 필요했다.
def rebuild_daily(d):
cur.execute("DELETE FROM stat_daily WHERE stat_date = %s", (d,))
cur.execute("""INSERT INTO stat_daily ... WHERE DATE(reg_date) = %s""", (d,))
def rebuild_range(start, end):
d = start
while d <= end:
rebuild_daily(d)
d += timedelta(days=1)
rebuild_weekly(start, end)
rebuild_monthly(start, end)
날짜 범위를 주면 그 구간을 다시 만든다. DELETE 하고 INSERT 하는 방식이라 여러 번 돌려도 결과가 같다.
늦게 들어온 자료도 있었다. 장치가 통신이 끊겼다가 나중에 몰아서 보낸다.
rebuild_daily(today - timedelta(days=1))
rebuild_daily(today)
오늘 집계할 때 어제 것도 다시 계산하게 했다. 이틀치만 다시 하면 대부분 잡히고 더 늦게 오는 것은 드물어서 주간 집계 때 일주일치를 다시 계산했다.
검증 — 실행 순서와 검산
셋을 같은 시각에 돌리면 겹친다.
02:00 일간
02:10 주간 (일간이 끝난 뒤)
02:20 월간 (일간이 끝난 뒤)
주간과 월간은 stat_daily 를 쓰므로 그 뒤여야 한다. 10분 간격을 준 것은 일간이 늦어질 수 있어서인데 더 확실하게 하려면 앞 배치의 완료를 확인하게 한다.
def wait_for_daily(d, timeout=600):
waited = 0
while waited < timeout:
cur.execute("SELECT COUNT(*) FROM stat_daily WHERE stat_date=%s", (d,))
if cur.fetchone()[0] > 0:
return True
time.sleep(10); waited += 10
return False
상한을 두고 기다리고 안 끝나면 실패로 끝낸다.
일간 합과 월간 값이 맞는지도 확인했다.
SELECT
(SELECT SUM(cnt) FROM stat_daily WHERE stat_date BETWEEN '2016-09-01' AND '2016-09-30') AS from_daily,
(SELECT cnt FROM stat_monthly WHERE stat_month = '2016-09') AS monthly;
두 값이 같아야 하는데 처음에 달랐다. 월 경계에서 하루가 빠져 있었고 BETWEEN 의 끝 조건이 시각까지 비교돼서 마지막 날 자료가 빠진 것이었다.
단계를 나누면 검산할 수 있는 지점이 생긴다. 한 번에 계산하면 맞는지 확인할 방법이 없다.
정리
- 주기가 다른 집계가 각자 원본을 훑으면 같은 구간을 여러 번 읽는다
- 상위 집계가 하위 집계 결과를 쓰면 읽는 양이 크게 준다
- 합칠 수 있는 값과 없는 값을 가른다
AVG는 합계와 개수를 저장해야 나중에 합쳐진다- 순 방문자 수 같은 것은 못 합치고 원본을 봐야 한다
DELETE뒤INSERT로 넣으면 여러 번 돌려도 결과가 같다- 늦게 오는 자료를 위해 어제 것도 다시 계산한다
- 실행 시각을 어긋나게 하되 앞 단계 완료를 확인하는 쪽이 확실하다
BETWEEN의 끝 조건이 시각까지 비교돼 마지막 날이 빠질 수 있다- 단계를 나누면 검산할 지점이 생긴다