Corp/backend/scripts/test_n_plus_one.py
lizerk 7dcc775b71 feat: денормализация unread_count для оптимизации чатов
- Добавлено поле unread_count в ChatReadState для кеширования счетчика
- Создана миграция 0030 с автоматическим пересчетом существующих данных
- Обновлен _auto_mark_read() для обнуления счетчика при прочтении
- Добавлен signal increment_unread_count_on_new_message для автоинкремента
- Упрощен get_queryset() - убраны подзапросы, используется Prefetch
- Обновлены сериализаторы для использования denormalized поля

Производительность: 4 SQL запроса вместо 100+ подзапросов
Решает проблему N+1 при загрузке списка чатов

Архитектура описана в backend/docs/architecture/UNREAD_COUNT_OPTIMIZATION.md
2026-03-10 18:09:06 +03:00

94 lines
3.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python
"""
Скрипт для демонстрации N+1 проблемы в текущей реализации
"""
import os
import sys
import django
from pathlib import Path
# Setup Django
sys.path.insert(0, str(Path(__file__).parent.parent))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
django.setup()
from django.contrib.auth import get_user_model
from django.db import connection, reset_queries
from django.db.models import Count, OuterRef, Subquery, Q
from django.db.models.functions import Coalesce
from communications.models import Chat, ChatReadState, ChatUserSettings, Message
from django.conf import settings
# Включаем отслеживание SQL
settings.DEBUG = True
Employee = get_user_model()
def test_current_implementation():
"""Текущая реализация с подзапросами"""
user = Employee.objects.first()
if not user:
print("Нет пользователей в базе")
return
print(f"Тестируем для пользователя: {user}")
print("=" * 80)
# Сбрасываем счетчик запросов
reset_queries()
# Текущая реализация из ChatViewSet.get_queryset()
read_state_subq = ChatReadState.objects.filter(
chat=OuterRef('pk'),
user=user
).values('last_read_message_id')[:1]
unread_count_subq = Message.objects.filter(
chat=OuterRef('pk'),
id__gt=Coalesce(Subquery(read_state_subq), 0),
is_deleted=False
).exclude(author=user).values('chat').annotate(
count=Count('id')
).values('count')
chats = Chat.objects.filter(
Q(participants=user) |
Q(department__in=user.departments_links.filter(
is_active=True
).values('department')) |
Q(include_all_employees=True)
).annotate(
unread_count=unread_count_subq
).distinct()[:10] # Берем первые 10 чатов
# Принудительно выполняем запрос
chat_list = list(chats)
print(f"\nЗагружено чатов: {len(chat_list)}")
print(f"Количество SQL запросов: {len(connection.queries)}")
print("\n" + "=" * 80)
# Показываем первые 3 запроса
for i, query in enumerate(connection.queries[:3], 1):
print(f"\nЗапрос #{i}:")
print(query['sql'][:500])
if len(query['sql']) > 500:
print("... (обрезано)")
print(f"Время: {query['time']}s")
if len(connection.queries) > 3:
print(f"\n... еще {len(connection.queries) - 3} запросов")
print("\n" + "=" * 80)
print("\nПРОБЛЕМА:")
print("Несмотря на то, что это ОДИН SQL запрос, внутри него есть")
print("ПОДЗАПРОСЫ (subqueries) для каждого чата в SELECT clause.")
print("PostgreSQL выполняет их для КАЖДОЙ строки результата.")
print("\nЭто не классический N+1, но похожая проблема производительности.")
return chat_list
if __name__ == '__main__':
test_current_implementation()