Post

데이터베이스 성능과 확장성을 위한 설계 원칙

데이터베이스 성능과 확장성을 위한 설계 원칙

데이터베이스의 성능과 확장성을 확보하는 데 있어 인덱스, 통계, 쿼리 설계는 필수적인 설계 원칙입니다. 이는 비즈니스 데이터를 효과적으로 저장, 연결, 보호하고 확장하기 위한 청사진의 핵심 요소이며, 특히 조회 성능 최적화에 중점을 둡니다.

1. 인덱스 설계

인덱스는 테이블에서 원하는 데이터를 빠르게 찾아갈 수 있도록 돕는 목차와 같은 역할을 합니다. WHERE 절을 사용하더라도 인덱스가 없다면 테이블 전체를 스캔(Full Scan)해야 하므로 성능 저하가 발생할 수 있습니다. 대부분의 관계형 데이터베이스(RDB)는 B-Tree 인덱스를 기본으로 사용하며, 이는 =, BETWEEN, LIKE 'abc%' 조건 및 ORDER BY를 사용하는 쿼리에서 효율적입니다. 인덱스는 SELECT뿐만 아니라 JOIN, ORDER BY, GROUP BY 절의 성능에도 영향을 미칩니다.

인덱스 설계의 주요 포인트:

  • 설계 기준: 자주 사용하는 WHERE, JOIN, ORDER BY, GROUP BY 절의 컬럼을 기준으로 설계해야 합니다.
    • WHERE user_id = ?와 같이 특정 값을 찾는 조건에는 해당 컬럼에 단일 인덱스를 생성합니다.
    • 최신 데이터를 조회(created_at DESC)하거나 특정 순서로 정렬하는 작업이 빈번하다면, 해당 컬럼의 정렬 방향을 고려하여 인덱스를 구성하는 것이 좋습니다.
    • LIKE 'abc%'처럼 검색 문자열이 앞에 오는 패턴은 B-Tree 인덱스가 효율적으로 동작하지만, LIKE '%abc%'와 같이 와일드카드가 앞에 오는 경우는 인덱스를 활용하지 못합니다. 이 경우 Fulltext 인덱스나 별도의 검색 엔진 도입을 고려해야 합니다.
  • 복합 인덱스 (Composite Index): 여러 컬럼을 묶어 생성하는 복합 인덱스는 인덱스를 구성하는 컬럼 순서 중 왼쪽부터 순서대로만 사용 가능합니다. 따라서 WHERE 절에서 자주 사용되는 조회 순서에 맞춰 컬럼 순서를 정하는 것이 중요합니다.
    • 일반적으로 선별도(Cardinality)가 높은, 즉 고유한 값의 비율이 높은 컬럼을 앞쪽에 배치하는 것이 유리합니다. 예를 들어 (name, email) 순서의 복합 인덱스가 있다면, name을 조건으로 사용하거나 nameemail을 모두 사용하는 쿼리는 인덱스를 활용할 수 있습니다. 하지만 email만 단독으로 사용하는 쿼리는 이 인덱스를 사용하지 못합니다.
  • 커버링 인덱스 (Covering Index): SELECT 쿼리에 필요한 모든 컬럼이 인덱스에 포함되어 있어, 테이블에 직접 접근하지 않고 인덱스만으로 결과를 반환하는 경우를 의미합니다. 테이블 데이터 블록에 접근하는 과정이 생략되므로 디스크 I/O가 줄어들어 성능이 크게 향상됩니다.
    • 예를 들어 orders 테이블에 (user_id, status) 복합 인덱스가 있을 때, SELECT user_id, status FROM orders WHERE user_id = 123 쿼리는 테이블을 읽지 않고 인덱스만으로 처리할 수 있습니다.
  • 주의사항:
    • 인덱스는 많을수록 좋은 것이 아닙니다. 인덱스가 추가될수록 INSERT, UPDATE, DELETE 작업 시 인덱스도 함께 수정해야 하므로 쓰기 성능이 저하되고, 추가적인 디스크 저장 공간이 필요합니다.
    • 조인(JOIN) 조건으로 사용되는 컬럼에도 인덱스가 필요합니다. 조인 성능은 대부분 관련 컬럼의 인덱스 유무에 따라 결정됩니다.
    • 잘못된 인덱스 설정은 Gap Lock을 유발하여 동시성 처리 문제를 일으킬 수 있습니다. 따라서 고유 인덱스를 사용하거나 조인 조건을 명확히 하는 것이 중요합니다.

2. 통계 갱신 (ANALYZE)

데이터베이스 관리 시스템(DBMS)의 옵티마이저는 쿼리 실행 계획을 수립할 때 내부적으로 관리하는 통계 정보를 활용합니다. 테이블의 데이터가 자주 변경되면 이 통계 정보가 실제 데이터 분포와 달라져, 비효율적인 쿼리 실행 계획을 생성할 수 있습니다. 따라서 주기적으로 통계를 갱신하여 옵티마이저가 최적의 실행 계획을 수립하도록 유도해야 합니다.

  • PostgreSQL에서는 ANALYZE user;, MySQL에서는 ANALYZE TABLE user;와 같은 명령어를 통해 통계를 갱신할 수 있습니다.

3. 쿼리 설계 및 최적화

쿼리 설계는 정의된 인덱스를 효율적으로 활용하고 데이터 접근 방식을 최적화하여 조회 성능을 극대화하는 과정입니다.

  • EXPLAIN으로 쿼리 성능 분석: EXPLAIN 명령어는 데이터베이스가 해당 쿼리를 어떻게 실행할 것인지에 대한 계획을 보여줍니다.
    • type (접근 방식, ALL은 Full Scan), key (실제 사용된 인덱스), rows (스캔할 것으로 예상되는 행 수), Extra (부가 정보) 등의 항목을 통해 쿼리 성능을 진단할 수 있습니다.
    • 인덱스 적용 후 성능 개선 결과를 보고할 때는 EXPLAIN 결과, 실행 시간, 옵티마이저의 인덱스 사용 여부를 포함하는 것이 좋습니다. 더 나아가 K6와 같은 부하 테스트 도구를 사용하여 P95, P99와 같은 백분위 응답 시간을 함께 제시하면 실무적으로 더 유용합니다.
    • 성능 테스트를 위한 데이터는 10만 건 이상이 이상적이나, 개발 환경의 제약을 고려하여 1~10만 건으로도 충분할 수 있습니다. 다만, 옵티마이저가 인덱스를 올바르게 선택하도록 하려면 데이터의 카디널리티(다양성)를 실제 데이터와 유사하게 구성하는 것이 매우 중요합니다.

실전 조회 성능 최적화 전략:

  • 범위 조건 이후 인덱스 무효화 주의: 복합 인덱스에서 <, >, BETWEEN과 같은 범위 조건이 사용되면, 해당 조건의 컬럼 뒤에 오는 인덱스 컬럼들은 활용되지 못합니다.
    • 특히 시간 범위 조건이 인덱스 앞부분에 있고, 그 뒤에 필터링에 자주 사용되는 다른 조건이 있다면 성능 저하가 발생할 수 있습니다. 카디널리티가 높은 컬럼을 인덱스 앞쪽에 배치하고, 범위 조건은 가능한 한 뒤쪽으로 구성하는 것이 좋습니다.
  • WHERE 조건별 인덱스 적용 전략:
    • AND: 여러 조건이 AND로 연결되면 결과 후보군이 줄어들어 인덱스의 효율이 증가합니다.
    • OR: 각 조건을 개별적으로 처리한 후 결과를 병합하는 방식으로 동작하여 Full Scan의 위험이 커질 수 있습니다. 경우에 따라 OR 대신 UNION으로 쿼리를 분리하는 것이 더 나은 성능을 보일 수 있습니다.
    • =IN: 인덱스의 다음 컬럼까지 활용할 수 있어 효율적입니다.
    • <, >, BETWEEN: 범위 조건으로 사용된 컬럼 이후의 인덱스 컬럼은 사용되지 않습니다.
  • Sync Schedule Strategy: 실시간성과 정합성 사이의 트레이드오프를 극복하기 위한 전략입니다. DB I/O를 줄여 결과적으로 정합성을 맞추는 환경을 구성하는 것을 목표로 합니다.
    • 삽입 및 수정이 빈번한 데이터에 인덱스를 과도하게 적용하면 쓰기 성능 저하와 디스크 용량 증가로 인해 오히려 시스템 전체 성능이 나빠질 수 있습니다.
    • 이러한 경우, 주기적으로 통계 데이터를 집계하는 배치 프로세스를 통해 별도의 통계용 테이블을 생성하고, 조회 시에는 이 통계용 테이블만 사용하도록 설계할 수 있습니다. 이를 통해 원본 데이터의 쓰기 부하를 줄이면서 조회 성능을 크게 향상시킬 수 있으며, 이는 Materialized View 패턴과 유사한 접근 방식입니다.
This post is licensed under CC BY 4.0 by the author.