Leistungsoptimierung für mssql-django

Dieser Artikel enthält Anleitungen zum Optimieren der Django-Anwendungsleistung bei Verwendung des mssql-django Back-Ends mit SQL Server.

Verbindungsoptimierung

Verringern Sie den Verbindungsaufwand, indem Sie Pool-, Persistenz- und Timeouteinstellungen optimieren.

Aktivieren von Verbindungspooling

Die Verbindungspooling ist standardmäßig aktiviert. Überprüfen Sie, ob es in Ihrem settings.py deaktiviert ist:

# Keep this True (or omit it entirely) for best connection performance
DATABASE_CONNECTION_POOLING = True

Verwenden Sie CONN_MAX_AGE

Legen Sie fest CONN_MAX_AGE , dass Datenbankverbindungen über Anforderungen hinweg geöffnet bleiben, und vermeiden Sie den Aufwand für die Einrichtung einer neuen Verbindung für jede Anforderung:

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "USER": "<your-username>",
        "PASSWORD": "<your-password>",
        "HOST": "<your-server>",
        "PORT": "1433",
        "CONN_MAX_AGE": 600,  # Keep connections open for 10 minutes
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
        },
    },
}

Abfrage-Timeout festlegen

Verhindern, dass lange ausgeführte Abfragen Ressourcen unbegrenzt verbrauchen:

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "USER": "<your-username>",
        "PASSWORD": "<your-password>",
        "HOST": "<your-server>",
        "PORT": "1433",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
            "query_timeout": 30,
        },
    },
}

Abfrageoptimierung

Reduzieren Sie die Anzahl von Roundtrips und Abfragen der Datenbank mit diesen ORM-Techniken.

Vermeiden von N+1-Abfragemustern

Verwenden Sie select_related für Fremdschlüsselbeziehungen (einzelne JOIN-Abfrage) und prefetch_related für Viele-zu-viele- oder umgekehrte Beziehungen (separate Abfrage mit IN-Klausel):

# Bad: N+1 queries
orders = Order.objects.all()
for order in orders:
    print(order.customer.name)  # Each access triggers a query

# Good: Single JOIN query
orders = Order.objects.select_related("customer").all()
for order in orders:
    print(order.customer.name)  # No additional queries

# Good: Two queries instead of N+1
orders = Order.objects.prefetch_related("items").all()
for order in orders:
    for item in order.items.all():  # Uses prefetched data
        print(item.name)

only() und defer() verwenden

Beschränken Sie die abgerufenen Spalten, wenn Sie nicht alle Felder benötigen:

# Retrieve only specific fields
products = Product.objects.only("name", "price").all()

# Defer loading of large fields
products = Product.objects.defer("description", "metadata").all()

Verwenden Sie values() und values_list()

Wenn Sie keine Modellinstanzen benötigen, verwenden values() Oder values_list() für leichtere Abfragen:

# Returns dictionaries instead of model instances
prices = Product.objects.values("name", "price")

# Returns tuples
names = Product.objects.values_list("name", flat=True)

Arbeiten innerhalb des Grenzwerts von 2.100 Parametern

SQL Server schränkt jede Abfrage auf 2.100 Parameter ein. Django generiert parametrisierte Abfragen, sodass Vorgänge, die große IN Klauseln oder Massenwertlisten erzeugen, diesen Grenzwert erreichen können.

Automatische Optimierung für große IN-Klauseln:

Wenn ein filter(field__in=list) Aufruf mehr als 2.048 Werte aufweist, fügt das mssql-django Back-End die Werte automatisch in eine temporäre Tabelle (in Batches von 1.000) ein und schreibt die Abfrage neu als WHERE field IN (SELECT params FROM #Temp_params). Diese Optimierung vermeidet den Parametergrenzwert ohne Codeänderungen. Sie gilt für alle __in Suchvorgänge, einschließlich der von prefetch_related() generierten. Der Schwellenwert von 2.048 wird durch max_in_list_size() des Backends festgelegt, um deutlich unter dem SQL-Server-Limit von 2.100 Parametern zu bleiben.

Diese Umschreibung hat ihren Preis: Das Erstellen und Befüllen von #Temp_params verursacht zusätzliche Roundtrips und Aktivität in tempdb. Für Listen in der Nähe des Schwellenwerts vergleichen Sie beide Ansätze in Ihrer Workload.

Wenn ein manueller Eingriff noch erforderlich ist:

Die automatische Temp-Table-Optimierung unterstützt __in Nachschlagevorgänge, aber diese Vorgänge können dennoch an die Grenze von 2.100 Parametern stoßen, da jeder Feldwert ein separater Parameter ist:

  • bulk_create() oder bulk_update() mit vielen Objekten und vielen Feldern
  • Komplexe Q() Ausdrücke mit vielen verketteten Bedingungen
  • Fälle, in denen Sie die erforderlichen Roundtrips zum Füllen von #Temp_params vermeiden möchten (z. B. wenn eine kleinere Liste und ein normales IN (...) schneller wären)

Lösungen:

  1. Verwenden batch_size bei Massenvorgängen, um jeden Batch unter dem Grenzwert zu halten:

    # Backend cap with 10 fields: min(1000, 2050 // 10 // 2) = 102 rows per batch
    # The backend applies the conservative // 2 divisor for both bulk_create and bulk_update.
    Product.objects.bulk_create(products, batch_size=100)
    
  2. Teilen Sie große IN Abfragen in Teile auf, wenn Sie den automatischen Temp-Tabellen-Mechanismus umgehen möchten:

    from itertools import islice
    
    def chunked_filter(queryset, field, values, chunk_size=2000):
        """Filter a queryset in chunks to stay within the 2,100 parameter limit."""
        results = []
        it = iter(values)
        while chunk := list(islice(it, chunk_size)):
            results.extend(queryset.filter(**{f"{field}__in": chunk}))
        return results
    
    # Returns a list of model instances, not a QuerySet
    products = chunked_filter(Product.objects, "pk", large_id_list)
    
  3. Verwenden Sie Unterabfragen , anstatt ID-Listen zu materialisieren:

    # Instead of: Order.objects.filter(product_id__in=list(Product.objects.values_list("id", flat=True)))
    # Use a subquery (Django generates a single SQL statement with no parameter explosion)
    Order.objects.filter(product__in=Product.objects.filter(active=True))
    
  4. Verwenden Sie Prefetch mit gefilterten Querysets, um die Anzahl der IDs zu begrenzen, die an prefetch_related() übergeben werden:

    from django.db.models import Prefetch
    
    orders = Order.objects.prefetch_related(
        Prefetch("items", queryset=OrderItem.objects.select_related("product"))
    )[:500]  # Limit parent queryset size
    

Massenvorgänge

Verwenden Sie Massenvorgänge, um die Anzahl der Datenbank-Roundtrips zu reduzieren:

from decimal import Decimal

from myapp.models import Product

# Bulk create
new_products = [Product(name=f"Item {i}", price=Decimal("1.99") * i) for i in range(1000)]
Product.objects.bulk_create(new_products, batch_size=500)

# Bulk update: refetch so each instance has a primary key
products = list(Product.objects.filter(name__startswith="Item "))
for product in products:
    product.price *= Decimal("1.10")
Product.objects.bulk_update(products, ["price"], batch_size=500)

Important

Bei Verwendung von bulk_create oder bulk_update legen Sie batch_size anhand der Anzahl der Felder pro Objekt fest. Das Backend bulk_batch_size() begrenzt jede Batch auf 1.000 Zeilen und wendet ein konservatives 2050 / (fields * 2) Parameterlimit sowohlbulk_create als auch auf bulk_update an. Das zusätzliche / 2 ist für die beiden Parameter pro Feld reserviert, die bulk_update verwendet (einer für den CASE-Abgleich, einer für den Wert), und derselbe Divisor wird auch auf bulk_create angewendet, sodass derselbe Codepfad für beide Operationen sicher funktioniert.

Wenn Sie weglassen batch_size, berechnet das Back-End automatisch einen sicheren Wert. Sie können auch batch_size angeben, und das Backend begrenzt den Wert zusätzlich auf den sicheren Grenzwert.

Weitere Informationen zu den Parametern return_rows_bulk_insert und default finden Sie unter Massenvorgänge mit mssql-django.

Indexstrategien

Django erstellt Indizes automatisch für ForeignKey, OneToOneFieldund Felder mit db_index=True. Verwenden Sie für zusätzliche Indizes Meta.indexes:

from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=100, db_index=True)
    category = models.CharField(max_length=50)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        indexes = [
            models.Index(fields=["category", "price"]),
            models.Index(fields=["-created_at"]),
        ]

Verwenden Sie für SQL Server-spezifische Indizes (z. B. Indizes mit INCLUDE Spalten) unformatierte SQL in Migrationen:

from django.db import migrations

class Migration(migrations.Migration):
    dependencies = [("myapp", "0001_initial")]
    operations = [
        migrations.RunSQL(
            sql="CREATE INDEX IX_product_category ON myapp_product (category) INCLUDE (name, price);",
            reverse_sql="DROP INDEX IX_product_category ON myapp_product;",
        ),
    ]

Das mssql-django Back-End unterstützt die Abdeckung von Indizes (supports_covering_indexes = True in mssql/features.py). In allen Django-Versionen, die von mssql-django unterstützt werden (3.2 und neuer), können Sie den Parameter include bei models.Index anstelle von rohem SQL verwenden:

class Product(models.Model):
    name = models.CharField(max_length=100)
    category = models.CharField(max_length=50)
    price = models.DecimalField(max_digits=10, decimal_places=2)

    class Meta:
        indexes = [
            models.Index(fields=["category"], include=["name", "price"], name="ix_product_cat_cover"),
        ]

Platzierung der Dateigruppe

Das mssql-django Backend ordnet Djangos db_tablespace der SQL-Server-ON filegroup-Klausel zu. Verwenden Sie dies, um Tabellen oder Indizes in bestimmten Dateigruppen zu platzieren:

class LargeAuditLog(models.Model):
    timestamp = models.DateTimeField(auto_now_add=True)
    message = models.TextField()

    class Meta:
        db_tablespace = "ARCHIVE_FG"

Dies generiert: CREATE TABLE ... ON [ARCHIVE_FG].

Important

Die Dateigruppe muss bereits in der SQL Server-Datenbank vorhanden sein, bevor Sie ausführenmigrate. Erstellen Sie sie mit ALTER DATABASE [<your-database>] ADD FILEGROUP [ARCHIVE_FG] , und fügen Sie ihr mindestens eine Datei hinzu.

Fensterfunktionen

Das Back-End unterstützt SQL Server Fensterfunktionen (supports_over_clause = True). Verwenden Sie Djangos Window Ausdrücke für die Rangfolge, das Ausführen von Summen und partitionierten Berechnungen:

from django.db.models import F, Window
from django.db.models.functions import Rank, RowNumber

# Rank products by price within each category
products = Product.objects.annotate(
    price_rank=Window(
        expression=Rank(),
        partition_by=F("category"),
        order_by=F("price").desc(),
    )
)

# Row numbers across the full result set
products = Product.objects.annotate(
    row_num=Window(
        expression=RowNumber(),
        order_by=F("created_at").asc(),
    )
)

Note

SQL Server unterstützt NTH_VALUE() nicht. Verwenden Sie stattdessen FIRST_VALUE, LAST_VALUE oder eine Problemumgehung mit einer Unterabfrage. Siehe Einschränkungen und nicht unterstützte Features in mssql-django.

Überwachen der Abfrageleistung

Verwenden Sie die integrierte Abfrageprotokollierung von Django, um langsame Abfragen während der Entwicklung zu identifizieren:

LOGGING = {
    "version": 1,
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
        },
    },
    "loggers": {
        "django.db.backends": {
            "level": "DEBUG",
            "handlers": ["console"],
        },
    },
}

Verwenden Sie für Staging- und Produktionsworkloads SQL Server Leistungstools, um die von Django generierte SQL zu analysieren:

  1. Beginnen Sie mit integrierten Leistungsberichten, bevor Sie DMVs direkt abfragen.

    Diese Berichte sind in der Regel der schnellste Weg, kostspielige Abfragen, Wartevorgänge, Blockierungen und Ressourcenengpässe zu erkennen, und bieten dabei weniger Spielraum für Fehler als Ad-hoc-DMV-Abfragen.

  2. Verwenden Sie Abfragespeicher, um die wichtigsten ressourcenaufwendigen Abfragen und Abfragen zu identifizieren, die kürzlich zurückgeschritten wurden.

  3. Verwenden Sie die Ansichten "Top Resource Consuming Queries", "Regressed Queries" und "Query Wait Statistics" in SQL Server Management Studio, um zu ermitteln, ob der Engpass CPU, E/A, Arbeitsspeicher oder Wartezeiten ist. Anleitungen finden Sie unter Bewährte Methoden zum Überwachen von Workloads mit Abfragespeicher.

  4. Öffnen Sie einen tatsächlichen Ausführungsplan für die langsame Anweisung, um sie auf Scans, teure Schlüsselsuchen, ungenaue Zeilenschätzungen und fehlende Indizes zu überprüfen.

  5. Wenn eine Abfrage nach einer Bereitstellung oder Schemaänderung langsamer wurde, vergleichen Sie ihre Pläne in Abfragespeicher, bevor Sie den Anwendungscode ändern. Ein DBA kann vorübergehend einen bekanntermaßen guten Ausführungsplan erzwingen, während Sie das zugrunde liegende Problem mit dem Index, den Statistiken oder der Abfrageform beheben.

Wenn Abfragespeicher Wartevorgänge statt hoher CPU-Zeit anzeigt, verwenden Sie Engpässe identifizieren, um CPU-, Arbeitsspeicher-, Datenträger-E/A-, Verbindungs- und Blockierungsprobleme zu unterscheiden.