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.
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:
# 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.
# 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.
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.