Duplicate Query Analyzer¶
What It Detects¶
The duplicate query analyzer identifies identical SQL statements -- same query text and same bound parameters -- that are executed more than once within a single request. Unlike N+1 patterns, duplicates typically arise when the same queryset is evaluated independently in multiple places (a view helper, a template tag, a context processor) rather than from iterating over a relation.
Problem Code¶
# views.py
def dashboard(request):
featured_count = Book.objects.filter(featured=True).count() # query 1
context = {
"featured_count": featured_count,
"books": Book.objects.all(),
}
return render(request, "dashboard.html", context)
{# dashboard.html #}
<p>{{ featured_count }} featured books</p>
{# A template tag that runs the same query again #}
{% load book_tags %}
<p>{% featured_book_count %}</p> {# query 2 -- identical to query 1 #}
# templatetags/book_tags.py
@register.simple_tag
def featured_book_count():
return Book.objects.filter(featured=True).count()
The featured=True count query runs twice with exactly the same SQL and
parameters.
Fix Code¶
Remove the redundant evaluation by passing the result through the template context or caching it:
# views.py
def dashboard(request):
featured_count = Book.objects.filter(featured=True).count()
context = {
"featured_count": featured_count,
"books": Book.objects.all(),
}
return render(request, "dashboard.html", context)
{# dashboard.html -- use the context variable directly #}
<p>{{ featured_count }} featured books</p>
<p>{{ featured_count }}</p> {# no extra query #}
Alternatively, for values needed across many templates, use Django's caching framework:
from django.core.cache import cache
def get_featured_count():
count = cache.get("featured_count")
if count is None:
count = Book.objects.filter(featured=True).count()
cache.set("featured_count", count, timeout=60)
return count
Prescription Output¶
[MEDIUM] Duplicate Query Detected
Location: views.py:4, templatetags/book_tags.py:5
Issue: Query `SELECT COUNT(*) FROM "app_book" WHERE "app_book"."featured" = ?`
executed 2 times with identical parameters.
Fix: Compute the value once and pass it through the template context,
or cache the result.
# views.py -- the value is already computed on line 4.
# Remove the duplicate call in templatetags/book_tags.py:5
# and use {{ featured_count }} in the template instead.
Configuration¶
| Setting | Default | Description |
|---|---|---|
DUPLICATE_THRESHOLD |
2 |
Minimum number of identical executions before a duplicate is reported. Raise this if your application legitimately issues a small number of repeated queries per request. |
Common Scenarios¶
Multiple View Helpers Querying the Same Data¶
When utility functions each build their own queryset for the same data, the same SQL can fire multiple times:
def get_active_users():
return User.objects.filter(is_active=True)
def view(request):
users = get_active_users() # query 1
admin_users = get_active_users().filter(is_staff=True) # query 2 (superset, not duplicate)
active_count = get_active_users().count() # query 3 (duplicate of query 1's table scan)
Fix: Assign the base queryset to a variable and reuse it.
Context Processors and Views¶
A context processor that runs a query already performed by the view produces a duplicate:
# context_processors.py
def notifications(request):
return {"unread": Notification.objects.filter(user=request.user, read=False).count()}
# views.py
def inbox(request):
unread = Notification.objects.filter(user=request.user, read=False).count()
...
Fix: Let the context processor be the single source of truth, or guard it
with a per-request cache using request attributes.
Pagination Computing Total Count Twice¶
Some pagination setups call .count() both in the paginator and in the
template:
paginator = Paginator(Book.objects.all(), 25) # .count() internally
page = paginator.get_page(request.GET.get("page"))
# template
<p>Total: {{ books.count }}</p> {# another .count() #}
Fix: Use paginator.count (already cached after first access) instead of
calling .count() again on the queryset.
Near-Duplicates
The duplicate analyzer also detects near-duplicates -- queries with the same fingerprint (normalized SQL) but different bound parameters. These are reported at a lower severity because they may indicate an N+1 pattern rather than a true duplicate.