Skip to main content

Values & Annotate

Control the shape of your query results with values() and annotate().

values() — Dict Results

Instead of returning Model instances, return dictionaries:

posts = await Post.objects.values("title", "views")
# → [{"title": "Hello", "views": 100}, {"title": "World", "views": 200}]

# All fields as dicts
all_dicts = await Post.objects.values()

values_list() — Tuple Results

titles = await Post.objects.values_list("title", flat=True)
# → ["Hello", "World", "Foo"]

pairs = await Post.objects.values_list("title", "views")
# → [("Hello", 100), ("World", 200)]

annotate() — Computed Fields

Add calculated columns to each result row:

posts = await Post.objects.annotate(
comment_count=Count("comments.id"),
is_popular=Case(
When(views__gte=1000, then=True),
default=False,
),
)

print(posts[0]["comment_count"]) # Access annotated field

values() + annotate() = GROUP BY

The most powerful pattern — group by one or more fields:

# Posts per author with stats
await (
Post.objects
.values("author_id")
.annotate(posts=Count("id"), total_views=Sum("views"))
.order_by("-total_views")
)
# → [
# {"author_id": 1, "posts": 15, "total_views": 5000},
# {"author_id": 2, "posts": 8, "total_views": 3200},
# ]

Multiple Group By Fields

# Posts per author per month
await (
Post.objects
.values("author_id", "status")
.annotate(count=Count("id"))
)
# → [
# {"author_id": 1, "status": "published", "count": 10},
# {"author_id": 1, "status": "draft", "count": 3},
# ]

Filtering Groups with exclude()

# Authors with more than 5 posts
await (
Post.objects
.values("author_id")
.annotate(count=Count("id"))
.filter(count__gt=5)
)

This generates a HAVING clause:

SELECT "author_id", COUNT("id") AS "count"
FROM "posts"
GROUP BY "author_id"
HAVING COUNT("id") > 5

Next Steps

Relationships — Work with related models