Read and write data in Python instead of SQL. Create, filter, order, update, delete, aggregate, and optimize queries using Django’s object-relational mapper.
Why: the Django shell loads your project so you can run ORM queries live and see results instantly — the fastest way to learn querying. Note: shell_plus (from django-extensions) auto-imports your models, but the built-in shell needs an explicit import.
python manage.py shell>>> from blog.models import PostWhen create(): builds and saves in one step. When save(): you need to set fields first, or change an existing object. Note: every model has an objects "manager" — the gateway to all queries for that table.
# one step
post = Post.objects.create(title="Hello", slug="hello", body="My first post")
# two steps — build, then save
post = Post(title="Draft", slug="draft", body="...")
post.published = True
post.save()
# update an existing object
post.title = "Hello, world"
post.save()When all(): every row. When filter(): the subset matching conditions — returns a QuerySet (possibly empty). When get(): exactly one row — it raises DoesNotExist if there are none and MultipleObjectsReturned if there are several, so only use it when you expect a single match.
Post.objects.all() # every post
Post.objects.filter(published=True) # a QuerySet (0..n)
Post.objects.get(slug="hello") # exactly one, or raises
Post.objects.filter(published=True).count()
Post.objects.filter(published=True).exists()
Post.objects.exclude(published=True) # the opposite of filterWhy: lookups are the double-underscore suffixes that build the WHERE clause — __gte (>=), __contains (LIKE), __in (IN), __isnull, and __icontains for case-insensitive search. You can also follow relationships with __, e.g. author__username.
Post.objects.filter(views__gte=100) # views >= 100
Post.objects.filter(title__icontains="django") # case-insensitive LIKE
Post.objects.filter(slug__in=["hello", "draft"]) # slug IN (...)
Post.objects.filter(created_at__year=2026) # date parts
Post.objects.filter(author__username="ada") # follow the FK
Post.objects.filter(subtitle__isnull=True)Note: QuerySets are lazy — no database hit happens until you iterate, index, or print. That lets you chain filters and ordering freely; the SQL runs once at the end. Slicing maps to LIMIT/OFFSET, so [:5] fetches only five rows.
Post.objects.order_by("-created_at") # newest first (- = descending)
Post.objects.order_by("title")[:5] # first 5, A→Z (LIMIT 5)
# chaining builds one query, executed only at the end
recent = (
Post.objects
.filter(published=True)
.exclude(title="")
.order_by("-created_at")[:10]
)When .update()/.delete() on a QuerySet: change or remove many rows in a single SQL statement — fast, but it skips save() and signals. When you need per-object logic: loop and call save() instead.
# one UPDATE statement for every matching row
Post.objects.filter(published=False).update(published=True)
# one DELETE statement
Post.objects.filter(created_at__year=2020).delete()
# single object
post = Post.objects.get(slug="hello")
post.delete()When aggregate(): one summary number for the whole QuerySet (total count, average). When annotate(): attach a computed value to each row — here every author row gets a post_count. Note: import the functions from django.db.models.
from django.db.models import Count, Avg, Sum
Post.objects.aggregate(Avg("views")) # {"views__avg": 42.0}
Post.objects.aggregate(total=Sum("views")) # {"total": 1280}
# count posts per author, attached to each author row
from django.contrib.auth.models import User
User.objects.annotate(post_count=Count("posts")).order_by("-post_count")Why: looping over posts and reading post.author runs one extra query per post — the "N+1" problem. When select_related: follows a ForeignKey/OneToOne with a JOIN (one query). When prefetch_related: handles ManyToMany/reverse relations with a second query.
# BAD: 1 query for posts + 1 per post for the author
for post in Post.objects.all():
print(post.author.username) # hidden query each loop
# GOOD: 1 query total, author joined in
for post in Post.objects.select_related("author"):
print(post.author.username)
# many-to-many / reverse relations
Post.objects.prefetch_related("tags")When transaction.atomic: a block of writes that must all succeed or all roll back (transfer money, create related rows). When raw(): the rare query the ORM cannot express. Why fixtures: dumpdata/loaddata move seed or test data in and out as JSON.
from django.db import transaction
with transaction.atomic(): # all-or-nothing
post.save()
Comment.objects.create(post=post, body="First!")
# raw SQL when you truly need it
Post.objects.raw("SELECT * FROM blog_post WHERE views > %s", [100])# export / import seed data as JSON
python manage.py dumpdata blog.Post --indent 2 > posts.json
python manage.py loaddata posts.json