Django MongoDB Extensions is a package that contains additional developer tools for the Django MongoDB Backend. One of these tools is the MongoDB Query Language (MQL) panel, and it shows MQL queries that your application runs during a request, including execution times and query plans. This tutorial shows you how to configure and use the panel.
We will highlight the functionality of Django Debug Toolbar by building a World Cup 2026 analytics application that queries match data using the Django ORM and raw aggregation pipelines. We'll use the Django Debug Toolbar to inspect the pipelines these queries generate as MQL.
There are two ways to follow this tutorial.
Option A: Clone the GitHub repo containing the finished code from this tutorial, set it up using the repo's README, and explore it on your own. Link to the GitHub project:django-mongodb-world-cup
Option B: Start the project from scratch by following the steps below.
Start the project from scratch
Prerequisites
NB: This tutorial uses config as the Django project name and world_cup as the app name throughout. If you use different names, you'll need to update every reference to them in the code examples that follow (imports, INSTALLED_APPS, and urls.py).
Before starting this tutorial, you will need to complete the following steps:
- Download and install Django MongoDB Backend
- Create a MongoDB deployment
- Create a connection string
- Create a Django project As a quick reference, you can create the Django project with the following command:
django-admin startproject config --template https://github.com/mongodb-labs/django-mongodb-project/archive/refs/heads/6.0.x.zip
(NB: I used config here, but you can also specify a Django project name of your choice, for example, world_cup_project)
- Configure your MongoDB connection
These steps are explained in detail in the Get Started with Django MongoDB Backend tutorial.
Once these prerequisites are complete, the next step is installing Django MongoDB Extensions.
Install Django MongoDB Extensions
Navigate to your project's root directory; either the folder created by startproject, or the folder created when you clone the repository (i.e., django-mongodb-world-cup). This is also where you should create and activate your virtual environment, if you haven't already:
python3 -m venv venv
source venv/bin/activate
- Create a requirements.txt file at the root of the project with the following contents:
#requirements.txt
django-mongodb-extensions
django-debug-toolbar
python-dotenv
Run the following command in your terminal to install the packages:
pip install -r requirements.txtCreate a .env file at the root of the project with the following contents and replace the placeholders with your own values.
For the SECRET_KEY, copy the value Django already generated in your settings.py when you ran startproject; it's normally hardcoded there by default, but we move it into .env to keep it out of version control, since it's used to sign session cookies, CSRF tokens, and other security-sensitive data.
#.env
SECRET_KEY=[REPLACE_WITH_YOUR_SECRET_KEY]
MONGODB_URI="mongodb+srv://<username>:<password>@<cluster>.mongodb.net/?retryWrites=true&w=majority&appName=world_cup"
DB_NAME=[REPLACE_WITH_YOUR_DB_NAME]
Then update DATABASES and SECRET_KEY variables in settings.py to read from the environment instead of being hardcoded. Your settings.py should now include the following:
import os
from dotenv import load_dotenv
load_dotenv()
SECRET_KEY = os.getenv('SECRET_KEY')
DATABASES = {
'default': {
'ENGINE': 'django_mongodb_backend',
'HOST': os.getenv('MONGODB_URI'),
'NAME': os.getenv('DB_NAME'),
},
}
- Add Django MongoDB Extensions and Django Debug Toolbar to your installed apps: Navigate to your project's(or cloned repo) settings.py file and add "django_mongodb_extensions" to the INSTALLED_APPS setting, as shown in the following example:
INSTALLED_APPS = [
'django_mongodb_extensions',
'debug_toolbar',
]
Register the MQL panel
Replace your existing DEBUG_TOOLBAR_PANELS setting with the list below. This is Django Debug Toolbar's default panel list with one change: SQLPanel is swapped out for MQLPanel.
DEBUG_TOOLBAR_PANELS = [
'debug_toolbar.panels.history.HistoryPanel',
'debug_toolbar.panels.versions.VersionsPanel',
'debug_toolbar.panels.timer.TimerPanel',
'debug_toolbar.panels.settings.SettingsPanel',
'debug_toolbar.panels.headers.HeadersPanel',
'debug_toolbar.panels.request.RequestPanel',
'django_mongodb_extensions.mql_panel.MQLPanel',
'debug_toolbar.panels.staticfiles.StaticFilesPanel',
'debug_toolbar.panels.templates.TemplatesPanel',
'debug_toolbar.panels.alerts.AlertsPanel',
'debug_toolbar.panels.cache.CachePanel',
'debug_toolbar.panels.signals.SignalsPanel',
'debug_toolbar.panels.community.CommunityPanel',
'debug_toolbar.panels.redirects.RedirectsPanel',
'debug_toolbar.panels.profiling.ProfilingPanel',
]
Configure Django Debug Toolbar Middleware
Add DebugToolbarMiddleware as the first item in MIDDLEWARE, and set INTERNAL_IPS:
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
'django.middleware.security.SecurityMiddleware',
# ... rest of middleware
]
INTERNAL_IPS = ['127.0.0.1']
Add Django Debug Toolbar’s URLs
In urls.py, add the following:
from django.conf import settings
from django.urls import include, path
urlpatterns = [
# ... your app urls
]
if settings.DEBUG:
urlpatterns += [path("__debug__/", include("debug_toolbar.urls"))]
Create an Application
From your project's root directory, run the following command to create a new Django app. This tutorial uses the name world_cup throughout, so if you use a different name, you'll need to update it in every file this tutorial references (INSTALLED_APPS, imports, urls.py):
python manage.py startapp world_cup
Next, include your app in your project by opening the settings.py file in config and editing your INSTALLED_APPS setting to resemble the following code:
INSTALLED_APPS = [
'config.apps.MongoAdminConfig',
'config.apps.MongoAuthConfig',
'config.apps.MongoContentTypesConfig',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_mongodb_backend',
'django_mongodb_extensions',
'debug_toolbar',
'world_cup',
]
Create models for Goal and Match data
The world_cup application stores the World Cup match data using two models: Match and Goal. The Goal model is embedded inside Match as an array of goal events because it is usually queried together with the corresponding matches.
Open the models.py file in the world_cup directory and replace its contents with the following code:
from django.db import models
from django_mongodb_backend.fields import EmbeddedModelArrayField
from django_mongodb_backend.managers import MongoManager
from django_mongodb_backend.models import EmbeddedModel
class Goal(EmbeddedModel):
team = models.CharField(max_length=100)
name = models.CharField(max_length=100)
minute = models.CharField(max_length=10)
penalty = models.BooleanField(default=False)
own_goal = models.BooleanField(default=False)
def __str__(self):
return f"{self.name} (({self.team}, {self.minute}')"
class Match(models.Model):
num = models.IntegerField(null=True, blank=True)
round = models.CharField(max_length=50)
date = models.DateField()
kickoff_time = models.CharField(max_length=20)
group = models.CharField(max_length=20, blank=True)
ground = models.CharField(max_length=100)
team1 = models.CharField(max_length=100)
team2 = models.CharField(max_length=100)
#Fields ending in "1" refer to team1, fields ending in "2" refer to team2
ft_score1 = models.IntegerField(null=True, blank=True) #ft = full-time
ft_score2 = models.IntegerField(null=True, blank=True)
ht_score1 = models.IntegerField(null=True, blank=True) #ht = half-time
ht_score2 = models.IntegerField(null=True, blank=True)
et_score1 = models.IntegerField(null=True, blank=True) #et = extra-time
et_score2 = models.IntegerField(null=True, blank=True)
pen_score1 = models.IntegerField(null=True, blank=True) #penalty score
pen_score2 = models.IntegerField(null=True, blank=True)
goals = EmbeddedModelArrayField(Goal, default=list, blank=True)
objects = MongoManager()
def __str__(self):
return f"{self.team1} vs {self.team2} ({self.round})"
The Goal model represents the world_cup.world_cup_match collection and stores information about the goals. This model contains fields such as name, minute, penalty, and own goal. The Goal model contains the embedded document values stored in the Match model.
Run Migrations
Generate and apply the migration for the world_cup app:
python manage.py makemigrations world_cup
python manage.py migrate
Loading Data
mkdir -p world_cup/management/commands
mkdir -p world_cup/data
touch world_cup/management/__init__.py
touch world_cup/management/commands/__init__.py
touch world_cup/management/commands/load_matches.py
For the World Cup data, we use the JSON file from Open Football GitHub source and save it to the data folder under the world_cup folder (/world_cup/data/worldcup_2026.json).
After obtaining the data (JSON file), we create a script (world_cup/management/commands/load_matches.py) to load the matches. Replace the following content in the load_matches.py file.
# world_cup/management/commands/load_matches.py
import json
from datetime import datetime
from pathlib import Path
from django.core.management.base import BaseCommand
from world_cup.models import Goal, Match
WORLD_CUP_DATA = Path(__file__).resolve().parent.parent.parent / "data" / "worldcup_2026.json"
def build_goals(raw_goals, team_name):
return [
Goal(
team=team_name,
name=g["name"],
minute=g["minute"],
penalty=g.get("penalty", False),
own_goal=g.get("owngoal", False),
)
for g in raw_goals
]
def to_match_kwargs(raw_match):
score = raw_match.get("score", {})
ft = score.get("ft", [None, None])
ht = score.get("ht", [None, None])
et = score.get("et", [None, None])
p = score.get("p", [None, None])
return dict(
num=raw_match.get("num"),
round=raw_match["round"],
date=datetime.strptime(raw_match["date"], "%Y-%m-%d").date(),
kickoff_time=raw_match["time"],
group=raw_match.get("group", ""),
ground=raw_match["ground"],
team1=raw_match["team1"],
team2=raw_match["team2"],
ft_score1=ft[0],
ft_score2=ft[1],
ht_score1=ht[0],
ht_score2=ht[1],
et_score1=et[0],
et_score2=et[1],
pen_score1=p[0],
pen_score2=p[1],
goals=build_goals(raw_match.get("goals1", []), raw_match["team1"]) + build_goals(raw_match.get("goals2", []), raw_match["team2"]),
)
class Command(BaseCommand):
help = (
"Load the curated World Cup 2026 matches (Groups A, B, I, J) into MongoDB. "
"Source: worldcup_data/worldcup_2026.json — 24 matches total, "
"16 completed with real goals, 8 unplayed (Matchday 14/17)."
)
def handle(self, *args, **options):
with open(WORLD_CUP_DATA) as f:
raw_matches = json.load(f)["matches"]
self.stdout.write(f"Found {len(raw_matches)} matches to load.")
batch_size = 5
created = 0
for i in range(0, len(raw_matches), batch_size):
batch = raw_matches[i : i + batch_size]
Match.objects.bulk_create([Match(**to_match_kwargs(m)) for m in batch])
created += len(batch)
self.stdout.write(f"Loaded {created}/{len(raw_matches)} matches...")
self.stdout.write(self.style.SUCCESS(f"Done: {created} matches loaded."))
Next, we load the data by running:
python manage.py load_matches
Once this is successful, we can see the results either by querying the database via the Python shell or via MongoDB Atlas’s Data Explorer. For example, to confirm the data loaded correctly and inspect an embedded Goal array, run:
python manage.py shell
In this shell:
>>> from world_cup.models import Match
>>> Match.objects.count()
100
>>> Match.objects.get(team1="France", team2="Senegal").goals1
Match results queried via Python shell

World Cup match stats after upload in MDB Atlas

Create Views
These views provide three pages: a total goals counter using the Django ORM, a top scorers leaderboard using a raw aggregation pipeline, and an interactive query shell that allows you to run five different query types.
Open the views.py file in your world_cup directory and replace its contents with the following code:
# world_cup/views.py
from django.db import connections
from django.db.models import F, Sum
from django.shortcuts import render
from .models import Match
def tournament_goals(request):
"""Pipeline 1 — built-in aggregate: total goals across completed matches."""
result = (
Match.objects.filter(ft_score1__isnull=False)
.annotate(total_goals=F("ft_score1") + F("ft_score2"))
.aggregate(tournament_goals=Sum("total_goals"))
)
return render(request, "world_cup/goals.html", {
"total_goals": result["tournament_goals"] or 0,
})
def top_scorers(request):
"""Pipeline 2 — raw_aggregate: leaderboard via $unwind + $group."""
pipeline = [
{"$unwind": "$goals"},
{"$match": {"goals.own_goal": {"$ne": True}}},
{"$group": {"_id": "$goals.name", "total_goals": {"$sum": 1}}},
{"$sort": {"total_goals": -1}},
{"$limit": 10},
]
results = Match.objects.raw_aggregate(pipeline)
scorers = [{"name": r.pk, "goals": r.total_goals} for r in results]
return render(request, "world_cup/scorers.html", {"scorers": scorers})
ALLOWED_QUERIES = {
"team": {
"description": "All matches for a team",
"example": "team France",
},
"round": {
"description": "Matches in a round (e.g. round Quarter-final)",
"example": "round Quarter-final",
},
"top scorers": {
"description": "Top 10 scorers via $unwind + $group pipeline",
"example": "top scorers",
},
"goals by round": {
"description": "Goals per round via $group pipeline",
"example": "goals by round",
},
"goals": {
"description": "Total tournament goals",
"example": "goals",
},
}
def _execute_query(query_text):
"""Parse and execute a user query, return result dict."""
q = query_text.strip().lower()
if q == "goals":
result = (
Match.objects.filter(ft_score1__isnull=False)
.annotate(total_goals=F("ft_score1") + F("ft_score2"))
.aggregate(tournament_goals=Sum("total_goals"))
)
return {
"results": [{"tournament_goals": result["tournament_goals"] or 0}],
}
if q in ("top scorers", "scorers", "top"):
pipeline = [
{"$unwind": "$goals"},
{"$match": {"goals.own_goal": {"$ne": True}}},
{"$group": {"_id": "$goals.name", "total_goals": {"$sum": 1}}},
{"$sort": {"total_goals": -1}},
{"$limit": 10},
]
results = Match.objects.raw_aggregate(pipeline)
return {
"results": [{"scorer": r.pk, "goals": r.total_goals} for r in results],
}
if q.startswith("team "):
team = query_text.strip()[5:].strip()
from django.db.models import Q
matches = Match.objects.filter(Q(team1__iexact=team) | Q(team2__iexact=team)).order_by("date")
return {
"query": f'db.world_cup_match.aggregate([{{$match: {{$or: [{{team1: {{$regex: "^{team}$", $options: "i"}}}}, {{team2: {{$regex: "^{team}$", $options: "i"}}}}]}}}}, {{$sort: {{date: 1}}}}])',
"results": [
{
"date": str(m.date),
"round": m.round,
"team1": m.team1,
"team2": m.team2,
"score": f"{m.ft_score1}-{m.ft_score2}" if m.ft_score1 is not None else "TBD",
}
for m in matches
],
}
if q.startswith("round "):
round_name = query_text.strip()[6:].strip()
matches = Match.objects.filter(round__icontains=round_name).order_by("date")
return {
"query": f'db.world_cup_match.aggregate([{{$match: {{round: {{$regex: "{round_name}", $options: "i"}}}}}}, {{$sort: {{date: 1}}}}])',
"results": [
{
"date": str(m.date),
"team1": m.team1,
"team2": m.team2,
"score": f"{m.ft_score1}-{m.ft_score2}" if m.ft_score1 is not None else "TBD",
}
for m in matches
],
}
if q in ("goals by round", "goals per round"):
pipeline = [
{"$match": {"ft_score1": {"$ne": None}}},
{"$group": {
"_id": "$round",
"total_goals": {"$sum": {"$add": ["$ft_score1", "$ft_score2"]}},
"matches": {"$sum": 1},
}},
{"$sort": {"total_goals": -1}},
]
results = Match.objects.raw_aggregate(pipeline)
return {
"results": [{"round": r.pk, "goals": r.total_goals, "matches": r.matches} for r in results],
}
return None
def query_shell(request):
"""Interactive query shell — form POST triggers full page reload for MQL panel."""
context: dict = {"queries": ALLOWED_QUERIES}
if request.method == "POST":
query_text = request.POST.get("query", "").strip()
context["last_query"] = query_text
if query_text:
result = _execute_query(query_text)
if result is None:
context["error"] = f"Unknown query. Try: {', '.join(ALLOWED_QUERIES.keys())}"
else:
context["mongo_query"] = result.get("query", "")
context["results"] = result.get("results", [])
return render(request, "world_cup/shell.html", context)
Configure URLs for your views
Create a new file called urls.py file in your world_cup directory. To map the views defined in the preceding step to URLs, paste the following code into urls.py:
from django.urls import path
from . import views
urlpatterns = [
path("goals/total/", views.tournament_goals, name="tournament_goals"),
path("goals/top-scorers/", views.top_scorers, name="top_scorers"),
path("shell/", views.query_shell, name="query_shell"),
]
`
Then, navigate to the config/urls.py file and replace its contents with the following code:
`
from django.conf import settings
from django.shortcuts import render
from django.urls import include, path
def landing(request):
return render(request, "world_cup/landing.html")
urlpatterns = [
path("", landing, name="landing"),
path("world_cup/", include("world_cup.urls")),
]
if settings.DEBUG:
urlpatterns += [path("__debug__/", include("debug_toolbar.urls"))]
Create Templates
In your world_cup directory, create a subdirectory called templates.
Under templates, create five HTML files to display the base page, goals page(showing total goals by aggregation), landing page, scorers page(top scorers), and shell page(interactive shell to run queries and aggregation pipelines interactively).
base.html: This is the shared layout (nav, styles) that the other four templates extend.
#world_cup/templates/base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}World Cup 2026{% endblock %}</title>
<style>
:root {
--nav-bg: #1C2D38;
--page-bg: #f0f2f5;
--card-bg: #ffffff;
--text-primary: #1C2D38;
--text-secondary: #5c6970;
--border: #e2e5e9;
--accent: #1a6b4b;
--accent-light: #e8f5ef;
--white: #ffffff;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--page-bg);
color: var(--text-primary);
min-height: 100vh;
}
nav {
background: var(--nav-bg);
padding: 0.9rem 2rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 3px solid var(--accent);
}
nav .brand {
color: var(--white);
font-size: 1.1rem;
font-weight: 700;
text-decoration: none;
}
nav .nav-links a {
color: rgba(255,255,255,0.85);
text-decoration: none;
margin-left: 2rem;
font-size: 0.9rem;
}
nav .nav-links a:hover {
color: var(--white);
}
.container {
max-width: 900px;
margin: 2rem auto;
padding: 0 1.5rem;
}
header {
margin-bottom: 1.5rem;
}
header h1 {
font-size: 1.6rem;
font-weight: 700;
color: var(--text-primary);
}
header p {
color: var(--text-secondary);
font-size: 0.9rem;
margin-top: 0.25rem;
}
.card {
background: var(--card-bg);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1rem;
border: 1px solid var(--border);
}
.card:hover {
border-color: var(--accent);
}
.card a {
text-decoration: none;
color: var(--text-primary);
display: block;
}
.card h3 {
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.3rem;
}
.card p {
color: var(--text-secondary);
font-size: 0.85rem;
}
.badge {
display: inline-block;
background: var(--accent);
color: var(--white);
font-size: 0.7rem;
font-weight: 600;
padding: 0.2rem 0.6rem;
border-radius: 4px;
margin-bottom: 0.5rem;
text-transform: uppercase;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 0.5rem;
}
th, td {
text-align: left;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border);
}
th {
color: var(--text-secondary);
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
}
td {
font-size: 0.9rem;
}
.back-link {
display: inline-block;
margin-bottom: 1rem;
color: var(--accent);
text-decoration: none;
font-size: 0.85rem;
}
.back-link:hover {
text-decoration: underline;
}
.stat-value {
font-size: 3rem;
font-weight: 700;
color: var(--accent);
}
.stat-label {
color: var(--text-secondary);
font-size: 0.9rem;
margin-top: 0.25rem;
}
</style>
</head>
<body>
<nav>
<a href="/" class="brand">World Cup 2026 Stats</a>
<div class="nav-links">
<a href="{% url 'tournament_goals' %}">Goals</a>
<a href="{% url 'top_scorers' %}">Scorers</a>
<a href="{% url 'query_shell' %}">Shell</a>
</div>
</nav>
<div class="container">
{% block content %}{% endblock %}
</div>
</body>
</html>
landing.html: This is the app's home page, linking out to the goals, scorers, and shell pages.
Endpoint: http://127.0.0.1:8000/
#world_cup/templates/landing.html
{% extends "world_cup/base.html" %}
{% block title %}World Cup 2026 — Home{% endblock %}
{% block content %}
<header>
<h1>⚽ World Cup 2026 Stats</h1>
</header>
<div class="card">
<a href="{% url 'tournament_goals' %}">
<span class="badge">Aggregation</span>
<h3>Total Goals</h3>
<p>Sum of all goals scored — view the MQL query in the Debug Toolbar</p>
</a>
</div>
<div class="card">
<a href="{% url 'top_scorers' %}">
<span class="badge">Pipeline</span>
<h3>Top Scorers</h3>
<p>$unwind + $group pipeline — view execution time in the MQL panel</p>
</a>
</div>
<div class="card">
<a href="{% url 'query_shell' %}">
<span class="badge">Interactive</span>
<h3>Query Shell</h3>
<p>Run find queries and aggregation pipelines </p>
</a>
</div>
{% endblock %}
goals.html: This displays the total goals count from the tournament_goals view's aggregation.
Endpoint: http://127.0.0.1:8000/world_cup/goals/total/
#world_cup/templates/goals.html
{% extends "world_cup/base.html" %}
{% block title %}Total Goals — World Cup 2026{% endblock %}
{% block content %}
<a href="/" class="back-link">← Back to Home</a>
<header>
<h1>Tournament Goals</h1>
<p>Total goals across all completed matches</p>
</header>
<div class="card" style="text-align: center; padding: 3rem;">
<div class="stat-value">{{ total_goals }}</div>
<div class="stat-label">goals scored</div>
</div>
{% endblock %}
scorers.html: This displays the top scorers leaderboard from the top_scorers view's raw pipeline.
Endpoint: http://127.0.0.1:8000/world_cup/goals/top-scorers/
#world_cup/templates/scorers.html
{% extends "world_cup/base.html" %}
{% block title %}Top Scorers — World Cup 2026{% endblock %}
{% block content %}
<a href="/" class="back-link">← Back to Home</a>
<header>
<h1>Top Scorers</h1>
<p>Leading goal scorers in the tournament</p>
</header>
<div class="card">
<table>
<thead>
<tr>
<th>#</th>
<th>Player</th>
<th>Goals</th>
</tr>
</thead>
<tbody>
{% for scorer in scorers %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ scorer.name }}</td>
<td>{{ scorer.goals }}</td>
</tr>
{% empty %}
<tr><td colspan="3">No scoring data available yet.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
shell.html: This is the interactive query shell which takes an input form plus a results table, for running any of the commands from ALLOWED_QUERIES and compares them in the MQL panel.
Endpoint: http://127.0.0.1:8000/world_cup/shell/
#/templates/shell.html
{% extends "world_cup/base.html" %}
{% block title %}Query Shell — World Cup 2026{% endblock %}
{% block content %}
<a href="/" class="back-link">← Back to Home</a>
<header>
<h1>Query Shell</h1>
<p>Each query reloads the page — check the MQL panel in the Debug Toolbar</p>
</header>
<div class="shell">
<div class="shell-output" id="output">
{% if not last_query %}
<div class="shell-line system">Click an example or type a query:</div>
<div class="shell-line system"></div>
<div class="shell-line system" style="color: var(--mdb-gray); opacity: 0.6;">— Queries (find) —</div>
<div class="shell-line example" data-query="team France">team France</div>
<div class="shell-line example" data-query="team England">team England</div>
<div class="shell-line example" data-query="round Quarter-final">round Quarter-final</div>
<div class="shell-line system"></div>
<div class="shell-line system" style="color: var(--mdb-gray); opacity: 0.6;">— Aggregation Pipelines —</div>
<div class="shell-line example" data-query="top scorers">top scorers</div>
<div class="shell-line example" data-query="goals by round">goals by round</div>
<div class="shell-line example" data-query="goals">goals</div>
{% endif %}
{% if last_query %}
<div class="shell-line query">{{ last_query }}</div>
{% endif %}
{% if error %}
<div class="shell-line error">{{ error }}</div>
{% endif %}
{% if mongo_query %}
<div class="shell-line mongo-query">{{ mongo_query }}</div>
{% endif %}
{% if results %}
<div class="shell-line result">
<table class="result-table">
<thead>
<tr>
{% for key in results.0.keys %}
<th>{{ key }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in results %}
<tr>
{% for val in row.values %}
<td>{{ val|default:"—" }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
<form method="post" action="{% url 'query_shell' %}" class="shell-input-row">
{% csrf_token %}
<span class="prompt">▶</span>
<input type="text" name="query" id="query-input" value="" placeholder="e.g. team France, top scorers, round Quarter-final" autofocus>
</form>
</div>
<style>
.shell {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
font-size: 0.85rem;
}
.shell-output {
padding: 1.5rem;
max-height: 500px;
overflow-y: auto;
}
.shell-line {
margin-bottom: 0.4rem;
line-height: 1.5;
}
.shell-line.system {
color: var(--text-secondary);
}
.shell-line.query {
color: var(--accent);
font-weight: 600;
}
.shell-line.query::before {
content: "▶ ";
opacity: 0.5;
}
.shell-line.mongo-query {
color: var(--text-secondary);
font-size: 0.78rem;
padding-left: 1rem;
background: var(--page-bg);
padding: 0.5rem 1rem;
border-radius: 4px;
margin: 0.3rem 0;
overflow-x: auto;
}
.shell-line.result {
color: var(--text-primary);
padding-left: 0;
}
.shell-line.error {
color: #c0392b;
}
.shell-line.example {
color: var(--accent);
cursor: pointer;
padding-left: 1rem;
opacity: 0.8;
}
.shell-line.example::before {
content: "▶ ";
opacity: 0.4;
}
.shell-line.example:hover {
opacity: 1;
text-decoration: underline;
}
.shell-input-row {
display: flex;
align-items: center;
border-top: 1px solid var(--border);
padding: 0.75rem 1.5rem;
background: var(--page-bg);
}
.prompt {
color: var(--accent);
margin-right: 0.75rem;
font-size: 0.8rem;
}
.shell-input-row input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: var(--text-primary);
font-family: inherit;
font-size: inherit;
}
.shell-input-row input::placeholder {
color: var(--text-secondary);
opacity: 0.5;
}
.result-table {
width: 100%;
border-collapse: collapse;
margin: 0.5rem 0;
}
.result-table th {
color: var(--text-secondary);
font-size: 0.7rem;
text-transform: uppercase;
text-align: left;
padding: 0.3rem 0.75rem;
border-bottom: 1px solid var(--border);
}
.result-table td {
padding: 0.3rem 0.75rem;
font-size: 0.8rem;
color: var(--text-primary);
}
</style>
<script>
document.querySelectorAll('.shell-line.example').forEach(el => {
el.addEventListener('click', () => {
document.getElementById('query-input').value = el.dataset.query;
el.closest('.shell').querySelector('form').submit();
});
});
</script>
{% endblock %}
Build an Aggregation Pipeline
The tournament_goals view uses the Django ORM's aggregate() and Sum() to total up goals, and the MQL panel captures that query automatically. For more advanced analysis, like ranking top scorers, you can use MongoDB's aggregation pipelines. The top_scorers view in views.py does exactly this: it uses $concatArrays to merge embedded goal arrays, $unwind to flatten them into individual documents, and $group to count goals per player.
The top_scorers view uses raw_aggregate(), a method which accepts a raw MongoDB aggregation pipeline directly, including stages like $unwind, and returns Django model instances. See the QuerySet API reference for more details.
When you navigate to the /world_cup/goals/top-scorers/ and open the MQL panel in the Django Debug Toolbar. You'll see the full aggregation pipeline that was executed, along with its timing. The MQL panel captures both ORM-generated queries and raw pipelines, so you can inspect and optimize either approach from the same interface.
Verify the MQL panel
Start your development server by running the following command:
python manage.py runserver
Navigate to any page in your application that queries MongoDB. Open the Django Debug Toolbar and select the MQL panel to view the MongoDB queries executed during the request, including their execution times and explain plans.
View worldcup.goals query information
Now, navigate to http://127.0.0.1:8000/world_cup/goals/total/ to view MongoDB queries executed during the request.
The Django Debug Toolbar appears on the right side of the page. Select the MQL label to view the MongoDB query used to calculate the total goals, which resembles the following code:
After, you can explore the query information by clicking on the following buttons:
Select the + button to the left of the query to view the call stack, which resembles the following call trace:
Next, select the Query button to the top-right of the query, in the ACTION column, to view the pretty-printed MQL. This MQL resembles the following code:
Now, select the Explain button to the right of the query, in the ACTION column, to view the explain plan for the query. This plan resembles the following code:
View the top scorers aggregation pipeline
Next, navigate to http://127.0.0.1:8000/world_cup/goals/top-scorers/ to see the aggregation pipeline in the MQL panel. We see that this page runs a raw multi-stage pipeline ($concatArrays, $unwind, $group, $sort, $limit). Select the MQL panel to view the pipeline stages, execution time, and explain plans.
View query shell queries
Finally, access the endpoint http://127.0.0.1:8000/world_cup/shell/ and submit a query such as team France. The page reloads with the results, and the MQL panel shows the aggregation pipeline that was executed. Even though the view uses the Django ORM's filter() and order_by(), the MongoDB backend translates these into an aggregation pipeline.
Next Steps
Congratulations on getting this far! You now have a Django application that uses Django MongoDB Extensions to view and analyze MongoDB queries in the Django Debug Toolbar.
Here are some debugging exercises you can experiment with:
- Run the "round" search in the query shell (e.g., round Group A) and check its query plan in the MQL panel. You'll see a
COLLSCANwhich is a full collection scan becauseround__icontainscompiles to an unanchored regex, which can't use an index no matter what you add. - Compare it against
round__iexacton the same field, and the panel shows IXSCAN instead. It is similar code but different queries underneath, and that is exactly what the Django Debug Toolbar makes evident.
To learn more about the Django MongoDB Extensions package, see django-mongodb-extensions and the django-mongodb-extensions repository on GitHub.
To learn how to query your data, see the Specify a Query guide.
Link to this GitHub Project: Django MongoDB World Cup project
Link to the Data: Open Football Dataset









Top comments (0)