How I Fix a Weird Django Error When Adding Annotations

If you’ve seen that line before, you know the vibe: everything looks fine until it isn’t. I hit this while wiring up comments on a Django forum and thought I’d document the root cause and the clean, production-friendly fix I shipped.

The Setup (and the Headache)

I wanted each Post to render with its comments. I tried to “pack” the comments into an annotate() call big mistake.

The original view (exactly as I wrote it)

def index(request):
    # check if there is any incomming data (comments) and make sure the user is authenticated
    POST = request.POST
    if POST != {} and request.user.is_authenticated:
        # a comment has been recived, time to move forward with creating it

        # figure out if the post even exists
        get_object_or_404(Post, id = POST['post_id'])

        # grab the user object
        user_active = User.objects.get(id = POST['user'])

        # grab the post object
        post_active = Post.objects.get(id = POST['post_id'])

        # make and save the comment
        n_comment = Comment(user = user_active, comment = POST['comment'], post = post_active)
        n_comment.save()

    posts = Post.objects.order_by("-published_date").annotate(
        num_likes=Count("likes"),
        com=Comment.objects.order_by("-published_date")
    )
    return render(request, 'home.html', {'posts': posts})

The model

class Comment(models.Model):
    comment = models.CharField(max_length=500)
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='comments')
    published_date = models.DateTimeField('date_published')

The template hiccup

I also had a tiny typo that made debugging more confusing:

<li><span>{{ u ser.username }}</span>

…which of course should be:

<li><span>{{ user.username }}</span></li>

Why the Error Happen

annotate() must produce single values per row things like Count(...), Sum(...), Exists(...), or a Subquery that returns exactly one column (and usually one row via [:1]).

I passed a whole queryset into annotate():

.annotate(
    num_likes=Count("likes"),
    com=Comment.objects.order_by("-published_date")  # ❌ not a scalar expression
)

Django tried to treat that queryset as a subquery, but because it returns multiple columns/rows, the database balked with:

ProgrammingError: subquery must return only one column

Rule of thumb I now follow

  • If I need a single scalar (count, latest value, boolean flag), I use Count, Exists, or Subquery(...values('field')[:1]).
  • If I need related objects (e.g., comment objects), I use prefetch_related (optionally with Prefetch to control ordering and to_attr).

The Right Patterns

Show all comments per post

Use prefetch_related (not annotate) to fetch related records efficiently:

from django.db.models import Count, Prefetch

posts = (
    Post.objects.order_by("-published_date")
    .annotate(num_likes=Count("likes"))
    .prefetch_related(
        Prefetch(
            "comments",
            queryset=Comment.objects.order_by("-published_date"),
            to_attr="ordered_comments",  # access as post.ordered_comments
        )
    )
)

Template:

{% for post in posts %}
  <h2>{{ post.title }}</h2>
  <div>Likes: {{ post.num_likes }}</div>

  <ul>
    {% for c in post.ordered_comments %}
      <li>
        <strong>{{ c.user.username }}</strong> — {{ c.comment }}
        <small>{{ c.published_date }}</small>
      </li>
    {% empty %}
      <li>No comments yet.</li>
    {% endfor %}
  </ul>
{% endfor %}

Get one scalar

Use Subquery + OuterRef and slice to a single row:

from django.db.models import OuterRef, Subquery, Count

latest_comment_qs = (
    Comment.objects
    .filter(post=OuterRef("pk"))
    .order_by("-published_date")
)

posts = (
    Post.objects.order_by("-published_date")
    .annotate(
        num_likes=Count("likes"),
        latest_comment=Subquery(latest_comment_qs.values("comment")[:1]),
        latest_commented_at=Subquery(latest_comment_qs.values("published_date")[:1]),
    )
)

Now each Post has latest_comment and latest_commented_at as scalar fields.

What I Shipped A Practical, Production Ready View

Along the way I also fixed a few real-world concerns:

  • Use request.method == "POST" instead of checking POST != {}.
  • Don’t trust POST['user']; use request.user to prevent spoofing.
  • Validate the input, show friendly messages, and redirect after POST (PRG pattern).
  • Add pagination.
  • Keep prefetch_related for ordered comments + Count for likes.
  • Fix the template typo.

Final view (core/views.py)

from django.contrib import messages
from django.core.paginator import Paginator
from django.db.models import Count, Prefetch
from django.shortcuts import get_object_or_404, render, redirect

def index(request):
    # Handle creation of a new comment safely
    if request.method == "POST":
        if not request.user.is_authenticated:
            messages.error(request, "You must be logged in to comment.")
            return redirect("home")

        post_id = request.POST.get("post_id")
        text = request.POST.get("comment", "").strip()
        post = get_object_or_404(Post, id=post_id)

        if not text:
            messages.error(request, "Comment cannot be empty.")
            return redirect("home")

        Comment.objects.create(
            user=request.user,   # ← do not trust POST['user']
            post=post,
            comment=text,
        )
        messages.success(request, "Comment added!")
        return redirect("home")

    # Query posts with useful aggregates and prefetches
    posts_qs = (
        Post.objects.order_by("-published_date")
        .annotate(num_likes=Count("likes"))
        .prefetch_related(
            Prefetch(
                "comments",
                queryset=Comment.objects.select_related("user")
                                        .order_by("-published_date"),
                to_attr="ordered_comments",
            )
        )
    )

    # Pagination
    paginator = Paginator(posts_qs, 10)  # 10 posts per page
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)

    return render(request, "home.html", {"page_obj": page_obj})

Template fixes and pagination (home.html)

{# auth snippet #}
{% if user.is_authenticated %}
  <li><span>{{ user.username }}</span></li>
  <li><a href="{% url 'logout' %}">Log Out</a></li>
{% else %}
  <li><a href="{% url 'signup' %}">Sign Up</a></li>
  <li><a href="{% url 'login' %}">Log In</a></li>
{% endif %}
{# posts with comments and a simple comment form #}
{% for post in page_obj %}
  <article>
    <h2>{{ post.title }}</h2>
    <div>Likes: {{ post.num_likes }}</div>

    <section>
      <h4>Comments</h4>
      <ul>
        {% for c in post.ordered_comments %}
          <li>
            <strong>{{ c.user.username }}</strong> — {{ c.comment }}
            <small>{{ c.published_date }}</small>
          </li>
        {% empty %}
          <li>No comments yet.</li>
        {% endfor %}
      </ul>

      {% if user.is_authenticated %}
        <form method="post">
          {% csrf_token %}
          <input type="hidden" name="post_id" value="{{ post.id }}">
          <textarea name="comment" rows="2" placeholder="Write a comment..."></textarea>
          <button type="submit">Comment</button>
        </form>
      {% endif %}
    </section>
  </article>
{% endfor %}

<nav class="pagination">
  {% if page_obj.has_previous %}
    <a href="?page={{ page_obj.previous_page_number }}">Previous</a>
  {% endif %}
  <span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
  {% if page_obj.has_next %}
    <a href="?page={{ page_obj.next_page_number }}">Next</a>
  {% endif %}
</nav>

Extra Practice Handy Annotations I Use

Count comments per post

from django.db.models import Count

posts = Post.objects.annotate(
    num_likes=Count("likes"),
    num_comments=Count("comments"),
)

Flag whether the current user

from django.db.models import Exists, OuterRef

user_liked = Like.objects.filter(post=OuterRef("pk"), user=request.user)
posts = Post.objects.annotate(
    user_has_liked=Exists(user_liked)
)

Template:

{% if post.user_has_liked %}You liked this{% endif %}

Pull the latest comment text/date as scalars

from django.db.models import OuterRef, Subquery

latest_qs = Comment.objects.filter(post=OuterRef("pk")).order_by("-published_date")
posts = Post.objects.annotate(
    latest_comment=Subquery(latest_qs.values("comment")[:1]),
    latest_commented_at=Subquery(latest_qs.values("published_date")[:1]),
)

Final Thought

The error message looked intimidating at first, but the solution turned out to be straightforward once I kept the right mental model in mind: annotate() is for scalars per row, while prefetch_related() is for lists of related objects. After applying that distinction, my pages loaded more efficiently thanks to proper prefetching, and the view logic became safer by preventing user spoofing and following clean POST handling. If you face the same issue, just match your goal to the right tool scalars go with annotate, objects go with prefetch.

Related blog posts