Внутренние соединения Django ORM и SQL

Я пытаюсь получить все объекты Horse, которые попадают в определенный диапазон from_date и to_date для связанного объекта списка. например.

Horse.objects.filter(listings__to_date__lt=to_date.datetime,
listings__from_date__gt=from_date.datetime)

Теперь, насколько я понимаю, этот запрос к базе данных создает внутреннее соединение, которое затем позволяет мне найти все мои объекты лошадей на основе соответствующих дат листинга.

Мой вопрос в том, как именно это работает, вероятно, это сводится к серьезному непониманию того, как на самом деле работают внутренние соединения. Должен ли этот запрос сначала «проверить» каждый объект-лошадь, чтобы выяснить, есть ли у него связанный объект списка? Я полагаю, что это может оказаться довольно неэффективным, потому что у вас может быть 5 миллионов объектов-лошадей без связанных объектов списка, но вам все равно придется сначала проверять каждый?

В качестве альтернативы я мог бы начать со своих списков и сначала сделать что-то вроде этого:

Listing.objects.filter(to_date__lt=to_date.datetime, 
from_date__gt=from_date.datetime)

А потом:

for listing in listing_objs:
    if listing.horse:
        horses.append(horse)

Но это тоже кажется довольно странным способом достижения моих результатов.

Если бы кто-нибудь мог помочь мне понять, как работают запросы в Django и какой способ выполнения такого запроса является наиболее эффективным, это было бы большим подспорьем!

Это моя текущая настройка модели:

class Listing(models.Model):

    to_date = models.DateTimeField(null=True, blank=True)
    from_date = models.DateTimeField(null=True, blank=True)
    promoted_to_date = models.DateTimeField(null=True, blank=True)
    promoted_from_date = models.DateTimeField(null=True, blank=True)

    # Relationships
    horse = models.ForeignKey('Horse', related_name='listings', null=True, blank=True)

class Horse(models.Model):
    created_date = models.DateTimeField(null=True, blank=True, auto_now=True)
    type = models.CharField(max_length=200, null=True, blank=True)
    name = models.CharField(max_length=200, null=True, blank=True)
    age = models.IntegerField(null=True, blank=True)
    colour = models.CharField(max_length=200, null=True, blank=True)
    height = models.IntegerField(null=True, blank=True)

person Zac    schedule 16.04.2015    source источник


Ответы (1)


То, как вы пишете свой запрос, действительно зависит от того, какую информацию вы хотите получить в большинстве случаев. Если вас интересуют лошади, запросите у Horse. Если вас интересуют списки, вам следует запросить Listing. Как правило, это правильно, особенно когда вы работаете с простыми внешними ключами.

Ваш первый запрос, вероятно, лучше подходит для Django. Я использовал несколько более простые модели, чтобы проиллюстрировать различия. Я создал поле active, а не использовал дату и время.

In [18]: qs = Horse.objects.filter(listings__active=True)

In [19]: print(qs.query)
SELECT 
"scratch_horse"."id", 
"scratch_horse"."name" 
FROM "scratch_horse" 
INNER JOIN "scratch_listing" 
ON ( "scratch_horse"."id" = "scratch_listing"."horse_id" ) 
WHERE "scratch_listing"."active" = True

Внутреннее соединение в приведенном выше запросе гарантирует, что вы получите только лошадей, которые есть в списке. (Большинство) баз данных очень хорошо используют объединения и индексы для фильтрации нежелательных строк.

Если бы Listing было очень маленьким, а Horse довольно большим, то я надеялся бы, что база данных будет смотреть только на таблицу Listing, а затем использовать индекс для получения правильных частей Horse без выполнения полного сканирования таблицы (проверки каждой лошади). Вам нужно будет запустить запрос и проверить, что делает ваша база данных. EXPLAIN (или любая другая база данных, которую вы используете) чрезвычайно полезна. Если вы предполагаете, что делает база данных, вы, вероятно, ошибаетесь.

Обратите внимание, что если вам нужно получить доступ к listings каждого horse, то вы будете выполнять другой запрос каждый раз, когда вы получаете доступ к horse.listings. prefetch_related может помочь вам, если вам нужен доступ к listings , выполнив один запрос и сохранив его в кеше.

Теперь ваш второй запрос:

In [20]: qs = Listing.objects.filter(active=True).select_related('horse')

In [21]: print(qs.query)
SELECT 
"scratch_listing"."id", 
"scratch_listing"."active", 
"scratch_listing"."horse_id", 
"scratch_horse"."id", 
"scratch_horse"."name" 
FROM "scratch_listing" 
LEFT OUTER JOIN "scratch_horse" 
ON ( "scratch_listing"."horse_id" = "scratch_horse"."id" ) 
WHERE "scratch_listing"."active" = True

Это делает ЛЕВОЕ соединение, что означает, что правая сторона может содержать NULL. В данном случае правая сторона Horse. Это было бы очень плохо, если бы у вас было много списков без лошади, потому что это вернет каждый активный список, независимо от того, была ли с ним связана лошадь. Вы можете исправить это с помощью .filter(active=True, horse__isnull=False).

Обратите внимание, что я использовал select_related, который объединяет таблицы, так что вы можете получить доступ к listing.horse, не вызывая другого запроса.

Теперь я, вероятно, должен спросить, почему все ваши поля могут быть нулевыми. Обычно это ужасный выбор дизайна, особенно для ForeignKeys. Будет ли у вас когда-нибудь объявление, не связанное с лошадью? Если нет, избавьтесь от нуля. Будет ли у вас когда-нибудь лошадь без имени? Если нет, избавьтесь от нуля.

Поэтому ответ таков: делайте то, что кажется естественным большую часть времени. Если вы знаете, что конкретная таблица будет большой, вы должны проверить планировщик запросов (EXPLAIN), изучить добавление/использование индексов для условий фильтрации/соединения или выполнить запрос с другой стороны связь.

person Josh Smeaton    schedule 16.04.2015