Django 앱을 PostgreSQL에서 SQL Server 마이그레이션

이 문서는 PostgreSQL(psycopg2또는psycopg)에서 SQL Server(mssql-django)로 이동하는 Django 애플리케이션에 대한 자세한 마이그레이션 가이드입니다. 데이터베이스에서 마이그레이션하는 일반적인 개요는 Django 앱을 다른 데이터베이스에서 SQL Server 마이그레이션을 참조하세요.

사전 요구 사항

  • Python 3.8 이상
  • Microsoft SQL Server용 ODBC 드라이버 17 또는 18. mssql-django 설치를 참조하세요.
  • SQL Server 2016 이상 또는 Azure SQL Database

데이터베이스 백 엔드 전환

settings.py에서 PostgreSQL 구성을 바꾸세요:

# Before (PostgreSQL)
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "mydb",
        "USER": "myuser",
        "PASSWORD": "mypassword",
        "HOST": "localhost",
        "PORT": "5432",
    },
}

# After (SQL Server)
DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "mydb",
        "USER": "myuser",
        "PASSWORD": "mypassword",
        "HOST": "localhost",
        "PORT": "1433",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
        },
    },
}

다음과 같이 requirements.txt를 업데이트합니다.

# Remove
# psycopg2-binary>=2.9
# or psycopg[binary]>=3.1

# Add
mssql-django>=1.5

django.contrib.postgres 기능 바꾸기

이 모듈은 django.contrib.postgres PostgreSQL 관련 필드, 함수 및 조회를 제공합니다. 이것들은 SQL Server에서는 작동하지 않습니다. 다음 섹션에서는 각 기능을 바꾸는 방법을 보여줍니다.

배열 필드

PostgreSQL ArrayField 은 배열을 기본적으로 저장합니다. SQL Server 배열 열 형식이 없습니다.

옵션 1: JSONField (Django 3.2 이상 버전에서 작동)

# Before
from django.contrib.postgres.fields import ArrayField

class Product(models.Model):
    tags = ArrayField(models.CharField(max_length=50), default=list)

# After
class Product(models.Model):
    tags = models.JSONField(default=list)

변경 사항 조회:

# Before (PostgreSQL)
Product.objects.filter(tags__contains=["sale"])
Product.objects.filter(tags__overlap=["sale", "new"])
Product.objects.filter(tags__len=3)

# After (SQL Server with JSONField)
# Use __contains for exact list matching
Product.objects.filter(tags__contains=["sale"])

# For overlap-style queries, use raw SQL
from django.db.models.expressions import RawSQL
Product.objects.filter(
    pk__in=RawSQL(
        """
        SELECT p.id FROM products_product p
        CROSS APPLY OPENJSON(p.tags) t
        WHERE t.value IN (%s, %s)
        """,
        ["sale", "new"],
    )
)

옵션 2: 관련 테이블 (정규화됨, 큰 배열 또는 빈번한 필터링에 더 적합)

class Product(models.Model):
    name = models.CharField(max_length=200)

class ProductTag(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name="tags")
    tag = models.CharField(max_length=50, db_index=True)

    class Meta:
        unique_together = [("product", "tag")]

HStoreField

다음으로 대체합니다.JSONField

# Before
from django.contrib.postgres.fields import HStoreField

class Profile(models.Model):
    metadata = HStoreField(default=dict)

# After
class Profile(models.Model):
    metadata = models.JSONField(default=dict)

JSONField 는 동일한 키 조회 구문을 지원합니다.

# Both backends support this
Profile.objects.filter(metadata__theme="dark")

범위 필드

PostgreSQL 범위 형식(IntegerRangeField, BigIntegerRangeField, DateRangeField, DateTimeRangeField) DecimalRangeField에는 동등한 SQL Server 없습니다. 다음 두 개의 별도 필드를 사용합니다.

# Before
from django.contrib.postgres.fields import DateRangeField

class Event(models.Model):
    dates = DateRangeField()

# After
class Event(models.Model):
    start_date = models.DateField()
    end_date = models.DateField()

별도의 필드 비교를 사용하도록 쿼리를 업데이트합니다. 이전에는 PostgreSQL의 DateRangeField:

from django.contrib.postgres.fields import DateRangeField
from psycopg2.extras import DateRange

Event.objects.filter(dates__contains=DateRange(start, end))

그 후 SQL Server에서 DateField 열이 두 개인 경우:

from datetime import date

start = date(2026, 1, 1)
end = date(2026, 12, 31)

Event.objects.filter(start_date__lte=start, end_date__gte=end)

CITextField 및 CIEmailField

PostgreSQL의 대/소문자를 구분하지 않는 텍스트 형식은 citext 확장을 사용합니다. SQL Server 기본 데이터 정렬(SQL_Latin1_General_CP1_CI_AS)은 이미 대/소문자를 구분하지 않으므로 표준 CharField 이며 EmailField 동일한 방식으로 동작합니다.

# Before
from django.contrib.postgres.fields import CITextField

class Tag(models.Model):
    name = CITextField(max_length=100)

# After - already case-insensitive with default SQL Server collation
class Tag(models.Model):
    name = models.CharField(max_length=100)

SearchVector, SearchQuery, SearchRank

PostgreSQL 전체 텍스트 검색은 Django와 긴밀하게 통합됩니다. SQL Server 자체 전체 텍스트 검색 엔진이 있지만 Django ORM 통합은 없습니다. 이 문서의 뒷 부분에서 전체 텍스트 검색 마이그레이션 을 참조하세요.

집계 함수

PostgreSQL 관련 집계를 대체합니다.

# Before
from django.contrib.postgres.aggregates import ArrayAgg, StringAgg

Product.objects.values("category").annotate(
    all_names=ArrayAgg("name"),
    name_list=StringAgg("name", delimiter=", "),
)

# After - use SQL Server equivalents via RawSQL
from django.db.models.expressions import RawSQL

Product.objects.values("category").annotate(
    name_list=RawSQL(
        "STRING_AGG(name, ', ') WITHIN GROUP (ORDER BY name)",
        [],
    ),
)

메모

STRING_AGG에는 SQL Server 2017 이상 또는 Azure SQL Database 필요합니다.

전체 텍스트 검색 마이그레이션

PostgreSQL 전체 텍스트 검색은 , tsvectortsquery 인덱스를 사용합니다GIN. SQL Server 별도의 전체 텍스트 검색 엔진이 있습니다.

SQL Server 전체 텍스트 검색 사용

-- Create a full-text catalog
CREATE FULLTEXT CATALOG [MyAppCatalog] AS DEFAULT;

-- Create a full-text index (table must have a unique index)
CREATE FULLTEXT INDEX ON [products_product]([name], [description])
KEY INDEX [PK_products_product]
WITH CHANGE_TRACKING AUTO;

Django에서 전체 텍스트 검색 쿼리

원시 SQL을 사용하여 SQL Server CONTAINSFREETEXT 함수에 액세스합니다.

from django.db.models.expressions import RawSQL

# Equivalent of PostgreSQL SearchVector + SearchQuery
def search_products(query):
    return Product.objects.filter(
        pk__in=RawSQL(
            """
            SELECT p.id FROM products_product p
            WHERE CONTAINS((p.name, p.description), %s)
            """,
            [query],
        )
    )

순위 결과의 경우(SearchRank와 동일):

def search_products_ranked(query):
    return Product.objects.raw(
        """
        SELECT p.*, ft.[RANK]
        FROM products_product p
        INNER JOIN CONTAINSTABLE(products_product, (name, description), %s) ft
            ON p.id = ft.[KEY]
        ORDER BY ft.[RANK] DESC
        """,
        [query],
    )

전문 인덱스 유지 관리 실행 가이드

마이그레이션 후 SQL Server 전체 텍스트 인덱스에 대한 유지 관리 계획:

  • 거의 실시간 업데이트에는 CHANGE_TRACKING AUTO를 사용합니다.
  • CHANGE_TRACKING MANUAL를 대량 로드 창에 사용한 다음 전체 채우기를 실행합니다.
  • sys.fulltext_indexessys.dm_fts_index_population을 통해 크롤링 상태 및 백로그를 추적합니다.

상태 확인:

SELECT
    OBJECT_NAME(i.object_id) AS table_name,
    i.change_tracking_state_desc,
    i.has_crawl_completed,
    i.crawl_type_desc
FROM sys.fulltext_indexes AS i;

수동 추적을 사용하여 큰 데이터를 로드한 후:

ALTER FULLTEXT INDEX ON [products_product] START FULL POPULATION;

Tip

트래픽이 적은 시간대에 전체 텍스트 인덱스를 재구축하거나 다시 채우십시오. 대규모 테이블에서는 전체 데이터 채우기에 비용이 많이 들 수 있습니다.

검색 관리자 만들기

깨끗한 액세스를 위해 관리자에서 원시 SQL을 래핑합니다.

class ProductSearchManager(models.Manager):
    def search(self, query):
        if not query:
            return self.none()
        return self.filter(
            pk__in=RawSQL(
                """
                SELECT p.id FROM products_product p
                WHERE CONTAINS((p.name, p.description), %s)
                """,
                [query],
            )
        )

class Product(models.Model):
    name = models.CharField(max_length=200)
    description = models.TextField()

    objects = ProductSearchManager()
    # Usage: Product.objects.search("mountain bike")

PostGIS 및 공간 데이터

mssql-django 는 GeoDjango GIS 백 엔드를 포함하지 않습니다. PostgreSQL 애플리케이션이 PostGIS를 통해 django.contrib.gis사용하는 경우 공간 쿼리를 SQL Server Django ORM으로 직접 마이그레이션할 수 없습니다.

SQL Server 기본적으로 지리기하 도형 데이터 형식을 지원합니다. 마이그레이션 후 공간 데이터를 사용하려면 다음을 수행합니다.

  • 원시 SQL 또는 SQL Server 지리 또는 기하 도형 열에 매핑되는 사용자 지정 모델 필드를 사용하여 공간 데이터를 저장합니다.
  • SQL Server 기본 제공 공간 함수와 함께 원시 SQL을 사용하여 공간 데이터를 쿼리합니다.
from django.db import connection

with connection.cursor() as cursor:
    cursor.execute(
        """
        SELECT id, name
        FROM stores
        WHERE location.STDistance(geography::Point(%s, %s, 4326)) <= %s
        """,
        [latitude, longitude, radius_meters],
    )
  • Django에 SQL Server 공간 지원을 추가하거나 다른 모든 항목에 ORM을 사용하는 동안 공간 쿼리를 원시 SQL로 유지하는 타사 라이브러리를 고려합니다.

메모

애플리케이션이 GeoDjango 공간 조회에 크게 의존하는 경우 마이그레이션 비용을 신중하게 평가합니다. 공간 쿼리를 원시 SQL로 이동하려면 각 GeoDjango 공간 필터를 다시 작성해야 합니다.

연결 풀링 마이그레이션

PostgreSQL 애플리케이션이 연결 풀링에 사용하는 pgbouncer 경우 Django의 기본 제공 연결 관리 또는 ODBC 연결 풀링으로 대체합니다.

Django 연결 재사용

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "HOST": "<your-server>",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
        },
        "CONN_MAX_AGE": 600,  # Reuse connections for 10 minutes
        "CONN_HEALTH_CHECKS": True,  # Django 4.1+
    },
}

자세한 내용은 mssql-django의 연결 풀링을 참조하세요.

DISTINCT ON 대안

PostgreSQL은 그룹당 하나의 행을 가져올 수 있도록 지원합니다 DISTINCT ON . SQL Server 이 구문을 지원하지 않습니다. 대신 창 함수를 사용합니다.

# Before (PostgreSQL)
Entry.objects.order_by("blog_id", "-pub_date").distinct("blog_id")

# After (SQL Server) - use raw SQL with ROW_NUMBER
Entry.objects.raw(
    """
    SELECT * FROM (
        SELECT *, ROW_NUMBER() OVER (PARTITION BY blog_id ORDER BY pub_date DESC) AS rn
        FROM blog_entry
    ) sub
    WHERE rn = 1
    """
)

JSONB 쿼리

PostgreSQL의 jsonb 형식은 풍부한 쿼리 연산자를 지원합니다. SQL Server 2016년 SQL Server 이후 사용할 수 있는 쿼리 함수를 사용하여 JSON을 nvarchar(max)로 저장합니다.

Django의 JSONField 조회 구문은 기본 작업을 위해 두 백 엔드에서 모두 작동합니다.

# Works on both PostgreSQL and SQL Server
Config.objects.filter(data__settings__theme="dark")
Config.objects.filter(data__has_key="settings")

Django의 ORM에서 지원되지 않는 고급 JSON 쿼리의 경우 SQL Server JSON_VALUEOPENJSON 함수를 사용합니다.

from django.db.models.expressions import RawSQL

# Query nested JSON values
Config.objects.annotate(
    theme=RawSQL("JSON_VALUE(data, '$.settings.theme')", [])
).filter(theme="dark")

PostgreSQL 종속성 제거

마이그레이션 후 프로젝트에서 PostgreSQL 패키지를 제거합니다.

pip uninstall psycopg2-binary psycopg2 psycopg

settings.py에서 INSTALLED_APPSdjango.contrib.postgres 제거:

INSTALLED_APPS = [
    # Remove this line:
    # "django.contrib.postgres",
    "django.contrib.admin",
    "django.contrib.auth",
    # ...
]

마이그레이션 검사 목록

Step Details
백엔드 전환 settings.py에서 django.db.backends.postgresql을(를) mssql(으)로 대체합니다.
contrib.postgres 바꾸기 ArrayField, HStoreField, 범위 필드 및 CI 필드를 서로 바꾸세요.
전체 텍스트 검색 업데이트 tsvector / tsquery에서 SQL Server CONTAINS/FREETEXT(으)로 마이그레이션합니다.
공간 쿼리 업데이트 SQL Server 공간 함수를 사용하여 GeoDjango 조회를 원시 SQL로 다시 작성합니다.
DISTINCT ON 교체 ROW_NUMBER() 윈도 함수를 사용하세요.
원시 SQL 업데이트 PostgreSQL 구문(LIMIT, ||, NOW())을 SQL Server 구문으로 변경합니다. 사용자 지정 SQL 업데이트를 참조하세요.
RCSI 사용 PostgreSQL MVCC 동작과 일치하도록 설정합니다 READ_COMMITTED_SNAPSHOT ON . 트랜잭션 격리 차이점을 참조하세요.
데이터 정렬 테스트 대/소문자 구분 동작이 예상과 일치하는지 확인합니다. 데이터 정렬 차이점을 참조하세요.
psycopg2 제거 psycopg2-binary 또는 psycopg 제거하세요. django.contrib.postgres제거합니다.
마이그레이션 다시 생성 이전 마이그레이션 파일을 삭제하고 makemigrationsmigrate을 새로 실행하세요.
데이터 마이그레이션 대규모 데이터 세트의 경우 dumpdata/loaddata 또는 ETL 도구를 사용합니다.