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

QuerySet تنبل است: ارزیابی، کش، filter/exclude و lookupها

تا وقتی لازم نباشد، هیچ کوئری‌ای اجرا نمی‌شود

وقتی می‌نویسید qs = Order.objects.filter(status="paid") هیچ SQLی اجرا نمی‌شود؛ فقط یک «توصیف» از کوئری ساخته می‌شود. می‌توانید ده بار filter و order_by و exclude را زنجیر کنید و باز هم هیچ اتفاقی در پایگاه داده نمی‌افتد. کوئری فقط در این لحظه‌ها اجرا می‌شود (ارزیابی):

  • پیمایش با for یا در قالب با {% for %}
  • list(qs)، len(qs)، bool(qs) و if qs:
  • برش با گام (qs[::2]) یا ایندکس تکی (qs[0])
  • repr(qs) — مثلاً وقتی در shell فقط نامش را می‌نویسید

کش نتیجه

بعد از اولین ارزیابی، نتیجه روی همان شیء QuerySet کش می‌شود. این رفتار هم کمک است هم دام:

orders = Order.objects.filter(status="paid")
for o in orders: ...          # کوئری ۱
for o in orders: ...          # از کش؛ بدون کوئری

Order.objects.filter(status="paid")[0]   # کوئری
Order.objects.filter(status="paid")[0]   # باز کوئری؛ هر بار QuerySet تازه

# خوب: وقتی فقط وجود/تعداد مهم است
if orders.exists(): ...       # SELECT 1 ... LIMIT 1
total = orders.count()        # SELECT COUNT(*)

# اما اگر قرار است بعداً پیمایش کنید، همان len() بهتر است
orders = list(orders)
if orders:
    print(len(orders))

filter، exclude و lookupها

شکل کلی یک شرط field__lookup=value است و با __ می‌توانید روی روابط هم جلو بروید:

lookupمثالSQL تقریبی
exact / iexactcode__iexact="ksh-12"= / ILIKE
contains / icontainsname__icontains="افشان"LIKE '%…%'
instatus__in=["paid", "shipped"]IN (…)
gt, gte, lt, ltetotal__gte=50_000_000>=
rangecreated_at__date__range=(d1, d2)BETWEEN
isnulldelivery_date__isnull=TrueIS NULL
startswithcustomer__user__mobile__startswith="0912"LIKE '0912%'
date / year / monthcreated_at__year=2025استخراج بخش تاریخ
qs = (Order.objects
      .filter(customer__city="کاشان", total__gte=100_000_000)
      .exclude(status="cancelled")
      .order_by("-created_at"))
print(qs.query)     # SQL تولیدشده برای دیباگ

جست‌وجوی فارسی

متن فارسی دو «ی» و دو «ک» دارد: فارسی (ی، ک) و عربی (ي، ك). اگر داده از منابع مختلف (اکسل قدیمی، کیبورد عربی) آمده باشد، icontains="کاشی" رکوردی را که با «كاشي» ذخیره شده پیدا نمی‌کند. راه درست: هم هنگام ذخیره و هم روی عبارت جست‌وجو، این حروف را یکسان کنید (فصل ۵).

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

  • .first() اگر ترتیبی تعریف نشده باشد خودش ORDER BY pk اضافه می‌کند؛ روی جدول بزرگ بدون ایندکس مناسب ممکن است کند باشد.
  • زنجیر کردن دو filter() روی رابطه‌ی چندتایی با یک filter() با دو شرط فرق دارد: اولی ممکن است دو قلم متفاوت را مطابق کند، دومی شرط هر دو را روی یک قلم می‌خواهد.
  • دادن یک QuerySet به __in (filter(customer__in=Customer.objects.filter(...))) یک زیرکوئری SQL می‌سازد، نه دو کوئری جدا؛ نیازی به values_list و list کردن نیست.
  • created_at__date=... تاریخ را در منطقه‌ی زمانی فعال (تهران) حساب می‌کند نه UTC؛ سفارش ساعت ۱ بامداد تهران در همان روز تهران شمرده می‌شود.
  • روی SQLite جست‌وجوی iexact و icontains فقط برای حروف ASCII غیرحساس به بزرگی است؛ برای فارسی مهم نیست، اما در متن‌های لاتین نتیجه با PostgreSQL فرق می‌کند.

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