Помощь с RAW SQL запросом для сущностей amoCRM в Django проекте

Есть три базовых сущности в БД: Lead, LeadEvent и LeadStatus.
Lead - это сделка, LeadEvent - событие связанное с одной сделкой, LeadStatus - статус события.
На каждую сделку приходится множество событий - пересечений статуса.
Модели Django выглядят следующим образом:

class LeadStatus(models.Model):
    status_id = models.PositiveIntegerField(verbose_name='ID статуса', unique=True)
    name = models.CharField(max_length=100, verbose_name='Имя', null=True, default='Неизвестный')

class Lead(models.Model):
    lead_id = models.PositiveIntegerField(verbose_name="ID сделки", null=True, unique=True)
    created_date_time = models.DateTimeField(verbose_name="Время и дата создания сделки", null=True)
    price = models.IntegerField(verbose_name='Бюджет сделки')
    current_status = models.ForeignKey(LeadStatus, on_delete=models.SET_NULL, verbose_name='Текущий статус', null=True)


class LeadEvent(models.Model):
    note_id = models.CharField(verbose_name="ID примечания", null=True, unique=True, max_length=64)
    lead = models.ForeignKey(Lead, on_delete=models.CASCADE)
    old_status = models.ForeignKey(LeadStatus, related_name='old_status', null=True, blank=True,
                                   on_delete=models.CASCADE)
    new_status = models.ForeignKey(LeadStatus, null=True, related_name='new_status', blank=True,
                                   on_delete=models.CASCADE)
    crossing_status_date_time = models.DateTimeField(verbose_name="Время и дата пересечения статуса", null=True)

Имеются 2 основных интересующих группы сделок: сделки-прибыль(is_profitt) и сделки-потери(is_loss) для которых свои статусы пересечения ( например 142 и 143 соответственно)

Задача звучит так: найти для каждой сделки (Lead) 1 событие пересечения ( LeadEvent ), которое удовлетворяет условию:
1) Пересечение должно быть строго по определенным статусам (псевдо-код запроса: old_status__id__not_in=[statuses_list] and new_status__id__in=[statuses_list] )
2) Пересечение если имеется, должно быть самым первым по дате пересечения (crossing_status_date_time) для данной сделки ( Lead )

И еще одна дополнительная подгруппа: сделки которые сразу в статусе (is_in_status):
3) У такой сделки (Lead) у самого первого по дате пересечения статуса (LeadEvent) старый статус является подходящим (old_status__id__in=[statuses_list])

Эти группы должны формироваться одним (под) запросом. На языке ORM Django эта конструкция выглядит следующим образом:

class LeadSubqueryExpression:
    """
    Класс для формирования подзапросов к лидам
    Для выборки пересечений по статусам
    """

    def __init__(self, filter_query: Q):
        self.filter_query = filter_query

    def get_expression(self):
        """ Получение выражения запроса на основе фильтра """
        # на одну аннотацию Django  - один обьект
        # в данном случае типа json

        json_notes = JsonBuildObject(
            Value('created_date_time'), F('crossing_status_date_time'),
            Value('status_name'), Concat(F('old_status__name'),
                                    Value(' -> '),
                                    F('new_status__name'),
                                    output_field=CharField()),
            Value('status_color'), Concat(F('old_status__color'),
                                   Value(' -> '),
                                   F('new_status__color'),
                                   output_field=CharField())
        )
        subquery = (
            LeadsNote
                .objects
                .select_related('old_status', 'new_status', 'company')
                .filter(self.filter_query, lead=OuterRef('pk'))
                .order_by('lead', 'crossing_status_date_time')
                .annotate(status_notes=json_notes)
                .distinct('lead')
                .values('status_notes')
        )
        return subquery


class LeadReportQuerySet(models.QuerySet):
    """ Последовательность данных по лидам """

    def annotate_for_report(
            self,
            loss_filter: Q,
            profit_filter: Q,
            in_status_filter: Q
    ):
        """ Аннотация последовательности """

        # включаем в аннотацию подзапросы по пересечениям статусов
        # для дохода, потери и сделок сразу в статусе
        # с целью последующей фильтрации
        profit_subquery = LeadSubqueryExpression(filter_query=profit_filter).get_expression()
        loss_subquery = LeadSubqueryExpression(filter_query=loss_filter).get_expression()
        in_status_subquery = LeadSubqueryExpression(filter_query=in_status_filter).get_expression()

        first_date_subquery = (
            LeadsNote
                .objects
                .filter(lead=OuterRef('pk'))
                .order_by('crossing_status_date_time')
                .values('crossing_status_date_time')[:1]
        )

        return (self
                    .annotate(
                        is_in_status=Exists(in_status_subquery),
                        is_loss=Exists(loss_subquery),
                        is_profit=Exists(profit_subquery))
                    .annotate(
                        status_notes=Case(
                            When(is_profit=True, then=profit_subquery),
                            When(is_loss=True, then=loss_subquery),
                            When(Q(is_profit=False) & Q(is_loss=False) & Q(is_in_status=True), then=in_status_subquery),
                            output_field=JSONField()
                        ))
                    .annotate(created_date_time=Cast(KeyTextTransform('created_date_time', 'status_notes'),
                                                     output_field=DateTimeField()),
                              status_name=Cast(KeyTextTransform('status_name', 'status_notes'),
                                                     output_field=CharField()),
                              status_color=Cast(KeyTextTransform('status_color', 'status_notes'),
                                               output_field=CharField()),
                              )
                    .annotate(
                        is_profit=Case(
                            When(Q(is_profit=False) & Q(is_loss=False) & Q(is_in_status=True), then=Value(True)),
                            default=F('is_profit'), output_field=BooleanField()
        )))

Запрос отрабатывает, но делает это медленно. Нужна помощь в переводе запроса на чистый SQL.


Ответы (0 шт):