Django’s Object-Relational Mapper (ORM) is celebrated for its expressiveness and declarative models. With simple Python syntax like Order.objects.filter(status='paid').select_related('customer'), developers can compose complex database queries without writing raw SQL.

However, ORM abstractions can easily hide performance pitfalls. A missing select_related() or prefetch_related() call can trigger the notorious N+1 query problem, turning a single web page render into hundreds of round-trip database queries. Furthermore, complex annotations and Q-object filters can produce convoluted SQL subqueries that miss database indexes entirely.

In this guide, we explore how to capture raw SQL generated by Django ORM, inspect execution plans, and debug database performance visually using native macOS tooling.


1. Extracting Raw SQL from Django QuerySets

Before debugging a query in a database GUI, you need to see the exact SQL statement Django compiles.

Method A: Inspecting the .query Attribute

In the Django shell (python manage.py shell):

from myapp.models import Order

qs = Order.objects.filter(status='completed', total_amount__gte=100)
print(qs.query)

Output:

SELECT "orders"."id", "orders"."customer_id", "orders"."total_amount", "orders"."status" 
FROM "orders" 
WHERE ("orders"."status" = 'completed' AND "orders"."total_amount" >= 100)

[!NOTE] qs.query only works on QuerySet objects (SELECT queries). It does not work on raw .create(), .update(), or .delete() operations.

Method B: Logging All Generated SQL via Django Settings

To inspect every query your Django backend executes during development, configure the database logger in settings.py:

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

2. Diagnosing Django N+1 Query Bottlenecks

The most common Django performance bug occurs when accessing related models inside a loop:

# BUG: Generates 1 query for orders, plus 50 separate queries for customers!
orders = Order.objects.all()[:50]
for order in orders:
    print(order.customer.email)  # Triggers a query on every iteration!

The Fix:

# OPTIMIZED: Performs an SQL INNER JOIN in a single query
orders = Order.objects.select_related('customer')[:50]

3. Profiling Execution Plans with SQLOZ OzExplain (macOS Native)

Once you have extracted the generated SQL query, pasting it into SQLOZ allows you to analyze its execution mechanics instantly.

SQLOZ SQL Editor and Execution Plan Diagnostics

What SQLOZ Delivers for Django Developers:

  1. Interactive Visual EXPLAIN Tree (OzExplain): In PostgreSQL and MySQL, running EXPLAIN ANALYZE outputs dense textual logs. SQLOZ parses this data into a visual cost tree, highlighting Sequential Table Scans in red and showing exactly where Django’s generated query missed an index.
  2. Missing Index Detection: If Django generated a complex filter on multiple annotated fields, SQLOZ suggests the precise composite index definition needed to achieve sub-millisecond execution.
  3. Inspect Django Local SQLite Files: Django defaults to db.sqlite3 for local prototyping. Drag your db.sqlite3 file directly into SQLOZ to inspect tables, alter data, or debug migration states without starting the Django server.

4. Migrating Django from SQLite to Production PostgreSQL

When transitioning your Django application from local development to production, migrating data from db.sqlite3 to remote PostgreSQL can be challenging. Standard python manage.py dumpdata often fails due to foreign key circular dependencies or UTF-8 encoding errors.

Using SQLOZ’s integrated Cross-Dialect Migration Tool, you can stream your local Django SQLite database directly into AWS RDS or DigitalOcean PostgreSQL in one click. SQLOZ handles type casting, foreign keys, and sequence resets automatically.


Summary

Django’s ORM is a powerful accelerator, but peak production performance requires inspecting the SQL under the hood. By capturing generated queries and diagnosing execution plans in native tools like SQLOZ, you can eliminate N+1 traps and keep your Django applications blazing fast.

To learn more about Django migrations and performance tuning, read our tutorial on Converting Django SQLite to PostgreSQL with Native GUIs and explore How to Inspect Indexes to Optimize Slow SQL Queries.