2010-11-19 6 views
72

Die betreffende Tabelle enthält ungefähr zehn Millionen Zeilen.Warum durchläuft ein großes Django QuerySet große Mengen an Speicher?

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

Dies führt dazu, dass die Speicherbelegung stetig auf 4 GB oder mehr steigt, wodurch die Zeilen schnell gedruckt werden. Die lange Wartezeit vor der ersten Zeile überraschte mich - ich erwartete, dass sie fast sofort drucken würde.

Ich versuchte auch Event.objects.iterator(), die auf die gleiche Weise verhielten.

Ich verstehe nicht, was Django in den Speicher lädt oder warum es das tut. Ich erwartete, dass Django die Ergebnisse auf Datenbankebene durchläuft, was bedeutet, dass die Ergebnisse ungefähr mit einer konstanten Rate gedruckt würden (anstatt auf einmal nach einer langen Wartezeit).

Was habe ich missverstanden?

(ich weiß nicht, ob es relevant ist, aber ich bin mit PostgreSQL.)

+3

Auf kleineren Maschinen dieses auch sofort verursachen kann "getötet" an die django Shell oder Server – Stefano

Antwort

78

Nate C war in der Nähe, aber nicht ganz.

Von the docs:

Sie eine QuerySet auf folgende Weise auswerten kann:

  • Iteration. Ein QuerySet ist iterierbar und führt seine Datenbankabfrage beim ersten Durchlauf aus. Zum Beispiel wird dies die Schlagzeile aller Einträge in der Datenbank drucken:

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

So Ihre zehn Millionen Zeilen abgerufen werden, alle auf einmal, wenn Sie zum ersten Mal, dass die Schleife eingeben und die Iterieren Form erhalten des Abfrage-Sets. Die Wartezeit, die Sie erleben, ist, dass Django die Datenbankzeilen lädt und Objekte für jeden erstellt, bevor er etwas zurückgibt, über das Sie tatsächlich iterieren können. Dann hast du alles im Gedächtnis und die Ergebnisse kommen heraus.

Von meinem Lesen der Dokumente, iterator() tut nichts mehr als QuerySet interne Caching-Mechanismen umgehen.Ich denke, dass es Sinn machen könnte, eins nach dem anderen zu machen, aber das würde umgekehrt zehn Millionen individuelle Treffer in Ihrer Datenbank erfordern. Vielleicht nicht so wünschenswert.

Iterieren über große Datenmengen effizient etwas haben wir noch nicht ganz richtig gemacht, aber es gibt einige Auszüge aus könnte es für Sie nützlich Ihre Zwecke für:

+1

Danke für die große Antwort, @ eternicode. Am Ende haben wir für die gewünschte Iteration auf Datenbankebene auf Raw SQL zurückgegriffen. – davidchambers

+2

@eternicode Schöne Antwort, nur dieses Problem. Gibt es seitdem in Django ein Update? –

+0

Immer noch MIA: Eine Version, die das mit Cursors macht, so dass Elemente nicht übersprungen werden .... – mlissner

6

Das von der Dokumentation ist: http://docs.djangoproject.com/en/dev/ref/models/querysets/

Keine Datenbankaktivität tatsächlich auftritt, bis Sie etwas tun, um Bewerten Sie das Abfrage-Set.

Also, wenn die print event die Abfrage Feuer ausgeführt wird (das ist ein Full Table Scan nach Ihrem Befehl.) Und lädt die Ergebnisse. Sie fragen nach allen Objekten und es gibt keine Möglichkeit, das erste Objekt zu erhalten, ohne alle Objekte zu erhalten.

Aber wenn Sie so etwas wie:

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

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

Dann ist es Versetzungen und Grenzen für die SQL-intern hinzufügen.

5

Für große Mengen von Aufzeichnungen, eine database cursor führt noch besser. Sie benötigen in Django Raw SQL, der Django-Cursor ist etwas anderes als ein SQL-Cursor.

Die von Nate C vorgeschlagene LIMIT - OFFSET - Methode könnte für Ihre Situation gut genug sein. Bei großen Datenmengen ist es langsamer als ein Cursor, da es dieselbe Abfrage immer wieder ausführen muss und immer mehr Ergebnisse überspringen muss.

+3

Frank, das ist definitiv ein guter Punkt, aber wäre schön, einige Code-Details zu sehen, die eine Lösung anstoßen ;-) (gut diese Frage ist jetzt ziemlich alt ...) – Stefano

25

vielleicht nicht die schneller oder am effizientesten sein, sondern als fertige Lösung, warum hier nicht dokumentiert django Kern der Paginator und Seite-Objekte verwenden:

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

Etwas wie folgt aus:

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

Kleine Verbesserungen seit Post möglich. 'Paginator' hat jetzt eine [' page_range'] (https://docs.djangoproject.com/de/dev/topics/pagination/#django.core.paginator.Paginator.page_range) Eigenschaft, um Standardwerte zu vermeiden. Wenn Sie nach minimalem Speicheraufwand suchen, können Sie ['object_list.iterator()' verwenden, das den Cache des Abfrage-Sets nicht auffüllt] (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) wird dann für den Prefetch benötigt –

5

Django hat keine gute Lösung zum Abrufen großer Artikel aus der Datenbank.

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 kann dazu verwendet werden, alle IDs, die in den Datenbanken zu holen und dann jedes Objekt einzeln holen. Im Laufe der Zeit werden große Objekte im Speicher erstellt und es wird kein Müll gesammelt, bis die Schleife beendet wird. Der obige Code führt nach jedem 100. Objekt eine manuelle Garbage Collection durch.

+0

Kann streamingHttpResponse eine Lösung sein? http://StackOverflow.com/Questions/15359768/Django-1-5-using-the-new-streaminghttpresponse – ratata

+1

Allerdings wird dies zu gleichen Treffern in der Datenbank als die Anzahl der Schleifen, ich habe Angst. – raratiru

4

Weil auf diese Weise Objekte für eine ganze Abfrage in den Speicher auf einmal geladen werden. Sie müssen Ihr Anfrage-Set in kleinere verdauliche Bits aufteilen. Das Muster dafür wird Löffelfütterung genannt. Hier ist eine kurze Implementierung.

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 

diese nutzen zu können, schreiben eine Funktion, die Operationen auf dem Objekt tut:

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

und als diese Funktion auf Ihrem queryset laufen:

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

Dies kann weiter verbessert werden mit Multiprocessing, um func für mehrere Objekte parallel auszuführen.

16

Djangos Standardverhalten besteht darin, das gesamte Ergebnis von QuerySet bei der Auswertung der Abfrage zwischenzuspeichern. Sie können die Iteratormethode des QuerySet verwenden diese Caching zu vermeiden:

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

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

Der Iterator() -Methode bewertet die queryset und liest dann die Ergebnisse direkt ohne Zwischenspeicherung auf der QuerySet Ebene zu tun.Diese Methode führt zu einer besseren Leistung und einer erheblichen Speicherreduktion beim Iterieren über eine große Anzahl von Objekten, auf die Sie nur einmal zugreifen müssen. Beachten Sie, dass das Caching immer noch auf Datenbankebene erfolgt.

Die Verwendung von Iterator() reduziert die Speicherauslastung für mich, aber es ist immer noch höher als ich erwartet hatte. Die Verwendung des von mpaf vorgeschlagenen Paginator-Ansatzes verbraucht viel weniger Speicher, ist aber für meinen Testfall 2-3x langsamer.

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

Hier ist eine Lösung, die len und zählen:

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) 

Verbrauch:

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

ich in der Regel roh MySQL roh Abfrage für diese Art von Aufgabe anstelle von Django ORM verwenden.

MySQL unterstützt den Streaming-Modus, sodass wir alle Datensätze sicher und schnell durchlaufen können, ohne dass ein Speicherfehler auftritt.

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