""" Пример оптимизации с сырым SQL для самых медленных запросов. Использовать только если ORM оптимизация недостаточна. """ from django.db import connection from django.db.models import Count, Sum, Avg, Q from decimal import Decimal def get_detailed_lesson_stats_raw_sql(mentor, start_date, end_date): """ Оптимизированная версия с сырым SQL. Использовать только если get_detailed_lesson_stats все еще медленный. Выигрыш: ~30-50% быстрее за счет одного сложного запроса вместо множественных. """ with connection.cursor() as cursor: # Один большой запрос вместо множественных cursor.execute(""" WITH lesson_stats AS ( SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE status = 'completed') as completed, COUNT(*) FILTER (WHERE status = 'cancelled') as cancelled, SUM(duration) FILTER (WHERE status = 'completed') as total_duration, AVG(duration) FILTER (WHERE status = 'completed') as avg_duration, SUM(price) FILTER (WHERE status = 'completed' AND price IS NOT NULL) as total_revenue, AVG(price) FILTER (WHERE status = 'completed' AND price IS NOT NULL) as avg_price, AVG(mentor_grade) FILTER (WHERE status = 'completed' AND mentor_grade IS NOT NULL) as avg_grade, MAX(mentor_grade) FILTER (WHERE status = 'completed' AND mentor_grade IS NOT NULL) as max_grade, MIN(mentor_grade) FILTER (WHERE status = 'completed' AND mentor_grade IS NOT NULL) as min_grade, SUM(price) FILTER (WHERE status = 'cancelled' AND price IS NOT NULL) as cancelled_revenue, COUNT(*) FILTER (WHERE status = 'completed' AND mentor_grade IS NOT NULL) as graded_count FROM lessons WHERE mentor_id = %s AND start_time >= %s AND start_time <= %s ), by_hour_stats AS ( SELECT EXTRACT(HOUR FROM start_time AT TIME ZONE 'UTC')::int as hour, COUNT(*) as count FROM lessons WHERE mentor_id = %s AND start_time >= %s AND start_time <= %s AND status = 'completed' GROUP BY hour ), time_of_day_stats AS ( SELECT COUNT(*) FILTER (WHERE EXTRACT(HOUR FROM start_time AT TIME ZONE 'UTC') >= 6 AND EXTRACT(HOUR FROM start_time AT TIME ZONE 'UTC') < 12) as morning, COUNT(*) FILTER (WHERE EXTRACT(HOUR FROM start_time AT TIME ZONE 'UTC') >= 12 AND EXTRACT(HOUR FROM start_time AT TIME ZONE 'UTC') < 18) as afternoon, COUNT(*) FILTER (WHERE EXTRACT(HOUR FROM start_time AT TIME ZONE 'UTC') >= 18 AND EXTRACT(HOUR FROM start_time AT TIME ZONE 'UTC') < 24) as evening, COUNT(*) FILTER (WHERE EXTRACT(HOUR FROM start_time AT TIME ZONE 'UTC') >= 0 AND EXTRACT(HOUR FROM start_time AT TIME ZONE 'UTC') < 6) as night FROM lessons WHERE mentor_id = %s AND start_time >= %s AND start_time <= %s AND status = 'completed' ), top_clients_stats AS ( SELECT c.id as client_id, u.first_name, u.last_name, u.email, COUNT(l.id) as lessons_count, COUNT(l.id) FILTER (WHERE l.status = 'completed') as completed_count, SUM(l.price) FILTER (WHERE l.status = 'completed' AND l.price IS NOT NULL) as total_revenue, AVG(l.mentor_grade) FILTER (WHERE l.status = 'completed' AND l.mentor_grade IS NOT NULL) as avg_grade FROM lessons l JOIN clients c ON l.client_id = c.id JOIN users u ON c.user_id = u.id WHERE l.mentor_id = %s AND l.start_time >= %s AND l.start_time <= %s GROUP BY c.id, u.first_name, u.last_name, u.email ORDER BY lessons_count DESC LIMIT 10 ) SELECT (SELECT row_to_json(s) FROM lesson_stats s) as stats, (SELECT json_agg(row_to_json(b)) FROM by_hour_stats b) as by_hour, (SELECT row_to_json(t) FROM time_of_day_stats t) as time_of_day, (SELECT json_agg(row_to_json(tc)) FROM top_clients_stats tc) as top_clients """, [ mentor.id, start_date, end_date, # lesson_stats mentor.id, start_date, end_date, # by_hour_stats mentor.id, start_date, end_date, # time_of_day_stats mentor.id, start_date, end_date, # top_clients_stats ]) row = cursor.fetchone() stats_data = row[0] by_hour_data = row[1] or [] time_of_day_data = row[2] or {} top_clients_data = row[3] or [] # Форматируем результат stats = stats_data.get('stats', {}) by_hour_dict = {item['hour']: item['count'] for item in by_hour_data} return { 'summary': { 'total_lessons': stats.get('total', 0), 'completed': stats.get('completed', 0), 'cancelled': stats.get('cancelled', 0), 'completion_rate': round( (stats.get('completed', 0) / stats.get('total', 1) * 100) if stats.get('total', 0) > 0 else 0, 1 ), }, 'time': { 'total_duration_minutes': stats.get('total_duration', 0) or 0, 'total_duration_hours': round((stats.get('total_duration', 0) or 0) / 60, 1), 'average_duration_minutes': round(stats.get('avg_duration', 0) or 0, 1), 'by_hour': [ {'hour': hour, 'count': by_hour_dict.get(hour, 0)} for hour in range(24) ], 'by_time_of_day': { 'morning': time_of_day_data.get('morning', 0) or 0, 'afternoon': time_of_day_data.get('afternoon', 0) or 0, 'evening': time_of_day_data.get('evening', 0) or 0, 'night': time_of_day_data.get('night', 0) or 0, }, }, 'revenue': { 'total': float(stats.get('total_revenue', 0) or 0), 'average_per_lesson': float(stats.get('avg_price', 0) or 0), 'potential_revenue': float(stats.get('cancelled_revenue', 0) or 0), }, 'grades': { 'average': round(stats.get('avg_grade', 0) or 0, 1), 'max': stats.get('max_grade', 0) or 0, 'min': stats.get('min_grade', 0) or 0, 'graded_count': stats.get('graded_count', 0) or 0, }, 'top_clients': [ { 'name': f"{item['first_name']} {item['last_name']}".strip() or item['email'], 'email': item['email'], 'lessons_count': item['lessons_count'], 'completed_count': item['completed_count'], 'total_revenue': float(item['total_revenue'] or 0), 'average_grade': round(item['avg_grade'] or 0, 1), } for item in top_clients_data ], } # ВАЖНО: Использовать только после измерения производительности! # Сначала убедитесь, что оптимизация ORM недостаточна.