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!
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.
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()
.
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)
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()
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.
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).
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()
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.
_ {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()
.
La meilleure réponse que j'ai trouvée jusqu'à présent était dans la documentation de sqlalchemy:
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.