Ladění výkonu pro mssql-django

Tento článek obsahuje pokyny k optimalizaci výkonu aplikace Django při použití back-endu mssql-django s SQL Server.

Optimalizace připojení

Snižte režii připojení laděním nastavení sdružování, trvalosti a časového limitu.

Povolení sdružování připojení

Sdružování připojení je ve výchozím nastavení povolené. Ověřte, že ve vašem počítači settings.pynení zakázaná:

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

Použijte CONN_MAX_AGE

Nastavte CONN_MAX_AGE , aby byla připojení k databázi otevřená napříč požadavky a vyhnula se režijním nákladům na vytvoření nového připojení pro jednotlivé požadavky:

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

Nastavení časového limitu dotazu

Zabránit dlouho běžícím dotazům v neomezeném spotřebovávání prostředků:

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

Optimalizace dotazů

Omezte počet komunikací s databází a dotazů pomocí těchto technik ORM.

Vyhněte se vzorům dotazů N+1

Používá se select_related pro relace cizích klíčů (jeden dotaz JOIN) a prefetch_related pro relace M:N nebo zpětné relace (samostatný dotaz s klauzulí IN):

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

Použijte only() a defer()

Omezte sloupce načtené v případě, že nepotřebujete všechna pole:

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

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

Použijte values() a values_list()

Pokud nepotřebujete instance modelu, použijte values() nebo values_list() pro méně náročné dotazy:

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

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

Práce v limitu 2 100 parametrů

SQL Server omezí každý dotaz na 2 100 parametrů. Django generuje parametrizované dotazy, takže operace, které vytvářejí velké IN klauzule nebo seznamy hromadných hodnot, mohou dosáhnout tohoto limitu.

Automatická optimalizace velkých klauzulí IN:

Když má volání filter(field__in=list) více než 2 048 hodnot, backend mssql-django automaticky vloží hodnoty do dočasné tabulky (v dávkách po 1 000) a přeformuluje dotaz na WHERE field IN (SELECT params FROM #Temp_params). Tato optimalizace zabraňuje omezení parametrů bez jakýchkoli změn kódu. Platí pro všechna __in vyhledávání, včetně těch, které generuje prefetch_related(). Prahová hodnota 2 048 je nastavená back-endem max_in_list_size() tak, aby byla bezpečně pod limitem 2 100 parametrů SQL Server.

Toto přepsání má svou cenu: vytvoření a naplnění #Temp_params znamená další komunikaci tam a zpět a vyšší zátěž databáze tempdb. U seznamů blížících se prahové hodnoty proveďte srovnávací test obou přístupů ve vaší úloze.

V případě potřeby ručního zásahu:

Automatická optimalizace temp-table pracuje s vyhledáváními __in, ale tyto operace mohou stále narazit na limit 2 100 parametrů, protože každá hodnota pole je samostatným parametrem:

  • bulk_create() nebo bulk_update() s mnoha objekty a mnoha poli
  • Komplexní Q() výrazy s mnoha zřetězenými podmínkami
  • Případy, kdy se chcete vyhnout cyklům potřebným k naplnění #Temp_params (například když je menší seznam a normální IN (...) by byl rychlejší)

Řešení:

  1. Použít batch_size při hromadných operacích, aby každá dávka byla pod limitem:

    # 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. Velké IN dotazy rozdělte na části, pokud chcete obejít automatický mechanismus dočasných tabulek:

    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. Místo materializace seznamů ID použijte poddotazy:

    # 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. Použití Prefetch s filtrovanými sadami dotazů k omezení počtu ID předaných do prefetch_related():

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

Hromadné operace

Používejte hromadné operace ke snížení počtu komunikací s databází:

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

Při použití bulk_create nebo bulk_update nastavte batch_size na základě počtu polí v jednom objektu. Backend bulk_batch_size() omezuje každou dávku na 1 000 řádků a používá konzervativní 2050 / (fields * 2) limit parametrů pro obabulk_create i bulk_update. Dodatečné / 2 je vyhrazeno pro dva parametry pro každé pole, které bulk_update používá (jeden pro porovnání CASE, jeden pro hodnotu), a stejný dělitel se použije i na bulk_create, takže stejná cesta kódu je bezpečná pro obě operace.

Pokud vynecháte batch_size, back-end automaticky vypočítá bezpečnou hodnotu. Můžete také zadat batch_size a back-end ho dále omezí na bezpečný limit.

Další informace o parametrech return_rows_bulk_insert a default parametrech naleznete v tématu Hromadné operace s mssql-django.

Strategie indexu

Django vytvoří indexy automaticky pro ForeignKey, OneToOneFielda pole s db_index=True. Pro další indexy použijte 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"]),
        ]

Pro SQL Server indexy (například indexy se INCLUDE sloupci) použijte v migracích nezpracovaný SQL:

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

Backend mssql-django podporuje krycí indexy (supports_covering_indexes = True v mssql/features.py). Ve všech verzích Django podporovaných v mssql-django (3.2 a novějších) můžete místo surového SQL použít parametr include u models.Index:

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

Umístění skupiny souborů

Back-end mssql-django mapuje Django db_tablespace na klauzuli SQL ServerON filegroup. Slouží k umístění tabulek nebo indexů do konkrétních skupin souborů:

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

    class Meta:
        db_tablespace = "ARCHIVE_FG"

Tím se vygeneruje: CREATE TABLE ... ON [ARCHIVE_FG].

Important

Skupina souborů již musí existovat v databázi SQL Server před spuštěním migrate. Vytvořte jej pomocí ALTER DATABASE [<your-database>] ADD FILEGROUP [ARCHIVE_FG] a přidejte do něj alespoň jeden soubor.

Funkce okna

Back-end podporuje funkce oken SQL Server (supports_over_clause = True). Výrazy Django Window slouží k hodnocení, průběžným součtům a děleným výpočtům:

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 nepodporuje NTH_VALUE(). Místo toho použijte FIRST_VALUE, LAST_VALUE nebo alternativní řešení s poddotazem. Viz Omezení a nepodporované funkce v mssql-django.

Monitorování výkonu dotazů

K identifikaci pomalých dotazů během vývoje použijte integrované protokolování dotazů Django:

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

Pro pracovní a produkční úlohy použijte nástroje pro výkon SQL Server k analýze SQL, který Django generuje:

  1. Začněte s integrovanými sestavami výkonu před přímým dotazem na zobrazení dynamické správy.

    Tyto reporty jsou obvykle nejrychlejším způsobem, jak najít náročné dotazy, čekání, blokování a zatížení prostředků, a přitom ponechávají menší prostor pro chyby než ad hoc dotazy DMV.

  2. Pomocí Query Store identifikujte dotazy s nejvyšší spotřebou prostředků a dotazy, u kterých nedávno došlo k regresi.

  3. Pomocí zobrazení Dotazy s nejvyšší spotřebou prostředků, Dotazy se zhoršeným výkonem a Statistiky čekání dotazů v SQL Server Management Studio určete, zda je úzkým hrdlem procesor, vstupně-výstupní operace, paměť nebo čekání. Pokyny najdete v tématu Osvědčené postupy pro monitorování úloh pomocí Query Store.

  4. Otevřete skutečný plán spuštění pro pomalý příkaz a vyhledejte skenování, nákladná vyhledání klíčů, nepřesné odhady počtu řádků a chybějící indexy.

  5. Pokud se dotaz po změně nasazení nebo schématu zpomalí, porovnejte plány v Query Store před změnou kódu aplikace. DBA může dočasně vynutit ověřený funkční plán, zatímco opravujete základní problém v indexu, statistikách nebo struktuře dotazu.

Pokud Query Store místo vysokého využití procesoru zobrazuje čekání, použijte Identifikovat úzká místa k rozlišení problémů souvisejících s procesorem, pamětí, diskovými vstupně-výstupními operacemi, přetížením připojení a blokováním.