فصل ۳: QuerySet حرفه‌ای — سریع، درست و بدون N+1

Subquery، Exists، عملیات دسته‌ای، only/defer و SQL خام

ابزارهای سطح بالاتر ORM

وقتی annotate ساده کافی نیست — مثلاً «تاریخ آخرین سفارش هر مشتری» یا «آیا سفارش در انتظار دارد؟» — به زیرکوئری‌های همبسته نیاز داریم. جنگو این‌ها را با Subquery، OuterRef و Exists می‌سازد.

from django.db.models import Exists, OuterRef, Subquery

last_order = (Order.objects
              .filter(customer=OuterRef("pk"))
              .order_by("-created_at"))

customers = Customer.objects.annotate(
    last_order_at=Subquery(last_order.values("created_at")[:1]),
    last_total=Subquery(last_order.values("total")[:1]),
    has_pending=Exists(Order.objects.filter(customer=OuterRef("pk"), status="pending")),
).filter(has_pending=False)

OuterRef("pk") یعنی «pk ردیف کوئری بیرونی». Exists از Count() > 0 بسیار سریع‌تر است چون پایگاه داده با اولین تطابق متوقف می‌شود.

عملیات دسته‌ای

# ساخت هزار طرح با چند کوئری، نه هزار کوئری
Carpet.objects.bulk_create(
    [Carpet(code=f"KSH-{i}", name=f"طرح {i}", density=1200, price=45_000_000) for i in range(1000)],
    batch_size=500,
)

# به‌روزرسانی دسته‌ای فیلدهای مشخص
for c in carpets:
    c.price = int(round(c.price * 1.15, -4))       # گرد کردن به ده‌هزار ریال
Carpet.objects.bulk_update(carpets, ["price"], batch_size=500)

# upsert: اگر بود به‌روز کن، نبود بساز
carpet, created = Carpet.objects.update_or_create(
    code="KSH-1200-17",
    defaults={"price": 52_000_000},                    # فقط هنگام به‌روزرسانی (و ساخت)
    create_defaults={"price": 52_000_000, "name": "افشان", "density": 1200},  # Django 5.0+
)

only و defer

only("id", "code", "price") فقط همین ستون‌ها را می‌خواند و defer("description") همه جز این‌ها را. برای جدول‌هایی با فیلد متنی حجیم (توضیحات، JSON) در صفحه‌ی فهرست تفاوت چشمگیری دارد.

SQL خام؛ آخرین گزینه

rows = Carpet.objects.raw(
    "SELECT * FROM catalog_carpet WHERE density = %s AND price < %s",
    [1200, 60_000_000],
)

from django.db import connection
with connection.cursor() as cur:
    cur.execute("SELECT city, COUNT(*) FROM orders_customer GROUP BY city")
    stats = cur.fetchall()

وقتش وقتی است که ORM واقعاً نمی‌تواند (توابع پنجره‌ای خاص، CTE بازگشتی، hint پایگاه داده) یا کوئری گزارشی پیچیده‌ای دارید که SQL خوانا از ORM نامفهوم بهتر است. پارامترها را همیشه جدا بفرستید.

نکته‌هایی که کمتر کسی می‌داند

  • هرگز SQL را با f-string نسازید (f"... WHERE code = '{code}'")؛ این دقیقاً SQL Injection است. %s و لیست پارامتر را به درایور بسپارید؛ حتی برای عدد.
  • bulk_create متد save() و سیگنال‌ها را اجرا نمی‌کند؛ فیلدهایی که در save خودکار پر می‌شوند (مثل کد رهگیری) خالی می‌مانند.
  • bulk_create(..., update_conflicts=True, unique_fields=["code"], update_fields=["price"]) یک upsert واقعی در یک کوئری است؛ برای همگام‌سازی لیست قیمت از اکسل عالی است.
  • get_or_create بدون قید unique روی فیلدهای جست‌وجو در شرایط هم‌زمانی ممکن است دو رکورد بسازد؛ قید unique آن را امن می‌کند چون جنگو IntegrityError را گرفته و دوباره get می‌زند.
  • دسترسی به فیلدی که با only کنار گذاشته شده، برای هر شیء یک کوئری جدا می‌زند؛ only بد می‌تواند N+1 تازه بسازد.

برای ذخیره‌ی پیشرفت و شرکت در آزمون، وارد شوید — رایگان است.