Itérateur / générateur SqlAlchemy intégré à mémoire efficace?


90

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.


J'ai quelque chose de très similaire, sauf que cela donne "chose". Fonctionne mieux que toutes les autres solutions
iElectric

2
N'est-ce pas Thing.id> lastThingID? Et qu'est-ce que les «lignes»?
synergique

Réponses:


118

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 Queryfonctionne 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é.

QueryOffre donc une option pour modifier ce comportement yield_per(). Cet appel amènera le Queryà 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.


Vous mentionnez que Query instancie tout pour comparer les identités. Cela pourrait-il être évité en triant sur la clé primaire et en ne comparant que les résultats consécutifs?
Tobu

le problème est que si vous cédez une instance avec l'identité X, l'application la prend en main, puis prend des décisions basées sur cette entité, et peut-être même la mute. Plus tard, peut-être (en fait généralement) même sur la ligne suivante, la même identité revient dans le résultat, peut-être pour ajouter plus de contenu à ses collections. L'application a donc reçu l'objet dans un état incomplet. le tri n'aide pas ici car le plus gros problème est le fonctionnement du chargement hâtif - le chargement "joint" et "sous-requête" a des problèmes différents.
zzzeek du

J'ai compris la chose "la ligne suivante met à jour les collections", auquel cas il suffit de regarder en avant d'une ligne de base de données pour savoir quand les collections sont terminées. La mise en œuvre du chargement hâtif devrait coopérer avec le tri, de sorte que les mises à jour de la collection soient toujours effectuées sur les lignes adjacentes.
Tobu

l'option yield_per () est toujours là lorsque vous êtes sûr que la requête que vous émettez est compatible avec la livraison d'ensembles de résultats partiels. J'ai passé une session marathon de plusieurs jours à essayer d'activer ce comportement dans tous les cas, il y avait toujours obscur, c'est-à-dire jusqu'à ce que votre programme utilise l'un d'entre eux, des bords qui échouaient. En particulier, le fait de se fier à la commande ne peut être supposé. Comme toujours, je suis bienvenu aux contributions de code réelles.
zzzeek

1
Étant donné que j'utilise postgres, il semble qu'il est possible d'utiliser une transaction en lecture seule répétable et d'exécuter toutes les requêtes fenêtrées dans cette transaction.
schatten

24

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 une whileboucle 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 cela stream_resultsfonctionne avec Postgres et l' pyscopg2adaptateur, 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.


1
Si l'on travaille sur postgres ou mysql (avec pymysql), cela devrait être la réponse acceptée à mon humble avis.
Yuki Inoue

1
J'ai sauvé ma vie, je voyais mes requêtes courir de plus en plus lentement. J'ai instrumenté ce qui précède sur pyodbc (du serveur sql à postgres) et cela fonctionne comme un rêve.
Ed Baker

C'était pour moi la meilleure approche. Comme j'utilise ORM, j'avais besoin de compiler le SQL dans mon dialecte (Postgres), puis de l'exécuter directement à partir de la connexion (pas de la session) comme indiqué ci-dessus. Le compiler "comment faire" que j'ai trouvé dans cette autre question stackoverflow.com/questions/4617291 . L'amélioration de la vitesse était importante. Le passage de JOINS à SUBQUERIES a également été une grande augmentation des performances. Recommande également d'utiliser sqlalchemy_mixins, l'utilisation de smart_query a beaucoup aidé à créer la requête la plus efficace. github.com/absent1706/sqlalchemy-mixins
Gustavo Gonçalves

14

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

Cela semble très simple et rapide. Je ne suis pas sûr que ce .all()soit nécessaire. Je remarque que la vitesse s'est beaucoup améliorée après le premier appel.
hamx0r

@ hamx0r Je me rends compte que c'est un vieux commentaire, alors laissez-le pour la postérité. Sans .all()la variable things est une requête qui ne supporte pas len ()
David

9

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

Things = query.slice (start, stop) .all () retournera [] à la fin et la boucle while ne sera jamais interrompue
Martin Reguly

4

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.


3

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.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.