
Не так чтобы часто, но с той самой неприятной регулярностью, когда уже забыл как это делал в прошлый раз, бывает нужно посчитать сколько запросов к БД генерирует тот или иной блок кода для django.
При этом мало что лучше закрепляется в памяти, чем очередная неудачная статья на хабре собственного сочинения. Штош, попробуем совместить полезное с неприятным.
Для чего вообще считать запросы?
Можно назвать несколько теоретических поводов:
Предотвращение деградации производительности: при изменениях в коде можно не заметить, что количество запросов увеличилось в разы из-за того что где-то потерялся волшебный prefetch_related или select_related.
Документирование оптимизации: тест явно фиксирует ожидаемое количество SQL-запросов.
Контроль отсутствия примесей: можно посмотреть какие именно запросы и в каком порядке выполняются, чтобы убедиться, что в логику не примешался код не имеющий прямого отношения к выполняемой задаче.
Из практики вспоминается как в DaData нужно было писать и поддерживать автотесты проверяющие, что только нужные запросы делаются в узких местах приложения - и мы даже описывали комментариями почему без этого запроса не получается.
На недавнем code-review коллега заметил у меня в коде подозрительный цикл и, чтобы убедиться в отсутвии N*K запросов, проще всего оказалось написать автотест на этот блок кода - так как воспроизвести всю схему данных и посмотреть на запросы в логах было слишком затратно по времени.
Таким образом, смысл в этом определённо есть. Осталось закрепить то, как это сделать на unittest и pytest.
Как посчитать запросы в unittest?
Django предоставляет контекстный менеджер assertNumQueries, доступный в django.test.TestCase.
Он позволяет проверить, что внутри блока выполняется ровно заданное количество SQL-запросов.
Пример базового использования:
from django.test import TestCase
from myapp.models import Book
class BookTests(TestCase):
def test_list_books_queries(self):
# Предварительно создаём данные
for i in range(5):
Book.objects.create(title=f"Book {i}")
# Проверяем количество запросов при выборке всех объектов
with self.assertNumQueries(1):
books = list(Book.objects.all())
self.assertEqual(len(books), 5)
А что с pytest?
Если вы используете pytest для написания автотестов с django, то можете использовать контекстный менеджер CaptureQueriesContext.
Код при использовании расширения pytest-django может выглядеть как-то так:
import pytest
from django.db import connection
from django.test.utils import CaptureQueriesContext
from myapp.models import Book
@pytest.mark.django_db
def test_books_queries():
Book.objects.create(title="Book 1")
Book.objects.create(title="Book 2")
with CaptureQueriesContext(connection) as ctx:
list(Book.objects.all())
assert len(ctx.captured_queries) == 1
Этот же трюк можно использовать и в unittest. Внутри ctx.captured_queries есть и sql соответствующих запросов.
Сохраните это себе в закладки.
А еще, если вам понравилась статья, то я с удовольствием писал бы для вас техническую документацию или статьи или, что более вероятно, код на python. Словом, я в поисках работы - напишите, пожалуйста, в личку и я отправлю вам резюме.