¿Por qué iterar a través de un gran QuerySet de Django consume grandes cantidades de memoria?


La tabla en cuestión contiene aproximadamente diez millones de filas.

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

Esto hace que el uso de memoria aumente constantemente a 4 GB aproximadamente, momento en el que las filas se imprimen rápidamente. El largo retraso antes de la primera fila impresa me sorprendió-esperaba que se imprimiera casi al instante.

También intenté Event.objects.iterator() que se comportó de la misma manera.

No entiendo lo que Django está cargando en la memoria o por qué está haciendo esto. Esperaba que Django repasara los resultados en el a nivel de base de datos, lo que significaría que los resultados se imprimirían aproximadamente a una velocidad constante (en lugar de todos a la vez después de una larga espera).

¿Qué he entendido mal?

(No se si es relevante, pero estoy usando PostgreSQL.)

Author: davidchambers, 2010-11-19

9 answers

Nate C estaba cerca, pero no del todo.

De los documentos :

Puede evaluar un QuerySet de las siguientes maneras:

  • Iteración. Un QuerySet es iterable y ejecuta su consulta de base de datos la primera vez que se itera sobre él. Por ejemplo, esto imprimirá el título de todas las entradas en la base de datos:

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

Así que sus diez millones de filas se recuperan, todas a la vez, cuando ingresa por primera vez ese bucle y obtiene el forma iterante del queryset. La espera que experimenta es Django cargando las filas de la base de datos y creando objetos para cada uno, antes de devolver algo que realmente puede iterar. Entonces tienes todo en la memoria, y los resultados vienen derramándose.

De mi lectura de los documentos, iterator() no hace nada más que omitir los mecanismos internos de almacenamiento en caché de QuerySet. Creo que podría tener sentido para él hacer una cosa uno por uno, pero eso requeriría a la inversa diez millones visitas individuales en su base de datos. Tal vez no tan deseable.

Iterar sobre grandes conjuntos de datos de manera eficiente es algo que todavía no hemos hecho muy bien, pero hay algunos fragmentos por ahí que puede encontrar útiles para sus propósitos:

 86
Author: eternicode,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-11-19 05:44:27

Podría no ser el más rápido o el más eficiente, pero como una solución lista para usar, ¿por qué no usar los objetos Paginator y Page de django core documentados aquí:

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

Algo como esto:

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
 29
Author: mpaf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-10-09 19:56:12

El comportamiento predeterminado de Django es almacenar en caché el resultado completo del conjunto de consultas cuando evalúa la consulta. Puedes usar el método iterador de QuerySet para evitar este almacenamiento en caché:

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

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

El método iterator() evalúa el queryset y luego lee los resultados directamente sin hacer caché a nivel de QuerySet. Este método se traduce en un mejor rendimiento y una reducción significativa en la memoria cuando iterar sobre un gran número de objetos a los que solo necesita acceder una vez. Tenga en cuenta que el almacenamiento en caché todavía se realiza a nivel de base de datos.

Usar iterator() reduce el uso de memoria para mí, pero sigue siendo más alto de lo que esperaba. Usando el enfoque de paginador sugerido por mpaf usa mucha menos memoria, pero es 2-3x más lento para mi caso de prueba.

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
 17
Author: Luke Moore,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-07-20 20:18:56

Esto es de los documentos: http://docs.djangoproject.com/en/dev/ref/models/querysets /

No se produce ninguna actividad de base de datos hasta que se hace algo para evaluar el queryset.

Así que cuando se ejecuta el print event se activa la consulta (que es un análisis completo de la tabla de acuerdo con su comando.) y carga los resultados. Su pregunta por todos los objetos y no hay manera de obtener el primer objeto sin obtener todos ellos.

Pero si haces algo como:

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

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

Luego agregará compensaciones y límites al sql internamente.

 6
Author: nate c,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-11-19 05:17:49

Para grandes cantidades de registros, un cursor de base de datos funciona aún mejor. Necesitas SQL raw en Django, el Django-cursor es algo diferente a un cursur SQL.

El método de COMPENSACIÓN de LÍMITE sugerido por Nate C podría ser lo suficientemente bueno para su situación. Para grandes cantidades de datos es más lento que un cursor porque tiene que ejecutar la misma consulta una y otra vez y tiene que saltar más y más resultados.

 6
Author: Frank Heikens,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-11-19 07:57:14

Django no tiene una buena solución para obtener elementos grandes de la base de datos.

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 se puede usar para obtener todos los ID de las bases de datos y luego obtener cada objeto por separado. Durante un tiempo se crearán objetos grandes en la memoria y no se recogerán basura hasta que se salga el bucle. El código anterior hace la recolección manual de basura después de que cada artículo 100 se consume.

 5
Author: Kracekumar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-06-30 14:10:48

Porque de esa manera los objetos de un conjunto de consultas completo se cargan en la memoria todos a la vez. Necesitas dividir tu queryset en trozos más pequeños digeribles. El patrón para hacer esto se llama spoonfeeding. Aquí hay una breve implementación.

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

Para usar esto, escriba una función que realice operaciones en su objeto:

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

Y luego ejecute esa función en su queryset:

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

Esto se puede mejorar aún más con multiprocesamiento para ejecutar func en múltiples objetos en paralelo.

 4
Author: F. Malina,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-04-15 16:52:09

Aquí una solución que incluye len y 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
 2
Author: danius,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-10-29 11:37:38

Normalmente uso raw MySQL raw query en lugar de Django Dj para este tipo de tareas.

MySQL admite el modo de transmisión para que podamos recorrer todos los registros de forma segura y rápida sin errores de 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()

Ref:

  1. Recuperando millones de filas de MySQL
  2. ¿Cómo funciona la transmisión de conjunto de resultados MySQL frente a la obtención de todo el conjunto de resultados JDBC a la vez
 0
Author: ThoQ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-02 01:43:26