인덱스 기초: 포인트 내역 조회 속도를 결정하는 (uid, created_at) 인덱스의 의미

포인트 원장(point_history)은 시간이 지날수록 계속 쌓입니다. 초기에는 수천 건이라 체감이 없지만, 몇 달만 지나도 수십만 건, 몇 년이면 수천만 건이 될 수 있습니다. 이때 가장 먼저 느려지는 화면이 보통 “내 포인트 내역 보기”입니다.

조회가 느려지면 사용자는 새로고침을 반복하고, 그 과정에서 요청이 중복되거나(재시도), 운영팀은 “왜 이렇게 느려졌지?”라는 원인 분석에 시간을 쓰게 됩니다. 그래서 포인트 시스템에서는 처음부터 조회 패턴에 맞는 인덱스를 설계해 두는 편이 안전합니다.

이 단원의 목적: 인덱스를 “추상 개념”이 아니라 “조회 패턴의 설계”로 이해하기

인덱스는 단순히 “빠르게 만드는 옵션”이 아닙니다. 어떤 인덱스를 만들지는 결국 어떤 질문을 자주 던질 것인가에 대한 답입니다. 이 글에서는 포인트 내역의 대표 조회 패턴을 기준으로, 인덱스가 왜 필요한지와 어떤 조합이 현실적으로 효과적인지를 정리합니다.

1) 인덱스는 “찾기 위한 목차”다

테이블을 책에 비유하면, 전체 데이터를 훑는 것은 책을 처음부터 끝까지 읽는 것과 비슷합니다. 반면 인덱스는 “목차”나 “색인”처럼 원하는 페이지로 바로 이동하게 해줍니다.

데이터베이스가 인덱스를 사용하면, 조건에 맞는 행을 찾기 위해 테이블 전체를 읽는 대신(풀 스캔), 인덱스에서 필요한 범위만 탐색한 뒤 해당 행을 접근합니다. 특히 데이터가 커질수록 이 차이는 압도적으로 커집니다.

2) 포인트 내역의 대표 조회 패턴 3가지

인덱스는 “자주 쓰는 조회”를 기준으로 잡는 것이 기본입니다. 포인트 내역(point_history)에서 거의 고정적으로 반복되는 패턴은 아래 3가지입니다.

  1. 사용자 기준: 특정 uid의 내역을 본다
  2. 기간 기준: 최근 30일, 특정 달 등 created_at 구간으로 자른다
  3. 정렬 기준: 최신순(created_at DESC, id DESC)으로 보여준다

그래서 인덱스 설계는 자연스럽게 (uid, created_at)로 모입니다.

3) (uid, created_at) 복합 인덱스가 강력한 이유

복합 인덱스는 컬럼 여러 개를 한 덩어리로 묶어 만든 인덱스입니다. (uid, created_at) 인덱스는 “특정 사용자(uid)의 데이터”를 먼저 좁힌 뒤, 그 안에서 “시간(created_at) 범위”를 빠르게 탐색하도록 돕습니다.

-- 대표 인덱스 CREATE INDEX idx_uid_created ON point_history(uid, created_at);

이 인덱스가 잘 맞는 대표 쿼리는 아래처럼 생겼습니다.

SET @uid := '11111111-1111-1111-1111-111111111111'; SELECT id, action_type, amount, ref_id, created_at FROM point_history WHERE uid = @uid AND created_at >= NOW() - INTERVAL 30 DAY ORDER BY created_at DESC, id DESC LIMIT 50;

위 쿼리는 포인트 내역 화면에서 가장 흔한 형태입니다. 인덱스가 없다면 “uid에 해당하는 행을 찾기 위해” 많은 데이터를 읽게 되고, created_at 범위와 정렬까지 수행하면서 비용이 크게 늘어납니다.

4) 인덱스의 “왼쪽부터” 규칙: 복합 인덱스는 선두 컬럼이 중요하다

복합 인덱스는 (A, B, C)로 만들면, (A) 또는 (A, B) 또는 (A, B, C) 조건에서 특히 잘 동작합니다. 흔히 “왼쪽(prefix) 규칙”이라고 부릅니다.

그래서 (uid, created_at) 인덱스는 다음과 같은 쿼리에 특히 유리합니다.

  • uid만 조건으로 조회
  • uid + created_at 조건으로 조회

반면 created_at만 단독으로 검색하는 쿼리에는 (uid, created_at) 인덱스가 큰 도움이 되지 않을 수 있습니다. 이럴 때는 별도의 created_at 인덱스가 필요할 수도 있습니다. 다만 포인트 내역은 “개인 화면”이 많아서 uid 중심 인덱스가 우선인 경우가 많습니다.

5) 정렬(ORDER BY)과 인덱스: 정렬 비용을 줄이는 방향

ORDER BY는 데이터가 많을수록 비용이 커집니다. 인덱스가 정렬 기준과 맞아떨어지면, 데이터베이스는 이미 정렬된 순서대로 결과를 읽어올 수 있어 정렬 비용이 줄어듭니다.

다만 여기서 주의할 점이 있습니다. (uid, created_at) 인덱스는 created_at 기준 정렬에 유리하지만, “created_at이 같은 행”이 있을 수 있으므로 id까지 함께 정렬하는 경우가 많습니다. 이때 완전히 동일한 정렬 순서를 인덱스로 커버하려면 (uid, created_at, id) 같은 형태를 고민할 수 있습니다.

-- 선택: 정렬 안정성을 더 강하게 가져가고 싶다면 CREATE INDEX idx_uid_created_id ON point_history(uid, created_at, id);

이런 확장은 데이터 규모/조회 빈도/쓰기 비용에 따라 결정됩니다. 인덱스는 “읽기”를 빠르게 하지만 “쓰기(INSERT/UPDATE)” 비용을 늘리기 때문입니다.

6) 인덱스는 많을수록 좋은가? 아니다: 쓰기 비용과 저장 공간

포인트 원장은 보통 “읽기(조회)”도 많지만 “쓰기(적립/차감 기록)”도 매우 많습니다. 인덱스가 추가될수록 INSERT 시 인덱스도 함께 갱신해야 하므로 쓰기 비용이 늘어납니다.

그래서 인덱스는 다음 기준으로 점검하는 게 좋습니다.

  • 정말 자주 쓰는 조회에만 인덱스를 건다
  • 비슷한 인덱스가 중복되면 정리한다
  • 인덱스가 쿼리의 WHERE/ORDER BY와 맞는지 확인한다

7) ref_id 인덱스는 언제 필요할까

고객 문의나 운영 점검에서 “주문번호/이벤트ID로 내역을 추적”하는 일이 자주 생기면 ref_id 인덱스가 도움이 됩니다. 특히 UNIQUE(uid, ref_id)를 걸었다면, 그 UNIQUE 자체가 인덱스 역할을 하기 때문에 별도의 ref_id 인덱스가 중복이 될 수도 있습니다.

따라서 ref_id 인덱스는 다음처럼 의도를 분리해서 생각하면 좋습니다.

  • (uid, ref_id) UNIQUE: “중복 방지 + 사용자 단위 추적”
  • ref_id 단독 인덱스: “ref_id만으로 전체 추적”이 필요할 때
-- 운영에서 ref_id로 단독 검색이 잦다면 CREATE INDEX idx_ref_id ON point_history(ref_id);

8) EXPLAIN으로 인덱스 사용 여부 확인하기(개념 맛보기)

인덱스를 만들었는데도 느릴 수 있습니다. 그 이유는 “DB가 그 인덱스를 안 쓰는 경우”가 있기 때문입니다. 이때 확인하는 도구가 EXPLAIN입니다. EXPLAIN은 쿼리를 어떻게 실행할지(인덱스를 쓰는지, 어떤 순서인지) 실행 계획을 보여줍니다.

EXPLAIN SELECT id, action_type, amount, created_at FROM point_history WHERE uid = @uid AND created_at >= NOW() - INTERVAL 30 DAY ORDER BY created_at DESC, id DESC LIMIT 50;

지금 단계에서는 EXPLAIN의 모든 항목을 해석할 필요는 없습니다. 일단 “인덱스를 탔는지(사용했는지)”를 확인하는 습관만 잡아도 큰 도움이 됩니다. EXPLAIN 해석은 후반의 성능 튜닝 단계에서 더 자세히 다룹니다.

9) 실전 체크리스트: 포인트 내역 인덱스 설계의 최소 기준

  • idx_uid_created는 거의 기본: (uid, created_at)
  • 정렬이 중요하면 (uid, created_at, id)도 고려
  • ref_id 검색이 잦으면 ref_id 단독 인덱스 고려
  • 인덱스는 “읽기”를 빠르게 하지만 “쓰기”를 느리게 만든다
  • EXPLAIN으로 실제 사용 여부를 확인한다

다음 글 예고: INSERT 기초로 적립/차감 이벤트를 “일관된 패턴”으로 기록하기

다음 글에서는 INSERT를 다룹니다. 포인트 시스템에서는 INSERT가 단순히 데이터를 넣는 행위가 아니라, “원장에 사실을 기록하는 행위”입니다. 어떤 컬럼을 필수로 채워야 하고, ref_id/request_id 같은 값은 어떻게 다루면 안전한지, 그리고 실수하기 쉬운 패턴(부호 정책 흔들림, created_at 누락)을 어떻게 예방하는지 정리합니다.

댓글

이 블로그의 인기 게시물

JOIN 기초: users와 point_history를 합쳐 ‘회원별 요약(잔액/최근 활동)’ 만들기

점검 SQL: “원장 합계(SUM) vs balance” 불일치를 찾아내고 원인을 좁히는 방법

EXPLAIN 기초: 점검/리포트 쿼리가 느려질 때 “왜 느린지” 확인하는 방법