Grokking Django
Vote

0% completed

​

Introduction to Django ORM

Trying Queries in the Shell

Creating Rows

Reading Rows: Querysets

Filtering

Querysets Are Lazy

Getting One Row

Sorting, Slicing and Searching

Counting and Aggregating

Updating Rows

Deleting Rows

Key Takeaways

Article.objects.filter(author="Ana Lopez").order_by("-view_count")

That one line asks the database for Ana Lopez's articles, the most read first, and it contains no SQL. Django writes the SQL for you, sends it to the database, and turns each row of the result into an Article object. The part of Django that does this translation is the object-relational mapper, or ORM: it maps tables to classes and rows to objects.

This lesson uses the Article model from the previous lesson, with its title, content, author, published_date and view_count fields, and it walks through the four basic operations on its rows: create, read, update and delete.

Trying Queries in the Shell

The easiest place to practice is the Django shell, a Python prompt that has your project's settings loaded:

python manage.py shell

Current versions of Django import your models into the shell automatically, but the examples below still show the import, so they also work in a view or a script. Any change you make in the shell is written to the real development database.

Creating Rows

To add a row, create an instance of the model and call its save() method:

from blog.models import Article article = Article( title="Getting Started with Django", content="Install Django and create a project.", author="Ana Lopez", ) print(article.id) article.save() print(article.id, article.published_date)
None 1 2026-11-05 22:35:42.793237+00:00

Creating the object does not touch the database, which is why id is still None on the first line. The save() call runs an INSERT, and the database then gives the row its id. The published_date was filled in by the field's default, and view_count got its default of 0.

The create() method does both steps in one call. It builds the object, saves it and returns it:

Article.objects.create( title="Understanding Querysets", content="A queryset is lazy.", author="Ben Carter", view_count=120, ) Article.objects.create( title="Django Models in Practice", content="Fields map to columns.", author="Ana Lopez", view_count=45, )

The objects attribute in these calls is the model's manager. Django adds a manager called objects to every model, and every query on the table starts from it.

Reading Rows: Querysets

The table now holds three articles. To get all of them, call all() on the manager:

articles = Article.objects.all() print(articles)
<QuerySet [<Article: Getting Started with Django>, <Article: Understanding Querysets>, <Article: Django Models in Practice>]>

The result is a queryset, which is a description of a query that Django runs when you need the rows. The articles print by title because the model defines __str__. You can print the SQL that a queryset will run through its query attribute:

print(articles.query)
SELECT "blog_article"."id", "blog_article"."title", "blog_article"."content", "blog_article"."author", "blog_article"."published_date", "blog_article"."view_count" FROM "blog_article"

Notice the table name blog_article, the name Django chose when it created the table.

Filtering

The filter() method keeps only the rows that match a condition, and it returns another queryset:

ana_articles = Article.objects.filter(author="Ana Lopez") print(ana_articles) print(ana_articles.query)
<QuerySet [<Article: Getting Started with Django>, <Article: Django Models in Practice>]> SELECT "blog_article"."id", "blog_article"."title", "blog_article"."content", "blog_article"."author", "blog_article"."published_date", "blog_article"."view_count" FROM "blog_article" WHERE "blog_article"."author" = Ana Lopez

The printed SQL shows the value without quotes because query is only a readable approximation. The real query sends Ana Lopez to the database as a separate parameter, which also protects the query from SQL injection.

A condition can do more than test equality. You add a lookup to the field name with two underscores. For example, view_count__gt=100 means "greater than 100", and gte, lt and lte work the same way:

print(Article.objects.filter(view_count__gt=100))
<QuerySet [<Article: Understanding Querysets>]>

The exclude() method is the opposite of filter(): it keeps the rows that do not match. And because each of these methods returns a queryset, you can chain them. Two chained filters must both match, and Django joins them with AND:

print(Article.objects.filter(author="Ana Lopez").filter(view_count__gt=40).query)
SELECT ... FROM "blog_article" WHERE ("blog_article"."author" = Ana Lopez AND "blog_article"."view_count" > 40)

Querysets Are Lazy

Building a queryset does not run any SQL. Django runs the query only when you use the rows, for example when you loop over the queryset, print it, or call list() on it. The code below counts the queries that reach the database:

from django.db import connection from django.test.utils import CaptureQueriesContext with CaptureQueriesContext(connection) as ctx: popular = Article.objects.filter(view_count__gt=40).order_by("-view_count") print("after building:", len(ctx.captured_queries)) for article in popular: print(article.title, article.view_count) print("after loop:", len(ctx.captured_queries)) for article in popular: pass print("after second loop:", len(ctx.captured_queries))
after building: 0 Understanding Querysets 120 Django Models in Practice 45 after loop: 1 after second loop: 1

Building the queryset with a filter and an ordering cost nothing. The first loop ran one query, and the second loop ran none, because the queryset kept the rows it had already fetched. This laziness lets you build a query in several steps, for example adding a filter only when a search box is filled in, and still send a single query to the database.

Each chained method only adds to the description of a query, and one SQL statement runs when the loop first asks for rows.
Each chained method only adds to the description of a query, and one SQL statement runs when the loop first asks for rows.

Notice in the picture that filter() and order_by() happen entirely in Python. The database sees only the one SELECT that the loop triggers.

Getting One Row

When you need exactly one object, use get(). It returns the object itself, not a queryset:

article = Article.objects.get(title="Understanding Querysets") print(article.author)
Ben Carter

The most common use is fetching a row by its primary key, as in Article.objects.get(pk=1). The name pk always means the primary key, which for this model is id.

The get() method raises an exception when it does not find exactly one row. With no match, it raises the model's DoesNotExist exception, and with more than one match, it raises MultipleObjectsReturned:

blog.models.Article.DoesNotExist: Article matching query does not exist. blog.models.Article.MultipleObjectsReturned: get() returned more than one Article -- it returned 2!

The second message comes from Article.objects.get(author="Ana Lopez"), because two articles match. Each model has its own copy of both exceptions, so you can catch them by name:

try: article = Article.objects.get(title="No such article") except Article.DoesNotExist: article = None

When a missing row is normal rather than an error, first() is shorter. It returns the first matching object, or None when nothing matches:

print(Article.objects.filter(author="Ana Lopez").first()) print(Article.objects.filter(author="Nobody").first())
Getting Started with Django None

Sorting, Slicing and Searching

The order_by() method sorts the results by one or more fields. A hyphen before the field name reverses the order:

print(Article.objects.order_by("view_count")) print(Article.objects.order_by("-view_count"))
<QuerySet [<Article: Getting Started with Django>, <Article: Django Models in Practice>, <Article: Understanding Querysets>]> <QuerySet [<Article: Understanding Querysets>, <Article: Django Models in Practice>, <Article: Getting Started with Django>]>

Slicing a queryset with Python's slice syntax limits the number of rows. Django turns the slice into a LIMIT clause, so the database returns only those rows:

top_two = Article.objects.order_by("-view_count")[:2] print(top_two) print(top_two.query)
<QuerySet [<Article: Understanding Querysets>, <Article: Django Models in Practice>]> SELECT ... FROM "blog_article" ORDER BY "blog_article"."view_count" DESC LIMIT 2

Always sort before you slice when the order matters. Without order_by(), the database may return rows in any order, so Article.objects.all()[:3] gives you three articles, but not necessarily the first three you created.

For text, the iexact lookup matches a whole value and ignores letter case, while icontains matches any value that contains the given text, also ignoring case:

print(Article.objects.get(title__iexact="understanding QUERYSETS")) print(Article.objects.filter(title__icontains="django"))
Understanding Querysets <QuerySet [<Article: Getting Started with Django>, <Article: Django Models in Practice>]>

The lookups without the i, exact and contains, compare the case as well. On SQLite, contains still ignores case, because SQLite's LIKE operator does not support case-sensitive matching. PostgreSQL follows the difference for both lookups. On MySQL, contains respects case, but whether exact does depends on the collation of the table, and the default collations ignore case.

Counting and Aggregating

To count rows, call count() on a queryset. Django runs a SELECT COUNT(*) and does not load the rows:

print(Article.objects.count()) print(Article.objects.filter(author="Ana Lopez").count())
3 2

For other totals, the aggregate() method computes a value over all the rows of a queryset in the database. The functions Count, Sum, Avg, Min and Max come from django.db.models, and the result is a dictionary:

from django.db.models import Avg, Count, Max, Sum print(Article.objects.aggregate(Count("id"))) print(Article.objects.aggregate(Avg("view_count"))) print(Article.objects.aggregate(total=Sum("view_count"), top=Max("view_count"))) print(Article.objects.filter(author="Ana Lopez").aggregate(Avg("view_count")))
{'id__count': 3} {'view_count__avg': 55.0} {'total': 165, 'top': 120} {'view_count__avg': 22.5}

Without names, each key is the field name, two underscores, and the function name, such as view_count__avg. With a keyword such as total=, you choose the key. The last line shows that aggregate() works on a filtered queryset too: Ana Lopez's two articles have 0 and 45 views, so their average is 22.5.

Updating Rows

To change one row, fetch the object, change its attributes and call save() again. Because the object already has an id, Django runs an UPDATE instead of an INSERT:

article = Article.objects.get(title="Getting Started with Django") article.view_count = 10 article.save()

Captured with CaptureQueriesContext, as in the laziness example, the save() call sends this SQL:

UPDATE "blog_article" SET "title" = 'Getting Started with Django', "content" = 'Install Django and create a project.', "author" = 'Ana Lopez', "published_date" = '2026-11-05 22:35:42.793237', "view_count" = 10 WHERE "blog_article"."id" = 1

The SQL shows that save() writes every column, not only the one you changed. If another request changed the title between your get() and your save(), your save() would write the old title back. To write only some columns, name them: article.save(update_fields=["view_count"]) runs UPDATE "blog_article" SET "view_count" = 10 WHERE "blog_article"."id" = 1.

To change many rows at once, call update() on a queryset. It runs a single UPDATE statement and returns the number of rows it changed:

changed = Article.objects.filter(author="Ana Lopez").update(author="Ana M. Lopez") print(changed)
2

The whole change is one statement:

UPDATE "blog_article" SET "author" = 'Ana M. Lopez' WHERE "blog_article"."author" = 'Ana Lopez'

The update() method does not load the objects and does not call their save() methods. That makes it much faster for many rows, but any extra logic you put in a model's save() method does not run.

Deleting Rows

To delete one row, call delete() on the object. To delete every row that matches a filter, call delete() on the queryset. Both return the number of rows deleted, and a dictionary with the count for each model:

article = Article.objects.get(title="Django Models in Practice") print(article.delete()) print(Article.objects.filter(author="Ana M. Lopez").delete())
(1, {'blog.Article': 1}) (1, {'blog.Article': 1})

The second call deleted one article because the first call had already removed the other article by the same author. After delete(), the Python object still exists with its field values, but its id is None, and the row is gone from the database. A deleted row cannot be recovered from Django, so be careful with a queryset delete() in the shell. Article.objects.all().delete() empties the table.

Key Takeaways

  • Every query starts from the model's manager, Article.objects. Methods such as all(), filter(), exclude() and order_by() return querysets, which you can chain.
  • A queryset is lazy. It runs its SQL when you use the rows, once, and it keeps the results. Print queryset.query to see the SQL.
  • get() returns one object and raises DoesNotExist or MultipleObjectsReturned otherwise. first() returns None when nothing matches.
  • Sort before you slice. count() and aggregate() compute in the database and return numbers, not objects.
  • save() writes every column of one row, while update() and delete() on a queryset change many rows in one statement without calling save().

Reading Progress

0%


Vote for new content

On This Page

Trying Queries in the Shell

Creating Rows

Reading Rows: Querysets

Filtering

Querysets Are Lazy

Getting One Row

Sorting, Slicing and Searching

Counting and Aggregating

Updating Rows

Deleting Rows

Key Takeaways