Django
QuerySet
SQL
Database
Python

Getting the SQL from a Django QuerySet

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Django's ORM generates SQL behind the scenes when you use QuerySets. Viewing the actual SQL is essential for debugging slow queries, understanding ORM behavior, verifying complex filters, and optimizing database performance. Django provides several ways to inspect the generated SQL — from the .query attribute to the Django Debug Toolbar.

Method 1: str(queryset.query)

The simplest way — access the .query attribute on any QuerySet:

python
1from myapp.models import Article
2
3qs = Article.objects.filter(status='published', views__gt=100).order_by('-created_at')
4
5print(qs.query)
6# SELECT "myapp_article"."id", "myapp_article"."title", ...
7# FROM "myapp_article"
8# WHERE ("myapp_article"."status" = published AND "myapp_article"."views" > 100)
9# ORDER BY "myapp_article"."created_at" DESC
10
11# As a string
12sql = str(qs.query)

Note: .query does not include parameter quoting — values appear inline without proper escaping. This SQL is for inspection only, not for direct execution.

Method 2: queryset.query.sql_with_params()

Get the SQL with parameterized placeholders:

python
1sql, params = qs.query.sql_with_params()
2print(sql)
3# SELECT ... FROM "myapp_article" WHERE ("myapp_article"."status" = %s AND ...)
4print(params)
5# ('published', 100)

This is closer to what Django actually sends to the database.

Method 3: django.db.connection.queries

Django logs all executed queries when DEBUG=True:

python
1from django.db import connection
2
3# Execute the queryset
4articles = list(Article.objects.filter(status='published')[:5])
5
6# View the last executed query
7print(connection.queries[-1])
8# {'sql': 'SELECT ... FROM "myapp_article" WHERE ... LIMIT 5',
9#  'time': '0.002'}
10
11# View all queries in the current request
12for query in connection.queries:
13    print(f"{query['time']}s: {query['sql']}")
14
15# Reset the query log
16from django.db import reset_queries
17reset_queries()

This only works when settings.DEBUG = True. In production, queries are not logged.

Method 4: EXPLAIN / EXPLAIN ANALYZE

Get the database's query execution plan:

python
1# Django 2.1+
2qs = Article.objects.filter(status='published').order_by('-created_at')
3
4print(qs.explain())
5# Seq Scan on myapp_article  (cost=0.00..25.00 rows=500 width=200)
6#   Filter: (status = 'published')
7
8# With ANALYZE (actually runs the query)
9print(qs.explain(analyze=True))
10# Seq Scan on myapp_article  (cost=0.00..25.00 rows=500 width=200) (actual time=0.01..0.05 rows=500 loops=1)
11
12# PostgreSQL-specific options
13print(qs.explain(analyze=True, verbose=True, format='JSON'))

EXPLAIN shows how the database plans to execute the query without running it. EXPLAIN ANALYZE runs the query and shows actual execution statistics.

Method 5: Django Debug Toolbar

The Django Debug Toolbar provides a visual SQL panel showing all queries per request:

bash
pip install django-debug-toolbar
python
1# settings.py
2INSTALLED_APPS = [
3    ...
4    'debug_toolbar',
5]
6
7MIDDLEWARE = [
8    'debug_toolbar.middleware.DebugToolbarMiddleware',
9    ...
10]
11
12INTERNAL_IPS = ['127.0.0.1']

The SQL panel shows each query, its execution time, duplicate queries, and the traceback showing where in your code each query was triggered.

Method 6: Logging SQL Queries

Configure Django's logging to print all SQL to the console:

python
1# settings.py
2LOGGING = {
3    'version': 1,
4    'handlers': {
5        'console': {
6            'class': 'logging.StreamHandler',
7        },
8    },
9    'loggers': {
10        'django.db.backends': {
11            'level': 'DEBUG',
12            'handlers': ['console'],
13        },
14    },
15}

Every SQL query will now appear in the console output during development.

Complex QuerySet Examples

Aggregation

python
1from django.db.models import Count, Avg
2
3qs = Article.objects.values('category').annotate(
4    count=Count('id'),
5    avg_views=Avg('views')
6)
7print(qs.query)
8# SELECT "myapp_article"."category",
9#        COUNT("myapp_article"."id") AS "count",
10#        AVG("myapp_article"."views") AS "avg_views"
11# FROM "myapp_article"
12# GROUP BY "myapp_article"."category"

Subqueries

python
1from django.db.models import Subquery, OuterRef
2
3newest = Article.objects.filter(
4    category=OuterRef('category')
5).order_by('-created_at')
6
7qs = Article.objects.filter(
8    id=Subquery(newest.values('id')[:1])
9)
10print(qs.query)
python
1qs = Article.objects.select_related('author', 'category').filter(status='published')
2print(qs.query)
3# SELECT "myapp_article"."id", ..., "auth_user"."username", ..., "myapp_category"."name"
4# FROM "myapp_article"
5# INNER JOIN "auth_user" ON ("myapp_article"."author_id" = "auth_user"."id")
6# INNER JOIN "myapp_category" ON ("myapp_article"."category_id" = "myapp_category"."id")
7# WHERE "myapp_article"."status" = published

Counting Queries in Tests

python
1from django.test.utils import override_settings
2from django.test import TestCase
3
4class ArticleTest(TestCase):
5    def test_query_count(self):
6        # Assert that the view makes at most 3 queries
7        with self.assertNumQueries(3):
8            response = self.client.get('/articles/')
9            self.assertEqual(response.status_code, 200)

Common Pitfalls

  • .query shows unquoted parameters: The SQL from str(qs.query) does not properly escape string values. Do not copy-paste it into a database client — it may have syntax errors. Use sql_with_params() for parameterized SQL.
  • N+1 query problem: Accessing related objects in a loop generates one query per object. Use select_related() (foreign keys) and prefetch_related() (reverse/many-to-many) to join them in a single query.
  • connection.queries only works with DEBUG=True: In production (DEBUG=False), the query log is disabled for performance. Use the django.db.backends logger or a monitoring tool like Sentry for production SQL inspection.
  • .query on evaluated QuerySets: .query is available even after the QuerySet is evaluated. It shows the SQL that was (or would be) executed, which is useful for debugging after the fact.
  • Raw SQL vs ORM: If .query shows unexpected SQL, check your filters and annotations. The ORM sometimes generates suboptimal queries — use .raw() or connection.cursor() for hand-tuned SQL when performance is critical.

Summary

  • Use str(qs.query) for quick SQL inspection during development
  • Use qs.query.sql_with_params() for parameterized SQL with proper placeholders
  • Use connection.queries to see all executed queries (requires DEBUG=True)
  • Use qs.explain(analyze=True) to get the database execution plan
  • Use Django Debug Toolbar for visual query analysis per request
  • Configure django.db.backends logger for automatic SQL logging to console

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.