Django оптимизация наборов запросов - предотвращение выбора аннотированных полей - PullRequest
2 голосов
/ 23 января 2020

Допустим, у меня есть следующие модели:

class Invoice(models.Model):
    ...

class Note(models.Model):
    invoice = models.ForeignKey(Invoice, related_name='notes', on_delete=models.CASCADE)
    text = models.TextField()

, и я хочу выбрать Счета с некоторыми примечаниями . Я написал бы, используя annotate / Exists, например:

Invoice.objects.annotate(
    has_notes=Exists(Note.objects.filter(invoice_id=OuterRef('pk')))
).filter(has_notes=True)

Это работает достаточно хорошо, фильтрует только Счета с примечаниями. Однако этот метод приводит к тому, что в результате запроса присутствует поле , которое мне не нужно и означает худшую производительность (SQL должен выполнить подзапрос 2 раза).

Я понимаю, что мог бы написать это, используя extra(where=) примерно так:

Invoice.objects.extra(where=['EXISTS(SELECT 1 FROM note WHERE invoice_id=invoice.id)'])

, что привело бы к идеальному SQL, но в целом это не рекомендуется использовать extra / raw SQL. Есть ли лучший способ сделать это?

Ответы [ 4 ]

2 голосов
/ 23 января 2020

Мы можем отфильтровать по Invoice с, которые, когда мы выполняем LEFT OUTER JOIN, не NULL как Note, и сделать запрос отличным (чтобы не возвращать один и тот же Invoice дважды).

Invoice.objects.<b>filter(notes__isnull=False).distinct()</b>
1 голос
/ 03 февраля 2020

Вы можете удалить аннотации из предложения SELECT, используя .values() метод набора запросов. Проблема с .values() состоит в том, что вы должны перечислять все имена, которые хотите сохранить, вместо имен, которые хотите пропустить, а .values() возвращает словари вместо экземпляров модели.

Django внутренне отслеживает удаленных аннотаций в QuerySet.query.annotation_select_mask. Таким образом, вы можете использовать его, чтобы сообщить Django, какие аннотации пропустить даже без .values():

class YourQuerySet(QuerySet):
    def mask_annotations(self, *names):
        if self.query.annotation_select_mask is None:
            self.query.set_annotation_mask(set(self.query.annotations.keys()) - set(names))
        else:
            self.query.set_annotation_mask(self.query.annotation_select_mask - set(names))
        return self

Затем вы можете написать:

invoices = (Invoice.objects
  .annotate(has_notes=Exists(Note.objects.filter(invoice_id=OuterRef('pk'))))
  .filter(has_notes=True)
  .mask_annotations('has_notes')
)

, чтобы пропустить has_notes из предложение SELECT и все еще получает отфильтрованные экземпляры счетов. Результирующий запрос SQL будет выглядеть примерно так:

SELECT invoice.id, invoice.foo FROM invoice
WHERE EXISTS(SELECT note.id, note.bar FROM notes WHERE note.invoice_id = invoice.id) = True

Просто обратите внимание, что annotation_select_mask - это внутренний Django API, который может изменяться в будущих версиях без предупреждения.

1 голос
/ 23 января 2020

Хорошо, я только что заметил в Django 3.0 документах , что они обновили работу Exists и могут использоваться непосредственно в filter:

Invoice.objects.filter(Exists(Note.objects.filter(invoice_id=OuterRef('pk'))))

Это гарантирует, что подзапрос не будет добавлен в столбцы SELECT , что может привести к повышению производительности.

Изменено в Django 3.0:

В предыдущих версиях Django необходимо было сначала аннотировать, а затем фильтровать по аннотации. Это приводило к тому, что аннотированное значение всегда присутствовало в результате запроса, и часто приводило к запросу, выполнение которого занимало больше времени.

Тем не менее, если кто-то знает лучший способ для Django 1.11, Я буду признателен. Нам действительно нужно обновить: (

0 голосов
/ 24 января 2020

Лучше всего оптимизировать код, если вы хотите получить данные из другой таблицы, ссылка на первичный ключ которой хранится в другой таблице Invoice.objects.filter (note__invoice_id = OuterRef ('pk'),)

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