Warum verbraucht das Durchlaufen eines großen Django QuerySet sehr viel Speicher?


110

Die betreffende Tabelle enthält ungefähr zehn Millionen Zeilen.

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

Dies führt dazu, dass die Speichernutzung stetig auf etwa 4 GB ansteigt. Zu diesem Zeitpunkt werden die Zeilen schnell gedruckt. Die lange Verzögerung vor dem Drucken der ersten Zeile hat mich überrascht - ich hatte erwartet, dass sie fast sofort gedruckt wird.

Ich habe auch versucht, Event.objects.iterator()was sich genauso verhält.

Ich verstehe nicht, was Django in den Speicher lädt oder warum es dies tut. Ich hatte erwartet, dass Django die Ergebnisse auf Datenbankebene durchläuft, was bedeuten würde, dass die Ergebnisse mit einer ungefähr konstanten Rate gedruckt werden (und nicht alle auf einmal nach einer langen Wartezeit).

Was habe ich falsch verstanden?

(Ich weiß nicht, ob es relevant ist, aber ich verwende PostgreSQL.)


6
Auf kleineren Maschinen kann dies sogar dazu führen, dass die Django-Shell oder der Django-Server sofort "getötet" wird
Stefano

Antworten:


112

Nate C war nah dran, aber nicht ganz.

Aus den Dokumenten :

Sie können ein QuerySet folgendermaßen auswerten:

  • Wiederholung. Ein QuerySet ist iterierbar und führt seine Datenbankabfrage aus, wenn Sie es zum ersten Mal durchlaufen. Dies gibt beispielsweise die Überschrift aller Einträge in der Datenbank aus:

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

Ihre zehn Millionen Zeilen werden also auf einmal abgerufen, wenn Sie diese Schleife zum ersten Mal betreten und die iterierende Form des Abfragesatzes erhalten. Das Warten, das Sie erleben, ist, dass Django die Datenbankzeilen lädt und Objekte für jede erstellt, bevor er etwas zurückgibt, über das Sie tatsächlich iterieren können. Dann haben Sie alles im Gedächtnis und die Ergebnisse kommen heraus.

Beim Lesen der Dokumente wird iterator()lediglich die internen Caching-Mechanismen von QuerySet umgangen. Ich denke, es könnte sinnvoll sein, eins nach dem anderen zu tun, aber das würde umgekehrt zehn Millionen einzelne Treffer in Ihrer Datenbank erfordern. Vielleicht gar nicht so wünschenswert.

Das effiziente Durchlaufen großer Datenmengen ist immer noch nicht ganz richtig, aber es gibt einige Ausschnitte, die Sie für Ihre Zwecke nützlich finden könnten:


1
Danke für die tolle Antwort, @eternicode. Am Ende haben wir uns für die gewünschte Iteration auf Datenbankebene auf Raw SQL beschränkt.
Davidchambers

2
@eternicode Schöne Antwort, schlagen Sie einfach dieses Problem. Gibt es seitdem ein ähnliches Update in Django?
Zólyomi István

2
In den Dokumenten seit Django 1.11 heißt es, dass iterator () serverseitige Cursor verwendet.
Jeff C Johnson

42

Könnte nicht die schnellere oder effizienteste sein, aber als fertige Lösung können Sie die hier dokumentierten Paginator- und Page-Objekte von django core verwenden:

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

Etwas wie das:

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

3
Kleine Verbesserungen seit Post möglich. Paginatorhat jetzt eine page_rangeEigenschaft, um Boilerplate zu vermeiden. Wenn Sie auf der Suche nach minimalem Speicheraufwand sind, können Sie verwenden, object_list.iterator()wodurch der Abfragesatz-Cache nicht gefüllt wird . prefetch_related_objectswird dann für den Prefetch benötigt
Ken Colton

28

Das Standardverhalten von Django besteht darin, das gesamte Ergebnis des QuerySet zwischenzuspeichern, wenn die Abfrage ausgewertet wird. Sie können die Iterator-Methode von QuerySet verwenden, um dieses Caching zu vermeiden:

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

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

Die iterator () -Methode wertet das Abfrageset aus und liest die Ergebnisse direkt, ohne das Caching auf QuerySet-Ebene durchzuführen. Diese Methode führt zu einer besseren Leistung und einer erheblichen Reduzierung des Arbeitsspeichers, wenn Sie über eine große Anzahl von Objekten iterieren, auf die Sie nur einmal zugreifen müssen. Beachten Sie, dass das Caching weiterhin auf Datenbankebene erfolgt.

Die Verwendung von iterator () reduziert die Speichernutzung für mich, ist aber immer noch höher als erwartet. Die Verwendung des von mpaf vorgeschlagenen Paginator-Ansatzes verbraucht viel weniger Speicher, ist jedoch 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

8

Dies ist aus den Dokumenten: http://docs.djangoproject.com/en/dev/ref/models/querysets/

Es tritt tatsächlich keine Datenbankaktivität auf, bis Sie etwas tun, um das Abfrageset auszuwerten.

Wenn das ausgeführt print eventwird, wird die Abfrage ausgelöst (dies ist ein vollständiger Tabellenscan gemäß 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 zu erhalten.

Aber wenn Sie etwas tun wie:

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

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

Dann werden dem SQL intern Offsets und Limits hinzugefügt.


7

Bei großen Mengen an Datensätzen ist ein Datenbankcursor noch leistungsfähiger. In Django benötigen Sie unformatiertes SQL. Der Django-Cursor unterscheidet sich von einem SQL-Cursor.

Die von Nate C vorgeschlagene LIMIT-OFFSET-Methode ist möglicherweise gut genug für Ihre Situation. Bei großen Datenmengen ist es langsamer als ein Cursor, da immer wieder dieselbe Abfrage ausgeführt werden muss und immer mehr Ergebnisse übersprungen werden müssen.


4
Frank, das ist definitiv ein guter Punkt, aber es wäre schön, einige Codedetails zu sehen, um eine Lösung zu finden ;-) (Nun, diese Frage ist jetzt ziemlich alt ...)
Stefano

7

Django hat keine gute Lösung, um große Objekte aus der Datenbank abzurufen.

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 verwendet werden, um alle IDs in den Datenbanken abzurufen und dann jedes Objekt separat abzurufen. Im Laufe der Zeit werden große Objekte im Speicher erstellt und kein Müll gesammelt, bis die Schleife beendet wird. Der obige Code führt eine manuelle Speicherbereinigung durch, nachdem jeder 100. Artikel verbraucht wurde.


Kann StreamingHttpResponse eine Lösung sein? stackoverflow.com/questions/15359768/…
ratata

2
Ich fürchte, dies führt jedoch zu gleichen Treffern in der Datenbank wie die Anzahl der Schleifen.
Raratiru

5

Auf diese Weise werden Objekte für einen gesamten Abfragesatz auf einmal in den Speicher geladen. Sie müssen Ihr Abfrageset in kleinere verdauliche Teile aufteilen. Das Muster dafür heißt Löffelfütterung. 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):
            yeild func(o)
        start += chunk

Um dies zu verwenden, schreiben Sie eine Funktion, die Operationen an Ihrem Objekt ausführt:

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

und dann führen Sie diese Funktion auf Ihrem Abfrageset aus:

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

Dies kann durch funcMehrfachverarbeitung weiter verbessert werden, um mehrere Objekte parallel auszuführen .


1
Sieht so aus, als würde dies mit iterate (chunk_size = 1000) in 1.12 eingebaut
Kevin Parker

3

Hier eine Lösung einschließlich len und count:

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)

Verwendung:

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

0

Normalerweise verwende ich für diese Art von Aufgabe eine rohe MySQL-Rohabfrage anstelle von Django ORM.

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

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. Millionen von Zeilen aus MySQL abrufen
  2. Wie funktioniert das Streaming von MySQL-Ergebnismengen im Vergleich zum gleichzeitigen Abrufen des gesamten JDBC-Ergebnissatzes?

Sie können weiterhin Django ORM verwenden, um eine Abfrage zu generieren. Verwenden Sie einfach resultierend queryset.queryfür in Ihrer Ausführung.
Pol
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.