Внутренний объединенный подзапрос Django ORM эквивалент - PullRequest
4 голосов
/ 05 февраля 2020

У меня есть три связанных таблицы:

  • Модули
+-------------+-------------+------+-----+---------+-------+
| Field       | Type        | Null | Key | Default | Extra |
+-------------+-------------+------+-----+---------+-------+
| module_id   | int(11)     | NO   | PRI | NULL    |       |
+-------------+-------------+------+-----+---------+-------+
  • События
+------------------+--------------------------+------+-----+---------+----------------+
| Field            | Type                     | Null | Key | Default | Extra          |
+------------------+--------------------------+------+-----+---------+----------------+
| event_id         | int(11)                  | NO   | PRI | NULL    | auto_increment |
| event_time       | datetime(4)              | NO   |     | NULL    |                |
| module_id        | int(11)                  | NO   | MUL | NULL    |                |
| file_id          | int(11)                  | YES  | MUL | NULL    |                |
+------------------+--------------------------+------+-----+---------+----------------+
  • Файлы
+--------------+-----------------------------+------+-----+---------+----------------+
| Field        | Type                        | Null | Key | Default | Extra          |
+--------------+-----------------------------+------+-----+---------+----------------+
| file_id      | int(11)                     | NO   | PRI | NULL    | auto_increment |
| path         | varchar(512)                | NO   | UNI | NULL    |                |
+--------------+-----------------------------+------+-----+---------+----------------+

Итак, есть Модули , События и Файлы . (Неиспользуемые поля обрезаны из таблиц для упрощения).

Цель: Я хочу получить последнее событие, которое произошло на каждом модуле, и его путь к файлу .

Что я пробовал: Итак, для этого я сначала создал наивную реализацию на Django, используя подзапрос:

last_event_subquery = Event.objects.filter(
        module_id__module_id=OuterRef('module__id')
    ).order_by('-event_time', '-event_id')

modules = Module.objects.all().annotate(
        last_event_path=Subquery(last_event_subquery.values('file_id__path')[:1])
    ).annotate(
        last_event_id=Subquery(last_event_subquery.values('event_id')[:1])
    ).annotate(
        last_event_datetime=Subquery(last_event_subquery.values('event_time')[:1])
    )

Но я обнаружил, что запуск этого более 1 миллион записей в таблице событий явно медленно. Конечно, есть несколько индексов для оптимизации всего этого, но даже при этом я не смог найти комбинацию индексов, для запуска которой требуется менее 5 секунд, что слишком много imo . Затем, я увидел причину, эквивалентный запрос SQL слишком глуп:

SELECT `module`.`module_id`,
       (SELECT U2.`path` FROM `events` U0 LEFT OUTER JOIN `files` U2 ON (U0.`file_id` = U2.`file_id`)
       WHERE U0.`module_id` = (`modules`.`module_id`) ORDER BY U0.`event_time` DESC, U0.`event_id` DESC  LIMIT 1)
       AS `last_event_path`,
       (SELECT U0.`event_id` FROM `events` U0
       WHERE U0.`module_id` = (`modules`.`module_id`) ORDER BY U0.`event_time` DESC, U0.`event_id` DESC  LIMIT 1)
       AS `last_event_id`,
       (SELECT U0.`event_time` FROM `events` U0
       WHERE U0.`module_id` = (`modules`.`module_id`) ORDER BY U0.`event_time` DESC, U0.`event_id` DESC  LIMIT 1)
       AS `last_event_time` FROM `events`

Как видите, он повторяет подзапрос три раза.

Итак, я решил дать это была лучшая попытка, которую я мог сделать в SQL, и я изо всех сил пытался заставить работать следующее:

SELECT module.module_id,
       events.event_id,
       events.event_time,
       files.path
       FROM modules INNER JOIN events ON events.event_id =
           (SELECT events.event_id FROM events
            WHERE modules.module_id = events.module_id
                ORDER BY events.event_time DESC, events.event_id DESC LIMIT 1)
       INNER JOIN files ON files.file_id = events.file_id;

Это работает за 0,001 с . Итак, проблема в том, что я не могу сделать это на языке Django ORM. Конечно, я мог бы просто разместить необработанный запрос SQL, и все было бы готово, но как бы мне жить с таким позором?

Я исследовал всюду по Django документам, боролся со всеми вопросами переполнения стека , но я не мог найти ответ. Самое близкое, что я получил, было это , но проблема в том, что я не могу получить этот ограниченный один результат на модуль.

Я также попробовал FilteredRelation, но не смог получить соответствующий фильтр. Я также не могу использовать select_related (), потому что это обратное отношение с ForeignKey. Я также не могу использовать Different () с полем столбца, потому что я использую MySQL (точнее, MariaDB версии 10.3).

У вас есть какой-нибудь совет, как подойти к этому?

Спасибо!

1 Ответ

0 голосов
/ 07 февраля 2020

Итак, я нашел ответ сам:)

Он генерирует точно такой же SQL, какой я хотел, за исключением порядка предложений, что никак не влияет на запрос. Ключ должен был использовать .filter () с кораблем отношений. Использование подзапроса внутри фильтра () (который я не знал, было возможно) сделало трюк. Я был вдохновлен этим ответом .

last_event_subquery = Event.objects.filter(
    module_id__module_id=OuterRef('module_id')
).order_by('-event_time', '-event_id')
modules = Module.objects.filter(
    event__event_id=Subquery(
        last_event_subquery.values('module_id')[:1])
).values('id', 'event__event_id', 'event__event_time', 'event__file__path')

Что приводит к следующему SQL:

SELECT `modules`.`module_id`,
       `events`.`event_id`,
       `events`.`event_time`,
       `files`.`path`
       FROM `modules`
       INNER JOIN `events` ON (`modules`.`module_id` = `events`.`module_id`)
       LEFT OUTER JOIN `files` ON (`events`.`file_id` = `files`.`file_id`)
       WHERE `events`.`event_id` =
            (SELECT U0.`event_id` FROM `events` U0
            WHERE U0.`module_id` = (`modules`.`module_id`)
                 ORDER BY U0.`event_time` DESC, U0.`event_id` DESC  LIMIT 1)

Я надеюсь, что это полезно для всех остальных.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...