web-dev-qa-db-fra.com

itérateur / générateur SqlAlchemy intégré à faible consommation de mémoire?

J'ai une table MySQL d'enregistrement de ~ 10M avec laquelle je me connecte à l'aide de SqlAlchemy. J'ai constaté que les requêtes sur de grands sous-ensembles de cette table consommaient trop de mémoire même si je pensais utiliser un générateur intégré qui récupérait intelligemment des morceaux de la taille d'une bouchée de l'ensemble de données:

for thing in session.query(Things):
    analyze(thing)

Pour éviter cela, je trouve que je dois créer 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 manque-t-il quelque chose 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.

71
Paul

La plupart des implémentations DBAPI tamponnent entièrement les lignes 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 des résultats est en mémoire.

Mais alors, la façon dont Query fonctionne est de charger entièrement 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 plusieurs fois la même identité d'objet dans un jeu de résultats (commun avec un chargement enthousiaste), l'ensemble complet des lignes doit être en mémoire afin que les résultats corrects puissent être renvoyés, sinon les collections et autres peut être partiellement rempli.

Donc Query offre une option pour changer ce comportement via yield_per() . Cet appel entraînera le Query pour générer des lignes par lots, où vous lui donnez la taille du lot. Comme l'indique la documentation, cela n'est approprié que si vous ne faites aucun type de chargement des collections, c'est donc essentiellement si vous savez vraiment ce que vous faites. De plus, si les lignes DBAPI sous-jacentes pré-tamponnent, il y aura toujours cette surcharge de mémoire, de sorte que l'approche évolue légèrement mieux que de ne pas l'utiliser.

J'utilise rarement yield_per(); à la place, j'utilise une meilleure version de l'approche LIMIT que vous proposez ci-dessus en utilisant les fonctions de fenêtre. LIMIT et OFFSET ont un énorme problème: les très grandes valeurs OFFSET ralentissent de plus en plus la requête, car un OFFSET de N la fait parcourir les N lignes - c'est comme faire la même requête cinquante fois au lieu d'une, à chaque lecture d'un de plus en plus de rangées. Avec une approche de fonction de fenêtre, je prérécupère 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 qui tirent chacune de l'une de ces fenêtres à la fois.

L'approche de la fonction 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. À mon humble avis, utiliser au moins Postgresql en vaut vraiment la peine - si vous utilisez une base de données relationnelle, vous pourriez aussi bien utiliser le meilleur.

108
zzzeek

J'ai cherché une traversée/pagination efficace avec SQLAlchemy et je voudrais 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
13
Joel

Je ne suis pas un expert en base de données, mais lorsque j'utilise SQLAlchemy comme une simple couche d'abstraction Python (c'est-à-dire, sans utiliser l'objet de requête ORM), j'ai trouvé une solution satisfaisante pour interroger un 300M- table 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 méthode SQLAlchemy fetchmany() pour parcourir les résultats dans une boucle infinie while:

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 toutes sortes d'agrégation de données sans surcharge de mémoire dangereuse.

NOTE le stream_results fonctionne avec Postgres et l'adaptateur pyscopg2, mais je suppose que cela ne fonctionnera avec aucune DBAPI, ni avec aucune pilote de base de données ...

Il y a une utilisation intéressante dans ce article de blog qui a inspiré ma méthode ci-dessus.

8
edouardtheron

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 things is None:
            break
        for thing in things:
            yield(thing)
        start += WINDOW_SIZE
6
Pietro Battiston

L'utilisation de LIMIT/OFFSET est mauvaise, car vous devez trouver toutes les colonnes {OFFSET} avant, donc plus grande est OFFSET - plus vous obtenez de demande. 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/4501 . 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, parce que j'ai utilisé cet index dans différents cas auparavant, mais je pensais que pour récupérer toutes les données, la requête fenêtrée serait mieux. Dans mon cas, j'avais tort.

3
Victor Gavro

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. Il est donc plus efficace que de créer une liste de toutes les entités avant l'itération, mais vous devez toujours récupérer toutes les données (brutes) en mémoire.

Ainsi, l'utilisation de LIMIT sur d'énormes tables me semble une bonne idée.

2
Pankrat