web-dev-qa-db-fra.com

Insertion en bloc avec SQLAlchemy ORM

Existe-t-il un moyen d'obtenir que SQLAlchemy effectue une insertion en bloc plutôt que d'insérer chaque objet individuellement. c'est à dire.,

faire:

INSERT INTO `foo` (`bar`) VALUES (1), (2), (3)

plutôt que:

INSERT INTO `foo` (`bar`) VALUES (1)
INSERT INTO `foo` (`bar`) VALUES (2)
INSERT INTO `foo` (`bar`) VALUES (3)

Je viens de convertir du code pour utiliser sqlalchemy plutôt que raw sql et bien qu'il soit maintenant beaucoup plus agréable de travailler avec cela semble être plus lent maintenant (jusqu'à un facteur 10), je me demande si c'est la raison.

Peut-être pourrais-je améliorer la situation en utilisant les sessions plus efficacement. Pour le moment, j'ai autoCommit=False et fais une session.commit() après avoir ajouté des éléments. Bien que cela semble rendre les données périmées si la base de données est modifiée ailleurs, même si je fais une nouvelle requête, les anciens résultats sont toujours restitués?

Merci de votre aide!

88
Nick Holden

SQLAlchemy a introduit cela dans la version 1.0.0:

Opérations en bloc - Documents SQLAlchemy

Avec ces opérations, vous pouvez maintenant faire des insertions en bloc ou des mises à jour!

Par exemple, vous pouvez faire:

s = Session()
objects = [
    User(name="u1"),
    User(name="u2"),
    User(name="u3")
]
s.bulk_save_objects(objects)
s.commit()

Ici, un insert en vrac sera fait.

112
Pierre

Autant que je sache, il n’ya aucun moyen de faire en sorte que l’ORM publie des inserts en vrac. Je pense que la raison sous-jacente est que SQLAlchemy doit garder trace de l'identité de chaque objet (c'est-à-dire de nouvelles clés primaires), et les insertions en bloc interfèrent avec cela. Par exemple, en supposant que votre table foo contient une colonne id et soit mappée à une classe Foo:

x = Foo(bar=1)
print x.id
# None
session.add(x)
session.flush()
# BEGIN
# INSERT INTO foo (bar) VALUES(1)
# COMMIT
print x.id
# 1

Comme SQLAlchemy a récupéré la valeur pour x.id sans émettre une autre requête, nous pouvons en déduire qu'il a obtenu la valeur directement à partir de l'instruction INSERT. Si vous n'avez pas besoin d'un accès ultérieur aux objets créés via les instances same, vous pouvez ignorer le calque ORM pour votre insertion:

Foo.__table__.insert().execute([{'bar': 1}, {'bar': 2}, {'bar': 3}])
# INSERT INTO foo (bar) VALUES ((1,), (2,), (3,))

SQLAlchemy ne peut pas faire correspondre ces nouvelles lignes avec des objets existants. Vous devrez donc les interroger à nouveau pour toute opération ultérieure.

En ce qui concerne les données obsolètes, il est utile de se rappeler que la session n'a pas de moyen intégré de savoir quand la base de données est modifiée en dehors de la session. Pour accéder aux données modifiées de manière externe par le biais d'instances existantes, les instances doivent être marquées comme expired. Cela se produit par défaut sur session.commit(), mais peut être effectué manuellement en appelant session.expire_all() ou session.expire(instance). Un exemple (SQL omis):

x = Foo(bar=1)
session.add(x)
session.commit()
print x.bar
# 1
foo.update().execute(bar=42)
print x.bar
# 1
session.expire(x)
print x.bar
# 42

session.commit() expire x, la première instruction print ouvre donc implicitement une nouvelle transaction et interroge à nouveau les attributs de x. Si vous commentez la première instruction print, vous remarquerez que la seconde prend maintenant la valeur correcte, car la nouvelle requête n'est émise qu'après la mise à jour.

Cela a du sens du point de vue de l’isolation transactionnelle - vous ne devez enregistrer que des modifications externes entre les transactions. Si cela vous pose problème, je suggérerais de clarifier ou de repenser les limites des transactions de votre application au lieu d’atteindre immédiatement session.expire_all().

27
dhaffey

Les documents sqlalchemy ont une excellente description des performances de diverses techniques pouvant être utilisées pour les insertions en vrac:

Les ORM ne sont fondamentalement pas conçus pour les plaquettes en vrac hautes performances - C’est la raison pour laquelle SQLAlchemy propose le Core en plus du ORM en tant que composant de première classe.

Pour le cas d'utilisation des insertions rapides en bloc, la génération SQL et Le système d’exécution construit par l’ORM fait partie du noyau . En utilisant directement ce système, nous pouvons produire un INSERT qui est compétitif avec l’utilisation directe de l’API de base de données brute.

Sinon, SQLAlchemy ORM propose la suite Bulk Operations de méthodes, qui fournissent des points d'ancrage dans des sous-sections de l'unité de travail processus afin d’émettre des constructions INSERT et UPDATE de niveau principal avec un petit degré d'automatisation basée sur ORM.

L'exemple ci-dessous illustre des tests basés sur le temps pour plusieurs différents méthodes d'insertion de lignes, allant du plus automatisé au moins . Avec cPython 2.7, les exécutions observées:

classics-MacBook-Pro:sqlalchemy classic$ python test.py
SQLAlchemy ORM: Total time for 100000 records 12.0471920967 secs
SQLAlchemy ORM pk given: Total time for 100000 records 7.06283402443 secs
SQLAlchemy ORM bulk_save_objects(): Total time for 100000 records 0.856323003769 secs
SQLAlchemy Core: Total time for 100000 records 0.485800027847 secs
sqlite3: Total time for 100000 records 0.487842082977 sec

Scénario:

import time
import sqlite3

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String,  create_engine
from sqlalchemy.orm import scoped_session, sessionmaker

Base = declarative_base()
DBSession = scoped_session(sessionmaker())
engine = None


class Customer(Base):
    __table= "customer"
    id = Column(Integer, primary_key=True)
    name = Column(String(255))


def init_sqlalchemy(dbname='sqlite:///sqlalchemy.db'):
    global engine
    engine = create_engine(dbname, echo=False)
    DBSession.remove()
    DBSession.configure(bind=engine, autoflush=False, expire_on_commit=False)
    Base.metadata.drop_all(engine)
    Base.metadata.create_all(engine)


def test_sqlalchemy_orm(n=100000):
    init_sqlalchemy()
    t0 = time.time()
    for i in xrange(n):
        customer = Customer()
        customer.name = 'NAME ' + str(i)
        DBSession.add(customer)
        if i % 1000 == 0:
            DBSession.flush()
    DBSession.commit()
    print(
        "SQLAlchemy ORM: Total time for " + str(n) +
        " records " + str(time.time() - t0) + " secs")


def test_sqlalchemy_orm_pk_given(n=100000):
    init_sqlalchemy()
    t0 = time.time()
    for i in xrange(n):
        customer = Customer(id=i+1, name="NAME " + str(i))
        DBSession.add(customer)
        if i % 1000 == 0:
            DBSession.flush()
    DBSession.commit()
    print(
        "SQLAlchemy ORM pk given: Total time for " + str(n) +
        " records " + str(time.time() - t0) + " secs")


def test_sqlalchemy_orm_bulk_insert(n=100000):
    init_sqlalchemy()
    t0 = time.time()
    n1 = n
    while n1 > 0:
        n1 = n1 - 10000
        DBSession.bulk_insert_mappings(
            Customer,
            [
                dict(name="NAME " + str(i))
                for i in xrange(min(10000, n1))
            ]
        )
    DBSession.commit()
    print(
        "SQLAlchemy ORM bulk_save_objects(): Total time for " + str(n) +
        " records " + str(time.time() - t0) + " secs")


def test_sqlalchemy_core(n=100000):
    init_sqlalchemy()
    t0 = time.time()
    engine.execute(
        Customer.__table__.insert(),
        [{"name": 'NAME ' + str(i)} for i in xrange(n)]
    )
    print(
        "SQLAlchemy Core: Total time for " + str(n) +
        " records " + str(time.time() - t0) + " secs")


def init_sqlite3(dbname):
    conn = sqlite3.connect(dbname)
    c = conn.cursor()
    c.execute("DROP TABLE IF EXISTS customer")
    c.execute(
        "CREATE TABLE customer (id INTEGER NOT NULL, "
        "name VARCHAR(255), PRIMARY KEY(id))")
    conn.commit()
    return conn


def test_sqlite3(n=100000, dbname='sqlite3.db'):
    conn = init_sqlite3(dbname)
    c = conn.cursor()
    t0 = time.time()
    for i in xrange(n):
        row = ('NAME ' + str(i),)
        c.execute("INSERT INTO customer (name) VALUES (?)", row)
    conn.commit()
    print(
        "sqlite3: Total time for " + str(n) +
        " records " + str(time.time() - t0) + " sec")

if __== '__main__':
    test_sqlalchemy_orm(100000)
    test_sqlalchemy_orm_pk_given(100000)
    test_sqlalchemy_orm_bulk_insert(100000)
    test_sqlalchemy_core(100000)
    test_sqlite3(100000)
16
Grant Humphries

Je le fais habituellement en utilisant add_all .

from app import session
from models import User

objects = [User(name="u1"), User(name="u2"), User(name="u3")]
session.add_all(objects)
session.commit()
12
reubano

Le support direct a été ajouté à SQLAlchemy à partir de la version 0.8

Selon les docs , connection.execute(table.insert().values(data)) devrait faire l'affaire. (Notez que ceci est pas identique à connection.execute(table.insert(), data), ce qui entraîne plusieurs insertions de lignes individuelles via un appel à executemany) Sur tout sauf une connexion locale, la différence de performance peut être énorme.

9
user3805082

SQLAlchemy a introduit cela dans la version 1.0.0:

Opérations en bloc - Documents SQLAlchemy

Avec ces opérations, vous pouvez maintenant faire des insertions en bloc ou des mises à jour!

Par exemple (si vous voulez la surcharge la plus faible pour les INSERTs de table simples), vous pouvez utiliser Session.bulk_insert_mappings() :

loadme = [
        (1, 'a')
    ,   (2, 'b')
    ,   (3, 'c')
    ]

dicts = []
for i in range(len(loadme)):
    dicts.append(dict(bar=loadme[i][0], fly=loadme[i][1]))

s = Session()
s.bulk_insert_mappings(Foo, dicts)
s.commit()

Ou, si vous le souhaitez, sautez les lignes loadme et écrivez les dictionnaires directement dans dicts (mais je trouve plus facile de laisser toute la verbosité en dehors des données et de charger une liste de dictionnaires dans une boucle).

6
juanitogan

La réponse de Piere est correcte, mais l'un des problèmes est que bulk_save_objects, par défaut, ne renvoie pas les clés primaires des objets, si cela vous concerne. Définissez return_defaults sur True pour obtenir ce comportement.

La documentation est ici .

foos = [Foo(bar='a',), Foo(bar='b'), Foo(bar='c')]
session.bulk_save_objects(foos, return_defaults=True)
for foo in foos:
    assert foo.id is not None
session.commit()
5
Matthew Moisen

C'est un moyen:

values = [1, 2, 3]
Foo.__table__.insert().execute([{'bar': x} for x in values])

Cela insérera comme ceci:

INSERT INTO `foo` (`bar`) VALUES (1), (2), (3)

Référence: SQLAlchemy FAQ inclut des tests de performances pour différentes méthodes de validation.

4
Eefret

_ {Tous les chemins mènent à Rome}, mais certains d'entre eux traversent des montagnes, nécessitent des ferries, mais si vous voulez vous y rendre rapidement, prenez simplement l'autoroute.


Dans ce cas, l’autoroute doit utiliser le execute_batch () feature de psycopg2 . La documentation dit le meilleur:

L’actuelle implémentation de executemany() (avec un euphémisme extrêmement charitable) n’est pas particulièrement performante. Ces fonctions peuvent être utilisées pour accélérer l'exécution répétée d'une instruction par rapport à un ensemble de paramètres. En réduisant le nombre d'allers-retours sur les serveurs, les performances peuvent être meilleures que par rapport à l'utilisation de executemany().

Dans mon propre test, execute_batch() est environ deux fois plus rapide que executemany() et donne la possibilité de configurer la taille de page pour une modification supplémentaire (si vous souhaitez extraire les derniers 2 ou 3% des performances du pilote).

La même fonctionnalité peut facilement être activée si vous utilisez SQLAlchemy en définissant use_batch_mode=True en tant que paramètre lorsque vous instanciez le moteur avec create_engine().

4
chjortlund

La meilleure réponse que j'ai trouvée jusqu'à présent était dans la documentation de sqlalchemy:

http://docs.sqlalchemy.org/en/latest/faq/performance.html#i-m-inserting-400-000-rows-with-the-orm-and-it-s-really-slow

Il existe un exemple complet de référence de solutions possibles.

Comme indiqué dans la documentation:

bulk_save_objects n'est pas la meilleure solution mais ses performances sont correctes.

La deuxième meilleure implémentation en termes de lisibilité était, je pense, avec SQLAlchemy Core:

def test_sqlalchemy_core(n=100000):
    init_sqlalchemy()
    t0 = time.time()
    engine.execute(
        Customer.__table__.insert(),
            [{"name": 'NAME ' + str(i)} for i in xrange(n)]
    )

Le contexte de cette fonction est donné dans l'article de documentation.

0
lelabo_m