2013-03-01 7 views
7

Załóżmy, że chcę wyświetlić listę biegaczy zamówioną przez ich ostatni czas sprintu.Django: Zamawianie zestawu zapytań na podstawie najnowszego pola modeli podrzędnych

class Runner(models.Model): 
    name = models.CharField(max_length=255) 

class Sprint(models.Model): 
    runner = models.ForeignKey(Runner) 
    time = models.PositiveIntegerField() 
    created = models.DateTimeField(auto_now_add=True) 

Jest to szybki szkic tego, co chciałbym zrobić w SQL:

SELECT runner.id, runner.name, sprint.time 
FROM runner 
LEFT JOIN sprint ON (sprint.runner_id = runner.id) 
WHERE 
    sprint.id = (
    SELECT sprint_inner.id 
    FROM sprint as sprint_inner 
    WHERE sprint_inner.runner_id = runner.id 
    ORDER BY sprint_inner.created DESC 
    LIMIT 1 
) 
    OR sprint.id = NULL 
ORDER BY sprint.time ASC 

W Django QuerySet documentation Zjednoczone:

Dopuszczalne jest określenie boiska wielofunkcyjnego o wartościach do nakazywania wyników według (na przykład pola ManyToManyField). Zwykle nie jest to rozsądna rzecz do zrobienia i jest to naprawdę zaawansowana funkcja użytkowania. Jeśli jednak wiesz, że twoje filtrowanie lub dostępne dane z zestawu zapytań oznacza, że ​​będzie tylko jeden element zamówienia dla każdego z głównych pozycji, które wybierzesz, kolejność może dokładnie wynosić , co chcesz zrobić. Skorzystaj z porządkowania na polach wielowartościowych z ostrożnością i sprawdź, czy wyniki są zgodne z oczekiwaniami.

Chyba muszę zastosować jakiś filtr tutaj, ale nie jestem pewien, co dokładnie Django oczekuje ...

Jedna uwaga, ponieważ nie jest oczywiste w tym przykładzie: tabela Runner będzie mieć kilka sto wpisów, sprintów będzie również kilkaset, aw niektóre dni później prawdopodobnie kilka tysięcy wpisów. Dane będą wyświetlane w widoku stronicowanym, więc sortowanie w Pythonie nie jest opcją.

Jedyną inną możliwością, którą widzę jest pisanie samego SQL, ale chciałbym tego uniknąć za wszelką cenę

Odpowiedz

2

Nie sądzę, istnieje sposób, aby to zrobić za pośrednictwem ORM tylko jednego zapytania, można pobrać listę biegaczy i skorzystać annotate dodać swoje najnowsze id Sprint - następnie filtrować i zamówić te sprinty .

>>> from django.db.models import Max 

# all runners now have a `last_race` attribute, 
# which is the `id` of the last sprint they ran 
>>> runners = Runner.objects.annotate(last_race=Max("sprint__id")) 

# a list of each runner's last sprint ordered by the the sprint's time, 
# we use `select_related` to limit lookup queries later on 
>>> results = Sprint.objects.filter(id__in=[runner.last_race for runner in runners]) 
...       .order_by("time") 
...       .select_related("runner") 

# grab the first result 
>>> first_result = results[0] 

# you can access the runner's details via `.runner`, e.g. `first_result.runner.name` 
>>> isinstance(first_result.runner, Runner) 
True 

# this should only ever execute 2 queries, no matter what you do with the results 
>>> from django.db import connection 
>>> len(connection.queries) 
2 

Jest to dość szybkie i nadal będzie korzystać z indeksów baz danych i pamięci podręcznej.

Kilka tysięcy zapisów to nie wszystko, to powinno działać całkiem dobrze dla tego rodzaju liczb. Jeśli zaczniesz napotykać problemy, proponuję ukąszenie bulletu i użycie surowego SQL.

+0

Czy to nie powoduje stosunkowo wysokiego zużycia pamięci? O ile widzę, wciąga co najmniej każdego biegacza do pamięci i buduje raczej dużą listę swoich identyfikatorów sprintu. Robiąc to na każdym widoku strony z kilkuset biegaczami w DB, czuję się * trochę * niewygodny. To jest przypadek, w którym buforowanie się rozpoczyna. – Strayer

+1

Po przetestowaniu tego z 10 000 uczestników, zużyto mniej niż 10 MB (3 MB faktycznie ...) pamięci RAM. Jeśli uważasz, że będziesz potrzebować czegoś więcej, naprawdę powinieneś używać surowego SQL. Jak zawsze najlepszym sposobem na to jest profilowanie - nie spekuluj. Przedwczesna optymalizacja i wszystko to ... – Matt

+0

A kilkaset płyt naprawdę nie jest dużo ... na pewno nie na tyle, aby uzasadnić obawy dotyczące optymalizacji wydajności. Kilkaset tysięcy rekordów jest zwykle tam, gdzie zaczynasz o tym myśleć, a nawet wtedy zwykle nie stanowi to problemu (wrzucanie do indeksu lub dwóch i jest rozwiązany). – Matt

0
def view_name(request): 
    spr = Sprint.objects.values('runner', flat=True).order_by(-created).distinct() 
    runners = [] 
    for s in spr: 
     latest_sprint = Sprint.objects.filter(runner=s.runner).order_by(-created)[:1] 
     for latest in latest_sprint: 
      runners.append({'runner': s.runner, 'time': latest.time}) 

    return render(request, 'page.html', { 
      'runners': runners, 
    }) 


{% for runner in runners %} 
    {{runner.runner}} - {{runner.time}} 
{% endfor %} 
+0

Problem nie polega na pobraniu najnowszego sprintu, ale zamówienie zestawu QuerySet Runner przez jego ostatnie pole 'time'. – Strayer

+0

To działa, tak. Problem polega na tym, że powoduje to porządkowanie biegaczy w aplikacji, co powoduje przynajmniej duże zużycie pamięci i stosunkowo wysokie użycie procesora. Zobacz zaktualizowane pytanie dotyczące rozmiarów tabel. Kolejny problem z tym podejściem polega na tym, że nie będzie on pokazywał żadnych biegaczy, którzy nie mają sprintu w ogóle. Chociaż można to również rozwiązać w kodzie Pythona, jest to idealna praca dla bazy danych, ponieważ może wykorzystywać jej indeksy i pamięci podręczne. To działa dla małych baz danych, ale nasz SysAdmin zabiłby mnie, gdybym zrobił to w ten sposób;) – Strayer

+0

hmmm ... to jest trudne. I jesteśmy tacy sami, jestem ostrożny w kodowaniu, jeśli chodzi o moją pracę z powodu oczekiwań mojego pracodawcy. :) – catherine