web-dev-qa-db-fra.com

Pourquoi l'itération à travers un grand Django QuerySet consomme des quantités massives de mémoire?

Le tableau en question contient environ dix millions de lignes.

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

Cela entraîne une augmentation constante de l'utilisation de la mémoire à environ 4 Go, point auquel les lignes s'impriment rapidement. Le long délai avant l'impression de la première ligne m'a surpris - je m'attendais à ce qu'il s'imprime presque instantanément.

J'ai également essayé Event.objects.iterator() qui se comportait de la même manière.

Je ne comprends pas ce que Django charge en mémoire ni pourquoi il le fait. Je m'attendais à ce que Django répète les résultats au niveau de la base de données, ce qui Cela signifierait que les résultats seraient imprimés à peu près à un rythme constant (plutôt que d'un seul coup après une longue attente).

Qu'est-ce que j'ai mal compris?

(Je ne sais pas si c'est pertinent, mais j'utilise PostgreSQL.)

93
davidchambers

Nate C était proche, mais pas tout à fait.

De les docs :

Vous pouvez évaluer un QuerySet des manières suivantes:

  • Itération. Un QuerySet est itérable et il exécute sa requête de base de données la première fois que vous l'itérez. Par exemple, cela imprimera le titre de toutes les entrées de la base de données:

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

Ainsi, vos dix millions de lignes sont récupérées, en une seule fois, lorsque vous entrez pour la première fois dans cette boucle et obtenez la forme itérative de l'ensemble de requêtes. L'attente que vous rencontrez est Django chargement des lignes de la base de données et création d'objets pour chacune, avant de retourner quelque chose que vous pouvez réellement parcourir. Ensuite, vous avez tout en mémoire et les résultats se répandent.

D'après ma lecture de la documentation, iterator() ne fait rien de plus que contourner les mécanismes de mise en cache interne de QuerySet. Je pense qu'il pourrait être judicieux de faire une chose une par une, mais cela nécessiterait à l'inverse dix millions de visites individuelles sur votre base de données. Peut-être pas tout à fait souhaitable.

Itérer efficacement de grands ensembles de données est quelque chose que nous n'avons toujours pas bien compris, mais il existe des extraits de code que vous pourriez trouver utiles pour vos besoins:

94
eternicode

Ce n'est peut-être pas la solution la plus rapide ou la plus efficace, mais en tant que solution prête à l'emploi, pourquoi ne pas utiliser Django les objets Paginator et Page du noyau documentés ici:

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

Quelque chose comme ça:

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
36
mpaf

Le comportement par défaut de Django consiste à mettre en cache l'intégralité du résultat du QuerySet lors de l'évaluation de la requête. Vous pouvez utiliser la méthode itérateur du QuerySet pour éviter cette mise en cache:

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

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

La méthode iterator () évalue l'ensemble de requêtes et lit ensuite les résultats directement sans effectuer de mise en cache au niveau de QuerySet. Cette méthode se traduit par de meilleures performances et une réduction significative de la mémoire lors de l'itération sur un grand nombre d'objets auxquels vous n'avez besoin d'accéder qu'une seule fois. Notez que la mise en cache est toujours effectuée au niveau de la base de données.

L'utilisation d'itérateur () réduit l'utilisation de la mémoire pour moi, mais elle est toujours supérieure à ce que j'attendais. L'utilisation de l'approche paginateur suggérée par mpaf utilise beaucoup moins de mémoire, mais est 2 à 3 fois plus lente pour mon cas de 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
24
Luke Moore

Ceci provient des documents: http://docs.djangoproject.com/en/dev/ref/models/querysets/

Aucune activité de base de données ne se produit réellement jusqu'à ce que vous fassiez quelque chose pour évaluer l'ensemble de requêtes.

Alors quand le print event exécute les incendies de requête (qui est une analyse complète de la table selon votre commande) et charge les résultats. Vous demandez tous les objets et il n'y a aucun moyen d'obtenir le premier objet sans les avoir tous.

Mais si vous faites quelque chose comme:

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

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

Ensuite, il ajoutera des décalages et des limites au sql en interne.

7
nate c

Django n'a pas de bonne solution pour récupérer de gros éléments de la base de données.

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 peut être utilisé pour récupérer tous les identifiants dans les bases de données, puis récupérer chaque objet séparément. Au fil du temps, de grands objets seront créés en mémoire et ne seront pas récupérés jusqu'à ce que la boucle soit fermée. Le code ci-dessus effectue la collecte manuelle des ordures après chaque 100e élément consommé.

6
Kracekumar

Pour de grandes quantités d'enregistrements, un curseur de base de données fonctionne encore mieux. Vous avez besoin de SQL brut dans Django, le curseur Django est quelque chose de différent d'un cursur SQL.

La méthode LIMIT - OFFSET suggérée par Nate C pourrait être assez bonne pour votre situation. Pour de grandes quantités de données, il est plus lent qu'un curseur car il doit exécuter la même requête encore et encore et doit sauter de plus en plus de résultats.

6
Frank Heikens

Parce que de cette façon, les objets pour un ensemble de requêtes entier sont chargés en même temps. Vous devez découper votre ensemble de requêtes en petits morceaux digestibles. Le schéma pour ce faire est appelé alimentation à la cuillère. Voici une brève mise en œuvre.

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

Pour l'utiliser, vous écrivez une fonction qui effectue des opérations sur votre objet:

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

et que d'exécuter cette fonction sur votre ensemble de requêtes:

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

Ceci peut être encore amélioré avec le multi-traitement pour exécuter func sur plusieurs objets en parallèle.

4
F. Malina

Voici une solution comprenant len ​​et 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)

Usage:

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

J'utilise généralement une requête brute MySQL brute au lieu de Django ORM pour ce type de tâche.

MySQL prend en charge le mode de streaming afin que nous puissions parcourir tous les enregistrements en toute sécurité et rapidement sans erreur de mémoire insuffisante.

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

Réf:

  1. Récupération de millions de lignes de MySQL
  2. Comment le streaming des jeux de résultats MySQL fonctionne-t-il par rapport à la récupération de l'ensemble de résultats JDBC en même temps
0
Tho