uchill/backend/apps/analytics/services_raw_sql.py

157 lines
8.1 KiB
Python
Raw Permalink 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.

"""
Пример оптимизации с сырым 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 недостаточна.