Perché l'iterazione attraverso un grande Django QuerySet consuma enormi quantità di memoria?


111

La tabella in questione contiene circa dieci milioni di righe.

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

Ciò fa sì che l'utilizzo della memoria aumenti costantemente fino a 4 GB o giù di lì, a quel punto le righe vengono stampate rapidamente. Il lungo ritardo prima che la prima riga fosse stampata mi ha sorpreso: mi aspettavo che venisse stampata quasi istantaneamente.

Ho anche provato Event.objects.iterator()che si è comportato allo stesso modo.

Non capisco cosa Django stia caricando in memoria o perché lo stia facendo. Mi aspettavo che Django iterasse i risultati a livello di database, il che significherebbe che i risultati sarebbero stati stampati a una velocità più o meno costante (piuttosto che tutti in una volta dopo una lunga attesa).

Cosa ho frainteso?

(Non so se sia rilevante, ma sto usando PostgreSQL.)


6
Su macchine più piccole questo può persino causare immediatamente "Killed" alla shell o al server di django
Stefano

Risposte:


113

Nate C era vicino, ma non del tutto.

Dai documenti :

Puoi valutare un QuerySet nei seguenti modi:

  • Iterazione. Un QuerySet è iterabile ed esegue la sua query di database la prima volta che si itera su di esso. Ad esempio, questo stamperà il titolo di tutte le voci nel database:

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

Quindi le tue dieci milioni di righe vengono recuperate, tutte in una volta, quando entri per la prima volta in quel ciclo e ottieni la forma iterativa del set di query. L'attesa che sperimenti è Django che carica le righe del database e crea oggetti per ciascuna, prima di restituire qualcosa su cui puoi effettivamente iterare. Quindi hai tutto in memoria ei risultati vengono fuori.

Dalla mia lettura dei documenti, iterator()non fa altro che bypassare i meccanismi di caching interni di QuerySet. Penso che potrebbe avere senso che faccia una cosa uno per uno, ma al contrario richiederebbe dieci milioni di visite individuali sul tuo database. Forse non è così desiderabile.

L'iterazione su grandi set di dati in modo efficiente è qualcosa che non abbiamo ancora capito bene, ma ci sono alcuni frammenti là fuori che potresti trovare utili per i tuoi scopi:


1
Grazie per l'ottima risposta, @eternicode. Alla fine siamo passati all'SQL grezzo per l'iterazione a livello di database desiderata.
davidchambers

2
@eternicode Bella risposta, colpisci questo problema. Da allora c'è qualche aggiornamento correlato in Django?
Zólyomi István

2
I documenti a partire da Django 1.11 dicono che iterator () usa i cursori lato server.
Jeff C Johnson,

42

Potrebbe non essere la soluzione più veloce o più efficiente, ma come soluzione già pronta perché non utilizzare Paginator e gli oggetti Page di django core documentati qui:

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

Qualcosa come questo:

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
Piccoli miglioramenti ora possibili dal post. Paginatorora ha una page_rangeproprietà per evitare boilerplate. Se sei alla ricerca di un sovraccarico di memoria minimo, puoi usare object_list.iterator()che non popolerà la cache del set di query . prefetch_related_objectsè quindi richiesto per il prefetch
Ken Colton

28

Il comportamento predefinito di Django consiste nel memorizzare nella cache l'intero risultato del QuerySet quando valuta la query. È possibile utilizzare il metodo iteratore di QuerySet per evitare questa memorizzazione nella cache:

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

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

Il metodo iterator () valuta il set di query e quindi legge i risultati direttamente senza eseguire la memorizzazione nella cache a livello di QuerySet. Questo metodo offre prestazioni migliori e una riduzione significativa della memoria durante l'iterazione su un numero elevato di oggetti a cui è necessario accedere una sola volta. Notare che la memorizzazione nella cache viene ancora eseguita a livello di database.

L'uso di iterator () riduce l'utilizzo della memoria per me, ma è ancora più alto di quanto mi aspettassi. L'utilizzo dell'approccio del paginatore suggerito da mpaf utilizza molta meno memoria, ma è 2-3 volte più lento per il mio caso di test.

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

Questo è dai documenti: http://docs.djangoproject.com/en/dev/ref/models/querysets/

Nessuna attività del database si verifica effettivamente fino a quando non si fa qualcosa per valutare il set di query.

Quindi, quando print eventviene eseguito, la query si attiva (che è una scansione completa della tabella in base al tuo comando) e carica i risultati. Stai chiedendo tutti gli oggetti e non c'è modo di ottenere il primo oggetto senza averli tutti.

Ma se fai qualcosa come:

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

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

Quindi aggiungerà offset e limiti a sql internamente.


7

Per grandi quantità di record, un cursore del database funziona ancora meglio. Hai bisogno di SQL grezzo in Django, il cursore Django è qualcosa di diverso da un cursore SQL.

Il metodo LIMIT - OFFSET suggerito da Nate C potrebbe essere abbastanza buono per la tua situazione. Per grandi quantità di dati è più lento di un cursore perché deve eseguire la stessa query più e più volte e deve saltare sempre più risultati.


4
Frank, questo è sicuramente un buon punto ma sarebbe bello vedere alcuni dettagli del codice per spingere verso una soluzione ;-) (beh, questa domanda è abbastanza vecchia ora ...)
Stefano

7

Django non ha una buona soluzione per il recupero di elementi di grandi dimensioni dal database.

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 può essere utilizzato per recuperare tutti gli ID nei database e quindi recuperare ogni oggetto separatamente. Nel tempo verranno creati oggetti di grandi dimensioni in memoria e non verranno raccolti in modo indesiderato fino all'uscita dal ciclo for. Il codice precedente esegue la garbage collection manuale dopo aver consumato ogni centesimo elemento.


StreamingHttpResponse può essere una soluzione? stackoverflow.com/questions/15359768/...
ratata

2
Tuttavia, questo si tradurrà in risultati uguali nel database rispetto al numero di loop, temo.
raratiru

5

Perché in questo modo gli oggetti per un intero set di query vengono caricati in memoria tutti in una volta. Devi suddividere il tuo set di query in bit più piccoli e digeribili. Il modello per farlo si chiama cucchiaio. Ecco una breve implementazione.

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

Per usarlo scrivi una funzione che esegue operazioni sul tuo oggetto:

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

e poi esegui quella funzione sul tuo set di query:

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

Questo può essere ulteriormente migliorato con il multiprocessing per l'esecuzione funcsu più oggetti in parallelo.


1
Sembra che questo sarà integrato nella 1.12 con iterate (chunk_size = 1000)
Kevin Parker

3

Ecco una soluzione che include len e 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)

Uso:

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

0

Di solito uso query raw MySQL non elaborate invece di Django ORM per questo tipo di attività.

MySQL supporta la modalità di streaming in modo da poter scorrere tutti i record in modo sicuro e veloce senza errori di memoria.

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()

Rif:

  1. Recupero di milioni di righe da MySQL
  2. Come funziona lo streaming del set di risultati MySQL rispetto al recupero dell'intero ResultSet JDBC contemporaneamente

È comunque possibile utilizzare Django ORM per generare query. Basta usare il risultato queryset.queryper nella tua esecuzione.
Pol
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.