2010-11-19 19 views
72

Tabela zawiera około dziesięć milionów wierszy.Dlaczego iterowanie przez duży Django QuerySet pochłania ogromne ilości pamięci?

for event in Event.objects.all(): 
    print event 

Powoduje to, że zużycie pamięci stale rośnie do 4 GB lub mniej, w którym to momencie wiersze są drukowane szybko. Długie opóźnienie przed pierwszym wydrukiem zaskoczyło mnie - spodziewałem się, że wydrukuje się niemal natychmiast.

Próbowałem również Event.objects.iterator(), który zachowywał się w taki sam sposób.

Nie rozumiem, co Django ładuje do pamięci lub dlaczego to robi. Spodziewałem się, że Django przejdzie przez wyniki na poziomie bazy danych, co oznacza, że ​​wyniki będą drukowane w przybliżeniu na stałym poziomie (a nie wszystkie od razu po długim oczekiwaniu).

Co źle zrozumiałem?

(nie wiem, czy to istotne, ale używam PostgreSQL).

+3

Na mniejszych maszynach może to nawet spowodować razu "Zabity" do powłoki django lub serwera – Stefano

Odpowiedz

78

Nate C był blisko, ale nie do końca.

Od the docs:

Można ocenić QuerySet w jeden z następujących sposobów:

  • iteracji. QuerySet jest iterowalne i po pierwszym wykonaniu iteracji wykonuje zapytanie do bazy danych. Na przykład, to będzie drukować nagłówek wszystkich wpisów w bazie danych:

    for e in Entry.objects.all(): 
        print e.headline 
    

więc dziesięć milionów wiersze są pobierane, wszystko na raz, kiedy po raz pierwszy wprowadzić tę pętlę i dostać formularz iteracji zestawu zapytań. Czekanie, którego doświadczasz, polega na tym, że Django ładuje wiersze bazy danych i tworzy obiekty dla każdego z nich, zanim zwróci coś, co możesz faktycznie powtórzyć. Wtedy masz wszystko w pamięci, a wyniki się rozlewają.

Z mojego czytania dokumentów, iterator() nic więcej nie obchodzi wewnętrznych mechanizmów buforowania QuerySet.Wydaje mi się, że sensowne byłoby zrobienie pojedynczej rzeczy, ale w odwrotnym przypadku wymagałoby to dziesięciu milionów pojedynczych trafień w bazie danych. Może nie wszystko, co jest pożądane.

Iteracja na dużych zbiorach danych efektywnie jest coś, czego jeszcze nie dostał całkiem dobrze, ale istnieją pewne fragmenty tam może się okazać przydatny dla swoich celów:

+1

Dzięki za wielką odpowiedzi, @eternicode. W końcu spadliśmy do surowego SQL dla pożądanej iteracji na poziomie bazy danych. – davidchambers

+2

@eternicode Dobra odpowiedź, po prostu naciśnij ten problem. Czy od tego czasu istnieje jakakolwiek powiązana aktualizacja w Django? –

+0

Nadal MIA: Wersja, która robi to za pomocą kursorów, więc elementy nie są pomijane .... – mlissner

6

To z docs: http://docs.djangoproject.com/en/dev/ref/models/querysets/

brak aktywności w bazie faktycznie występuje aż coś zrobić, aby oceń zestaw zapytań.

Kiedy uruchamiane jest print event, uruchamiane jest zapytanie (które jest pełnym skanowaniem tabeli zgodnie z poleceniem użytkownika) i ładuje wyniki. Twoja prośba o wszystkie przedmioty i nie ma sposobu na zdobycie pierwszego obiektu bez zdobycia ich wszystkich.

Ale jeśli zrobisz coś takiego:

Event.objects.all()[300:900] 

http://docs.djangoproject.com/en/dev/topics/db/queries/#limiting-querysets

Następnie doda offsety i ograniczenia do SQL wewnętrznie.

5

Dla dużych ilości rekordów, database cursor działa jeszcze lepiej. Potrzebujesz surowego SQL w Django, kursor Django jest czymś innym niż kursor SQL.

LIMIT - OFFSET metoda zaproponowana przez Nate C może być wystarczająco dobre dla danej sytuacji. W przypadku dużych ilości danych jest wolniejszy niż kursor, ponieważ musi wielokrotnie powtarzać to samo zapytanie i musi przeskakiwać coraz więcej wyników.

+3

Frank, to zdecydowanie dobry punkt, ale byłoby miło zobaczyć pewne szczegóły kodu, aby skierować je w stronę rozwiązania ;-) (dobrze to pytanie jest dość stary ...) – Stefano

25

może nie być szybciej lub najbardziej wydajne, ale jako gotowe rozwiązanie, dlaczego nie skorzystać paginator Django rdzeń i obiektów stronę udokumentowane tutaj:

https://docs.djangoproject.com/en/dev/topics/pagination/

coś takiego:

from django.core.paginator import Paginator 
from djangoapp.models import model 

paginator = Paginator(model.objects.all(), 1000) # chunks of 1000, you can 
               # change this to desired chunk size 

for page in range(1, paginator.num_pages + 1): 
    for row in paginator.page(page).object_list: 
     # here you can do whatever you want with the row 
    print "done processing page %s" % page 
+0

Teraz możliwe są niewielkie ulepszenia od czasu ogłoszenia. 'Paginator' ma teraz właściwość [' page_range'] (https://docs.djangoproject.com/en/dev/topics/pagination/#django.core.paginator.Paginator.page_range), aby uniknąć podstaw. Jeśli w poszukiwaniu minimalnego obciążenia pamięci, możesz użyć ['object_list.iterator()', która nie zapełni bufora zestawów zapytań] (https://docs.djangoproject.com/en/dev/ref/models/querysets /#django.db.models.query.QuerySet.iterator). ["prefetch_related_objects'] (https://docs.djangoproject.com/en/1.10/ref/models/querysets/#prefetch-related-objects) jest wówczas wymagane do pobrania z prefiksem –

5

Django nie ma dobrego rozwiązania do pobierania dużych elementów z bazy danych.

import gc 
# Get the events in reverse order 
eids = Event.objects.order_by("-id").values_list("id", flat=True) 

for index, eid in enumerate(eids): 
    event = Event.object.get(id=eid) 
    # do necessary work with event 
    if index % 100 == 0: 
     gc.collect() 
     print("completed 100 items") 

values_list można stosować aby pobrać wszystkie identyfikatory w bazach danych, a następnie pobierania każdego obiektu oddzielnie. Z czasem duże obiekty zostaną utworzone w pamięci i nie będą zbierane śmieci aż do zakończenia pętli. Powyższy kod powoduje ręczne usuwanie śmieci po zużyciu co setnego elementu.

+0

Czy streamingHttpResponse może być rozwiązaniem? http://stackoverflow.com/questions/15359768/django-1-5-using-the-new-streaminghttpresponse – ratata

+1

Jednak spowoduje to równe trafienia w bazie danych jako liczbę pętli, boję się. – raratiru

4

ponieważ w ten sposób przedmioty dla całego queryset ładowane do pamięci na raz. Musisz zgrupować swój zestaw zapytań na mniejsze przysłówki. Wzór do zrobienia nazywa się karmieniem łyżeczką. Oto krótka implementacja.

def spoonfeed(qs, func, chunk=1000, start=0): 
    ''' Chunk up a large queryset and run func on each item. 

    Works with automatic primary key fields. 

    chunk -- how many objects to take on at once 
    start -- PK to start from 

    >>> spoonfeed(Spam.objects.all(), nom_nom) 
    ''' 
    while start < qs.order_by('pk').last().pk: 
     for o in qs.filter(pk__gt=start, pk__lte=start+chunk): 
      func(o) 
     start += chunk 

W tym celu użyć można napisać funkcję, która robi operacje na obiekcie:

def set_population_density(town): 
    town.population_density = calculate_population_density(...) 
    town.save() 

i niż uruchomić tę funkcję na swoim queryset:

spoonfeed(Town.objects.all(), set_population_density) 

ten można jeszcze poprawić na w trybie wieloprocesowym do wykonania func na wielu obiektach równolegle.

16

domyślne zachowanie Django jest buforowanie cały wynik queryset gdy ocenia zapytania. Można użyć metody iteracyjnej w queryset by uniknąć tego buforowanie:

for event in Event.objects.all().iterator(): 
    print event 

https://docs.djangoproject.com/en/dev/ref/models/querysets/#iterator

Metoda iterator() ocenia queryset a następnie odczytuje wyniki bezpośrednio bez robienia buforowanie na poziomie queryset.Ta metoda zapewnia lepszą wydajność i znaczną redukcję pamięci podczas iteracji na dużej liczbie obiektów, do których dostęp ma tylko jeden raz. Należy pamiętać, że buforowanie jest nadal wykonywane na poziomie bazy danych.

Użycie iteratora() zmniejsza zużycie pamięci, ale jest nadal wyższe niż się spodziewałem. Używanie metody paginatora sugerowanej przez mpaf zużywa znacznie mniej pamięci, ale jest o 2-3 razy wolniejsze w przypadku mojego testu.

from django.core.paginator import Paginator 

def chunked_iterator(queryset, chunk_size=10000): 
    paginator = Paginator(queryset, chunk_size) 
    for page in range(1, paginator.num_pages + 1): 
     for obj in paginator.page(page).object_list: 
      yield obj 

for event in chunked_iterator(Event.objects.all()): 
    print event 
2

Oto rozwiązanie w tym len i liczyć:

class GeneratorWithLen(object): 
    """ 
    Generator that includes len and count for given queryset 
    """ 
    def __init__(self, generator, length): 
     self.generator = generator 
     self.length = length 

    def __len__(self): 
     return self.length 

    def __iter__(self): 
     return self.generator 

    def __getitem__(self, item): 
     return self.generator.__getitem__(item) 

    def next(self): 
     return next(self.generator) 

    def count(self): 
     return self.__len__() 

def batch(queryset, batch_size=1024): 
    """ 
    returns a generator that does not cache results on the QuerySet 
    Aimed to use with expected HUGE/ENORMOUS data sets, no caching, no memory used more than batch_size 

    :param batch_size: Size for the maximum chunk of data in memory 
    :return: generator 
    """ 
    total = queryset.count() 

    def batch_qs(_qs, _batch_size=batch_size): 
     """ 
     Returns a (start, end, total, queryset) tuple for each batch in the given 
     queryset. 
     """ 
     for start in range(0, total, _batch_size): 
      end = min(start + _batch_size, total) 
      yield (start, end, total, _qs[start:end]) 

    def generate_items(): 
     queryset.order_by() # Clearing... ordering by id if PK autoincremental 
     for start, end, total, qs in batch_qs(queryset): 
      for item in qs: 
       yield item 

    return GeneratorWithLen(generate_items(), total) 

Zastosowanie:

events = batch(Event.objects.all()) 
len(events) == events.count() 
for event in events: 
    # Do something with the Event 
0

Zwykle używam MySQL surowe surowy zapytania zamiast Django ORM dla tego rodzaju zadania.

MySQL obsługuje tryb transmisji strumieniowej, dzięki czemu możemy bezpiecznie i szybko przechodzić przez wszystkie rekordy bez błędu braku pamięci.

import MySQLdb 
db_config = {} # config your db here 
connection = MySQLdb.connect(
     host=db_config['HOST'], user=db_config['USER'], 
     port=int(db_config['PORT']), passwd=db_config['PASSWORD'], db=db_config['NAME']) 
cursor = MySQLdb.cursors.SSCursor(connection) # SSCursor for streaming mode 
cursor.execute("SELECT * FROM event") 
while True: 
    record = cursor.fetchone() 
    if record is None: 
     break 
    # Do something with record here 

cursor.close() 
connection.close() 

Ref:

  1. Retrieving million of rows from MySQL
  2. How does MySQL result set streaming perform vs fetching the whole JDBC ResultSet at once
Powiązane problemy