How show raw SQL queries for your django queries or querysets

How show raw SQL queries for your django queries or querysets

Django's Object-Relational Mapper (ORM) is designed to abstract database dialect differences away from developers. However, building performant, production-grade applications requires knowing exactly what SQL is generated under the hood.

Understanding where and how to inspect SQL depends entirely on the lifecycle stage of a QuerySet—from lazy definition in Python memory to final execution over the network wire.


The QuerySet Lifecycle & SQL Inspection Map

┌───────────────────────────────────────────────────────────────────────────────┐
│                           QUERYSET LIFECYCLE                                  │
└───────────────────────────────────────────────────────────────────────────────┘
  1. CONSTRUCTION / CHAINING          ▼   [Lazy AST in Python memory]
     Book.objects.filter(...)       ──────►  str(qs.query)
  2. COMPILATION / PARAMETERIZATION   ▼   [SQL Template + Param Tuple]
     qs.query.get_compiler()        ──────►  compiler.as_sql()
  3. QUERY PLAN ESTIMATION            ▼   [Database Query Optimizer]
     qs.explain()                   ──────►  EXPLAIN / EXPLAIN ANALYZE
  4. EXECUTION / DISPATCH             ▼   [Driver Cursor / Network Transit]
     Iterating, .save(), .update()  ──────►  connection.execute_wrapper()
  5. POST-EXECUTION AUDIT             ▼   [In-Memory Query Buffer]
     assertNumQueries, list(qs)     ──────►  connection.queries
  6. GLOBAL TELEMETRY / LOGGING       ▼   [Application-Wide I/O Stream]
     settings.py / shell_plus       ──────►  django.db.backends / --print-sql

1. Domain Model Setup

We will use an Author and Book relational schema containing one-to-many relationships and aggregations:

# bookstore/models.py
from django.db import models


class Author(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField(unique=True)
    rating = models.FloatField(default=5.0)

    def __str__(self):
        return self.name


class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(
        Author, on_delete=models.CASCADE, related_name="books"
    )
    price = models.DecimalField(max_digits=6, decimal_places=2)
    published_date = models.DateField()
    is_published = models.BooleanField(default=True)

    def __str__(self):
        return self.title

Stage 1: Construction & Chaining (Pre-Execution)

When you write qs = Book.objects.filter(...), Django builds an abstract query node tree without dispatching network calls.

Technique: str(queryset.query)

Use this to preview the generated SELECT statement directly in Python.

from datetime import date
from django.db.models import Q
from bookstore.models import Book

qs = (
    Book.objects.filter(
        Q(price__lt=20.00) | Q(author__rating__gte=4.5),
        published_date__gte=date(2023, 1, 1),
        is_published=True,
    )
    .select_related("author")
    .order_by("-price")
)

print(str(qs.query))

Output:

SELECT "bookstore_book"."id",
       "bookstore_book"."title",
       "bookstore_book"."author_id",
       "bookstore_book"."price",
       "bookstore_book"."published_date",
       "bookstore_book"."is_published",
       "bookstore_author"."id",
       "bookstore_author"."name",
       "bookstore_author"."email",
       "bookstore_author"."rating"
FROM "bookstore_book"
INNER JOIN "bookstore_author" ON ("bookstore_book"."author_id" = "bookstore_author"."id")
WHERE (
    ("bookstore_book"."price" < 20.0 OR "bookstore_author"."rating" >= 4.5)
    AND "bookstore_book"."published_date" >= 2023-01-01
    AND "bookstore_book"."is_published" = true
)
ORDER BY "bookstore_book"."price" DESC

Limitations:

  • str(qs.query) only works on QuerySets. It does not work on mutating calls like .create(), .update(), .bulk_create(), or raw model instances (instance.save()).
  • The output inlines parameters for readability; it does not show driver placeholders.

Stage 2: Compilation & Parameterization

Before querying the database, Django compiles the QuerySet into a parameterized SQL string and a tuple of sanitized arguments to prevent SQL injection.

Technique: compiler.as_sql()

from bookstore.models import Book

qs = Book.objects.filter(title__startswith="Django", price__gt=35.50)

# Resolve SQL compiler for the target database alias
compiler = qs.query.get_compiler(using="default")
sql_template, params = compiler.as_sql()

print("Parameterized SQL:\n", sql_template)
print("\nParameter Tuple:\n", params)

Output:

Parameterized SQL:
 SELECT "bookstore_book"."id", "bookstore_book"."title", "bookstore_book"."author_id", "bookstore_book"."price", "bookstore_book"."published_date", "bookstore_book"."is_published" FROM "bookstore_book" WHERE ("bookstore_book"."price" > %s AND "bookstore_book"."title"::text LIKE %s)

Parameter Tuple:
 ('35.50', 'Django%')

Stage 3: Query Plan Estimation (EXPLAIN)

To inspect how the database engine indexes, scans, and optimizes your QuerySet, use explain().

Technique: qs.explain(analyze=True)

from bookstore.models import Book

# Explain a complex subquery lookup
qs = Book.objects.filter(
    author__in=Book.objects.filter(price__gt=50).values("author_id")
)

# Analyze runs the query inside EXPLAIN for true execution times (PostgreSQL/MySQL)
print(qs.explain(analyze=True, costs=True))

Output (PostgreSQL):

Hash Join  (cost=21.45..42.80 rows=12 width=148) (actual time=0.082..0.086 rows=5 loops=1)
  Hash Cond: (bookstore_book.author_id = subquery.author_id)
  ->  Seq Scan on bookstore_book  (cost=0.00..18.50 rows=850 width=148) (actual time=0.011..0.024 rows=120 loops=1)
  ->  Hash  (cost=21.30..21.30 rows=12 width=4) (actual time=0.051..0.052 rows=3 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 9kB
        ->  HashAggregate  (cost=21.18..21.30 rows=12 width=4) (actual time=0.038..0.040 rows=3 loops=1)
              Group Key: bookstore_book_sub.author_id
              ->  Seq Scan on bookstore_book bookstore_book_sub  (cost=0.00..20.62 rows=225 width=4) (actual time=0.008..0.021 rows=15 loops=1)
                    Filter: (price > '50'::numeric)
Planning Time: 0.215 ms
Execution Time: 0.141 ms

Stage 4: Execution / Dispatch (Capturing Real-Time Write & Read SQL)

Operations like .create(), .bulk_update(), .delete(), and raw cursor calls bypass qs.query. To capture what reaches the database driver in real time, use connection.execute_wrapper().

Technique: Custom Capture Context Manager

import time
from contextlib import contextmanager
from django.db import connection
from bookstore.models import Author, Book


@contextmanager
def capture_raw_sql():
    records = []

    def wrapper(execute, sql, params, many, context):
        start_time = time.perf_counter()
        try:
            return execute(sql, params, many, context)
        finally:
            duration = (time.perf_counter() - start_time) * 1000  # in ms
            records.append(
                {
                    "sql": sql,
                    "params": params,
                    "duration_ms": round(duration, 3),
                    "many": many,
                }
            )

    with connection.execute_wrapper(wrapper):
        yield records


# Execute both read and write operations inside the context
with capture_raw_sql() as query_log:
    # 1. INSERT
    author = Author.objects.create(
        name="Frank Herbert", email="frank@arrakis.org", rating=4.9
    )

    # 2. BULK INSERT
    Book.objects.bulk_create(
        [
            Book(
                title="Dune",
                author=author,
                price=25.00,
                published_date="1965-08-01",
            ),
            Book(
                title="Dune Messiah",
                author=author,
                price=22.50,
                published_date="1969-07-01",
            ),
        ]
    )

    # 3. UPDATE
    Book.objects.filter(author=author).update(is_published=True)

# Inspect the captured entries
for i, item in enumerate(query_log, 1):
    print(f"[{i}] Duration: {item['duration_ms']}ms | Many: {item['many']}")
    print(f"SQL:    {item['sql']}")
    print(f"PARAMS: {item['params']}\n" + "-" * 70)

Output:

[1] Duration: 1.124ms | Many: False
SQL:    INSERT INTO "bookstore_author" ("name", "email", "rating") VALUES (%s, %s, %s) RETURNING "bookstore_author"."id"
PARAMS: ('Frank Herbert', 'frank@arrakis.org', 4.9)
----------------------------------------------------------------------
[2] Duration: 0.845ms | Many: False
SQL:    INSERT INTO "bookstore_book" ("title", "author_id", "price", "published_date", "is_published") VALUES (%s, %s, %s, %s, %s), (%s, %s, %s, %s, %s) RETURNING "bookstore_book"."id"
PARAMS: ('Dune', 1, '25.00', '1965-08-01', True, 'Dune Messiah', 1, '22.50', '1969-07-01', True)
----------------------------------------------------------------------
[3] Duration: 0.612ms | Many: False
SQL:    UPDATE "bookstore_book" SET "is_published" = %s WHERE "bookstore_book"."author_id" = %s
PARAMS: (True, 1)
----------------------------------------------------------------------

Stage 5: Post-Execution Inspection & Unit Testing

When running automated test suites or debugging in DEBUG = True mode, you can inspect execution logs recorded in memory.

Technique 5.1: connection.queries

from django.db import connection, reset_queries
from bookstore.models import Author

# Clear the query log
reset_queries()

# Evaluate queries via list()
authors = list(
    Author.objects.prefetch_related("books").filter(name__icontains="Herbert")
)

# Inspect connection.queries
print(f"Total SQL queries executed: {len(connection.queries)}")
for q in connection.queries:
    print(f"[{q['time']}s] {q['sql']}")

Output:

Total SQL queries executed: 2
[0.0008s] SELECT "bookstore_author"."id", "bookstore_author"."name", "bookstore_author"."email", "bookstore_author"."rating" FROM "bookstore_author" WHERE UPPER("bookstore_author"."name"::text) LIKE UPPER('%Herbert%')
[0.0012s] SELECT "bookstore_book"."id", "bookstore_book"."title", "bookstore_book"."author_id", "bookstore_book"."price", "bookstore_book"."published_date", "bookstore_book"."is_published" FROM "bookstore_book" WHERE "bookstore_book"."author_id" IN (1)

Technique 5.2: Unit Test Assertions (assertNumQueries)

Ensure code does not introduce $N+1$ query regressions during test runs:

from django.test import TestCase
from bookstore.models import Author, Book


class BookQueryPerformanceTest(TestCase):

    def setUp(self):
        author = Author.objects.create(name="Author A", email="a@test.com")
        for i in range(10):
            Book.objects.create(
                title=f"Book {i}",
                author=author,
                price=10.0,
                published_date="2024-01-01",
            )

    def test_book_listing_query_budget(self):
        # Assert that fetching 10 books and their authors executes exactly 1 query
        with self.assertNumQueries(1):
            books = list(Book.objects.select_related("author").all())
            for book in books:
                _ = book.author.name  # Does not trigger additional queries

Stage 6: Application-Wide Telemetry & Development Logs

Technique 6.1: Real-time Terminal Logging via settings.py

Pipe every SQL query executed across your entire project directly into standard output:

# settings.py
LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "handlers": {
        "console": {
            "level": "DEBUG",
            "class": "logging.StreamHandler",
        },
    },
    "loggers": {
        "django.db.backends": {
            "handlers": ["console"],
            "level": "DEBUG",
            "propagate": False,
        },
    },
}

Output Stream in Terminal:

(0.001) SELECT "bookstore_book"."id", "bookstore_book"."title" FROM "bookstore_book" WHERE "bookstore_book"."is_published" LIMIT 5; args=()
(0.000) SAVEPOINT "s140735824969536_x1"; args=None
(0.002) UPDATE "bookstore_author" SET "rating" = 4.8 WHERE "bookstore_author"."id" = 1; args=(4.8, 1)
(0.000) RELEASE SAVEPOINT "s140735824969536_x1"; args=None

Technique 6.2: Terminal Inspection with django-extensions

If you use django-extensions, run shell_plus with SQL printing enabled:

python manage.py shell_plus --print-sql
>>> from bookstore.models import Book
>>> Book.objects.filter(price__lte=15.00).first()

Output:

SELECT "bookstore_book"."id",
       "bookstore_book"."title",
       "bookstore_book"."author_id",
       "bookstore_book"."price",
       "bookstore_book"."published_date",
       "bookstore_book"."is_published"
  FROM "bookstore_book"
 WHERE "bookstore_book"."price" <= '15.00'
 ORDER BY "bookstore_book"."id" ASC
 LIMIT 1

Execution time: 0.000412s [Database: default]
<Book: Basic Programming>

Complete Lifecycle Comparison Matrix

Stage Inspection Target Method Read/Write Supported? Safe for Production?
1. Construction Query string preview str(qs.query) Reads Only Yes
2. Compilation Placeholder & Parameter tuple qs.query.get_compiler().as_sql() Reads Only Yes
3. Plan Estimation Optimizer execution cost qs.explain(analyze=True) Reads Only Exercise caution with analyze=True on heavy DBs
4. Execution Live cursor wrapper connection.execute_wrapper() Reads & Writes (INSERT/UPDATE/DELETE) Yes
5. Post-Execution Historical session buffer connection.queries Reads & Writes No (DEBUG = True required)
6. System-wide Standard I/O output stream django.db.backends logger Reads & Writes No (creates high logging volume)

SUBSCRIBE FOR NEW ARTICLES

@
comments powered by Disqus