J'ai une très grande table. Il est actuellement dans une base de données MySQL. J'utilise django.
J'ai besoin d'itérer sur chaque élément de la table de pré-calculer certaines données particulier (peut-être que si j'étais mieux que je pouvais faire autrement, mais ce n'est pas le point).
J'aimerais garder l'itération aussi vite que possible avec une utilisation constante de la mémoire.
Comme il est déjà clairement en Limitant l'Utilisation de la Mémoire dans un *Grand* Django QuerySet et Pourquoi est-à itérer un grand Django QuerySet consommer d'énormes quantités de mémoire?, une simple itération sur tous les objets django va tuer la machine car il permettra de récupérer TOUS les objets de la base de données.
Vers une solution
Tout d'abord, afin de réduire votre consommation de mémoire, vous devriez être sûr de DÉBOGAGE est Faux (ou le singe patch le curseur: désactiver la journalisation SQL tout en gardant les paramètres.DEBUG?) pour être sûr de django n'est pas de stocker des trucs en connections
pour le débogage.
Mais même avec ça,
for model in Model.objects.all()
est un no go.
Pas de même avec la légère amélioration de la forme:
for model in Model.objects.all().iterator()
À l'aide de iterator()
vous fera économiser de la mémoire en ne stockant pas le résultat de la mémoire cache interne (mais pas nécessairement sur PostgreSQL!); mais toujours récupérer l'ensemble des objets de la base de données, apparemment.
Une solution naïve
La solution à la première question est de couper les résultats basés sur un compteur par un chunk_size
. Il y a plusieurs façons de l'écrire, mais, fondamentalement, ils viennent tous à un OFFSET + LIMIT
de requêtes en SQL.
quelque chose comme:
qs = Model.objects.all()
counter = 0
count = qs.count()
while counter < count:
for model in qs[counter:counter+count].iterator()
yield model
counter += chunk_size
Alors que c'est efficace en terme de mémoire (constante de l'utilisation de la mémoire proportionnelle à l' chunk_size
), c'est vraiment pauvre en terme de vitesse: OFFSET grandit, MySQL et PostgreSQL (et probablement la plupart des DBs) va commencer d'étouffement et de ralentir.
Une meilleure solution
Une meilleure solution est disponible dans ce post par Thierry Schellenbach. Il filtre sur le PK, qui est beaucoup plus rapide que la compensation (à quelle vitesse dépend probablement sur le DB)
pk = 0
last_pk = qs.order_by('-pk')[0].pk
queryset = qs.order_by('pk')
while pk < last_pk:
for row in qs.filter(pk__gt=pk)[:chunksize]:
pk = row.pk
yield row
gc.collect()
Cela commence à être satisfaisante. Maintenant la Mémoire = O(C), et la Vitesse ~= O(N)
Des problèmes avec la "meilleure" solution
La meilleure solution ne fonctionne que lorsque le PK est disponible dans le QuerySet. Malheureusement, ce n'est pas toujours le cas, en particulier lorsque le QuerySet contient des combinaisons distinctes (group_by) et/ou de valeurs (ValueQuerySet).
Pour cette situation, la "meilleure solution" ne peut pas être utilisé.
Pouvons-nous faire mieux?
Maintenant je me demande si on peut aller plus vite et d'éviter le problème de la QuerySets sans PK. Peut-être à l'aide de quelque chose que j'ai trouvé dans d'autres réponses, mais seulement en pur SQL: à l'aide de curseurs.
Depuis que je suis très mauvais avec SQL brut, en particulier dans Django, voici la vraie question:
comment pouvons-nous construire un meilleur Django QuerySet Itérateur pour les grandes tables
De mon point de vue de ce que j'ai lu, c'est que nous devrions utiliser les curseurs côté serveur (apparemment (voir les références) à l'aide d'un standard de Django Curseur ne permettrait pas d'atteindre le même résultat, car, par défaut, python-MySQL et psycopg connecteurs de mettre en cache les résultats).
Serait-ce vraiment un plus rapide (et/ou plus efficace) solution?
Cela peut être fait à l'aide de matières SQL dans django? Ou devrions-nous écrire spécifiques du code python en fonction du connecteur de base de données?
Des curseurs Côté serveur dans PostgreSQL et MySQL
C'est tout ce que j'ai pu obtenir pour le moment...
Django chunked_iterator()
Maintenant, bien sûr, le mieux serait d'avoir cette méthode de travail en tant que queryset.iterator()
, plutôt que d' iterate(queryset)
, et de faire partie de django de base ou au moins une enfichable de l'application.
Mise à jour Grâce à "T" dans les commentaires pour trouver un django billet qui transportent des informations supplémentaires. Les différences dans le connecteur comportements de faire en sorte que, probablement, la meilleure solution serait de créer l' chunked
méthode plutôt que d'étendre de façon transparente iterator
(sonne comme une bonne approche pour moi).
Une mise en œuvre stub existe, mais il n'y a pas eu de travail dans une année, et il ne ressemble pas à l'auteur est prêt à sauter sur celui-là.
Supplémentaires Refs:
- Pourquoi MYSQL LIMITE supérieure de décalage ralentir la requête vers le bas?
- Comment puis-je accélérer une requête MySQL avec un grand décalage dans la clause LIMIT?
- http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
- postgresql: offset + limite devient très lent
- L'amélioration de DÉCALAGE de performances dans PostgreSQL
- http://www.depesz.com/2011/05/20/pagination-with-fixed-order/
- Comment obtenir une ligne-par-ligne MySQL ResultSet en python Curseur Côté Serveur MySQL
Modifications:
Django 1.6 est l'ajout de connexions persistantes aux bases de données
Django Base De Données Les Connexions Persistantes
Cela devrait faciliter, sous certaines conditions, l'utilisation de curseurs. Néanmoins, il est hors de mes compétences actuelles (et de temps pour apprendre) comment mettre en œuvre une telle solution..
Aussi, la "meilleure solution" certainement ne fonctionne pas dans toutes les situations et ne peut pas être utilisé comme une approche générique, à seulement un tampon pour être adapté au cas par cas...