****************************************************************************************************************************************************** Антипаттерн: N+1 запросов — как заметить и починить Вы берёте список сущностей, а потом в цикле для каждой тянете связанные данные. В итоге - 1 запрос за «родителями» + N запросов за «детьми». Латентность растёт линейно от размера выборки. Симптомы - В логах много одинаковых коротких запросов. - Кол-во запросов ≈ размеру списка. - Страница/endpoint сильно «замедляется» при росте данных. Плохой пример (SQL + псевдокод) -- Берём пользователей SELECT id, name FROM users WHERE active = true; -- Потом в цикле по каждому: SELECT count(*) FROM orders WHERE user_id = :id; Правильно (SQL, PostgreSQL) — сетевое мышление: SELECT u.id, u.name, count(o.*) AS orders_cnt FROM users u LEFT JOIN orders o ON o.user_id = u.id WHERE u.active = true GROUP BY u.id, u.name; Django ORM # Плохо: в шаблоне/цикле обращаемся к user.orders -> N+1 users = User.objects.filter(active=True) # Хорошо: подгрузим связи заранее users = (User.objects .filter(active=True) .prefetch_related('orders')) # для 1:N # для 1:1 / ForeignKey используйте select_related('profile') # Агрегация без цикла from django.db.models import Count users = (User.objects.filter(active=True) .annotate(orders_cnt=Count('orders'))) SQLAlchemy from sqlalchemy.orm import selectinload, joinedload # 1:N — безопаснее selectinload (батчирует IN (...)) users = (session.query(User) .options(selectinload(User.orders)) .filter(User.active.is_(True)) .all()) # 1:1 — joinedload user = (session.query(User) .options(joinedload(User.profile)) .get(user_id)) Практические советы - Логируйте кол-во запросов на эндпойнт/страницу. В Django - django-debug-toolbar, assertNumQueries в тестах; в SQLAlchemy - echo/интеграция с логгером. - Индексы: обязательно orders(user_id); если фильтруете по статусу - составной (user_id, status). - Батчинг вместо циклов: тяните детей одним запросом WHERE user_id IN (...), затем мапьте в памяти. - Осторожно с joinedload для 1:N на больших выборках - риск «взрыва» строк. Для 1:N чаще выбирайте selectinload. - Колонки по делу: не тащите SELECT *, берите только нужные поля. - Пагинация: уменьшает N и давление на сеть/память. - EXPLAIN (ANALYZE, BUFFERS) - проверяйте планы и кардинальности. 💡Думайте наборами, а не циклами. Eager loading + агрегаты закрывают 90% случаев N+1. Настройте мониторинг количества запросов - и ловите проблему до продакшена. Сохрани, чтобы не наступить снова. Поделись с коллегами. А как вы ловите N+1 у себя?