SQLAlchemy ORM ile toplu ekleme


131

Her bir nesneyi ayrı ayrı eklemek yerine SQLAlchemy'nin toplu ekleme yapmasını sağlamanın herhangi bir yolu var mı? yani

yapıyor:

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

ziyade:

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

Az önce bazı kodları ham sql yerine sqlalchemy kullanmak için dönüştürdüm ve şimdi onunla çalışmak çok daha güzel görünse de (10 faktörüne kadar), nedeninin bu olup olmadığını merak ediyorum.

Belki seansları daha verimli kullanarak durumu iyileştirebilirim. Şu anda autoCommit=Falsebir session.commit()şeyler ekledikten sonra yaptım ve yapıyorum . Bu, DB başka bir yerde değiştirilirse verilerin eskimesine neden gibi görünse de, örneğin yeni bir sorgu yapsam bile eski sonuçları geri alıyorum?

Yardımınız için teşekkürler!



1
Nick, bunun çok eski bir gönderi olduğunu anlıyorum . Başlığı "SQLAlchemy ORM ile çoklu kayıt girişi" gibi doğru bir şekilde güncellemek mümkün olabilir mi ? Sağladığınız gibi çoklu kayıt ekleme ifadeleri, veritabanı düzeyindeki toplu yükleme işlemlerinden oldukça farklıdır. Toplu eklemeler, genellikle büyük veri kümelerinden 1k + veri yüklemeleri için tasarlanmıştır ve REST işlemleri veya uygulama düzeyi kodu tarafından değil, uygulama yöneticileri tarafından yapılır. Terminolojimizi doğru şekilde kullanalım.
W4t3randWind

Sqlalchemy Core'da (ORM değil) toplu işlemler hakkında bilgi ararken bu soruya takılanlar için başka bir soruya verdiğim cevaba bakın .
Nickolay

Yanıtlar:


174

SQLAlchemy bunu sürümde tanıttı 1.0.0:

Toplu işlemler - SQLAlchemy Docs

Bu işlemlerle artık toplu ekleme veya güncelleme yapabilirsiniz!

Örneğin şunları yapabilirsiniz:

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

Burada toplu giriş yapılacaktır.


30
Ayrıca kayıtları gerçekten kaydetmek için s.commit () 'e de ihtiyacınız var (bunu çözmem biraz zaman aldı).
horcle_buzz

3
Bunu sqlachemy 1.0.11 ile denedim ve hala 3 insert ifadesi yapıyor. Ancak normal orm operasyonlarından çok daha hızlıdır.
zidarsk8

3
OP sorusu ile ilgili olmasa da, bunun ORM'nin belirli özelliklerini bozduğunu belirtmekte fayda var. docs.sqlalchemy.org/en/rel_1_0/orm/…
dangel

@dangel evet bunu gönderdiğiniz için teşekkür ederim. OP'nin başlığı "toplu yükleme" ile ilgili olmasına rağmen, çoklu kayıt ekleme ifadeleri hakkındaki sorusunun, sqlalchemy'nin toplu yükleme özelliği ile ilgisi yoktur.
W4t3randWind

\copyCSV'den psql ile aynı verileri eklemeye kıyasla (aynı istemciden aynı sunucuya), sunucu tarafında performansta yaklaşık 10 kat daha fazla ekleme / s ile sonuçlanan büyük bir fark görüyorum . Görünüşe göre, istemciden sunucuya iletişimde bir paketleme kullanarak \copy(veya COPYsunucuda) toplu yükleme , SQLAlchemy aracılığıyla SQL kullanmaktan çok daha iyi. Daha fazla bilgi: PostgreSQL ile ... arasındaki büyük toplu uç performans farkı .
gertvdijk

41

Sqlalchemy dokümanları, toplu uçlar için kullanılabilecek çeşitli tekniklerin performansı hakkında bir yazıya sahiptir :

ORM'ler temelde yüksek performanslı toplu eklemeler için tasarlanmamıştır - SQLAlchemy'nin birinci sınıf bileşen olarak ORM'ye ek olarak Çekirdeği sunmasının tek nedeni budur.

Hızlı toplu eklemelerin kullanım durumu için, ORM'nin üzerine kurduğu SQL oluşturma ve yürütme sistemi Çekirdeğin bir parçasıdır. Bu sistemi doğrudan kullanarak, ham veritabanı API'sini doğrudan kullanarak rekabet eden bir INSERT üretebiliriz.

Alternatif olarak, SQLAlchemy ORM, Core düzeyinde INSERT ve UPDATE yapılarını küçük bir ORM tabanlı otomasyonla yaymak için iş süreci biriminin alt bölümlerine kancalar sağlayan Toplu İşlemler yöntemleri paketini sunar.

Aşağıdaki örnek, en otomatik olandan en azına giden birkaç farklı satır ekleme yöntemi için zamana dayalı testleri göstermektedir. CPython 2.7 ile gözlemlenen çalışma zamanları:

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

Senaryo:

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):
    __tablename__ = "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 __name__ == '__main__':
    test_sqlalchemy_orm(100000)
    test_sqlalchemy_orm_pk_given(100000)
    test_sqlalchemy_orm_bulk_insert(100000)
    test_sqlalchemy_core(100000)
    test_sqlite3(100000)

1
Teşekkür ederim. Gerçekten yararlı ve eksiksiz.
Steve B.

Bindparam kullanan başka bir örnek gördüm. Sözdizimi kısa görünüyor, bu iyi mi?
Jay

35

Bildiğim kadarıyla, ORM'nin toplu girişler yayınlamasını sağlamanın bir yolu yok. Bunun altında yatan nedenin, SQLAlchemy'nin her nesnenin kimliğini (yani, yeni birincil anahtarları) takip etmesi gerektiğine ve toplu girişlerin buna engel olduğuna inanıyorum. Örneğin, footablonuzun bir idsütun içerdiğini ve bir Foosınıfa eşlendiğini varsayarsak :

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

SQLAlchemy x.idbaşka bir sorgu yayınlamadan değerini aldığından , değeri doğrudan INSERTifadeden aldığı sonucuna varabiliriz . Oluşturulan nesnelere aynı örnekler üzerinden daha sonra erişmeniz gerekmiyorsa , eklemeniz için ORM katmanını atlayabilirsiniz:

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

SQLAlchemy bu yeni satırları mevcut nesnelerle eşleştiremez, bu nedenle sonraki işlemler için bunları yeniden sorgulamanız gerekir.

Eski veriler söz konusu olduğunda, oturumun veritabanının oturum dışında ne zaman değiştirildiğini bilmek için yerleşik bir yolu olmadığını hatırlamak yararlıdır. Mevcut örnekler üzerinden harici olarak değiştirilmiş verilere erişmek için, örneklerin süresi dolmuş olarak işaretlenmelidir . Bu, varsayılan olarak açık olur session.commit(), ancak session.expire_all()veya arayarak manuel olarak da yapılabilir session.expire(instance). Bir örnek (SQL ihmal edildi):

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()süresi dolduğundan x, ilk print ifadesi örtük olarak yeni bir işlemi açar ve xözniteliklerini yeniden sorgular . İlk print ifadesini yorumlarsanız, ikincisinin şimdi doğru değeri aldığını fark edeceksiniz, çünkü yeni sorgu güncelleme sonrasına kadar yayınlanmayacaktır.

Bu, işlemsel izolasyon açısından mantıklıdır - işlemler arasında yalnızca harici değişiklikleri almalısınız. Bu sizi sorun yaratıyorsa, hemen ulaşmak yerine uygulamanızın işlem sınırlarını netleştirmenizi veya yeniden düşünmenizi öneririm session.expire_all().


Cevabınız için teşekkürler, buna bir şans vereceğim. Süresi dolan sorunu WRT, gördüğüm şey tamamen aynı değildi. Turbogears'ta kapsamlı bir oturum kullanıyorum. Bir getSession (). Sorgusu (Foo) .filter .... gerçekleştirilmesi, isteğe bağlı olarak farklı şeyler döndürdü, ayrıca yeniden başlatana kadar db'de bulunan güncellenmiş kayıtları da döndürmedi. Bu sorunu bir autocommit = True yaparak ve istek tamamlandıktan sonra oturumu .remove () yapan bir şey ekleyerek düzelttim (yine de bunu yapmanız gerekiyor).
Nick Holden

Sanırım isteğe bağlı olarak farklı şeyler döndürdü çünkü havuzda iş parçacığı başına kapsamlı bir oturum vardı ve oturumlar farklı durumlarda mıydı? Yine de yeni bir talepten sonra sa'nın yeni verileri almaması biraz garip görünüyordu. Autocommit = False'ın ne yaptığını anlamadığımı umuyorum
Nick Holden

İle autocommit=False, session.commit()istek tamamlandığında aramanız gerektiğine inanıyorum (TurboGears'a aşina değilim, bu nedenle sizin için çerçeve düzeyinde ele alınıyorsa bunu dikkate almayın). Değişikliklerinizin veritabanında yapıldığından emin olmanın yanı sıra, bu oturumdaki her şeyin süresinin dolmasına neden olur. Bir sonraki işlem, o oturumun bir sonraki kullanımına kadar başlamaz, bu nedenle aynı iş parçacığı üzerindeki gelecekteki istekler eski verileri görmez.
dhaffey

10
Alternatif stil:session.execute(Foo.__table__.insert(), values)
Joril

6
Sqlalchemy'nin yeni sürümlerinin toplu ekleme yeteneklerine sahip olduğunu unutmayın: docs.sqlalchemy.org/en/latest/orm/…
Wayne Werner

18

Genelde kullanarak yaparım 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()

2
Bunun çalıştığından emin misin? Her .addseferinde bir seansa denk gelmekle aynı şey yapmıyor mu?
Alec

Bu, yöntem adı göz önüne alındığında sezgisel olacaktır, dokümanlar ayrıntıya girmez: Add the given collection of instances to this Session.Toplu ekleme yapmadığına inanmak için herhangi bir nedeniniz var mı?
reubano

3
Bunun çok mantık dışı olduğunu düşünmüyorum - aslında ondan istediğiniz her şeyi ekliyor . Her şeyi oturuma eklemekle ilgili hiçbir şey, temeldeki SQL ifadelerinin yayınlandığı anlamına gelmez. Kaynağa bakıldığında : github.com/zzzeek/sqlalchemy/blob/… aslında her bir öğeye ayrı ayrı gözüküyor . .add
Alec

A bulk_save_objects()ile karşılaştırıldığında iyi çalışıyor flush(), nesnenin kimliğini alabiliriz, ancak bulk_save_objects()alamayız ( flush()çağrılan olay ).
coanor

14

0.8 sürümünden itibaren SQLAlchemy'ye doğrudan destek eklendi

Gereğince dokümanlar , connection.execute(table.insert().values(data))hile yapmak gerekir. (Bunun, bir çağrı yoluyla birçok ayrı satır eklemesine neden olanla aynı olmadığını unutmayın ). Yerel bir bağlantı dışında herhangi bir şeyde performanstaki fark çok büyük olabilir.connection.execute(table.insert(), data)executemany


Hangisinin daha performanslı olduğunu açıklayabilir misiniz?
Jacob Lee

10

SQLAlchemy bunu sürümde tanıttı 1.0.0:

Toplu işlemler - SQLAlchemy Docs

Bu işlemlerle artık toplu ekleme veya güncelleme yapabilirsiniz!

Örneğin (basit tablo INSERT'leri için en düşük ek yükü istiyorsanız), şunları kullanabilirsiniz Session.bulk_insert_mappings():

loadme = [(1, 'a'),
          (2, 'b'),
          (3, 'c')]
dicts = [dict(bar=t[0], fly=t[1]) for t in loadme]

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

Ya da isterseniz, loadmetuple'ları atlayın ve sözlükleri doğrudan içine yazın dicts(ancak tüm sözcükleri verinin dışında bırakmak ve bir döngüde bir sözlük listesi yüklemek daha kolay buluyorum).


7

Piere'nin cevabı doğrudur, ancak bir sorun, bulk_save_objectssizi ilgilendiriyorsa, varsayılan olarak nesnelerin birincil anahtarlarını döndürmemesidir. Bu davranışı elde return_defaultsetmek Trueiçin ayarlayın .

Belgeler burada .

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()

2
Bayrak ile dikkatli olunmalıdır. Sırayla bir seferde bir nesne ekleyecektir ve önemli performans kazancı orada olmayabilir [1]. Benim durumumda, ek yükten dolayı şüphelendiğim performans düştü. [1]: docs.sqlalchemy.org/en/13/orm/…
dhfromkorea

6

Tüm Yollar Roma'ya Çıkıyor , ancak bazıları dağları geçiyor, feribotlar gerekiyor, ancak oraya hızlı bir şekilde ulaşmak istiyorsanız, sadece otoyola gidin.


Bu durumda otoyol, psycopg2'nin execute_batch () özelliğini kullanacaktır . Belgeler en iyisi olduğunu söylüyor:

Şu anki uygulaması executemany()(son derece hayırsever bir eksiklik kullanarak) özellikle başarılı değil. Bu işlevler, bir ifadenin bir dizi parametreye karşı tekrarlanan yürütülmesini hızlandırmak için kullanılabilir. Sunucu gidiş-dönüş sayısını azaltarak, performans, kullanmaktan daha büyük siparişler olabilir executemany().

Kendi testte execute_batch()olduğu iki kat daha hızlı yaklaşık olarak executemany()ve (eğer sürücünün dışarı performansının son 2-3% sıkmak istiyorsanız) ayrıca verdiği için page_size yapılandırma seçeneği sunar.

Aynı özellik, SQLAlchemy kullanıyorsanız use_batch_mode=True, motoru başlattığınızda bir parametre olarak ayarlayarak kolayca etkinleştirilebilir.create_engine()


Not: psycopg2'ler execute_values, toplu ekleme yaparken psycopg2'lerden daha hızlıdırexecute_batch !
Fierr

5

Bu bir yoldur:

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

Bu, şöyle ekleyecektir:

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

Referans: SQLAlchemy SSS , çeşitli kesinleştirme yöntemleri için karşılaştırmalar içerir.


3

Şimdiye kadar bulduğum en iyi cevap sqlalchemy belgelerinde oldu:

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

Olası çözümlerin karşılaştırmalı değerlendirmesinin eksiksiz bir örneği var.

Belgelerde gösterildiği gibi:

bulk_save_objects en iyi çözüm değildir, ancak performansı doğrudur.

Okunabilirlik açısından en iyi ikinci uygulama bence SQLAlchemy Core ile oldu:

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)]
    )

Bu işlevin içeriği dokümantasyon makalesinde verilmiştir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.