簡體   English   中英

使用 SQLAlchemy ORM 批量插入

[英]Bulk insert with SQLAlchemy ORM

有什么方法可以讓 SQLAlchemy 進行批量插入而不是插入每個單獨的對象。 IE,

正在做:

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

而不是:

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

我剛剛將一些代碼轉換為使用 sqlalchemy 而不是原始 sql,雖然現在使用它更好,但現在似乎更慢(最多 10 倍),我想知道這是否是原因。

也許我可以更有效地使用會話來改善這種情況。 目前我有autoCommit=False並在添加一些東西后執行session.commit() 盡管如果在其他地方更改數據庫,這似乎會導致數據過時,例如即使我執行新查詢,我仍然會得到舊結果?

謝謝你的幫助!

SQLAlchemy 在1.0.0版中介紹了這一點:

批量操作 - SQLAlchemy 文檔

通過這些操作,您現在可以進行批量插入或更新!

例如,您可以執行以下操作:

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

在這里,將進行批量插入。

sqlalchemy 文檔有一篇關於可用於批量插入的各種技術的性能的文章:

ORM 基本上不用於高性能批量插入——這就是 SQLAlchemy 提供 Core 以及 ORM 作為一流組件的全部原因。

對於快速批量插入的用例,ORM 構建在其之上的 SQL 生成和執行系統是 Core 的一部分。 直接使用這個系統,我們可以生成一個可以與直接使用原始數據庫 API 競爭的 INSERT。

或者,SQLAlchemy ORM 提供了批量操作方法套件,它提供了工作過程單元子部分的掛鈎,以便以基於 ORM 的小程度自動化發出核心級 INSERT 和 UPDATE 構造。

下面的示例說明了幾種不同的插入行方法的基於時間的測試,從最自動化到最不自動化。 使用 cPython 2.7,運行時觀察到:

 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

腳本:

 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)

據我所知,沒有辦法讓 ORM 發出批量插入。 我相信根本原因是 SQLAlchemy 需要跟蹤每個對象的身份(即新的主鍵),而批量插入會干擾這一點。 例如,假設您的foo表包含一個id列並映射到一個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

由於 SQLAlchemy 在沒有發出另一個查詢的情況下獲取了x.id的值,我們可以推斷它是直接從INSERT語句中獲取的值。 如果您不需要通過相同的實例對創建的對象進行后續訪問,您可以跳過插入的 ORM 層:

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

SQLAlchemy 無法將這些新行與任何現有對象匹配,因此您必須重新查詢它們以進行任何后續操作。

就陳舊數據而言,記住會話沒有內置方法可以知道何時在會話之外更改數據庫是有幫助的。 為了通過現有實例訪問外部修改的數據,必須將實例標記為expired 這在session.commit()上默認發生,但可以通過調用session.expire_all()session.expire(instance)手動完成。 一個例子(SQL省略):

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()使x過期,因此第一個打印語句隱式地打開一個新事務並重新查詢x的屬性。 如果您注釋掉第一個打印語句,您會注意到第二個現在選擇了正確的值,因為直到更新之后才會發出新查詢。

從事務隔離的角度來看,這是有道理的 - 您應該只在事務之間進行外部修改。 如果這給您帶來了麻煩,我建議您澄清或重新考慮應用程序的事務邊界,而不是立即訪問session.expire_all()

從 0.8 版開始,對 SQLAlchemy 添加了直接支持

根據文檔connection.execute(table.insert().values(data))應該可以解決問題。 (注意,這是一樣的connection.execute(table.insert(), data) ,其經由到調用導致許多個體行插入executemany )。 除了本地連接之外,在任何情況下,性能差異都可能是巨大的。

我通常使用add_all來做到這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()

SQLAlchemy 在1.0.0版中介紹了這一點:

批量操作 - SQLAlchemy 文檔

通過這些操作,您現在可以進行批量插入或更新!

例如(如果您希望簡單表插入的開銷最低),您可以使用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()

或者,如果你願意,跳過loadme元組和直接寫字典入dicts (但我覺得它更容易讓所有的廢話出來的數據,並加載詞典列表中的循環)。

皮埃爾的回答是正確的,但一個問題是,默認情況下, bulk_save_objects不返回對象的主鍵,如果您擔心的話。 return_defaults設置為True以獲得此行為。

文檔在這里

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

條條大路通羅馬,但其中一些要穿越山脈,需要渡輪,但如果您想快速到達那里,只需走高速公路即可。


在這種情況下,高速公路將使用psycopg2execute_batch()功能。 文檔說的是最好的:

executemany()的當前實現(使用極其慈善的輕描淡寫)不是特別有效。 這些函數可用於加速針對一組參數的語句的重復執行。 通過減少服務器往返次數,性能可以比使用executemany()executemany()數量級。

在我自己的測試中, execute_batch()速度大約executemany()兩倍,並且提供了配置 page_size 以進行進一步調整的選項(如果您想從驅動程序中擠出最后 2-3% 的性能)。

如果您使用 SQLAlchemy,在使用create_engine()實例化引擎時將use_batch_mode=True設置為參數,則可以輕松啟用相同的功能

這是一種方式:

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

這將像這樣插入:

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

參考:SQLAlchemy常見問題解答包括各種提交方法的基准。

到目前為止,我找到的最佳答案是在 sqlalchemy 文檔中:

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

有一個完整的可能解決方案基准示例。

如文檔所示:

bulk_save_objects 不是最好的解決方案,但它的性能是正確的。

我認為在可讀性方面第二好的實現是使用 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)]
    )

該函數的上下文在文檔文章中給出。

Sqlalchemy 支持批量插入

bulk_list = [
    Foo(
        bar=1,
    ),
    Foo(
        bar=2,
    ),
    Foo(
        bar=3,
    ),
]
db.session.bulk_save_objects(bulk_list)
db.session.commit()

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM