mssql-django için performans ayarlama

Bu makale, SQL Server ile mssql-django arka ucunu kullanırken Django uygulama performansını iyileştirmeye yönelik rehberlik sunar.

Bağlantı iyileştirme

Bağlantı havuzu, kalıcılık ve zaman aşımı ayarlarını ince ayarlayarak bağlantı ek yükünü azaltın.

Bağlantı havuzunu etkinleştirme

Bağlantı havuzu varsayılan olarak etkindir. settings.py öğenizde devre dışı olmadığını doğrulayın:

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

CONN_MAX_AGE kullanın

Her istek için yeni bir bağlantı kurma yükünden kaçınarak, istekler arasında veritabanı bağlantılarını açık tutacak şekilde ayarlayın CONN_MAX_AGE :

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

Sorgu zaman aşımını ayarlama

Uzun süre çalışan sorguların kaynakları süresiz olarak kullanmasını önleyin:

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

Sorgu iyileştirme

Bu ORM teknikleriyle veritabanı gidiş dönüşlerini ve sorgu sayısını azaltın.

N+1 sorgu desenlerinden kaçının

Yabancı anahtar ilişkileri (tek JOIN sorgusu) için select_related, çoktan çoğa veya ters ilişkiler içinse prefetch_related kullanın (IN koşuluyla ayrı sorgu):

# 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() ve defer() kullanın

Tüm alanlara ihtiyacınız olmadığında alınan sütunları sınırlayın:

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

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

values() ve values_list() kullanma

Model örneklerine ihtiyaç duymadığınızda, daha hafif sorgular için values() veya values_list() kullanın:

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

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

2.100 parametre sınırı içinde çalışma

SQL Server her sorguyu 2.100 parametreyle sınırlar. Django parametreli sorgular oluşturduğu için büyük IN yan tümceler veya toplu değer listeleri üreten işlemler bu sınıra gelebilir.

Büyük IN koşulları için otomatik optimizasyon:

Bir filter(field__in=list) çağrısında 2.048'den fazla değer olduğunda, mssql-django arka ucu değerleri otomatik olarak geçici bir tabloya (1.000'lik gruplar halinde) ekler ve sorguyu WHERE field IN (SELECT params FROM #Temp_params) olarak yeniden yazar. Bu iyileştirme, kod değişikliği olmadan parametre sınırını önler. prefetch_related() tarafından oluşturulanlar da dahil olmak üzere tüm __in aramaları için geçerlidir. 2.048 eşiği, arka uç max_in_list_size() tarafından SQL Server 2.100 parametre sınırı altında güvende kalmak için ayarlanır.

Bu yeniden yazmanın bir maliyeti vardır: #Temp_params oluşturup doldurmak ek gidiş-dönüşler ve tempdb'de etkinlik yaratır. Eşiğin yakınındaki listeler için iş yükünüzdeki her iki yaklaşımı da kıyaslayın.

El ile müdahale gerekli olduğunda:

Otomatik geçici tablo optimizasyonu, __in aramalarını işler; ancak her alan değeri ayrı bir parametre olduğundan bu işlemler yine de 2.100 parametre sınırına takılabilir:

  • bulk_create() veya bulk_update() birçok nesne ve birçok alanla
  • Birçok zincirleme koşula sahip karmaşık Q() ifadeler
  • #Temp_params öğesini doldurmak için gereken gidiş-dönüşlerden kaçınmak istediğiniz durumlar (örneğin, daha küçük bir liste ve normal bir IN (...) daha hızlı olacaksa)

Çözümler:

  1. batch_size kullanarak toplu işlemlerde her partiyi sınırın altında tutun:

    # 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. Büyük IN sorguları, otomatik geçici tablo mekanizmasını atlamak istediğinizde parçalara ayırın:

    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. Kimlik listelerini gerçekleştirmek yerine alt sorgular kullanın:

    # 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. Prefetch öğesini filtrelenmiş sorgu kümeleriyle kullanarak prefetch_related() öğesine geçirilen kimliklerin sayısını sınırlayın:

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

Toplu işlemler

Veritabanı gidiş dönüş sayısını azaltmak için toplu işlemleri kullanın:

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

bulk_create veya bulk_update kullanırken, batch_size öğesini nesne başına düşen alan sayısına göre ayarlayın. Arka uçta, bulk_batch_size() her partiyi 1.000 satırla sınırlar ve ihtiyatlı bir 2050 / (fields * 2) parametre sınırını hembulk_create hem de bulk_update için uygular. Ek / 2, bulk_update’in alan başına kullandığı iki parametre için ayrılmıştır (biri CASE eşleşmesi, diğeri değer için) ve aynı bölen bulk_create için de uygulanır; böylece aynı kod yolu her iki işlem için de güvenli kalır.

batch_size öğesini atlarsanız, arka uç güvenli bir değeri otomatik olarak hesaplar. Ayrıca, bir batch_size belirtebilirsiniz ve arka uç bunu ayrıca güvenli sınırda sınırlar.

return_rows_bulk_insert ve default parametreleri hakkında daha fazla bilgi için, mssql-django ile toplu işlemler konusuna bakın.

Dizin stratejileri

Django, ForeignKey, OneToOneField ve db_index=True olan alanlar için otomatik olarak dizinler oluşturur. Ek dizinler için kullanın 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"]),
        ]

SQL Server özgü dizinler (sütunlu INCLUDE dizinler gibi) için geçişlerde ham SQL kullanın:

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

mssql-django arka ucu, mssql/features.py içindeki supports_covering_indexes = True kapsayıcı dizinleri destekler. mssql-django tarafından desteklenen tüm Django sürümlerinde (3.2 ve sonrası), ham SQL yerine models.Index üzerinde include parametresini kullanabilirsiniz:

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

Dosya grubu yerleşimi

mssql-django arka ucu, Django'nun db_tablespace öğesini SQL Server'ın ON filegroup yan tümcesine eşler. Tabloları veya dizinleri belirli dosya gruplarına yerleştirmek için bunu kullanın:

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

    class Meta:
        db_tablespace = "ARCHIVE_FG"

Bu şunu oluşturur: CREATE TABLE ... ON [ARCHIVE_FG].

Important

dosyasını çalıştırmadan migrateönce dosya grubunun SQL Server veritabanında zaten mevcut olması gerekir. Bunu ALTER DATABASE [<your-database>] ADD FILEGROUP [ARCHIVE_FG] ile oluşturun ve buna en az bir dosya ekleyin.

Pencere işlevleri

Arka uç, SQL Server pencere işlevlerini (supports_over_clause = True) destekler. Sıralama, çalıştırma toplamları ve bölümlenmiş hesaplamalar için Django'nun Window ifadelerini kullanın:

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 desteklemezNTH_VALUE(). Bunun yerine , FIRST_VALUEveya bir alt sorgu geçici çözümü kullanınLAST_VALUE. Bkz. mssql-django'daki sınırlamalar ve desteklenmeyen özellikler.

Sorgu performansını izleme

Geliştirme sırasında yavaş sorguları belirlemek için Django'nun yerleşik sorgu günlüğünü kullanın:

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

Hazırlama ve üretim iş yükleri için SQL Server performans araçlarını kullanarak Django tarafından oluşturulan SQL'i analiz edin:

  1. DMV'leri doğrudan sorgulamadan önce yerleşik performans raporlarıyla başlayın.

    Bu raporlar genellikle geçici DMV sorgularından daha az hataya yer olan pahalı sorgular, beklemeler, engelleme ve kaynak baskısı bulmanın en hızlı yoludur.

  2. En çok kaynak tüketen sorguları ve yakın zamanda gerileyen sorguları belirlemek için Query Store kullanın.

  3. Performans sorununun CPU, G/Ç, bellek veya beklemeler olup olmadığını belirlemek için SQL Server Management Studio En Çok Kaynak Tüketen Sorgular, Gerileyen Sorgular ve Sorgu Bekleme İstatistikleri görünümlerini kullanın. Yönergeler için bkz. Query Store ile iş yüklerini izlemeye yönelik en iyi yöntemler.

  4. Taramaları, maliyetli anahtar aramalarını, hatalı satır tahminlerini ve eksik dizinleri incelemek için yavaş çalışan ifade için gerçek yürütme planını açın.

  5. Bir dağıtım veya şema değişikliğinden sonra sorgu yavaşlarsa, uygulama kodunu değiştirmeden önce Query Store içindeki planlarını karşılaştırın. DBA, altta yatan dizin, istatistik veya sorgu biçimi sorununu düzeltirken iyi olduğu bilinen bir planı geçici olarak zorlayabilir.

Query Store, yüksek CPU süresi yerine beklemeleri gösteriyorsa CPU, bellek, disk G/Ç, bağlantı yükü ve engelleme sorunlarını birbirinden ayırmak için Darboğazları belirleme özelliğini kullanın.