[백엔드] DB 설계와 쿼리 튜닝 필수 지식

핵심 요약

  • 인덱스 설계: 선택도(Selectivity)가 높은 컬럼을 활용하고, 커버링 인덱스로 쿼리 효율을 극대화해야 함.
  • 조회 성능 최적화: 사전 집계, No-Offset 페이징, 전체 카운팅 지양 등을 통해 불필요한 부하를 줄여야 함.
  • 안정적인 운영 관리: 쿼리 타임아웃, 배치 처리, 트랜잭션 범위 최소화로 시스템 장애를 예방해야 함.

목차

1. 효율적인 인덱스 설계

1-1. 선택도(Selectivity) 고려

  • 인덱스 생성 시 데이터의 선택도가 높은 컬럼을 사용해야 함.
  • 선택도가 높다는 것은 해당 컬럼의 값으로 데이터의 범위를 좁힐 수 있는 정도가 크다는 의미임.
컬럼 데이터 종류 수 선택도 인덱스 적합성
station 약 10,000개 높음 적합
type 5개 (hub 등) 낮음 부적합

1-2. 커버링 인덱스 활용

  • 커버링 인덱스: 쿼리 실행에 필요한 모든 컬럼을 포함하고 있는 인덱스.
  • 실제 데이터 테이블(디스크)에 접근하지 않고 인덱스만으로 데이터를 추출할 수 있어 속도가 매우 빠름.
-- 인덱스: IDX_TIME_TYPE (time, type) 생성 시

-- [Good] 인덱스만으로 조회 가능 (커버링 인덱스)
SELECT time, type FROM metars WHERE time > '2026-01-01' AND type = 'METAR';

-- [Bad] report 컬럼을 찾기 위해 실제 테이블 접근 필요
SELECT time, type, report FROM metars WHERE time > '2026-01-01' AND type = 'METAR';

1-3. FULLTEXT 인덱스에 대한 오해

  • 문자열 포함 검색 시 LIKE가 풀스캔을 유발한다는 이유로 무조건 FULLTEXT 인덱스를 권장하는 것은 잘못된 접근임.
  • FULLTEXT 인덱스는 아래 조건들이 만족될 때 고려해야 함.
  • 적용 조건:
    • 데이터가 10만 건 이상으로 매우 많을 것.
    • 검색 대상이 TEXT, VARCHAR 등 긴 문자열일 것.
    • 검색 단어가 전체 데이터 중 아주 일부(5% 미만)에만 포함될 것.

핵심 포인트: 인덱스는 데이터의 범위를 효과적으로 줄일 수 있을 때만 의미가 있음.

2. 조회 성능 높이기

2-1. 사전 집계(Pre-aggregation) 활용

  • A 테이블과 연관된 B 테이블의 값을 매번 집계(COUNT 등)하는 것은 성능 저하의 주범임.
  • 예시: 게시글 목록 조회 시 좋아요 개수를 매번 SELECT COUNT(id) FROM likes로 계산하는 경우.
  • 해결: posts 테이블에 like_count 컬럼을 추가하고, 좋아요 발생 시점에 값을 갱신해 두면 조회 쿼리가 단순해짐.

2-2. No-Offset 페이징 (목록 조회 최적화)

  • 일반적인 OFFSET 기반 페이징은 뒤로 갈수록 조회 속도가 급격히 느려짐.
  • 이유: LIMIT 10 OFFSET 50000은 앞선 5만 개 데이터를 모두 읽고 버린 뒤 10개를 가져오기 때문임.
  • 해결: 인덱스가 걸린 id를 활용해 범위를 좁히는 방식 사용.
-- [Bad] 앞선 5만 개를 다 읽어야 함
SELECT * FROM posts ORDER BY id DESC LIMIT 10 OFFSET 50000;

-- [Good] 인덱스를 타서 바로 해당 위치부터 조회함
SELECT * FROM posts WHERE id < 50010 ORDER BY id DESC LIMIT 10;

2-3. 전체 카운팅(COUNT) 금지

  • 전체 데이터 개수를 세는 COUNT(*) 쿼리는 지양해야 함.
  • 이유: InnoDB 엔진은 트랜잭션 격리 수준을 보장하기 위해, 현재 시점에서 유효한 행을 일일이 추려내며 풀 스캔을 수행하기 때문임.
  • 대안:
    • SHOW TABLE STATUS 또는 information_schema의 통계치 활용 (약간의 오차 허용 시).
    • 별도의 통계 테이블을 만들어 개수를 따로 관리.

핵심 포인트: 실시간 계산을 줄이고, 인덱스를 활용해 읽어야 할 데이터의 양 자체를 줄이는 것이 성능 튜닝의 핵심임.

3. 기타 관리 포인트

3-1. 쿼리 타임아웃 설정

  • 쿼리 실행 시간이 무한정 길어지도록 방치해선 안 됨.
  • 사용자는 응답이 늦으면(5초 등) 새로고침을 반복하며, 이는 서버에 요청 대기열을 폭증시킴.
  • 해결: 적절한 타임아웃을 설정해 오래 걸리는 쿼리는 강제로 종료시켜야 서버 전체의 부하를 막을 수 있음.

3-2. 적절한 배치 처리

  • 대량의 데이터를 한 번에 삽입하거나 집계하면 DB 리소스를 독점하게 됨.
  • 해결: 데이터를 적절한 크기로 쪼개서 수행해야 함.
  • 예시: 10년 치 데이터를 1년 단위로 분할 삽입, 월간 집계를 일간 집계 30회로 나누어 수행.

3-3. DB 커넥션 개수 관리

  • 트래픽 증가로 API 서버를 증설하더라도, DB의 최대 커넥션(Max Connections) 수가 고정되어 있으면 병목이 발생함.
  • DB 서버의 CPU, 메모리 자원 상황을 고려해 커넥션 풀 크기를 적절히 조절해야 함.

3-4. 트랜잭션 범위 관리

  • 하나의 트랜잭션 안에 외부 API 호출 등 DB와 무관한 작업이 섞이면 위험함.
  • 예시: [회원가입 Insert] → [환영 메일 발송 API] → [Commit].
  • 문제: 메일 서버 장애 시 회원가입까지 롤백되어 버림. 또한 API 응답 대기 시간만큼 DB 커넥션을 점유하게 됨.
  • 해결: 트랜잭션은 순수하게 DB 작업 단위로만 짧게 묶어야 함.

핵심 포인트: 쿼리 성능뿐만 아니라 연결, 시간, 트랜잭션 범위 등 운영 레벨의 제약 조건을 관리하는 것이 안정성의 기본임.

이 글은 어떠셨나요? 자유롭게 의견을 남겨주세요! 💬