200만 행 규모의 테이블에 DDL을 적용하기 전 미리 알았더라면 좋았을 것들

200만 행 규모의 테이블을 운영 중인 SaaS 환경에서 스키마 변경(DDL) 시 발생할 수 있는 서비스 중단 리스크와 이를 방지하기 위한 PostgreSQL 기반의 안전한 마이그레이션 전략을 다룹니다. 특히 PostgreSQL 버전에 따른 잠금 메커니즘의 차이와 인덱스 생성 시 쓰기 차단을 피하는 구체적인 기술적 해법을 제시합니다.

AI 요약

운영 1년 차를 맞이한 프로젝트 관리 SaaS는 Issue 테이블 200만 행, 변경 로그 테이블 2000만 행을 돌파하며 성능 저하 문제에 직면했습니다. 사용자가 필터를 전환할 때마다 5초의 대기 시간이 발생하고, 슬로우 쿼리 로그의 p95 수치가 3초를 기록하는 등 개선이 시급한 상황에서 단순히 DDL을 적용하는 것은 매우 위험할 수 있습니다. 200만 행 규모의 거대 테이블에 DDL을 실행할 경우, 특정 버전의 데이터베이스나 설정에서는 테이블 전체에 락(Lock)이 걸려 서비스가 마비될 수 있기 때문입니다. 본 기사는 PostgreSQL 10 이하 버전에서의 컬럼 추가 문제, 인덱스 생성 시의 쓰기 차단 문제, 그리고 락 대기열로 인한 후속 쿼리 마비 현상 등을 실제 사례와 함께 분석합니다. 이를 통해 'NULL 허용 후 배치 업데이트' 방식이나 'CONCURRENTLY' 옵션 활용 등 대규모 트래픽 환경에서도 중단 없는 DB 운영을 위한 전문적인 가이드를 제공합니다.

핵심 인사이트

  • PostgreSQL 버전별 차이: PostgreSQL 11 이후 버전은 NOT NULL DEFAULT 컬럼 추가 시 카탈로그 정보만 업데이트하지만, 10 이하 버전은 200만 행 전체를 재작성하여 ACCESS EXCLUSIVE LOCK을 유발합니다.
  • 인덱스 생성 전략: 일반적인 CREATE INDEXSHARE LOCK을 획득하여 모든 쓰기 작업을 차단하므로, 운영 중인 서비스에서는 CONCURRENTLY 옵션이 필수적입니다.
  • 퍼셜 인덱스(Partial Index) 활용: 소프트 딜리트(Soft Delete)를 사용하는 테이블에서 WHERE deleted_at IS NULL 조건을 가진 퍼셜 인덱스를 생성하면 인덱스 크기를 20% 이상 줄이고 성능을 최적화할 수 있습니다.
  • 락 큐(Lock Queue)의 위험성: ALTER TABLE이 락을 획득하기 위해 대기하는 동안, 뒤따르는 모든 SELECT 쿼리까지 대기열에 묶여 시스템 전체 장애로 번질 수 있습니다.

주요 디테일

  • 3단계 안전 업데이트: 거대 테이블에 컬럼 추가 시 (1)NULL 허용 컬럼 추가, (2)1만 건 단위 배치 업데이트, (3)NOT NULL 제약 조건 추가의 순서를 통해 락 시간을 최소화합니다.
  • Django 마이그레이션 주의사항: CONCURRENTLY 인덱스 생성 시 Django의 기본 트랜잭션 기능을 끄기 위해 atomic = False 설정과 RunSQL을 사용해야 합니다.
  • 성능 수치 측정: 필터링 쿼리가 p95 기준 3초를 초과했을 때 인덱스 추가를 결정했으며, 소프트 딜리트 레코드가 전체의 20%일 경우 퍼셜 인덱스로 동일한 비율의 용량 절감이 가능함을 확인했습니다.
  • 운영 환경 불일치 리스크: 본방 DB 버전이 높더라도 스테이징 환경과의 버전 차이 혹은 미래의 다운그레이드 가능성을 고려하여 가장 보수적이고 안전한 3단계 방식을 상시 적용할 것을 권장합니다.

향후 전망

  • DB 마이그레이션 자동화 도구의 진화: 단순한 스키마 변경을 넘어 DB 부하와 락 상태를 실시간으로 감지하며 안전하게 DDL을 수행하는 자동화 로직이 더욱 중요해질 것입니다.
  • 데이터 아키텍처의 고도화: 테이블 규모가 지속적으로 커짐에 따라 물리 삭제(Hard Delete)와 데이터 아카이빙 전략이 소프트 딜리트의 대안으로 다시 주목받을 가능성이 높습니다.
Share

이것도 읽어보세요

댓글

이 소식에 대한 의견을 자유롭게 남겨주세요.

댓글 (0)

불러오는 중...