Django 5.2에서 선언적 테이블 파티셔닝 사용해본 케이스 공유

도둑맞은사슴 @thiefbird@hackers.pub
목표와 접근 방법
최근 프로젝트에서는 대용량 데이터를 처리해야 해서 테이블 파티셔닝을 사용해볼 법한 상황이었다. 내 목표는 Django의 고수준 ORM API를 최대한 활용하면서도 관리 포인트를 줄이고, PostgreSQL의 내장 선언적 파티셔닝 기능을 효과적으로 사용하는 것이었다. PostgreSQL의 공식 Table Partitioning 문서를 참조하여 구현을 진행했다.
초기 문제점과 해결 시도
Django에서는 PostgreSQL의 선언적 파티셔닝과 관련된 바인드된 API가 제공되지 않는다. 이로 인해 처음에는 RunSQL을 사용하여 raw SQL로 파티셔닝을 구현하고 마이그레이션을 관리하는 방법을 시도했다.
Django ORM에서는 모델의 Meta
클래스에서 managed=False
옵션(Options.managed)을 사용하여 고수준 API에서 마이그레이션을 관리하지 않는 테이블을 ORM 모델 클래스를 통해 조작할 수 있다.
이 접근 방식을 사용하면 raw SQL로 파티션들의 논리적 부모 역할을 하는 부모 테이블과 자식 파티션들을 생성한 후, 부모 테이블에 대해 unmanaged ORM 모델을 연결할 수 있고, 그렇게 함으로써 PostgreSQL에 직접 접속해 SQL 쿼리할 때와 동일한 방식으로 부모 테이블에 대해 질의하는 것만으로 PostgreSQL 쿼리 플래너가 자동으로 자식 테이블들에 대해 쿼리하도록 할 수 있다.
잠깐.. 왜 raw SQL을? - PostgreSQL 파티셔닝의 중요한 제약사항..
PostgreSQL에서 파티션 테이블을 사용할 때 반드시 고려해야 할 중요한 제약사항이 있다. PostgreSQL 공식 문서에 따르면, 파티션 테이블에 기본 키(primary key)나 고유 제약(unique constraint)을 정의할 때, 해당 제약 조건에 파티션 키가 반드시 포함되어야 한다. 따라서 BY RANGE
구문에서 지정한 파티셔닝 키 컬럼(여기서는 created_at
)은 복합 primary key에 포함시켜야 한다.
Django에서 네이티브로 위 제약에 맞는 PK를 가지고 테이블 파티셔닝을 해주는 API는 없기 때문에 raw SQL로 굳이 RunSQL 메서드를 써서 작업해야 했던 것이다.
최신 Django 버전의 복합 PK 지원 활용
그런데 개발 중 팀에서 PostgreSQL 확장인 TimescaleDB 사용을 제안하는 의견이 나와서(TimescaleDB 확장을 사용하면 자식 파티션을 직접 생성하지 않아도 되고 여러 편의 도구를 내장하고 있음), 기존 마이그레이션을 물리고 모델 설계를 다시 검토하던 중 최근 릴리즈된 Django 버전에서 복합 PK를 지원하게 되었다는 것을 발견했다.
복합 PK를 ORM 수준에서 선언하면 managed=False
옵션을 사용한 뒤 직접 복합 PK와 다른 필드를 raw SQL로 생성할 필요도 없고 makemigrations 커맨드로 마이그레이션 파일을 생성 후 해당 테이블에 대해 내장 파티셔닝 기능 사용을 선언하거나 TimescaleDB hypertable로 변환하는 것만으로 테이블 마이그레이션이 가능하다. 즉 managed=False
상태의 모델과 달리 스키마를 수정하고 싶을 때마다 raw SQL을 포함한 마이그레이션 파일을 손수 작성할 필요가 없어진다!
구현 코드
class Example(models.Model):
# https://www.postgresql.org/docs/current/ddl-partitioning.html#DDL-PARTITIONING-DECLARATIVE-LIMITATIONS
# Declarative Partitioning 사용시
# 반드시 Partitioning Key를 Composite PK에 포함해 설정
pk = CompositePrimaryKey("uuid", "created_at")
uuid = models.UUIDField(default=uuid.uuid4, editable=False, auto_created=True)
# Partitioning Key
created_at = models.DateTimeField(
db_index=True,
auto_now_add=True,
)
# ... (나머지 필드)
...
# 마이그레이션 파일에서..
migrations.RunSQL(
sql="SELECT create_hypertable('example', 'created_at');",
),
결과 및 향후 개선점
이 방법을 사용하면 여전히 makemigrations
커맨드로 생성된 파일에 RunSQL로 변환 SQL문을 추가해주어야 하지만, managed=True
상태로 ORM 모델을 관리할 수 있다. 또 바닐라 ORM 모델에 쿼리하는 것과 같은 방식으로 쿼리하는 것만으로 자식 파티션 테이블들에 대해 PostgreSQL 내장 스케줄러를 사용한 쿼리가 가능하다.
물론 data retention 등 정책은 Django custom command나 TimescaleDB 확장 쿼리로 따로 관리해주어야 한다.
또 위 사용 사례는 timescaledb에서 제공하는 확장 함수 create_hypertable을 사용해 ORM API로 생성한 테이블을 변환해 사용했는데, timescaledb 없이 PostgreSQL 내장 파티셔닝 기능을 사용하려면 여전히 raw SQL을 사용해야 한다. PostgreSQL에는 기존 테이블을 파티션된 테이블로 변환하는 내장된 기능이 없다.
당장은 만족스러운 편이지만 더 나은 방법이 있을 수 있으므로 찾아보고 있다 . . .