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 беше доста голям, тогава бих се надявал, че базата данни ще разглежда само листинг таблицата и след това ще използва индекс, за да извлече правилните части на 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, без да налагате друго запитване.

Сега вероятно трябва да попитам защо всичките ви полета са nullable. Това обикновено е ужасен избор на дизайн, особено за ForeignKeys. Ще имате ли някога обява, която не е свързана с кон? Ако не, отървете се от нулата. Ще имате ли някога кон, който няма да има име? Ако не, отървете се от нулата.

Така че отговорът е, правете това, което изглежда естествено през повечето време. Ако знаете, че дадена таблица ще бъде голяма, тогава трябва да проверите плановика на заявки (ОБЯСНЕТЕ), да разгледате добавянето/използването на индекси при условия за филтриране/съединяване или заявки от другата страна на отношение.

person Josh Smeaton    schedule 16.04.2015