Query Debugging Utilities

We have some utilities that let you capture and inspect the SQL Django executes during a function call. They are built on CaptureQueriesContext and work even when DEBUG=False.

Capturing queries around a function call

from gyrinx.query import capture_queries, log_query_info
from gyrinx.core.models import MyModel

# Capture queries executed inside the lambda
result, info = capture_queries(lambda: MyModel.objects.filter(foo="bar").count())

print(info.count, "queries in", info.total_time, "seconds")
for q in info.queries:
    print(q["time"], q["sql"])

# Or log them nicely
log_query_info(info)

Example log output:

DEBUG:gyrinx.db:Captured 3 queries in 0.012 seconds
DEBUG:gyrinx.db:    1. (0.003s) SELECT ...
DEBUG:gyrinx.db:    2. (0.004s) SELECT ...
DEBUG:gyrinx.db:    3. (0.005s) SELECT ...

Using as a decorator

from gyrinx.query import with_query_capture

@with_query_capture()
def load_stuff():
    return list(MyModel.objects.all())

Now when you call:

result = load_stuff()

you’ll see a summary of the queries automatically logged.

The decorator logs query info but returns only the original function result (unlike capture_queries, which returns (result, info)).

Options

  • Database alias: Pass using="replica" to target a different connection.

  • Logging verbosity: log_query_info(info, limit=5, level=logging.INFO)

    • limit: max number of queries to show (None = show all).

    • level: logging level (defaults to DEBUG).

When to use

  • Profiling views or functions in local dev.

  • Writing tests that assert on query counts.

  • Inspecting query behavior in management commands.

⚠️ Note: Avoid leaving decorators or capture calls in production code paths, since they alter return values and add logging noise. They're best for temporary diagnostics and tests.

Using in pytest tests

You can use capture_queries inside tests to make assertions about the number or cost of queries. This helps catch N+1 issues or overly expensive ORM patterns.

import pytest
from gyrinx.query import capture_queries
from gyrinx.core.models import MyModel


@pytest.mark.django_db
def test_list_view_queries(client):
    # Example: ensure hitting the list view doesn't explode into N+1 queries
    def call_view():
        return client.get("/mymodels/")

    response, info = capture_queries(call_view)

    assert response.status_code == 200
    # Fail the test if more than 5 queries are executed
    assert info.count <= 5, f"Too many queries: {info.count}\n{info.queries}"


@pytest.mark.django_db
def test_queryset_is_prefetched():
    def run_query():
        return list(MyModel.objects.select_related("author")[:10])

    result, info = capture_queries(run_query)

    assert len(result) == 10
    # Ensure exactly 1 query was used (no N+1 on author)
    assert info.count == 1

Last updated