Migrieren von Django-Apps von PostgreSQL zu SQL Server

Dieser Artikel ist ein detaillierter Migrationsleitfaden für Django-Anwendungen, die von PostgreSQL (psycopg2 oder psycopg) zu SQL Server (mssql-django) wechseln. Eine allgemeine Übersicht über die Migration aus einer beliebigen Datenbank finden Sie unter Migrieren von Django-Apps aus anderen Datenbanken zu SQL Server.

Voraussetzungen

  • Python 3.8 oder höher
  • Microsoft ODBC-Treiber 17 oder 18 für SQL Server. Siehe Installieren von mssql-django.
  • SQL Server 2016 oder höher oder Azure SQL-Datenbank

Wechseln des Datenbank-Back-End

Ersetzen Sie Ihre PostgreSQL-Konfiguration in 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",
        },
    },
}

Aktualisieren Sie requirements.txt:

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

# Add
mssql-django>=1.5

Ersetzen Sie die Funktionen von django.contrib.postgres

Das django.contrib.postgres Modul bietet PostgreSQL-spezifische Felder, Funktionen und Nachschlagevorgänge. Diese funktionieren nicht mit SQL Server. In den folgenden Abschnitten wird gezeigt, wie Sie die einzelnen Features ersetzen.

ArrayField

PostgreSQL ArrayField speichert Arrays nativ. SQL Server verfügt nicht über einen Arrayspaltentyp.

Option 1: JSONField (funktioniert mit Django 3.2 und höheren Versionen)

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

Änderungen abfragen:

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

Option 2: Verwandte Tabelle (normalisiert, besser für große Arrays oder häufige Filterung)

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

Ersetzen durch 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 unterstützt dieselbe Schlüsselsuchsyntax:

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

Bereichsfelder

PostgreSQL-Bereichstypen (IntegerRangeField, BigIntegerRangeField, DateRangeField, DateTimeRangeFieldDecimalRangeField, ) haben keine SQL Server Entsprechung. Verwenden Sie zwei separate Felder:

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

Aktualisieren Sie Abfragen, um separate Feldvergleiche zu verwenden. Vorher, mit PostgreSQL DateRangeField:

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

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

Danach mit zwei DateField Spalten auf 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 und CIEmailField

Für die nicht zwischen Groß- und Kleinschreibung unterscheidenden Texttypen von PostgreSQL wird die Erweiterung citext verwendet. Die Standardkollation (SQL_Latin1_General_CP1_CI_AS) von SQL Server berücksichtigt die Groß-/Kleinschreibung bereits nicht, sodass sich die Standardwerte CharField und EmailField gleich verhalten:

# 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

Die Full-Text-Suche von PostgreSQL ist tief in Django integriert. SQL Server verfügt über eine eigene Volltext-Suchmaschine, aber keine Django ORM-Integration. Siehe Migration der Volltextsuche weiter unten in diesem Artikel.

Aggregatfunktionen

Ersetzen Sie PostgreSQL-spezifische Aggregate:

# 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_AGGerfordert SQL Server 2017 oder höher oder Azure SQL-Datenbank.

Migration der Volltextsuche

Die Volltextsuche von PostgreSQL verwendet tsvector, tsqueryund GIN Indizes. SQL Server verfügt über eine separate Volltextsuchmaschine.

Aktivieren der Volltextsuche in 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;

Volltextsuche von Django abfragen

Verwenden Sie raw SQL, um auf SQL Server CONTAINS und FREETEXT Funktionen zuzugreifen:

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

Für bewertete Ergebnisse (äquivalent zu 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],
    )

Runbook für die Wartung von Volltextindizes

Planen Sie die Wartung für SQL Server Volltextindizes nach der Migration:

  • Verwenden Sie CHANGE_TRACKING AUTO für Aktualisierungen nahezu in Echtzeit.
  • Verwenden Sie CHANGE_TRACKING MANUAL für Fenster für Massenladevorgänge, und führen Sie dann eine vollständige Befüllung aus.
  • Verfolgen Sie den Crawler-Status und den Backlog über sys.fulltext_indexes und sys.dm_fts_index_population.

Status überprüfen:

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;

Nach großen Datenladevorgängen mit manueller Nachverfolgung:

ALTER FULLTEXT INDEX ON [products_product] START FULL POPULATION;

Tip

Erstellen Sie Volltextindizes in Zeiten mit geringem Datenverkehr neu oder füllen Sie sie neu auf. Vollständige Populationen können in großen Tabellen teuer sein.

Erstellen eines Such-Managers

Umschließen Sie die rohe SQL in einem Manager für sauberen Zugriff:

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 und räumliche Daten

mssql-django enthält kein GeoDjango GIS-Back-End. Wenn Ihre PostgreSQL-Anwendung PostGIS über django.contrib.gisverwendet, können Sie räumliche Abfragen nicht direkt zum Django ORM auf SQL Server migrieren.

SQL Server unterstützt geografie- und Geometriedatentypen nativ. So arbeiten Sie nach der Migration mit räumlichen Daten:

  • Speichern Sie räumliche Daten mithilfe von unformatierten SQL- oder benutzerdefinierten Modellfeldern, die den Geografie- oder Geometriespalten SQL Server zugeordnet sind.
  • Abfragen von räumlichen Daten mithilfe von rohem SQL mit den integrierten räumlichen Funktionen von 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],
    )
  • Erwägen Sie Drittanbieterbibliotheken, die SQL Server räumliche Unterstützung zu Django hinzufügen, oder behalten Sie räumliche Abfragen als rohes SQL bei, während Sie das ORM für alles andere verwenden.

Note

Wenn Ihre Anwendung stark von räumlichen Abfragen in GeoDjango abhängt, bewerten Sie die Migrationskosten sorgfältig. Das Verschieben räumlicher Abfragen in raw SQL erfordert das Umschreiben jedes GeoDjango-Raumfilters.

Migration des Verbindungspoolings

Wenn Ihre PostgreSQL-Anwendung pgbouncer für das Verbindungspooling verwendet, ersetzen Sie es durch Djangos integrierte Verbindungsverwaltung oder ODBC-Verbindungspooling.

Django-Verbindung wiederverwenden

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+
    },
}

Weitere Details finden Sie unter Connection pooling in mssql-django.

Ersatz für DISTINCT ON

PostgreSQL unterstützt DISTINCT ON, um eine Zeile pro Gruppe zu erhalten. SQL Server unterstützt diese Syntax nicht. Verwenden Sie stattdessen Fensterfunktionen:

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

Der Typ von PostgreSQL jsonb unterstützt umfangreiche Abfrageoperatoren. SQL Server speichert JSON als nvarchar(max) mit Abfragefunktionen, die seit SQL Server 2016 verfügbar sind.

Djangos JSONField Nachschlagesyntax funktioniert für grundlegende Vorgänge auf beiden Back-Ends:

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

Verwenden Sie für erweiterte JSON-Abfragen, die von Djangos ORM nicht unterstützt werden, SQL Server JSON_VALUE und OPENJSON Funktionen:

from django.db.models.expressions import RawSQL

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

Entfernen von PostgreSQL-Abhängigkeiten

Entfernen Sie nach der Migration PostgreSQL-Pakete aus Ihrem Projekt:

pip uninstall psycopg2-binary psycopg2 psycopg

django.contrib.postgres aus INSTALLED_APPS in settings.py entfernen:

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

Migrationscheckliste

Step Details
Backend wechseln Ersetzen Sie django.db.backends.postgresql durch mssql in settings.py.
Ersetzen sie contrib.postgres Tauschen Sie ArrayField, HStoreField, Bereichsfelder und CI-Felder aus.
Aktualisieren der Volltextsuche Migrieren von tsvector/tsquery zu SQL Server CONTAINS/FREETEXT.
Aktualisieren räumlicher Abfragen Schreiben Sie GeoDjango-Nachschlagevorgänge mithilfe von SQL Server räumlichen Funktionen als rohes SQL um.
Ersetzen DISTINCT ON Verwenden Sie ROW_NUMBER() Fensterfunktionen.
Aktualisieren der rohen SQL-Daten Ändern Sie die PostgreSQL-Syntax (LIMIT, ||, NOW()) in SQL Server Syntax. Siehe Aktualisieren von benutzerdefiniertem SQL.
RCSI aktivieren READ_COMMITTED_SNAPSHOT ON so festlegen, dass es dem PostgreSQL-MVCC-Verhalten entspricht. Siehe Unterschiede bei der Transaktionsisolation.
Testsortierung Überprüfen Sie, ob das Verhalten bei der Groß-/Kleinschreibung Ihren Erwartungen entspricht. Siehe Sortierungsunterschiede.
Psycopg2 entfernen Deinstallieren Sie psycopg2-binary oder psycopg. Entfernen Sie django.contrib.postgres.
Neu generieren von Migrationen Löschen Sie alte Migrationsdateien und führen Sie makemigrations und migrate erneut aus.
Datenmigration Verwenden Sie dumpdata/loaddata oder ein ETL-Tool für große Datensätze.