mssql-django에 대한 성능 튜닝

이 문서에서는 SQL Server 백 엔드를 사용할 mssql-django 때 Django 애플리케이션 성능을 최적화하는 방법에 대한 지침을 제공합니다.

연결 최적화

풀링, 지속성 및 시간 제한 설정을 조정하여 연결 오버헤드를 줄입니다.

연결 풀링 활성화

연결 풀링이 기본적으로 사용하도록 설정됩니다. settings.py에서 비활성화되지 않았는지 확인하세요:

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

CONN_MAX_AGE 사용

요청 간에 데이터베이스 연결을 열어 두도록 설정 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",
        },
    },
}

쿼리 시간 제한 설정

장기 실행 쿼리가 리소스를 무기한 사용하지 않도록 방지합니다.

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

쿼리 최적화

이러한 ORM 기술을 사용하여 데이터베이스 왕복 및 쿼리 수를 줄입니다.

N+1 쿼리 패턴 방지

외래 키 관계(단일 JOIN 쿼리)에는 select_related를 사용하고, 다대다 또는 역방향 관계(IN 절이 있는 별도의 쿼리)에는 prefetch_related를 사용합니다:

# 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() 및 defer() 사용

모든 필드가 필요하지 않은 경우 검색되는 열을 제한합니다.

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

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

값() 및 values_list() 사용

모델 인스턴스가 필요하지 않은 경우 더 가벼운 쿼리를 사용 values() 하거나 values_list() 사용합니다.

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

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

2,100개 매개 변수 제한 내에서 작업

SQL Server 각 쿼리를 2,100개의 매개 변수로 제한합니다. Django는 매개 변수가 있는 쿼리를 생성하므로 큰 IN 절 또는 대량 값 목록을 생성하는 작업은 이 제한에 도달할 수 있습니다.

큰 IN 절에 대한 자동 최적화:

filter(field__in=list) 호출에 2,048개 이상의 값 mssql-django 이 있는 경우 백 엔드는 자동으로 임시 테이블(1,000개 일괄 처리)에 값을 삽입하고 쿼리를 다시 WHERE field IN (SELECT params FROM #Temp_params)작성합니다. 이 최적화는 코드를 변경하지 않고 매개 변수 제한을 방지합니다. prefetch_related()에 의해 생성된 조회를 포함한 모든 __in 조회에 적용됩니다. 2,048 임계값은 백 엔드가 max_in_list_size() SQL Server 2,100 매개 변수 제한에 안전하게 유지되도록 설정됩니다.

이 재작성에는 비용이 있습니다. #Temp_params를 만들고 채우면 추가적인 왕복 통신과 tempdb 활동이 늘어납니다. 임계값에 가까운 목록의 경우 워크로드의 두 접근 방식을 모두 벤치마킹합니다.

수동 개입이 여전히 필요한 경우:

자동 임시 테이블 최적화는 조회를 __in 처리하지만 각 필드 값이 별도의 매개 변수이므로 이러한 작업은 여전히 2,100개의 매개 변수 제한에 도달할 수 있습니다.

  • bulk_create() 또는 bulk_update() 여러 개체 및 여러 필드가 있는 경우
  • 연결된 조건이 많은 복합 Q()
  • #Temp_params을 채우는 데 필요한 왕복 호출을 피하고 싶은 경우(예: 더 작은 목록과 일반적인 IN (...)을 사용하는 편이 더 빠른 경우)

해결 방법:

  1. 대량 작업에 batch_size을 사용하여 각 배치가 제한을 넘지 않도록 하세요:

    # 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. 자동 임시 테이블 메커니즘을 우회하려는 경우 IN쿼리를 청크로 나누세요:

    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. ID 목록을 구체화하는 대신 하위 쿼리를 사용합니다.

    # 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를 필터링된 쿼리셋과 함께 사용하여prefetch_related()에 전달되는 ID의 수를 제한합니다.

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

대량 작업

대량 작업을 사용하여 데이터베이스 왕복 횟수를 줄입니다.

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 또는 bulk_update를 사용하는 경우, 개체당 필드 수에 따라 batch_size를 설정합니다. 백 엔드의 bulk_batch_size() 각 일괄 처리는 1,000개 행으로 제한되며 두 행 2050 / (fields * 2)bulk_create에 보수적인 bulk_update 매개 변수 제한을 적용합니다. 추가 / 2 항목은 사용하는 필드 bulk_update 당 두 개의 매개 변수(CASE 일치에 대해 하나, 값에 대해 하나)에 대해 예약되어 있으며 동일한 수수가 적용 bulk_create 되므로 동일한 코드 경로가 두 작업 모두에 안전합니다.

생략 batch_size하면 백 엔드가 안전 값을 자동으로 계산합니다. 또한 batch_size를 지정할 수도 있으며, 백엔드가 이를 안전한 한도로 추가 제한합니다.

return_rows_bulk_insertdefault 매개 변수에 대한 자세한 내용은 mssql-django를 사용한 대량 작업을 참조하세요.

인덱스 전략

Django는 ForeignKey, OneToOneFielddb_index=True가 있는 필드에 대해 자동으로 인덱스를 생성합니다. 추가 인덱스의 경우 다음을 사용합니다 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 관련 인덱스(예: 열이 있는 INCLUDE 인덱스)의 경우 마이그레이션에서 원시 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;",
        ),
    ]

mssql-django 백엔드는 커버링 인덱스(supports_covering_indexes = True in mssql/features.py)를 지원합니다. mssql-django에서 지원하는 모든 Django 버전(3.2 이상)에서는 원시 SQL 대신 models.Index에서 include 매개변수를 사용할 수 있습니다.

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

파일 그룹 배치

mssql-django 백엔드는 Django의 db_tablespace를 SQL Server의 ON filegroup 절에 매핑합니다. 이를 사용하여 특정 파일 그룹에 테이블 또는 인덱스를 배치합니다.

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

    class Meta:
        db_tablespace = "ARCHIVE_FG"

다음을 생성합니다: CREATE TABLE ... ON [ARCHIVE_FG]

Important

실행하기 전에 migrate파일 그룹이 SQL Server 데이터베이스에 이미 있어야 합니다. ALTER DATABASE [<your-database>] ADD FILEGROUP [ARCHIVE_FG]로 이를 만들고 하나 이상의 파일을 추가합니다.

창 함수

백 엔드는 SQL Server 창 함수(supports_over_clause = True)를 지원합니다. 순위, 실행 합계 및 분할된 계산에 Django의 Window 식을 사용합니다.

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는 NTH_VALUE()을 지원하지 않습니다. 대신 , FIRST_VALUE또는 하위 쿼리 해결 방법을 사용합니다LAST_VALUE. mssql-django에서 제한 사항 및 지원되지 않는 기능을 참조하세요.

쿼리 성능 모니터링

Django의 기본 제공 쿼리 로깅을 사용하여 개발 중에 느린 쿼리를 식별합니다.

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

스테이징 및 프로덕션 워크로드의 경우 SQL Server 성능 도구를 사용하여 Django에서 생성하는 SQL을 분석합니다.

  1. DMV를 직접 쿼리하기 전에 기본 제공 성능 보고서로 시작합니다.

    이러한 보고서는 일반적으로 임시 DMV 쿼리보다 실수의 여지가 적은 비용이 드는 쿼리, 대기, 차단 및 리소스 압력을 찾는 가장 빠른 방법입니다.

  2. 쿼리 저장소 사용하여 최근 회귀된 상위 리소스 소비 쿼리 및 쿼리를 식별합니다.

  3. SQL Server Management Studio 리소스 사용 상위 쿼리, 회귀 쿼리쿼리 대기 통계 보기를 사용하여 병목 상태가 CPU, I/O, 메모리 또는 대기인지 확인합니다. 지침은 쿼리 저장소 사용하여 워크로드를 모니터링하는 모범 사례를 참조하세요.

  4. 느리게 실행되는 문에 대한 실제 실행 계획을 열고 스캔, 비용이 많이 드는 키 조회, 부정확한 행 수 추정치 및 누락된 인덱스를 확인합니다.

  5. 배포 또는 스키마 변경 후 쿼리가 느려지면 애플리케이션 코드를 변경하기 전에 쿼리 저장소 계획을 비교합니다. DBA는 기본 인덱스, 통계 또는 쿼리 셰이프 문제를 해결하는 동안 알려진 양호한 계획을 일시적으로 강제 적용할 수 있습니다.

쿼리 저장소 높은 CPU 시간 대신 대기를 표시하는 경우 병목 상태 식별을 사용하여 CPU, 메모리, 디스크 I/O, 연결 압력 및 차단 문제를 구분합니다.