A QuerySet is a lazy, chainable collection of database queries represented by django.db.models.QuerySet, providing methods for filtering (filter, exclude), ordering (order_by), slicing, aggregation (aggregate, annotate), and relationship traversal (select_related, prefetch_related) that only executes SQL when evaluated.
The QuerySet API solves the problem of building complex database queries programmatically without writing raw SQL. It uses lazy evaluation — chaining .filter().exclude().order_by() builds an internal query plan but executes no SQL until iteration, list(), len(), bool(), or explicit .all(). This allows dynamic query composition based on runtime conditions while deferring expensive database round-trips.
- Manager access —
Model.objectsreturns aManagerwith baseQuerySet - Chaining filters — Each method returns new
QuerySetwith modifiedqueryattribute - Query compilation — On evaluation,
QuerySet.querycompiles to SQL viaSQLCompiler - SQL execution — Database cursor executes; rows fetched
- Result hydration — Rows converted to model instances (or dicts/values_list tuples)
- Caching — Evaluated QuerySet caches results; re-iteration uses cache
- Laziness: No SQL until evaluation;
qs = Post.objects.all()hits DB zero times - Immutability: Each method returns new QuerySet; original unchanged
- Caching: First evaluation populates
_result_cache; subsequent use cached - Field lookups:
field__lookupsyntax —exact,iexact,contains,icontains,gt,gte,lt,lte,in,startswith,endswith,range,date,year,month,day,isnull,regex - Optimization:
select_related(FK, O2O → JOIN),prefetch_related(M2M, reverse FK → separate query + Python join)
- Built from: ORM — QuerySet operates on model tables
- Built from: Model Managers — Entry point via
Model.objects - Built from: Field Lookups —
__syntax for filters - Builds into: Exclusion —
filter(),exclude(),Q()objects - Builds into: Annotation —
aggregate(),annotate(),Count,Sum,Avg - Builds into: Relationship Optimization —
select_related,prefetch_related - Builds into: Bulk Operations —
bulk_create,bulk_update,update(),delete() - Contrasts with: SQLAlchemy Query — Explicit session, more flexible joins
- Contrasts with: Raw SQL — Full control, no ORM overhead
- Related: Transactions —
atomic()for multi-query atomicity - Related: Pagination —
Paginatorslices QuerySet for pages
- QuerySet cloning:
qs.filter(...)returns new QuerySet; modifyingqsin place doesn’t work - Slicing evaluates:
qs[:10]executes SQL withLIMIT;qs[5:10]usesOFFSET/LIMIT len(qs)vsqs.count():len()evaluates and caches;count()always doesSELECT COUNT(*)exists()vsbool(qs):exists()doesSELECT 1 ... LIMIT 1;bool()evaluates full QuerySet- M2M
filter()vsexclude():Post.objects.filter(tags__name='django')vsexclude(tags__name='django')—excludematches posts with NO matching tags, not posts where ALL tags don’t match