Using the extra() QuerySet modifier in Django for WeGoEat
Since I actually used this method to reduce the number of Update:”explicit” SQL calls made in WeGoEat, I figured I’d write a little blog explaining the context in which it was used, and maybe, just maybe, it’ll help shed some light on how others can take advantage of this neat little function.
Background
As a Django “proof-of-concept”, I’m working on a local restaurant review site for my home state of Hawai`i. (I actually just released it yesterday). For each restaurant, I want to be able to calculate the average of all reviews and display this listing in a paginated view. (Yes, I do realize there’s no average rating, but that has to do with there being no users. ;P).
The Problem
Having a serious “wtf was I thinking moment”, I initially wrote a Restaurant model function that returned the average (review) rating for each restaurant instance. Little did I realize that when I actually displayed the restaurant’s average reviews, I would be making an additional SQL avg() call for every restaurant. Though I’m paging “n” records at a time, this function added an additional “n” SQL calls for every view that contained a restaurant listing, just to name a few.
In pseudo-code, my initial naive function resembled the following: (I’m sure we’re all guilty of writing something of the sort… ok, fine, I know I was. ;P)
1 2 3 4 5 6 | def get_average_review(self): query = 'QUERY TO GET AVERAGE (SELECT AVG(rating)...); (I have the query below)' # Get cursor from connection cursor = connection.cursor() cursor.execute(query) return cursor.fetchall() |
Duh.
Here’s a picture of the number of queries it took:

The “extra()” solution
After profiling my application and realizing what a bone-headed mistake I made, I began researching the extra() Queryset modifier. Yes, I realize that these extra lookups aren’t the most portable and often violate the DRY principle, but it’ll probably suffice for most of all my personal projects.
Since I’m already retrieving a list of Restaurants and filtering them via letter, island, and what not, I figured I could add an average rating subquery. The entire call looks as such:
1 2 3 4 5 6 7 | restaurants = Restaurant.objects.filter(name__istartswith = letter).extra( select={'<strong>avg_rating</strong>': 'SELECT AVG(overall_rating) FROM restaurants_restaurant as res, reviews_review, django_content_type \ WHERE restaurants_restaurant.id = res.id \ AND res.id = reviews_review.object_id \ AND reviews_review.content_type_id = django_content_type.id \ AND django_content_type.model = \'restaurant\''}, ) |
As you can see, I’m exploiting the fact that restaurants_restaurant will be available from the Restaurant.objects.filter() call. (I know, I know… bad for portability).
But voila!
Now, in my templates, when I iterate over the restaurants, I can get issue the following:
1 2 3 4 5 6 7 8 9 10 11 | {% for restaurant in restaurant_list %} <tr> <td><a href="{{restaurant.get_absolute_url}}">{{ restaurant.name }}</a></td> <td>{% if restaurant.avg_rating %} {% load show_stars %} <span class="average-rating"> {% show_stars <strong>restaurant.avg_rating</strong> of 5 round to quarter %} </span> {% endif %}</td> </tr> {% endfor %} |
Notice how I used my show_stars template tag that I blogged about a few weeks ago to display the average restaurant rating. (Cheap shameless plug, but damn effective! :P) I’d link to a page in action, but since I just opened up my site to a few select users, I’ll update this post when I actually have any reviews.
Oh, and before I forget, thanks to my co-worker Stephen for assisting me with my SQL issues!
Here’s a picture of the final result:

Note:
As an added bonus, I also realized a few other ’spots’ where the .extra() Queryset modifier would come in handy. Since I’m also using the wonderful django-voting application from Jonathan Buchanan, I came across this post about accessing a dictionary via a template in the Django-users Google Group.
Basically, I had come across the same issue as the poster. Since I allow users to vote on reviews (similar to Amazon, Yelp, etc.), I wanted to retrieve the score of each Review instance to display on a paginated listing of all Reviews. Using the same extra() modifier, I was able to inject the total number of votes and the score when I retrieved all Reviews as such:
Btw, I just injected most of the code from Jonathan’s template tag.
1 2 3 4 5 6 7 8 9 10 11 | .extra(select={'total_votes': 'SELECT COUNT(vote) FROM votes as v, reviews_review as rev, django_content_type \ WHERE reviews_review.id = rev.id \ AND v.object_id = reviews_review.id \ AND v.content_type_id = django_content_type.id \ AND django_content_type.model = \'review\'', 'score': 'SELECT SUM(vote) FROM votes as v, reviews_review as rev, django_content_type \ WHERE reviews_review.id = rev.id \ AND v.object_id = reviews_review.id \ AND v.content_type_id = django_content_type.id \ AND django_content_type.model = \'review\''},) |
Pretty neat right?
Now, when iterating through the reviews, I can use the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 | {% for review in object_list %} <tr> <td><a href="{{review.content_object.get_absolute_url}}">{{ review.content_object.name }}</a></td> <td><a href="{% url profile-detail username=review.user.username %}">{{ review.user.username }}</a></td> <td><nobr>{% load show_stars %} <span class="rating">{% show_stars review.overall_rating of 5 round to half %}</span> </nobr> </td> <td>"<span style="font-weight:bold; color:#092e20;">{{ review.get_recommendation_display }}</span>"</td> <td><span style="font-size:.875em;">{{ review.submit_date|timesince }} ago</span></td> <strong><td>Total of {{ review.score|default:0 }} from {{ review.total_votes }} {{ review.total_votes|pluralize:"person,people" }}.</td></strong> </tr> {% endfor %} |
Hope y’all learned something like I did!
Oh, and before I forget my standard disclaimer, “since this is on my blog, feel free to take/use/steal/distribute/copy/modify any code you see fit, but if you find any bugs, have any comments, or think the code can be cleaner, I’d love to hear from you.”
