Django / SQL

How to Format SQL from
Django ORM Output

Django generates correct SQL. It also generates completely unreadable SQL. Here's how to turn it into something you can actually debug.

6 min read·Updated Mar 2026

How to format SQL from Django ORM output is something every Django developer needs at some point — usually when chasing a slow query, debugging an unexpected JOIN, or trying to understand what a complex queryset with annotate(), prefetch_related() and filter() chains is actually doing to the database.

Step 1 — Get the Raw SQL Out of Django

There are three ways to extract SQL from Django, depending on your situation:

1 str(queryset.query) — for a specific queryset
# In your view or Django shell
qs = User.objects.filter(
    is_active=True
).select_related('profile').annotate(
    order_count=Count('orders')
)
print(str(qs.query))

Best for inspecting a specific queryset. Note: parameters are shown unescaped — don't use this output directly in a database client.

2 LOGGING config — all queries during a request
# settings.py
LOGGING = {
    'version': 1,
    'handlers': {'console': {'class': 'logging.StreamHandler'}},
    'loggers': {
        'django.db.backends': {
            'handlers': ['console'],
            'level': 'DEBUG',
        },
    },
}

Shows every query fired during a request with execution time. Requires DEBUG=True.

3 connection.queries — in the Django shell
from django.db import connection, reset_queries
reset_queries()

# Run your queryset here
users = list(User.objects.filter(is_active=True))

for q in connection.queries:
    print(q['sql'])
    print(f"Time: {q['time']}s")

Useful in the shell for counting queries and spotting N+1 issues.

Step 2 — What the Raw Output Looks Like

This is what Django typically produces for a moderately complex queryset:

SELECT "users"."id", "users"."email", "users"."date_joined", "profiles"."bio", "profiles"."avatar_url", COUNT("orders"."id") AS "order_count" FROM "users" INNER JOIN "profiles" ON ("users"."id" = "profiles"."user_id") LEFT OUTER JOIN "orders" ON ("users"."id" = "orders"."user_id") WHERE "users"."is_active" = true GROUP BY "users"."id", "profiles"."bio", "profiles"."avatar_url" ORDER BY "order_count" DESC

That's 340 characters, all on one line, with no way to visually parse which JOIN belongs to which table or where the WHERE clause starts. Now formatted:

SELECT
  "users"."id",
  "users"."email",
  "users"."date_joined",
  "profiles"."bio",
  "profiles"."avatar_url",
  COUNT("orders"."id") AS "order_count"
FROM "users"
INNER JOIN "profiles"
  ON ("users"."id" = "profiles"."user_id")
LEFT OUTER JOIN "orders"
  ON ("users"."id" = "orders"."user_id")
WHERE "users"."is_active" = true
GROUP BY
  "users"."id",
  "profiles"."bio",
  "profiles"."avatar_url"
ORDER BY "order_count" DESC

Now you can immediately see: two JOINs, the GROUP BY includes profile columns (potential N+1 hint), and the ORDER BY is on an aggregated column which may need an index.

Step 3 — Format It Safely

Paste the raw SQL into ResourceCentral's SQL Formatter. It's client-side — your query, including all your real table and column names, never leaves your browser. For production queries containing sensitive schema details this matters.

Note on str(queryset.query) output

The SQL from str(qs.query) uses Python string interpolation, not proper SQL parameterisation. The output may show raw values like WHERE email = 'user@example.com'. Sanitize any real values before sharing the formatted query publicly — use the Log Sanitizer to strip emails and IDs.

What to Look for in Formatted Django ORM SQL

What You See What It Means Fix
Multiple similar queries in a loop N+1 query problem Use select_related() or prefetch_related()
Large GROUP BY with many columns Over-fetching — may be slower than needed Use values() to limit columns
Subquery in FROM or WHERE Django Subquery() expression Consider annotate() with JOIN instead
CAST() around a column Type coercion — may prevent index use Check column types match query expectations
ORDER BY on non-indexed column Full table sort — slow on large tables Add db_index=True to model field

Format Your Django ORM SQL — Free

Client-side only. Your schema never leaves your browser.

Open SQL Formatter →

FAQ

How do I format SQL from Django ORM output? +

Enable SQL logging with django.db.backends logger or use str(queryset.query), copy the raw output, then paste it into ResourceCentral's SQL Formatter. The formatter runs in your browser and adds proper indentation and line breaks.

How do I see the SQL Django ORM generates? +

Three ways: str(queryset.query) for a specific queryset, the django.db.backends logger in settings for all queries during a request, or connection.queries in the shell after running queries.

Why is Django ORM SQL so hard to read? +

Django generates SQL programmatically and outputs it as a single line with no formatting. Complex querysets with multiple JOINs, annotations and subqueries produce queries hundreds of characters long. A formatter applies indentation, line breaks and keyword capitalisation to make the structure visible.

Does this work for SQLAlchemy, Rails and Hibernate too? +

Yes. All ORM frameworks produce the same unformatted single-line SQL. Enable query logging in your framework (echo=True in SQLAlchemy, show_sql=true in Hibernate, ActiveRecord::Base.logger in Rails), copy the output, and paste into the formatter.

Is it safe to share Django ORM SQL publicly? +

Not without scrubbing it first. ORM-generated SQL contains your real table and column names which expose your data model. Before posting to StackOverflow or ChatGPT, replace sensitive column names with generic equivalents. See What SQL Queries Reveal About Your Database Schema for a full breakdown.

Related