Skip to content

Instantly share code, notes, and snippets.

@denisxab
Created June 26, 2024 16:33
Show Gist options
  • Save denisxab/43e10f4963089d5b7bb7373235672bb5 to your computer and use it in GitHub Desktop.
Save denisxab/43e10f4963089d5b7bb7373235672bb5 to your computer and use it in GitHub Desktop.
Каждый бекенд-разработчик желает знать, сколько запрос к СУБД формирует Django ORM
import re
import traceback
from functools import wraps
from typing import Any
from django.db.backends.utils import CursorWrapper
class TraceSQL:
"""Класс для трассировки SQL запросов.
Позволяет узнать в какой части кода выполнялся SQL запрос, для того чтобы его оптимизировать.
Обычно вы можете узнать только какие SQL запросы выполнялись, но не знаете в какой части кода они выполнились,
и тогда нужно тратить время и угадывать какая строка кода вызывает эти SQL запросы.
Пример использования:
class ИмяApiViewTests(APITestCase):
def test_имя(self) -> None:
# В path_trace_compile укажем регулярное выражение для фильтрации стека трассировки
with TraceSQL(path_trace_compile="/code/api/(?!.*tests).*") as trace:
response = self.client.put(
'URL',
data={
"имя": "имя",
"пол": "мужской",
"дата рождения": "01.01.2000",
},
format="json",
)
# Трассировка выполненных запросов
print(trace.stack_sql)
"""
def __init__(self, path_trace_compile: str):
"""Инициализация.
Args:
path_trace_compile: Регулярное выражение для фильтрации файлов, которые мы хотим трассировать.
Например, укажите `/api/*`, чтобы игнорировать в трассировке файлы библиотек и тестов.
"""
self.path_trace_compile = re.compile(path_trace_compile)
# Список запросов
self.stack_sql: list[dict[str, Any]] = []
# Сохраним оригинальные функции
self.original_execute = CursorWrapper.execute
self.original_executemany = CursorWrapper.executemany
def __enter__(self):
def base_trace_stack(sql: str, params: list):
stack: traceback.StackSummary = traceback.extract_stack()[:-1]
filter_stack: list[traceback.FrameSummary] = [
frame for frame in stack if self.path_trace_compile.search(frame.filename)
]
self.stack_sql.append({"sql": sql, "stack": filter_stack, "params": params})
@wraps(CursorWrapper.executemany)
def execute_with_trace(cursor, sql, params=None):
"""Трассировка запросов для execute."""
base_trace_stack(sql, params)
return self.original_execute(cursor, sql, params)
@wraps(CursorWrapper.executemany)
def executemany_with_trace(cursor, sql, param_list):
"""Трассировка запросов для executemany."""
base_trace_stack(sql, param_list)
return self.original_executemany(cursor, sql, param_list)
# Модифицируем функции для трассировки
CursorWrapper.execute = execute_with_trace
CursorWrapper.executemany = executemany_with_trace
return self
def __exit__(self, type, value, traceback):
"""Возвращает оригинальные функции."""
CursorWrapper.execute = self.original_execute
CursorWrapper.executemany = self.original_executemany
@denisxab
Copy link
Author

denisxab commented Jun 26, 2024

Заголовок

Каждый бекенд-разработчик желает знать, сколько запрос к СУБД формирует Django ORM

Django ORM - мощный инструмент для работы с базами данных, но он может создавать множество неявных SQL-запросов. Для бекенд-разработчиков критически важно понимать, сколько запросов генерируется и откуда они исходят. В этой статье мы рассмотрим проблему отслеживания SQL-запросов в Django и представим эффективное решение - TraceSQL.

Описание проблемы

Django ORM предоставляет удобный интерфейс для взаимодействия с базой данных, однако его использование может приводить к возникновению большого количества неявных запросов. Для оптимизации производительности критически важно знать, сколько и откуда выполняются SQL-запросы.

При разработке через TDD (Test-Driven Development) в legacy-проектах с большим количеством SQL-запросов часто возникает проблема: непонятно, откуда берутся запросы и где нужно оптимизировать код.

Чтобы отслеживать количество выполняемых SQL запросов во время выполнения API метода, есть несколько вариантов:

  1. Использовать плагин django-debug-toolbar
  • + - Показывает место в коде где вызывается SQL запрос.
  • - - Нельзя использовать во время тестов, поэтому не подходит для разработки через TDD.
  1. Использовать self.assertNumQueries(ОжидаемоеКоличествоЗапросов)
  • + - Можно использовать во время тестов, показывает, какие SQL запросы выполнялись.
  • - - Не показывает место в коде, где вызывается SQL запрос.
  1. Компромисс TraceSQL
  • + - Можно использовать во время тестов.
  • + - Показывает место в коде, где вызывается SQL запрос.
  • - - Слишком подробный отчёт, который не особо нужен в тестах. Поэтому, когда вы уже оптимизировали SQL-запросы, используйте просто подсчёт количества выполняемых запросов через self.assertNumQueries.

Как указано выше, стандартный self.assertNumQueries не показывает место в коде, откуда вызваны SQL-запросы. Когда таких запросов более десяти, уже трудно понять, откуда они берутся, и приходится угадывать эти места. Поэтому я исследовал интернет, спросил у GPT и понял, что мало где написано про такой вариант.

Код и пример TraceSQL

TraceSQL.py

Заключение

Использование TraceSQL помогает более эффективно отслеживать и оптимизировать SQL-запросы в Django проекте. Этот инструмент является компромиссом между возможностями django-debug-toolbar и self.assertNumQueries, позволяя разработчикам легко определять места в коде, вызывающие лишние SQL-запросы.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment