J'ai une table MySQL d'enregistrement d'environ 10M avec laquelle j'interface en utilisant SqlAlchemy. J'ai constaté que les requêtes sur de grands sous-ensembles de cette table consommeraient trop de mémoire même si je pensais utiliser un générateur intégré qui récupérait intelligemment des morceaux de l'ensemble de données:
for thing in session.query(Things):
analyze(thing)
Pour éviter cela, je trouve que je dois construire mon propre itérateur qui mord en morceaux:
lastThingID = None
while True:
things = query.filter(Thing.id < lastThingID).limit(querySize).all()
if not rows or len(rows) == 0:
break
for thing in things:
lastThingID = row.id
analyze(thing)
Est-ce normal ou y a-t-il quelque chose qui me manque concernant les générateurs intégrés SA?
La réponse à cette question semble indiquer que la consommation de mémoire n'est pas à prévoir.
python
mysql
sqlalchemy
Paul
la source
la source
Réponses:
La plupart des implémentations DBAPI mettent entièrement les lignes en mémoire tampon au fur et à mesure qu'elles sont extraites - donc généralement, avant que l'ORM SQLAlchemy n'obtienne même un résultat, l'ensemble de résultats est en mémoire.
Mais alors, la façon dont
Query
fonctionne est qu'il charge complètement le jeu de résultats donné par défaut avant de vous rendre vos objets. La justification ici concerne les requêtes qui sont plus que de simples instructions SELECT. Par exemple, dans les jointures à d'autres tables qui peuvent renvoyer la même identité d'objet plusieurs fois dans un jeu de résultats (commun avec un chargement hâtif), l'ensemble complet de lignes doit être en mémoire afin que les résultats corrects puissent être renvoyés sinon des collections et autres pourrait n'être que partiellement peuplé.Query
Offre donc une option pour modifier ce comportementyield_per()
. Cet appel amènera leQuery
à produire des lignes par lots, où vous lui donnez la taille du lot. Comme l'état de la documentation, cela n'est approprié que si vous n'effectuez aucun chargement impatient de collections, c'est donc essentiellement si vous savez vraiment ce que vous faites. De plus, si le DBAPI sous-jacent pré-tamponne les lignes, il y aura toujours cette surcharge de mémoire, donc l'approche ne s'adapte que légèrement mieux que de ne pas l'utiliser.Je n'utilise presque jamais
yield_per()
; à la place, j'utilise une meilleure version de l'approche LIMIT que vous suggérez ci-dessus en utilisant les fonctions de fenêtre. LIMIT et OFFSET ont un énorme problème: de très grandes valeurs OFFSET font que la requête devient de plus en plus lente, car un OFFSET de N la fait parcourir N lignes - c'est comme faire la même requête cinquante fois au lieu d'une, chaque fois en lisant un nombre de rangées de plus en plus grand. Avec une approche de fonction de fenêtre, je pré-extrait un ensemble de valeurs de «fenêtre» qui se réfèrent à des morceaux de la table que je veux sélectionner. J'émets ensuite des instructions SELECT individuelles que chacune tire de l'une de ces fenêtres à la fois.L'approche de la fonction de fenêtre est sur le wiki et je l'utilise avec beaucoup de succès.
Notez également: toutes les bases de données ne prennent pas en charge les fonctions de fenêtre; vous avez besoin de Postgresql, Oracle ou SQL Server. IMHO utiliser au moins Postgresql en vaut vraiment la peine - si vous utilisez une base de données relationnelle, vous pouvez aussi bien utiliser le meilleur.
la source
Je ne suis pas un expert en bases de données, mais en utilisant SQLAlchemy comme une simple couche d'abstraction Python (c'est-à-dire sans utiliser l'objet ORM Query), j'ai trouvé une solution satisfaisante pour interroger une table de 300 millions de lignes sans exploser l'utilisation de la mémoire ...
Voici un exemple factice:
from sqlalchemy import create_engine, select conn = create_engine("DB URL...").connect() q = select([huge_table]) proxy = conn.execution_options(stream_results=True).execute(q)
Ensuite, j'utilise la
fetchmany()
méthode SQLAlchemy pour parcourir les résultats dans unewhile
boucle infinie :while 'batch not empty': # equivalent of 'while True', but clearer batch = proxy.fetchmany(100000) # 100,000 rows at a time if not batch: break for row in batch: # Do your stuff here... proxy.close()
Cette méthode m'a permis de faire tout type d'agrégation de données sans aucune surcharge mémoire dangereuse.
NOTE
celastream_results
fonctionne avec Postgres et l'pyscopg2
adaptateur, mais je suppose que cela ne fonctionnera avec aucun DBAPI, ni avec aucun pilote de base de données ...Il y a un cas d'utilisation intéressant dans ce billet de blog qui a inspiré ma méthode ci-dessus.
la source
pymysql
), cela devrait être la réponse acceptée à mon humble avis.J'ai étudié la traversée / pagination efficace avec SQLAlchemy et j'aimerais mettre à jour cette réponse.
Je pense que vous pouvez utiliser l'appel de tranche pour limiter correctement la portée d'une requête et vous pouvez la réutiliser efficacement.
Exemple:
window_size = 10 # or whatever limit you like window_idx = 0 while True: start,stop = window_size*window_idx, window_size*(window_idx+1) things = query.slice(start, stop).all() if things is None: break for thing in things: analyze(thing) if len(things) < window_size: break window_idx += 1
la source
.all()
soit nécessaire. Je remarque que la vitesse s'est beaucoup améliorée après le premier appel..all()
la variable things est une requête qui ne supporte pas len ()Dans l'esprit de la réponse de Joel, j'utilise ce qui suit:
WINDOW_SIZE = 1000 def qgen(query): start = 0 while True: stop = start + WINDOW_SIZE things = query.slice(start, stop).all() if len(things) == 0: break for thing in things: yield thing start += WINDOW_SIZE
la source
L'utilisation de LIMIT / OFFSET est mauvaise, car vous devez trouver toutes les colonnes {OFFSET} avant, donc le plus grand est OFFSET - plus la demande est longue. L'utilisation de la requête fenêtrée pour moi donne également de mauvais résultats sur une grande table avec une grande quantité de données (vous attendez les premiers résultats trop longtemps, ce n'est pas bon dans mon cas pour une réponse Web fragmentée).
Meilleure approche donnée ici https://stackoverflow.com/a/27169302/450103 . Dans mon cas, j'ai résolu le problème en utilisant simplement l'index sur le champ datetime et en récupérant la requête suivante avec datetime> = previous_datetime. Stupide, car j'ai utilisé cet index dans différents cas auparavant, mais je pensais que pour récupérer toutes les données, une requête fenêtrée serait meilleure. Dans mon cas, j'avais tort.
la source
AFAIK, la première variante obtient toujours tous les tuples de la table (avec une requête SQL) mais construit la présentation ORM pour chaque entité lors de l'itération. C'est donc plus efficace que de construire une liste de toutes les entités avant d'itérer, mais vous devez toujours récupérer toutes les données (brutes) en mémoire.
Ainsi, utiliser LIMIT sur d'énormes tables me semble une bonne idée.
la source