SQLAlchemy ORMによる一括挿入


130

個々のオブジェクトを挿入するのではなく、SQLAlchemyに一括挿入を実行させる方法はありますか?つまり、

行う:

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)

生のSQLではなくsqlalchemyを使用するようにいくつかのコードを変換しました。これで作業するのがはるかに良くなりましたが、現在は遅くなっています(最大で10倍)。これが理由かどうか疑問に思います。

セッションをより効率的に使用して状況を改善できるかもしれません。現時点ではautoCommit=Falsesession.commit()いくつかの要素を追加した後、追加しています。新しいクエリを実行しても、DBが他の場所で変更されると、データが古くなるように見えますが、古い結果が返されますか?

ご協力いただきありがとうございます!


1
このかもしれないのヘルプ:stackoverflow.com/questions/270879/...
ショーン・ヴィエイラ

1
ニック、私はこれが非常に古い記事であることを理解しています。タイトルを「SQLAlchemy ORMを使用した複数レコードの挿入」などの正しいものに更新することは可能ですか?あなたが提供したようなマルチレコード挿入ステートメントは、データベースレベルでのバルクロード操作とはかなり異なります。一括挿入は、通常は大きなデータセットからの1k以上のデータのアップロードを目的としており、REST操作やアプリケーションレベルのコードではなく、アプリケーションマネージャーによって行われます。命名法を適切に使用しましょう。
W4t3randWind 2017年

(ORMではなく)sqlalchemy コアの一括操作に関する情報を探しているときにこの質問に出くわした人は、別の質問への私の回答を参照しください。
Nickolay

回答:


173

SQLAlchemyはそれをバージョンで導入しました1.0.0

一括操作-SQLAlchemyドキュメント

これらの操作により、一括挿入または更新を実行できるようになりました。

たとえば、次のことができます。

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

ここでは、一括挿入が行われます。


30
実際にレコードを保存するには、s.commit()も必要です(これを理解するには少し時間がかかりました)。
horcle_buzz

3
私はsqlachemy 1.0.11でこれを試しましたが、それでも3つの挿入ステートメントが作成されます。しかし、通常のorm操作よりもはるかに高速です。
zidarsk8 2016年

3
OPの質問には関係ありませんが、これはORMの特定の機能を損なうことに言及する価値があります。docs.sqlalchemy.org/en/rel_1_0/orm/...
dangel

@dangelはい、これを投稿していただきありがとうございます。OPのタイトルは「一括読み込み」に関係していますが、マルチレコード挿入ステートメントに関する彼の質問は、sqlalchemyの一括読み込み機能とは関係ありません。
W4t3randWind 2017年

\copypsqlを使用して(同じクライアントから同じサーバーに)CSVから同じデータを挿入する場合と比較すると、サーバー側のパフォーマンス大きな違いがあり、挿入数/秒が約10倍になります。SQLAlchemyを介したSQLを使用するよりも、クライアントからサーバーへの通信にパッキングを使用して\copy(またはCOPYサーバー上で)バルクロードをLOTする方が明らかに優れています。さらに詳しい情報:大一括挿入のパフォーマンス差PostgreSQLの対...
gertvdijk

42

SQLAlchemyのドキュメントを有するWRITEUPバルク挿入のために使用することができる様々な技術の性能にします。

ORMは基本的に高性能の一括挿入を目的としたものではありません。これが、SQLAlchemyがORMに加えてコアをファーストクラスのコンポーネントとして提供する理由です。

高速一括挿入の使用例では、ORMがその上に構築するSQL生成および実行システムはコアの一部です。このシステムを直接使用することで、生のデータベース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)

1
ありがとうございました。本当に役に立ち、徹底しています。
スティーブB.

bindparamsを使用した別の例を見ました。構文は簡潔に見えますが、それは良いですか?
ジェイ

35

私の知る限り、ORMに一括挿入を発行させる方法はありません。根本的な理由は、SQLAlchemyが各オブジェクトのID(つまり、新しい主キー)を追跡する必要があり、一括挿入がそれを妨害するためだと思います。たとえば、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はこれらの新しい行を既存のオブジェクトと一致させることができないため、後続の操作のためにそれらを新たにクエリする必要があります。

古いデータに関する限り、セッションには、データベースがセッション外で変更された時期を知る方法が組み込まれていないことを覚えておくと役に立ちます。既存のインスタンスを介して外部で変更されたデータにアクセスするには、インスタンスに期限切れのマークを付ける必要があります。これはデフォルトでは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になるため、最初のprintステートメントは暗黙的に新しいトランザクションを開き、xの属性を再クエリします。最初の印刷ステートメントをコメントアウトすると、新しいクエリが更新されるまで発行されないため、2番目のステートメントが正しい値を取得するようになります。

これは、トランザクションの分離という観点からは理にかなっています。トランザクション間の外部の変更のみを取得する必要があります。これにより問題が発生する場合は、すぐにに到達するのではなく、アプリケーションのトランザクション境界を明確にするか、再考することをお勧めしますsession.expire_all()


お返事ありがとうございます。WRT期限切れの問題、私が見たものはまったく同じではありませんでした。私はターボギアでスコープ付きセッションを使用しています。getSession()。query(Foo).filter .... all()を実行すると、リクエストに応じてさまざまなものが返されました。また、再起動するまで、データベースにある更新されたレコードは返されませんでした。autocommit = Trueを実行し、リクエストの完了後にセッションを.remove()dで追加することで、この問題を修正しました(とにかくそれを実行することを意図して収集しています)。
Nick Holden

プール内のスレッドごとにスコープ指定されたセッションがあり、セッションの状態が異なるため、リクエストに応じて異なるものが返されたと思いますか?しかし、saが新しい要求の後に新しいデータを取得しないのは少し奇妙に思われました。autocommit = Falseが何をしているのか誤解していると思います
Nick Holden

ではautocommit=Falsesession.commit()リクエストの完了時に呼び出す必要があると思います(TurboGearsに詳しくないので、フレームワークレベルで処理する場合は無視してください)。変更がデータベースに反映されていることを確認することに加えて、これによりセッションのすべてが期限切れになります。次のトランザクションは、そのセッションが次に使用されるまで開始されないため、同じスレッドでの以降のリクエストでは古いデータが表示されません。
dhaffey

10
代替スタイル:session.execute(Foo.__table__.insert(), values)
Joril、2013

6
SQLAlchemyのの新しいバージョンが一括挿入機能を持っていること注:docs.sqlalchemy.org/en/latest/orm/...
ウェイン・ヴェルナー

18

通常はを使用して行い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
これで問題ありませんか?.add一度に1つずつセッションにアクセスするのと同じことを行うだけではありませんか?
アレック

メソッド名を考えると、それは直観に反します。ドキュメントは詳細には触れませんAdd the given collection of instances to this Session.。一括挿入を行わないと信じる理由はありますか?
reubano

3
直感に反しすぎるとは思いません。実際、あなたが求めるすべてのことが追加 されます。セッションにすべてのものを追加することについては、基礎となるSQLステートメントが発行されることを意味するようには見えません。ソースを見る:github.com/zzzeek/sqlalchemy/blob/…実際には、各アイテムを個別に扱っているように見えます。.add
アレック

これは、と比較して、うまく機能bulk_save_objects()して、flush()我々は、オブジェクトのIDを取得することができますが、bulk_save_objects()できません(イベントとflush()呼ばれます)。
コアナー2018

14

バージョン0.8以降、SQLAlchemyに直接サポートが追加されました

あたりとしてドキュメントconnection.execute(table.insert().values(data))トリックを行う必要があります。(これは、への呼び出しを介して多くの個々の行が挿入される結果と同じではないことに注意してください)。ローカル接続以外では、パフォーマンスの違いは非常に大きくなる可能性があります。connection.execute(table.insert(), data)executemany


10

SQLAlchemyはそれをバージョンで導入しました1.0.0

一括操作-SQLAlchemyドキュメント

これらの操作により、一括挿入または更新を実行できるようになりました。

たとえば(単純なテーブルINSERTのオーバーヘッドを最小限にしたい場合)、次のように使用できます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ます(ただし、データからすべての単語を除外して、ループ内の辞書のリストをロードする方が簡単です)。


7

Piereの答えは正しいですが、問題がある場合bulk_save_objects、デフォルトではオブジェクトの主キーが返されないという問題があります。に設定return_defaultsTrueて、この動作を取得します。

ドキュメントはこちらです。

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
フラグには注意が必要です。一度に1つのオブジェクトが順番に挿入され、パフォーマンスが大幅に向上しない場合があります[1]。私の場合、オーバーヘッドが原因でパフォーマンスが低下したと思われます。[1]:docs.sqlalchemy.org/en/13/orm/...
dhfromkorea

6

すべての道路はローマに通じていますが、一部は山を横切り、フェリーが必要ですが、そこにすばやく行きたい場合は高速道路を利用してください。


この場合、高速道路はpsycopg2のexecute_batch()機能を使用します。ドキュメントはそれが最高だと言っています:

の現在の実装executemany()は、(非常に慈善的な控えめな表現を使用して)特に実行されていません。これらの関数を使用して、一連のパラメーターに対するステートメントの繰り返し実行を高速化できます。サーバーの往復回数を減らすことで、パフォーマンスはを使用するよりも桁違いに向上しexecutemany()ます。

私自身のテストでexecute_batch()は、の約2倍の速さでexecutemany()、さらに調整するためにpage_sizeを構成するオプションが提供されます(ドライバーからパフォーマンスの最後の2〜3%を絞りたい場合)。

SQLAlchemyを使用しuse_batch_mode=Trueている場合は、エンジンをインスタンス化するときにパラメーターとして設定することにより、同じ機能を簡単に有効にすることができます。create_engine()


注:psycopg2のはexecute_valuesある速い psycopg2のよりexecute_batch一括挿入を行う際に!
Fierr

5

これは方法です:

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

これは次のように挿入されます:

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

参照:SQLAlchemy FAQには、さまざまなcommitメソッドのベンチマークが含まれています。


3

これまでに見つけた最良の答えは、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は最適なソリューションではありませんが、パフォーマンスは適切です。

読みやすさの点で2番目に優れた実装は、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)]
    )

この関数のコンテキストは、ドキュメントの記事に記載されています。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.