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.)
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:
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
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
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.
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é.
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.
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.
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
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: