Pourquoi itérer à travers un grand QuerySet Django consomme-t-il 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 provoque une augmentation constante de l'utilisation de la mémoire à 4 Go environ, à partir de laquelle les lignes s'impriment rapidement. Le long délai avant la première rangée imprimée m'a surpris – je m'attendais à ce qu'il imprimer presque instantanément.
j'ai aussi essayé Event.objects.iterator()
qui se comportaient de la même manière.
Je ne comprends pas ce que Django charge dans la mémoire ou pourquoi il fait ce. Je m'attendais à ce que Django répète les résultats au niveau de la base de données, ce qui signifie que les résultats seraient imprimés à une vitesse à peu près constante (plutôt que tous en même temps après une longue attente).
Qu'ai-je mal compris?
(Je ne sais pas si c'est pertinent, mais J'utilise PostgreSQL.)
9 réponses
Nate C était proche, mais pas tout à fait.
à Partir de les docs :
vous pouvez évaluer un QuerySet de la façon suivante:
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 itérez sur elle. Par exemple, cela affichera le titre de toutes les entrées dans la base de données:
for e in Entry.objects.all(): print e.headline
donc vos dix millions de lignes sont récupérées, d'un seul coup, quand vous entrez dans cette boucle et obtenez la forme itérative du queryset. L'attente que vous rencontrez est Django chargeant les lignes de base de données et créant des objets pour chacun, avant de retourner quelque chose que vous pouvez réellement itérer plus. Alors vous avez tout en mémoire, et les résultats se répandent.
D'après ma lecture des docs, iterator()
does rien de plus que de contourner les mécanismes de mise en cache interne de QuerySet. Je pense qu'il pourrait être logique de faire une chose un par un, mais cela nécessiterait à l'inverse dix millions de visites individuelles sur votre base de données. Peut-être pas souhaitable.
itérer efficacement sur de grands ensembles de données est quelque chose que nous n'avons pas encore tout à fait raison, mais il ya quelques bribes là-bas, vous pourriez trouver utile pour vos besoins:
N'est peut-être pas la solution la plus rapide ou la plus efficace, mais en tant que solution toute faite, pourquoi ne pas utiliser le Paginateur de django core et les objets de Page 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
le comportement par défaut de Django est de mettre en cache tout le résultat du QuerySet quand il évalue la requête. Vous pouvez utiliser la méthode iterator de 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 le queryset et lit ensuite les résultats directement sans faire de mise en cache au niveau du QuerySet. Cette méthode aboutit à une meilleure performance et une réduction significative de la mémoire lors de l'itération d'un grand nombre d'objets auxquels vous n'avez besoin d'accéder qu'une seule fois. Notez que la mise en cache se fait toujours au niveau base de données.
en utilisant iterator() réduit l'utilisation de la mémoire pour moi, mais il est toujours plus élevé que je m'y attendais. En utilisant l'approche de pagination suggérée par mpaf utilise beaucoup moins de mémoire, mais est 2-3x plus lent 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
c'est du docs: http://docs.djangoproject.com/en/dev/ref/models/querysets /
aucune activité de base de données ne se produit en fait jusqu'à ce que vous fassiez quelque chose pour évaluer le queryset.
ainsi quand le print event
est lancé les feux de requête (qui est un scan de table complet selon votre commande.) et les charges les résultats. Votre demande tous les objets et il n'existe aucun moyen pour obtenir le premier objet sans se faire tous les d'entre eux.
Mais si vous faites quelque chose comme:
Event.objects.all()[300:900]
http://docs.djangoproject.com/en/dev/topics/db/queries/#limiting-querysets
alors il ajoutera des offsets et des limites au sql en interne.
pour de grandes quantités d'enregistrements, un curseur de base de données fonctionne encore mieux. Vous avez besoin de SQL brut à Django, le curseur Django est quelque chose de différent d'un curseur SQL.
la méthode limite-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 que l'on doit exécuter la même requête à maintes reprises et a pour sauter de plus en plus de résultats.
Django n'a pas de bonne solution pour récupérer les gros articles 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 ID dans les bases de données et ensuite 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 collectés jusqu'à ce que boucle soit terminée. Le code ci-dessus fait la collecte manuelle des ordures après chaque 100ème article est consommé.
parce que de cette façon les objets pour un queryset entier sont chargés en mémoire tout d'un coup. Vous devez découper votre queryset en petits morceaux digestibles. Le schéma pour faire ceci est appelé alimentation de 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 utiliser ceci vous écrivez une fonction qui fait 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 queryset:
spoonfeed(Town.objects.all(), set_population_density)
cela peut être plus amélioré avec multiprocessing pour exécuter func
sur plusieurs objets en parallèle.
ici une solution incluant 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)
Utilisation:
events = batch(Event.objects.all())
len(events) == events.count()
for event in events:
# Do something with the Event
j'utilise habituellement la requête brute MySQL au lieu de Django ORM pour ce genre de tâche.
MySQL prend en charge le mode de diffusion en continu afin que nous puissions boucler tous les enregistrements en toute sécurité et rapidement sans erreur de mémoire.
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:
- "151990920 de" Récupération des millions de lignes de MySQL
- Comment MySQL result set streaming effectuer vs aller chercher de l'ensemble de JDBC Ensemble de résultats à la fois