Migrar aplicativos Django do PostgreSQL para SQL Server

Este artigo é um guia de migração detalhado para aplicativos Django que migram do PostgreSQL (psycopg2 ou psycopg) para SQL Server (mssql-django). Para obter uma visão geral da migração de qualquer banco de dados, consulte Migrar aplicativos Django de outros bancos de dados para SQL Server.

Pré-requisitos

  • Python 3.8 ou posterior
  • Microsoft Driver ODBC 17 ou 18 para SQL Server. Consulte Instalação do mssql-django.
  • SQL Server 2016 ou posterior, ou Banco de Dados SQL do Azure

Alternar o back-end do banco de dados

Substitua a configuração do PostgreSQL em settings.py:

# 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",
        },
    },
}

Atualização requirements.txt:

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

# Add
mssql-django>=1.5

Substituir os recursos do django.contrib.postgres

O django.contrib.postgres módulo fornece campos, funções e pesquisas específicos do PostgreSQL. Não funcionam com SQL Server. As seções a seguir mostram como substituir cada recurso.

Campo de matriz

O PostgreSQL ArrayField armazena matrizes nativamente. O SQL Server não possui um tipo de coluna array.

Opção 1: JSONField (funciona com o Django 3.2 e versões posteriores)

# 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)

Consultando alterações:

# 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"],
    )
)

Opção 2: tabela relacionada (normalizada, melhor para grandes matrizes ou filtragem frequente)

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

Substitua por 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 dá suporte à mesma sintaxe de pesquisa de chave:

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

Campos de intervalo

Os tipos de intervalo do PostgreSQL (IntegerRangeField, BigIntegerRangeField, DateRangeField, DateTimeRangeField, DecimalRangeField) não têm equivalente no SQL Server. Use dois campos separados:

# 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()

Atualize as consultas para usar comparações de campo separadas. Antes, com PostgreSQL:DateRangeField

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

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

Depois, com duas DateField colunas no SQL Server:

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 e CIEmailField

Os tipos de texto que não diferenciam maiúsculas de minúsculas do PostgreSQL usam a citext extensão. A ordenação padrão do SQL Server (SQL_Latin1_General_CP1_CI_AS) já não diferencia maiúsculas de minúsculas, portanto os padrões CharField e EmailField se comportam da mesma maneira:

# 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

A pesquisa de texto completo do PostgreSQL está profundamente integrada ao Django. O SQL Server tem seu próprio mecanismo de busca de texto completo, mas não tem integração com o ORM do Django. Consulte a migração de pesquisa de texto completo mais adiante neste artigo.

Funções de agregação

Substitua agregações específicas do 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)",
        [],
    ),
)

Note

STRING_AGG requer SQL Server 2017 ou versões posteriores ou o Banco de Dados SQL do Azure.

Migração de pesquisa de texto completo

A pesquisa de texto completo do PostgreSQL usa tsvectortsquerye GIN índices. SQL Server tem um mecanismo de pesquisa de texto completo separado.

Habilitar a pesquisa de texto completo no 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;

Consultar a pesquisa de texto completo do Django

Use SQL bruto para acessar as funções CONTAINS e FREETEXT do SQL Server:

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],
        )
    )

Para resultados classificados (equivalente a 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],
    )

Guia operacional de manutenção de índice de texto completo

Planeje a manutenção dos índices de texto completo do SQL Server após a migração:

  • Use CHANGE_TRACKING AUTO para atualizações quase em tempo real.
  • Use CHANGE_TRACKING MANUAL para janelas de carregamento em massa e execute uma população completa.
  • Acompanhe o status do rastreamento e as pendências por meio de sys.fulltext_indexes e sys.dm_fts_index_population.

Verifique o status:

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;

Após grandes cargas de dados com acompanhamento manual:

ALTER FULLTEXT INDEX ON [products_product] START FULL POPULATION;

Tip

Recompile ou repovoe índices de texto completo durante janelas de baixo tráfego. Populações completas podem ser caras em tabelas grandes.

Criar um gerenciador de pesquisa

Encapsular o SQL bruto em um gerenciador para acesso limpo:

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 e dados espaciais

mssql-django não inclui um backend GIS do GeoDjango. Se o aplicativo PostgreSQL usar o PostGIS por meiodjango.contrib.gis, você não poderá migrar consultas espaciais diretamente para o ORM do Django no SQL Server.

SQL Server dá suporte a tipos de dados de geografia e geometria nativamente. Para trabalhar com dados espaciais após a migração:

  • Armazene dados espaciais usando SQL bruto ou campos de modelo personalizados que são mapeados para as colunas de geografia ou geometria do SQL Server.
  • Consulte dados espaciais usando SQL bruto com as funções espaciais internas do SQL Server:
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],
    )
  • Considere bibliotecas de terceiros que adicionem suporte espacial do SQL Server ao Django, ou mantenha as consultas espaciais em SQL puro, usando o ORM para todo o restante.

Note

Se o aplicativo depender muito das pesquisas espaciais do GeoDjango, avalie cuidadosamente o custo da migração. Mover consultas espaciais para o SQL bruto requer a reescrita de cada filtro espacial GeoDjango.

Migração de pool de conexões

Se o aplicativo PostgreSQL usar pgbouncer para o pool de conexões, substitua-o pelo gerenciamento de conexão interno do Django ou pelo pool de conexões ODBC.

Reutilização da conexão do 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+
    },
}

Para obter mais detalhes, consulte Pooling de conexões no mssql-django.

Substituição de DISTINCT ON

O PostgreSQL suporta DISTINCT ON para retornar uma linha por grupo. SQL Server não dá suporte a essa sintaxe. Em vez disso, use funções de janela:

# 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
    """
)

Consultas JSONB

O tipo do jsonb PostgreSQL dá suporte a operadores de consulta avançados. SQL Server armazena JSON como nvarchar(max) com funções de consulta disponíveis desde SQL Server 2016.

A sintaxe de consulta JSONField do Django funciona em ambos os backends para operações básicas:

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

Para consultas JSON avançadas não compatíveis com o ORM do Django, use as funções JSON_VALUE e OPENJSON do SQL Server:

from django.db.models.expressions import RawSQL

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

Remover dependências do PostgreSQL

Após a migração, remova os pacotes postgreSQL do seu projeto:

pip uninstall psycopg2-binary psycopg2 psycopg

Remover django.contrib.postgres de INSTALLED_APPS em settings.py:

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

Lista de verificação de migração

Step Detalhes
Alternar back-end Substitua django.db.backends.postgresql por mssql em settings.py.
Substituir contrib.postgres Troque ArrayField, HStoreField, os campos de intervalo e os campos de CI.
Atualizar pesquisa de texto completo Migrar de tsvector/tsquery para SQL Server CONTAINS/FREETEXT.
Atualizar consultas espaciais Reescreva consultas do GeoDjango como SQL puro usando funções espaciais do SQL Server.
Substituir DISTINCT ON Use ROW_NUMBER() funções de janela.
Atualizar SQL bruto Altere a sintaxe do PostgreSQL (LIMIT, ||, NOW()) para a sintaxe do SQL Server. Confira Atualizar SQL personalizado.
Habilitar o RCSI Defina READ_COMMITTED_SNAPSHOT ON para corresponder ao comportamento de MVCC do PostgreSQL. Consulte diferenças no isolamento de transações.
Ordenação de teste Verifique se o comportamento da sensibilidade a maiúsculas e minúsculas corresponde às suas expectativas. Confira as diferenças de ordenação.
Remover psycopg2 Desinstalar psycopg2-binary ou psycopg. Remova django.contrib.postgres.
Regenerar migrações Exclua os arquivos de migração antigos e execute makemigrations e migrate do zero.
Migrar dados Use dumpdata/loaddata ou uma ferramenta ETL para grandes conjuntos de dados.