~16 min read • Updated Mar 10, 2026
1. Introduction to Django Queries
Once your models are defined, Django automatically provides a high‑level ORM API that lets you create, retrieve, update, and delete records without writing SQL manually. A model class represents a database table, and each instance represents a row in that table.
Throughout this guide, we use the following blog models:
class Blog(models.Model):
name = models.CharField(max_length=100)
tagline = models.TextField()
class Author(models.Model):
name = models.CharField(max_length=200)
email = models.EmailField()
class Entry(models.Model):
blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
headline = models.CharField(max_length=255)
body_text = models.TextField()
pub_date = models.DateField()
mod_date = models.DateField(default=date.today)
authors = models.ManyToManyField(Author)
number_of_comments = models.IntegerField(default=0)
number_of_pingbacks = models.IntegerField(default=0)
rating = models.IntegerField(default=5)
2. Creating Objects
To create a new object, instantiate the model class with keyword arguments and call save():
b = Blog(name="Beatles Blog", tagline="All the latest Beatles news.")
b.save()
This triggers an SQL INSERT statement. Django does not touch the database until save() is called.
You can also create and save an object in one step using create():
Blog.objects.create(name="My Blog", tagline="Hello world")
3. Saving Changes to Existing Objects
To update an existing object, modify its attributes and call save() again:
b5.name = "New name"
b5.save()
This triggers an SQL UPDATE statement.
4. Saving ForeignKey and ManyToManyField Values
Updating a ForeignKey
Assign a model instance of the correct type:
entry = Entry.objects.get(pk=1)
cheese_blog = Blog.objects.get(name="Cheddar Talk")
entry.blog = cheese_blog
entry.save()
Updating a ManyToManyField
Use add() to attach related objects:
joe = Author.objects.create(name="Joe")
entry.authors.add(joe)
You can add multiple authors at once:
entry.authors.add(john, paul, george, ringo)
5. Retrieving Objects
To retrieve objects, you use a QuerySet obtained from a model’s Manager. Every model has at least one Manager named objects.
Blog.objects.all()
A Manager is only accessible through the model class, not through instances.
6. Retrieving All Objects
The simplest query retrieves all rows:
all_entries = Entry.objects.all()
7. Filtering QuerySets
To retrieve a subset of objects, use filter() or exclude():
filter(**kwargs)→ returns objects matching the criteriaexclude(**kwargs)→ returns objects NOT matching the criteria
Example: entries from 2006
Entry.objects.filter(pub_date__year=2006)
8. Chaining Filters
Each refinement returns a new QuerySet, so you can chain them:
Entry.objects.filter(headline__startswith="What")
.exclude(pub_date__gte=datetime.date.today())
.filter(pub_date__gte=datetime.date(2005, 1, 30))
This produces entries whose headline starts with “What” and were published between January 30, 2005 and today.
9. Filtered QuerySets Are Independent
Each filtered QuerySet is a separate object:
q1 = Entry.objects.filter(headline__startswith="What")
q2 = q1.exclude(pub_date__gte=datetime.date.today())
q3 = q1.filter(pub_date__gte=datetime.date.today())
These QuerySets do not affect each other. q1 remains unchanged.
Conclusion
Django’s ORM provides a powerful and intuitive API for interacting with your database. Understanding how to create, save, filter, and chain QuerySets is essential for building dynamic, data‑driven applications. With these tools, you can write expressive, efficient queries without writing SQL manually.
1. QuerySets Are Lazy
QuerySets in Django are lazy, meaning that creating or refining a QuerySet does not immediately hit the database. Django waits until the QuerySet is actually evaluated.
Example:
q = Entry.objects.filter(headline__startswith="What")
q = q.filter(pub_date__lte=datetime.date.today())
q = q.exclude(body_text__icontains="food")
print(q)
Although this looks like three separate queries, Django executes only one SQL query when print(q) forces evaluation.
A QuerySet is evaluated when you:
- iterate over it,
- convert it to a list,
- print it,
- or otherwise access its results.
2. Retrieving a Single Object with get()
filter() always returns a QuerySet, even if it contains only one object. If you know exactly one object matches your criteria, use get():
one_entry = Entry.objects.get(pk=1)
Difference Between get() and filter()[0]
- If no object matches:
get()→ raises DoesNotExist
- If more than one object matches:
get()→ raises MultipleObjectsReturned
filter()[0]→ raises IndexError if empty
3. Limiting QuerySets with Slicing
You can use Python slicing syntax to limit results, similar to SQL’s LIMIT and OFFSET.
First 5 objects (LIMIT 5):
Entry.objects.all()[:5]
Objects 6 through 10 (OFFSET 5 LIMIT 5):
Entry.objects.all()[5:10]
Slicing normally returns a new QuerySet without evaluating it. The exception is when using a step:
Entry.objects.all()[:10:2]
This forces evaluation because Django must fetch results to apply the step.
Retrieving a single object using indexing:
Entry.objects.order_by("headline")[0]
This is equivalent to LIMIT 1 in SQL.
4. Field Lookups
Field lookups define SQL WHERE conditions. They use the syntax field__lookup=value (double underscore).
Example:
Entry.objects.filter(pub_date__lte="2006-01-01")
Equivalent SQL:
SELECT * FROM blog_entry WHERE pub_date <= '2006-01-01';
Common lookup types:
- exact – exact match
- iexact – case‑insensitive exact match
- contains / icontains
- lt, lte, gt, gte
- startswith / istartswith
- in
ForeignKey shortcut:
You can filter by the raw primary key using field_id:
Entry.objects.filter(blog_id=4)
Conclusion
Django’s lazy QuerySets allow efficient query construction, get() provides a clean way to retrieve a single object, slicing enables LIMIT/OFFSET behavior, and field lookups give you expressive control over SQL WHERE clauses. Mastering these tools is essential for writing clean, efficient, and powerful Django queries.
1. Case‑Insensitive and Containment Lookups
iexact
iexact performs a case‑insensitive exact match:
Blog.objects.get(name__iexact="beatles blog")
This matches “Beatles Blog”, “beatles blog”, or even “BeAtlES blOG”.
contains / icontains
contains performs a case‑sensitive substring search:
Entry.objects.get(headline__contains="Lennon")
Equivalent SQL:
SELECT ... WHERE headline LIKE '%Lennon%';
icontains is the case‑insensitive version.
startswith / endswith
These lookups match prefixes and suffixes. Case‑insensitive versions are istartswith and iendswith.
2. Lookups That Span Relationships
Django automatically handles SQL JOINs when you use double underscores to traverse relationships.
Example: filter entries by blog name
Entry.objects.filter(blog__name="Beatles Blog")
Reverse lookups
By default, Django uses the lowercase model name for reverse relations:
Blog.objects.filter(entry__headline__contains="Lennon")
Handling missing intermediate relations
If a related object is missing, Django treats it as NULL rather than raising an error.
Blog.objects.filter(entry__authors__name__isnull=True)
This returns blogs with authors whose name is NULL and blogs whose entries have no authors at all.
3. Spanning Multi‑Valued Relationships
When filtering across ManyToMany or reverse ForeignKey relations, you must decide whether all conditions must match the same related object.
Conditions must match the same related object:
Blog.objects.filter(
entry__headline__contains="Lennon",
entry__pub_date__year=2008,
)
Conditions may match different related objects:
Blog.objects.filter(entry__headline__contains="Lennon")
.filter(entry__pub_date__year=2008)
The second query is more permissive and may return duplicates because it performs multiple JOINs.
Example output:
, , ]>
4. exclude() Does Not Behave Like filter()
Unlike filter(), exclude() does not guarantee that conditions refer to the same related object.
Incorrect (conditions may apply to different entries):
Blog.objects.exclude(
entry__headline__contains="Lennon",
entry__pub_date__year=2008,
)
Correct way to exclude entries matching both conditions:
Blog.objects.exclude(
entry__in=Entry.objects.filter(
headline__contains="Lennon",
pub_date__year=2008,
)
)
5. Comparing Fields with F Expressions
F expressions allow you to compare fields within the same model or perform arithmetic inside queries.
More comments than pingbacks:
Entry.objects.filter(number_of_comments__gt=F("number_of_pingbacks"))
More than twice as many comments as pingbacks:
Entry.objects.filter(number_of_comments__gt=F("number_of_pingbacks") * 2)
Rating less than comments + pingbacks:
Entry.objects.filter(
rating__lt=F("number_of_comments") + F("number_of_pingbacks")
)
Spanning relationships with F expressions:
Entry.objects.filter(authors__name=F("blog__name"))
Date arithmetic:
Entry.objects.filter(mod_date__gt=F("pub_date") + timedelta(days=3))
Bitwise operations:
F("somefield").bitand(16)
(Oracle does not support XOR.)
Conclusion
Django’s advanced lookup system, relationship spanning, multi‑value filtering rules, and F expressions provide a powerful toolkit for writing expressive and efficient database queries. Mastering these features allows you to build complex logic without writing raw SQL, while keeping your code clean and maintainable.
1. Expressions Can Reference Transforms
Django allows transforms to be used inside F expressions.
Example: entries published in the same year they were modified:
Entry.objects.filter(pub_date__year=F("mod_date__year"))
Finding the earliest publication year:
Entry.objects.aggregate(first_published_year=Min("pub_date__year"))
Using Subquery and OuterRef:
Find the highest rating and total comments for each year:
Entry.objects.values("pub_date__year").annotate(
top_rating=Subquery(
Entry.objects.filter(
pub_date__year=OuterRef("pub_date__year")
).order_by("-rating").values("rating")[:1]
),
total_comments=Sum("number_of_comments"),
)
2. The pk Lookup Shortcut
pk stands for “primary key” and is equivalent to id__exact.
These three statements are identical:
Blog.objects.get(id__exact=14)
Blog.objects.get(id=14)
Blog.objects.get(pk=14)
More examples:
Blog.objects.filter(pk__in=[1, 4, 7])
Blog.objects.filter(pk__gt=14)
pk lookups also work across joins:
Entry.objects.filter(blog__pk=3)
3. Escaping % and _ in LIKE Statements
Lookups that translate to SQL LIKE (contains, icontains, startswith, etc.) automatically escape % and _.
Example:
Entry.objects.filter(headline__contains="%")
SQL generated:
WHERE headline LIKE '%\%%'
4. QuerySet Caching
Each QuerySet has an internal cache. The first evaluation populates the cache, and subsequent evaluations reuse it.
Bad example (two queries executed):
print([e.headline for e in Entry.objects.all()])
print([e.pub_date for e in Entry.objects.all()])
Correct approach:
queryset = Entry.objects.all()
print([e.headline for e in queryset])
print([e.pub_date for e in queryset])
5. When QuerySets Are Not Cached
Partial evaluation (such as slicing or indexing) does not populate the cache.
Example:
queryset = Entry.objects.all()
print(queryset[5]) # Hits the database
print(queryset[5]) # Hits the database again
But if the entire QuerySet has been evaluated:
queryset = Entry.objects.all()
list(queryset) # Populates cache
print(queryset[5]) # Uses cache
Other actions that populate the cache:
[entry for entry in queryset]
bool(queryset)
entry in queryset
list(queryset)
Note: printing the QuerySet does NOT populate the cache.
6. Asynchronous Queries
In async views, you cannot call synchronous ORM methods. Django provides async variants such as aget() and adelete().
Async iteration:
async for entry in Authors.objects.filter(name__startswith="A"):
...
Combining filter() with afirst():
user = await User.objects.filter(username=my_input).afirst()
If you forget await, you may see errors like “coroutine object has no attribute …”.
7. Knowing Which Methods Are Async-Safe
Methods that return new QuerySets (filter, exclude, etc.) are non‑blocking and safe in async code.
Methods that evaluate the QuerySet (get, first, delete, etc.) have async versions and must be awaited.
Conclusion
Django’s advanced ORM features—transforms, Subquery, pk shortcuts, LIKE escaping, QuerySet caching, and async query support—provide a powerful toolkit for building efficient, scalable, and expressive database logic. Mastering these tools helps you write clean, optimized, and modern Django applications.
1. Transactions and Asynchronous Code
Django does not currently support database transactions inside asynchronous ORM operations. Attempting to use a transaction in async code raises SynchronousOnlyOperation.
To use transactions safely, place ORM logic inside a synchronous function and call it using sync_to_async.
2. Querying JSONField
JSONField lookups behave differently from other fields because they support key, index, and path transforms.
Example model:
class Dog(models.Model):
name = models.CharField(max_length=200)
data = models.JSONField(null=True)
3. Storing and Querying None
Storing None normally results in SQL NULL. To store JSON null explicitly, use:
Value(None, JSONField())
Both SQL NULL and JSON null are returned as Python None, making them indistinguishable at retrieval time.
Query behavior:
data=None→ matches JSON nulldata__isnull=True→ matches SQL NULL
Example:
Dog.objects.filter(data=None) # JSON null
Dog.objects.filter(data__isnull=True) # SQL NULL
To avoid ambiguity, set null=False and provide a default such as default=dict.
4. Key, Index, and Path Transforms
You can query nested JSON structures using double underscores.
Query by key:
Dog.objects.filter(data__breed="collie")
Nested keys:
Dog.objects.filter(data__owner__name="Bob")
Index transforms:
Dog.objects.filter(data__owner__other_pets__0__name="Fishy")
Negative indices:
Not allowed directly, but possible via dictionary unpacking:
Dog.objects.filter(**{"data__owner__other_pets__-1__name": "Fishy"})
Note: MySQL, MariaDB, and Oracle do not support negative JSON indices.
Missing keys:
Dog.objects.filter(data__owner__isnull=True)
5. KT() Expressions
KT() extracts the text value of a JSON key/index/path transform and allows annotation or filtering.
Example:
from django.db.models.fields.json import KT
Dog.objects.annotate(
first_breed=KT("data__breed__1"),
owner_name=KT("data__owner__name")
).filter(first_breed__startswith="lhasa", owner_name="Bob")
Warning: Any unknown lookup name is treated as a JSON key. Typos will not raise errors.
6. Backend-Specific Notes
- MariaDB & Oracle: ordering by JSON transforms sorts by string representation.
- Oracle:
exclude(..., None)behaves differently and may include objects without the path. - PostgreSQL: uses
->for single-level and#>for multi-level paths. - SQLite: strings "true", "false", "null" are interpreted as JSON booleans/null.
7. Containment and Key Lookups
contains
Matches objects whose JSON contains the given key-value pairs at the top level.
Dog.objects.filter(data__contains={"owner": "Bob"})
Not supported on Oracle or SQLite.
contained_by
The inverse of contains—matches objects whose JSON is a subset of the given dict.
Dog.objects.filter(data__contained_by={"breed": "collie", "owner": "Bob"})
Not supported on Oracle or SQLite.
has_key
Dog.objects.filter(data__has_key="owner")
has_keys
Dog.objects.filter(data__has_keys=["breed", "owner"])
has_any_keys
Dog.objects.filter(data__has_any_keys=["owner", "breed"])
Conclusion
Django’s JSONField querying system is powerful and flexible, supporting nested lookups, index transforms, containment operations, and KT expressions. Understanding SQL NULL vs JSON null, backend differences, and the behavior of key-path lookups is essential for writing correct and efficient JSON queries in Django.
1. Building Complex Queries with Q Objects
In Django, keyword arguments in filter() and exclude() are combined using AND. To build OR or more complex logical expressions, you use Q objects.
Basic Q object:
Q(question__startswith="What")
OR between two conditions:
Q(question__startswith="Who") | Q(question__startswith="What")
Negation (NOT):
Q(question__startswith="Who") | ~Q(pub_date__year=2005)
Using Q objects in queries:
Poll.objects.get(
Q(question__startswith="Who"),
Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),
)
Important:
Q objects must come before keyword arguments in filter(), get(), or exclude().
2. Comparing Model Instances
To compare two model instances, use the Python equality operator ==. Django compares their primary keys internally.
Example:
some_entry == other_entry
# Equivalent to:
some_entry.id == other_entry.id
If the primary key has a different name, Django still compares that field.
3. Deleting Objects
Deleting a single object:
e.delete()
# Output:
(1, {'blog.Entry': 1})
Bulk delete:
Entry.objects.filter(pub_date__year=2005).delete()
Note: Bulk delete does not call the delete() method on each instance. If you rely on custom delete logic, delete objects individually.
Cascade delete:
Deleting an object also deletes related objects via ForeignKey (default ON DELETE CASCADE).
b = Blog.objects.get(pk=1)
b.delete() # Deletes Blog and all related Entry objects
4. Copying Model Instances
Django has no built‑in copy method, but you can duplicate an instance by resetting its primary key.
Simple copy:
blog.pk = None
blog._state.adding = True
blog.save()
Copying inherited models:
For inherited models, both pk and id must be set to None:
django_blog.pk = None
django_blog.id = None
django_blog._state.adding = True
django_blog.save()
Copying ManyToMany relationships:
old_authors = entry.authors.all()
entry.pk = None
entry._state.adding = True
entry.save()
entry.authors.set(old_authors)
Copying OneToOne relationships:
You must duplicate the related object to avoid violating the one‑to‑one constraint.
5. Bulk Updating with update()
To update many objects at once, use update().
Example:
Entry.objects.filter(pub_date__year=2007).update(headline="Everything is the same")
Updating a ForeignKey:
Entry.objects.update(blog=b)
Important notes about update():
- It executes directly as SQL.
- It does NOT call save().
- It does NOT trigger pre_save or post_save signals.
- It does NOT update auto_now fields.
- It can only update fields on the model’s main table.
Using F expressions in update():
Entry.objects.update(number_of_pingbacks=F("number_of_pingbacks") + 1)
Limitation: F expressions in update() cannot introduce joins:
Entry.objects.update(headline=F("blog__name")) # Raises FieldError
Conclusion
Q objects allow you to build complex logical queries. Deleting and copying objects requires understanding Django’s cascade and relation behavior. Bulk updates with update() are powerful but bypass model methods and signals. Mastering these tools enables you to write efficient, expressive, and production‑ready Django ORM code.
1. Introduction to Related Objects
Whenever a model defines a relationship (ForeignKey, OneToOneField, ManyToManyField), Django automatically provides an API to access related objects. This includes both forward access (from the model defining the field) and reverse access (from the related model back to the source model).
For example, an Entry instance can access its related Blog via e.blog, while a Blog instance can access all related entries via b.entry_set.all().
2. One‑to‑Many Relationships
Forward Access
When a model has a ForeignKey, each instance can access the related object directly:
e = Entry.objects.get(id=2)
e.blog
You can assign a new related object and save:
e.blog = some_blog
e.save()
If null=True is set, you may assign None to remove the relationship.
Caching Forward Access
The first time a foreign key is accessed, Django hits the database and caches the result. Subsequent access uses the cache:
print(e.blog) # Database hit
print(e.blog) # Cached
select_related() prepopulates this cache:
e = Entry.objects.select_related().get(id=2)
print(e.blog) # No database hit
3. Reverse Access
Reverse access is available through a manager named FOO_set by default:
b = Blog.objects.get(id=1)
b.entry_set.all()
b.entry_set.filter(headline__contains="Lennon")
b.entry_set.count()
Using related_name
You can override entry_set using related_name:
blog = ForeignKey(Blog, related_name="entries", ...)
Then:
b.entries.all()
4. Custom Reverse Managers
You can specify which manager to use for reverse relations:
b.entry_set(manager="entries").all()
This allows custom filtering or custom methods:
b.entry_set(manager="entries").is_published()
Prefetching with a custom manager
Use Prefetch() to prefetch using a custom manager:
prefetch = Prefetch("entry_set", queryset=Entry.entries.all())
Blog.objects.prefetch_related(prefetch)
5. Additional Methods for Handling Related Objects
The reverse manager provides extra methods:
- add(obj1, obj2, ...) – Add objects to the relation.
- create(**kwargs) – Create and add a new related object.
- remove(obj1, obj2, ...) – Remove objects from the relation.
- clear() – Remove all related objects.
- set(objs) – Replace the entire related set.
Example:
b.entry_set.set([e1, e2])
These operations write to the database immediately.
6. Many‑to‑Many Relationships
Both sides of a many‑to‑many relationship get access to the related objects.
Examples:
e = Entry.objects.get(id=3)
e.authors.all()
e.authors.filter(name__contains="John")
a = Author.objects.get(id=5)
a.entry_set.all()
Using related_name
If related_name='entries' is set on the ManyToManyField:
a.entries.all()
Primary key support in add(), set(), remove()
Many‑to‑many methods accept both model instances and primary keys:
a.entry_set.set([e1, e2])
a.entry_set.set([e1.pk, e2.pk])
Conclusion
Django provides a powerful and intuitive API for navigating relationships. Forward and reverse access, custom managers, and many‑to‑many utilities make it easy to work with relational data. Understanding these tools helps you write clean, expressive, and efficient ORM code.
1. Filtering on Many‑to‑Many Relationships
When using filter() across a many‑to‑many relationship, Django performs the join between the main model and the intermediary table only once. This creates a restrictive behavior known as a “sticky join”.
Example:
Suppose an Entry is authored by both Anna and Gloria:
anna.entry_set.filter(authors__name="Gloria")
You might expect this to return the shared Entry, but it returns an empty QuerySet. Why?
- The join happens only once.
- No single row in the intermediary table represents both Anna and Gloria at the same time.
Workaround: chain two filters
anna.entry_set.filter().filter(authors__name="Gloria")
This forces two separate joins and returns the expected result.
exclude() is also sticky
exclude() behaves similarly in this scenario:
anna.entry_set.exclude(authors__name="Gloria")
This incorrectly keeps the Entry, even though Gloria is a co‑author.
But chaining two excludes works:
anna.entry_set.exclude().exclude(authors__name="Gloria")
Other cases may differ; see Django’s documentation on multi‑valued relationships.
2. One‑to‑One Relationships
One‑to‑one relations behave like ForeignKey but guarantee a single related object.
Example:
class EntryDetail(models.Model):
entry = models.OneToOneField(Entry, on_delete=models.CASCADE)
details = models.TextField()
Forward access:
ed.entry
Reverse access:
e.entrydetail
If no related object exists, Django raises DoesNotExist.
3. How Backward Relations Are Created
Django follows the DRY principle: you define the relationship on only one side, and Django automatically creates the reverse relation.
This is possible because of the app registry:
- Django loads all apps in
INSTALLED_APPS. - It imports each app’s
models.py. - Whenever a model is created, Django registers its relationships.
- If the related model hasn’t been loaded yet, Django defers the relation until it is.
Therefore, all models must be inside apps listed in INSTALLED_APPS.
4. Querying Related Objects
You can filter using either the related object itself or its primary key.
These three queries are identical:
Entry.objects.filter(blog=b)
Entry.objects.filter(blog=b.id)
Entry.objects.filter(blog=5)
5. Falling Back to Raw SQL
If a query is too complex for Django’s ORM, you can write raw SQL manually using Django’s raw SQL APIs.
Remember: Django is just an interface. Your database remains fully accessible through any other tool or language.
Conclusion
Filtering across many‑to‑many relationships requires understanding Django’s single‑join behavior. One‑to‑one relations behave similarly to ForeignKey but return a single object. Django automatically builds reverse relations using the app registry. And when ORM limitations arise, raw SQL is always an option.
Written & researched by Dr. Shahin Siami