Update annotated Django querysets using subqueries

How-To guide to update a Django queryset with annotation and subquery.

© 2018 Paolo Melchiorre “Photo of Cerrano beach in Abruzzo, Italy”
© 2018 Paolo Melchiorre “Photo of Cerrano beach in Abruzzo, Italy”
Django under the hood (3 part series)
  1. Update annotated Django querysets using subqueries
  2. Django 3.2: Compressed fixtures, fixtures compression
  3. μDjango (micro Django) 🧬

Preface

In the official Django documentation there is no info about using Django ORM update() and annotate() functions to update all rows in a queryset by using an annotated value.

We are going to show a way to update an annotated Django queryset using only Django ORM subquery() without using extra() functions or SQL code.

Models

First, we use the weblog application code, found in the Django Documentation under “Making Queries”.

python

from django.db import models


class Blog(models.Model):
    name = models.CharField(
        max_length=100
    )
    rating = models.DecimalField(
        max_digits=3,
        decimal_places=2,
        default=5,
    )

    def __str__(self):
        return self.name


class Entry(models.Model):
    blog = models.ForeignKey(
        Blog, on_delete=models.CASCADE
    )
    headline = models.CharField(
        max_length=255
    )
    rating = models.IntegerField(
        default=5
    )

    def __str__(self):
        return self.headline

Issue

One way to update the Blog’s rating based on the average rating from all the entries could be:

python

from django.db.models import Avg
from blog.models import Blog

for blog in Blog.objects.annotate(
    avg_rating=Avg("entry__rating")
):
    blog.rating = blog.avg_rating or 0
    blog.save()

The code above may be very inefficient and slow if we have a lot of Entries or Blogs because Django ORM performs a SQL query for each step of the for-cycle.

If we want to avoid the code above and perform an update operation in a single SQL-request, we can try and use a code like this:

python

Blog.objects.update(
    rating=Avg("entry__rating")
)

But this doesn’t work and we will read an error similar to this:

Traceback (most recent call last):
...
FieldError: Joined field references are not permitted in this query

Solution

With Django 1.11+ it is possible to use Django ORM but using subquery():

Subquery() expressions

You can add an explicit subquery to a QuerySet using the Subquery expression.

see documentation

python

from django.db.models import (
    Avg,
    OuterRef,
    Subquery,
)
from blog.models import Blog

Blog.objects.update(
    rating=Subquery(
        Blog.objects.filter(
            id=OuterRef("id")
        )
        .annotate(
            avg_rating=Avg(
                "entry__rating"
            )
        )
        .values("avg_rating")[:1]
    )
)

On PostgreSQL, the SQL looks like:

SQL

UPDATE "blog_blog"
SET "rating" = (
    SELECT AVG(U1."rating") AS "avg_rating"
    FROM "blog_blog" U0
    LEFT OUTER JOIN "blog_entry" U1 ON (U0."id" = U1."blog_id")
    WHERE U0."id" = ("blog_blog"."id")
    GROUP BY U0."id"
    LIMIT 1
)

Stack Overflow

I wrote this solution the first time as an answer on Stack Overflow.

If you found this article useful, you can vote for my answer on Stack Overflow and read my other answers on my profile.