Об'ємна вставка з 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)

Я щойно перетворив якийсь код для використання sqlalchemy, а не необробленого sql, і хоча зараз з ним набагато приємніше працювати, зараз здається, що це повільніше (до 10 разів), мені цікаво, чи це причина.

Можливо, я міг би покращити ситуацію, використовуючи ефективніші сеанси. На даний момент я autoCommit=Falseі займаюся session.commit()після того, як я додав деякі речі. Хоча це, мабуть, призводить до застарілих даних, якщо БД змінюється в іншому місці, як, навіть якщо я роблю новий запит, я все одно отримую старі результати?

Спасибі за вашу допомогу!


1
Це може допомогти: stackoverflow.com/questions/270879/…
Шон Віейра,

1
Нік, я розумію, це дуже старий пост. Чи можна було б оновити заголовок на щось правильне, як-от "вставка декількох записів за допомогою SQLAlchemy ORM". Виписки з декількома записами на зразок тієї, яку ви надали, сильно відрізняються від операцій масового завантаження на рівні бази даних. Масові вставки призначені для завантаження даних 1k +, як правило, з великих наборів даних і здійснюються менеджерами програм, а не операціями REST або кодом рівня програми .... Давайте правильно використовувати нашу номенклатуру.
W4t3randWind

Для тих, хто натикається на це питання, шукаючи інформацію про масові операції в 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

3
Я спробував це з sqlachemy 1.0.11, і він все ще робить 3 вставки. Але це набагато швидше, ніж звичайні операції з ормою.
zidarsk8

3
Хоча це не стосується питання щодо ОП, варто згадати, що це порушує деякі особливості ОРМ. docs.sqlalchemy.org/uk/rel_1_0/orm/…
dangel

@dangel так, дякую, що ви написали це. Хоча заголовок ОП стосується "масового завантаження", його питання щодо заяв про вставлення з декількома записами не має нічого спільного з функцією масового завантаження sqlalchemy.
W4t3randWind

Порівняно з тим, щоб вставляти ті самі дані з CSV з \copyдопомогою psql (від того самого клієнта до того ж сервера), я бачу величезну різницю в продуктивності на стороні сервера, що призводить до приблизно в 10 разів більше вставок / с. Мабуть, це масове завантаження з використанням \copy(або COPYна сервері) з використанням упаковки для спілкування з клієнта на сервер ЛОТ краще, ніж використання SQL через SQLAlchemy. Додаткова інформація: Велика об'ємна вставка різниця в продуктивності PostgreSQL проти ... .
gertvdijk

42

Документи sqlalchemy містять опис на виконання різних методик, які можна використовувати для масових вставок:

ORM в основному не призначені для високоефективних об'ємних вставок - це вся причина, чому SQLAlchemy пропонує Core на додаток до ORM в якості першокласного компонента.

У випадку використання швидких масових вставок система генерації та виконання SQL, яку ORM будує поверх, є частиною Core. Використовуючи цю систему безпосередньо, ми можемо створити INSERT, який є конкурентоспроможним безпосередньо з використанням API-сировини бази даних.

В якості альтернативи, SQLAlchemy ORM пропонує набір методів масових операцій, які надають гачки в підрозділи одиниці робочого процесу з метою випромінювання базових структур INSERT та UPDATE з невеликим ступенем автоматизації на основі ORM.

Наведений нижче приклад ілюструє випробування на основі часу для декількох різних способів вставлення рядків, переходячи від найбільш автоматизованих до найменших. На 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
Дякую. Дійсно корисна та ретельна.
Стів Б.

Я бачив ще один приклад використання bindparams. Синтаксис виглядає лаконічним, це добре?
Джей

35

Наскільки я знаю, немає способу змусити ОРМ видавати масові вставки. Я вважаю, що основна причина полягає в тому, що 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 не може співставити ці нові рядки з будь-якими існуючими об'єктами, тому вам доведеться запитувати їх заново для будь-яких наступних операцій.

Що стосується застарілих даних, то корисно пам’ятати, що сеанс не має вбудованого способу знати, коли база даних змінюється поза сеансом. Щоб отримати доступ до зовнішньо модифікованих даних через існуючі екземпляри, екземпляри повинні бути позначені як закінчені . Це відбувається за замовчуванням на 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().


Дякую за вашу відповідь, я збираюся сказати це. Написав питання, що закінчується, те, що я побачив, було зовсім не таким. Я використовую широкомасштабний сеанс в турбогерах. Виконуючи запит getSession (). (Foo) .filter .... all () повертав різні речі залежно від запиту, також не повертав оновлені записи, що були в db, поки я не перезапустив його. Я вирішив цю проблему, зробивши функцію autocommit = True і додавши щось, що .remove () d сеанс після завершення запиту (я вважаю, ви маєте на увазі зробити це все одно).
Нік Холден

Я думаю, що він повертав різні речі залежно від запиту, оскільки у нього був сеанс масштабування на одну нитку в пулі, а сеанси були в різних станах? Здавалося, трохи дивно, що sa не отримає нових даних після нового запиту. Я очікую, що я не розумію, що робить автокомісія = False
Нік Холден

З autocommit=False, я вважаю , ви повинні називати session.commit()по завершенню запиту (я не знайомий з TurboGears, так що ігнорувати це , якщо це обробляється для вас на рівні каркаса). Окрім того, щоб переконатися, що ваші зміни внесли його до бази даних, це втратить чинність у сеансі. Наступна транзакція не розпочнеться до наступного використання цього сеансу, тому майбутні запити в цьому ж потоці не побачать застарілих даних.
dhaffey

10
Альтернативний стиль:session.execute(Foo.__table__.insert(), values)
Жоріл

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їх сеансу один за одним?
Алек

Це було б протилежно інтуїтивно, враховуючи назву методу, документи не вникають у деталі: Add the given collection of instances to this Session.Чи є у вас підстави вважати, що це не робить об'ємну вставку?
reubano

3
Я не думаю, що це занадто контрсуєтно - це насправді додає все те, про що ви просите. Нічого про додавання всіх речей до сеансу не здається, що це означатиме те, що випускаються основні оператори SQL. Дивлячись на джерело: github.com/zzzeek/sqlalchemy/blob/… , насправді це здається просто .addкожним елементом окремо.
Алек

Це добре працює, порівняно bulk_save_objects()з a flush(), ми можемо отримати ідентифікатор об'єкта, але bulk_save_objects()не можемо (подія з flush()викликом).
coanor

14

Пряма підтримка була додана до SQLAlchemy з версії 0.8

Згідно з документами , connection.execute(table.insert().values(data))слід робити трюк. (Зверніть увагу, що це не те саме, connection.execute(table.insert(), data)що призводить до багатьох окремих вставок рядків через виклик до executemany). У будь-якому випадку, крім локального зв'язку, різниця в продуктивності може бути величезною.


10

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(але мені легше залишити всю складність даних і завантажити список словників у циклі).


7

Відповідь П'єра правильна, але одне питання полягає в тому, що 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

Всі дороги ведуть до Риму , але деякі з них перетинають гори, потрібні пороми, але якщо ви хочете швидко дістатися, просто їдьте по автостраді.


В цьому випадку автомагістраль використовувати execute_batch () особливість psycopg2 . Документація говорить про це найкраще:

Нинішня реалізація executemany()(за допомогою надзвичайно добродійного заниження) не є особливо ефективною. Ці функції можна використовувати для прискорення повторного виконання оператора проти набору параметрів. За рахунок зменшення кількості зворотних маршрутів на сервері продуктивність може бути на порядок краща, ніж використання executemany().

У моєму тесті execute_batch()це приблизно в два рази швидше , як executemany()і дає можливість конфігурувати PAGE_SIZE для подальшої настройки (якщо ви хочете вичавити останні 2-3% продуктивність з драйвера).

Ця ж функція може бути легко увімкнена, якщо ви використовуєте SQLAlchemy, встановивши use_batch_mode=Trueяк параметр, коли інстанціювати двигунcreate_engine()


Примітка: psycopg2 - х execute_valuesце швидше , ніж psycopg2 - х execute_batchпри виконанні об'ємних вставок!
Фірр

5

Це спосіб:

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

Це буде вставлено так:

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

Довідка: Поширені питання SQLAlchemy містять орієнтири для різних методів фіксації.


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:

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

Контекст цієї функції наведений у статті документації.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.