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/…
Sean Vieira

1
닉,이게 아주 오래된 게시물 이라는 것을 이해합니다 . 제목을 "SQLAlchemy ORM을 사용한 다중 레코드 삽입"과 같은 올바른 것으로 업데이트 할 수 있습니까? 제공 한 것과 같은 다중 레코드 삽입 문은 데이터베이스 수준의 대량로드 작업과는 상당히 다릅니다. 대량 삽입은 일반적으로 대규모 데이터 세트에서 1k 이상의 데이터 업로드를위한 것이며 REST 작업이나 애플리케이션 수준 코드가 아닌 애플리케이션 관리자가 수행합니다 .... 우리의 명명법을 적절하게 사용합시다.
W4t3randWind 2017

sqlalchemy Core (ORM이 아님)의 대량 작업에 대한 정보를 찾는 동안이 질문을 우연히 발견 한 사람들은 다른 질문에 대한 내 대답을 참조하십시오 .
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 2015

3
나는 sqlachemy 1.0.11로 이것을 시도했지만 여전히 3 개의 삽입 문을 만듭니다. 그러나 일반적인 orm 작업보다 훨씬 빠릅니다.
zidarsk8 2016 년

3
OP 질문과 관련이 없지만 이것이 ORM의 특정 기능을 손상 시킨다는 점을 언급 할 가치가 있습니다. docs.sqlalchemy.org/en/rel_1_0/orm/…
dangel

@dangel yes 이것을 게시 해 주셔서 감사합니다. OP의 제목은 "대량로드"에 관한 것이지만 다중 레코드 삽입 문에 대한 그의 질문은 sqlalchemy의 대량로드 기능과 관련이 없습니다.
W4t3randWind

\copypsql (동일 클라이언트에서 동일한 서버로)을 사용하여 CSV에서 동일한 데이터를 삽입하는 것과 비교할 때 서버 측 에서 성능 큰 차이가있어 초당 10 배 더 많은 삽입이 발생합니다. 분명히 SQLAlchemy를 통해 SQL을 사용하는 것보다 훨씬 더 나은 클라이언트-서버 통신 패킹을 사용 하여 \copy(또는 COPY서버에서) 대량로드하는 것입니다. 추가 정보 : 대 ... 성능 차이 PostgreSQL을 삽입 대형 벌크 .
gertvdijk

42

sqlalchemy 문서에는 대량 삽입에 사용할 수있는 다양한 기술의 성능에 대한 이 있습니다.

ORM은 기본적으로 고성능 대량 삽입을위한 것이 아닙니다. 이것이 SQLAlchemy가 최고급 구성 요소로 ORM 외에도 Core를 제공하는 전체 이유입니다.

빠른 대량 삽입의 사용 사례의 경우 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)

1
감사합니다. 정말 도움이되고 철저합니다.
Steve B.

bindparams를 사용하는 또 다른 예를 보았습니다. 구문이 간결 해 보입니다. 좋은가요?
Jay

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()expires x이므로 첫 번째 print 문은 암시 적으로 새 트랜잭션을 열고 x의 속성을 다시 쿼리 합니다. 첫 번째 print 문을 주석 처리하면 새 쿼리가 업데이트 이후까지 생성되지 않기 때문에 두 번째 문이 올바른 값을 선택한다는 것을 알 수 있습니다.

이것은 트랜잭션 격리의 관점에서 의미가 있습니다. 트랜잭션 사이의 외부 수정 만 선택해야합니다. 이로 인해 문제가 발생하는 경우 즉시 .NET Framework에 도달하는 대신 애플리케이션의 트랜잭션 경계를 명확히하거나 다시 생각하는 것이 좋습니다 session.expire_all().


답장 해주셔서 감사합니다. 만료되는 문제 WRT, 내가 본 것은 완전히 동일하지 않았습니다. 터보 기어에서 범위 지정 세션을 사용하고 있습니다. getSession (). query (Foo) .filter .... all ()을 수행하면 요청에 따라 다른 결과가 반환되고 다시 시작할 때까지 db에 있던 업데이트 된 레코드가 반환되지 않았습니다. 나는 autocommit = True를 수행하고 요청이 완료된 후 세션을 .remove () 처리 한 것을 추가하여이 문제를 해결했습니다 (어쨌든 그렇게 할 것이라고 생각합니다).
Nick Holden

풀의 스레드 당 범위가 지정된 세션이 있고 세션이 다른 상태에 있기 때문에 요청에 따라 다른 것을 반환했다고 생각합니다. sa가 새로운 요청 후에 새로운 데이터를 얻지 못하는 것은 약간 이상하게 보였습니다. 나는 = 거짓이하고있는 자동 커밋 무엇 missunderstanding하고 기대
닉 홀든

를 사용하면 요청 완료시 autocommit=False전화해야한다고 생각합니다 session.commit()(TurboGears에 익숙하지 않으므로 프레임 워크 수준에서 처리되는 경우 무시하십시오). 변경 사항이 데이터베이스에 적용되었는지 확인하는 것 외에도 세션의 모든 것이 만료됩니다. 다음 트랜잭션은 해당 세션을 다음에 사용할 때까지 시작되지 않으므로 동일한 스레드에 대한 향후 요청에는 오래된 데이터가 표시되지 않습니다.
dhaffey

10
대체 스타일 :session.execute(Foo.__table__.insert(), values)
Joril 2013

6
최신 버전의 sqlalchemy에는 대량 삽입 기능이 있습니다. docs.sqlalchemy.org/en/latest/orm/…
Wayne Werner

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한 번에 하나씩 세션 에 연결 하는 것과 동일한 작업을 수행하지 않습니까?
Alec

메서드 이름을 고려하면 직관적이지 않습니다. 문서는 자세히 설명 Add the given collection of instances to this Session.하지 않습니다. 대량 삽입을 수행하지 않는다고 믿을 이유가 있습니까?
reubano

3
나는 그것이 너무 반 직관이라고 생각하지 않습니다. 사실 그것은 당신이 요청하는 모든 것을 추가 합니다 . 세션에 모든 것을 추가하는 것은 기본 SQL 문이 실행되는 것을 암시하는 것처럼 보이지 않습니다. 출처를 살펴보면 : github.com/zzzeek/sqlalchemy/blob/… 실제로 .add각 항목이 개별적으로 표시됩니다.
Alec

그것은에 비해 잘 작동 bulk_save_objects()와 함께, flush()우리는 개체의 ID를 얻을 수 있지만, bulk_save_objects()수 없습니다 (이벤트와 flush()이라고도 함).
coanor

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

2
깃발에주의를 기울여야합니다. 한 번에 하나의 개체를 순차적으로 삽입하므로 성능이 크게 향상되지 않을 수 있습니다 [1]. 제 경우에는 오버 헤드로 인해 성능이 저하되었다고 생각했습니다. [1] : docs.sqlalchemy.org/en/13/orm/…
dhfromkorea

6

모든 도로는 로마 로 연결되지만 일부는 산을 가로 질러 페리가 필요하지만 빠르게 도착하려면 고속도로를 이용하십시오.


이 경우 고속도로는 psycopg2execute_batch () 기능 을 사용하는 것 입니다. 문서는 그것이 최고라고 말합니다.

의 현재 구현 executemany()은 (매우 자선적인 과소 표현 사용) 특별히 수행되지 않습니다. 이러한 함수는 매개 변수 세트에 대해 명령문의 반복 실행 속도를 높이는 데 사용할 수 있습니다. 서버 왕복 수를 줄이면을 사용하는 것보다 성능이 훨씬 더 좋아질 수 있습니다 executemany().

내 자신의 테스트에서 execute_batch()입니다 빨리 약 두 배executemany() , 그리고 (당신이 드라이버에서 성능의 마지막 2~3%을 짠다 할 경우) 더 미세 조정에 대한 PAGE_SIZE를 구성 할 수있는 옵션을 제공합니다.

다음을 사용 use_batch_mode=True하여 엔진을 인스턴스화 할 때 매개 변수 로 설정하여 SQLAlchemy를 사용하는 경우 동일한 기능을 쉽게 사용할 수 있습니다.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 에는 다양한 커밋 방법에 대한 벤치 마크가 포함되어 있습니다.


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는 최상의 솔루션은 아니지만 성능은 정확합니다.

가독성 측면에서 두 번째로 좋은 구현은 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.