이메일 자동발송에서 가장 무거웠던 작업(전체 DB 부하의 69.6%)을 실서버 기준 약 14배 빠르게 만들었습니다. 보내는 메일 결과는 100% 그대로, 속도만 개선했습니다.
시스템은 계정마다 “지금 보낼 메일이 있는지”를 쉴 새 없이 확인합니다(하루 수백만 번). 그런데 오래된 계정일수록 이미 발송이 끝난 기록 수십만 건을 매번 처음부터 훑은 뒤 버리고 있었습니다 — 헛수고가 반복됐습니다.
sequence_enrollments의 인덱스가 (user_email_account_id) 단일 컬럼이라, status='active' 조건을 인덱스가 아닌 데이터 본문에서 걸러내고 있었음(heap filter). 베테랑 계정일수록 비active 행을 대량 스캔 후 폐기(예: active 0건인데 10.8만 행 스캔).
‘발송중인 것’만 곧장 찾도록 색인 기준을 바꿨습니다. 발송이 끝난 계정은 즉시 건너뜁니다.
(user_email_account_id, status)로 교체. 발송 조건을 인덱스 단계에서 바로 좁혀(Index Condition), 0-active 계정은 Bitmap Index Scan으로 즉시 종료. 단일 인덱스는 prefix로 완전 대체되어 제거. 무중단 적용(CREATE INDEX CONCURRENTLY) + idempotent migration(0484).
메일 발송 횟수를 세는 부분에, 서버가 갑자기 꺼지면 카운트가 영구히 잘못 남는 드문 오류가 있었는데 함께 제거했습니다.
INCR+EXPIRE 사이 crash 시 TTL 누락(영구 카운터) race를 단일 Redis Lua(EVAL)로 원자화. RTT 2→1, 4→1 동시 감소. beta Redis 8.6.1에서 동작 검증.
| 검증 항목 | alpha | beta |
|---|---|---|
| 복합 인덱스 실트래픽 사용(scan↑) | 16,629 ↑ | 30,512 ↑ |
| 옛 단일 인덱스 제거 | ✅ | ✅ |
| 로더 쿼리 새 인덱스 사용 | ✅ | ✅ |
| migration 0484 | ✅ applied | ⏳ 정식 sync (DB 인덱스 live) |
| Redis fix 배포 · 에러 | ✅ 배포 · 에러 0 | ⏳ sync (Lua 검증완료) |
| 회귀(부작용) | 0 | 0 |
인덱스 scan 카운트가 양쪽 모두 계속 증가 = production 트래픽이 실제로 새 인덱스를 타고 있음.
scheduled_idx 95,907회·idx_step_contents_lead 372,536회 사용 중 → 회귀 위험has_pending_work 게이팅 — beta에 컬럼 부재 + false-negative 시 발송 누락(치명)→ 모든 적용은 실서버·테스트서버 양쪽 dual-DB 실측으로 검증, 결과 동일·회귀 0인 것만 반영.
| 항목 | 비중 | 이유 |
|---|---|---|
| #2/#3 전역 로더 | 17.7% | 코드 구조(마이그 dual-run)+데이터 정합 → 팀 트랙(마이그 완료·reconciler) |
| admin·funnel·cleanup 쿼리 | ~0.6% | 저빈도 + 대상 테이블 작거나 이미 적절히 색인됨 → 안전한 신규 개선점 없음 |