Chèn hàng loạt với SQLAlchemy ORM


131

Có cách nào để SQLAlchemy thực hiện chèn hàng loạt thay vì chèn từng đối tượng riêng lẻ. I E,

đang làm:

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

hơn là:

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

Tôi vừa chuyển đổi một số mã để sử dụng sqlalchemy thay vì sql thô và mặc dù bây giờ nó tốt hơn nhiều để làm việc với nó có vẻ chậm hơn bây giờ (lên đến hệ số 10), tôi tự hỏi liệu đây có phải là lý do không.

Có thể tôi có thể cải thiện tình hình bằng cách sử dụng các phiên hiệu quả hơn. Hiện tại tôi có autoCommit=Falsevà đang làm session.commit()sau khi tôi đã thêm một số thứ. Mặc dù điều này dường như khiến dữ liệu bị cũ nếu DB được thay đổi ở nơi khác, chẳng hạn như ngay cả khi tôi thực hiện một truy vấn mới, tôi vẫn nhận được kết quả cũ?

Cảm ơn bạn đã giúp đỡ!


1
Điều này có thể hữu ích: stackoverflow.com/questions/270879/…
Sean Vieira

1
Nick, tôi hiểu đây là một bài viết rất cũ. Có thể cập nhật tiêu đề thành một cái gì đó chính xác như "chèn nhiều bản ghi bằng SQLAlchemy ORM" không. Các câu lệnh chèn nhiều bản ghi như câu bạn đã cung cấp khá khác với các hoạt động tải hàng loạt ở cấp cơ sở dữ liệu. Chèn hàng loạt nhằm mục đích tải lên hơn 1 nghìn dữ liệu, thường là từ các tập dữ liệu lớn và được thực hiện bởi người quản lý ứng dụng, không phải hoạt động REST hoặc mã cấp ứng dụng .... Hãy sử dụng danh pháp của chúng tôi đúng cách.
W4t3randWind

Đối với những người vấp phải câu hỏi này trong khi tìm kiếm thông tin về các hoạt động hàng loạt trong sqlalchemy Core (không phải ORM), hãy xem câu trả lời của tôi cho một câu hỏi khác .
Nickolay

Câu trả lời:


174

SQLAlchemy đã giới thiệu điều đó trong phiên bản 1.0.0:

Hoạt động hàng loạt - tài liệu SQLAlchemy

Với các thao tác này, giờ đây bạn có thể thực hiện chèn hoặc cập nhật hàng loạt!

Ví dụ, bạn có thể làm:

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

Tại đây, một số lượng lớn sẽ được thực hiện.


30
Bạn cũng cần s.commit () để thực sự lưu các bản ghi (tôi phải mất một chút thời gian để tìm ra điều này).
horcle_buzz

3
Tôi đã thử điều này với sqlachemy 1.0.11 và nó vẫn tạo ra 3 câu lệnh chèn. Nhưng nó nhanh hơn rất nhiều so với các hoạt động orm bình thường.
zidarsk 8

3
trong khi không liên quan đến câu hỏi OP, điều đáng nói là điều này phá vỡ một số tính năng nhất định của ORM. docs.sqlalchemy.org/en/rel_1_0/orm/…
dangel

@dangel vâng, cảm ơn bạn đã đăng bài này. Mặc dù tiêu đề của OP liên quan đến "tải hàng loạt" câu hỏi của anh ấy về các câu lệnh chèn nhiều bản ghi không liên quan gì đến tính năng tải hàng loạt của sqlalchemy.
W4t3randWind

So với việc chèn cùng một dữ liệu từ CSV với \copypsql (từ cùng một máy khách đến cùng một máy chủ), tôi thấy sự khác biệt rất lớn về hiệu suất ở phía máy chủ, dẫn đến số lần chèn / s nhiều hơn khoảng 10 lần. Rõ ràng là tải hàng loạt bằng cách sử dụng \copy(hoặc COPYtrên máy chủ) bằng cách sử dụng đóng gói trong giao tiếp từ máy khách đến máy chủ tốt hơn RẤT NHIỀU so với sử dụng SQL qua SQLAlchemy. Thông tin thêm: số lượng lớn lớn chèn khác biệt hiệu suất PostgreSQL vs ... .
gertvdijk,

42

Các tài liệu SQLAlchemy có một writeup về việc thực hiện các kỹ thuật khác nhau có thể được sử dụng để chèn số lượng lớn:

ORM về cơ bản không nhằm mục đích chèn số lượng lớn hiệu suất cao - đây là toàn bộ lý do SQLAlchemy cung cấp Core ngoài ORM như một thành phần hạng nhất.

Đối với trường hợp sử dụng chèn hàng loạt nhanh, hệ thống tạo và thực thi SQL mà ORM xây dựng trên đó là một phần của Core. Sử dụng trực tiếp hệ thống này, chúng tôi có thể tạo ra một INSERT cạnh tranh với việc sử dụng trực tiếp API cơ sở dữ liệu thô.

Ngoài ra, SQLAlchemy ORM cung cấp bộ phương pháp Bulk Operations, cung cấp các móc nối vào các phần con của đơn vị quy trình công việc để tạo ra các cấu trúc INSERT và UPDATE cấp Core với mức độ tự động hóa dựa trên ORM nhỏ.

Ví dụ dưới đây minh họa các bài kiểm tra dựa trên thời gian cho một số phương pháp chèn hàng khác nhau, đi từ tự động nhất đến ít nhất. Với cPython 2.7, thời gian chạy quan sát được:

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

Kịch bản:

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
Cảm ơn bạn. Thực sự hữu ích và kỹ lưỡng.
Steve B.

Tôi đã thấy một ví dụ khác sử dụng bindparams. Cú pháp trông ngắn gọn, điều đó có tốt không?
Jay

35

Theo như tôi biết, không có cách nào để ORM phát hành số lượng chèn hàng loạt. Tôi tin rằng lý do cơ bản là SQLAlchemy cần theo dõi danh tính của từng đối tượng (tức là các khóa chính mới) và việc chèn hàng loạt can thiệp vào điều đó. Ví dụ: giả sử foobảng của bạn chứa một idcột và được ánh xạ tới một Foolớp:

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

Vì SQLAlchemy chọn giá trị cho x.idmà không đưa ra một truy vấn khác, chúng ta có thể suy ra rằng nó nhận giá trị trực tiếp từ INSERTcâu lệnh. Nếu bạn không cần quyền truy cập tiếp theo vào các đối tượng đã tạo thông qua các phiên bản giống nhau , bạn có thể bỏ qua lớp ORM cho phần chèn của mình:

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

SQLAlchemy không thể đối sánh các hàng mới này với bất kỳ đối tượng hiện có nào, vì vậy bạn sẽ phải truy vấn lại chúng cho bất kỳ hoạt động tiếp theo nào.

Đối với dữ liệu cũ có liên quan, sẽ rất hữu ích khi nhớ rằng phiên không có cách tích hợp nào để biết khi nào cơ sở dữ liệu được thay đổi bên ngoài phiên. Để truy cập dữ liệu được sửa đổi bên ngoài thông qua các phiên bản hiện có, các phiên bản phải được đánh dấu là đã hết hạn . Điều này xảy ra theo mặc định trên session.commit(), nhưng có thể được thực hiện thủ công bằng cách gọi session.expire_all()hoặc session.expire(instance). Một ví dụ (SQL bị bỏ qua):

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()hết hạn x, vì vậy câu lệnh in đầu tiên mặc nhiên mở một giao dịch mới và xcác thuộc tính của truy vấn lại . Nếu bạn nhận xét ra câu lệnh in đầu tiên, bạn sẽ nhận thấy rằng câu lệnh thứ hai bây giờ chọn giá trị chính xác, vì truy vấn mới không được phát cho đến sau khi cập nhật.

Điều này có ý nghĩa theo quan điểm của cách ly giao dịch - bạn chỉ nên chọn các sửa đổi bên ngoài giữa các giao dịch. Nếu điều này khiến bạn gặp rắc rối, tôi khuyên bạn nên làm rõ hoặc suy nghĩ lại ranh giới giao dịch của ứng dụng của bạn thay vì ngay lập tức tiếp cận session.expire_all().


Cảm ơn câu trả lời của bạn, tôi sẽ thử. WRT vấn đề hết hạn, những gì tôi thấy không hoàn toàn giống nhau. Tôi đang sử dụng một phiên phạm vi trong turbogears. Thực hiện truy vấn getSession (). (Foo) .filter .... all () trả về những thứ khác nhau tùy thuộc vào yêu cầu, cũng không trả lại các bản ghi đã cập nhật trong db cho đến khi tôi khởi động lại nó. Tôi đã khắc phục sự cố này bằng cách thực hiện autocommit = True và thêm vào một cái gì đó .remove () d phiên sau khi yêu cầu hoàn tất (tôi hiểu rằng bạn có ý định làm điều đó dù sao).
Nick Holden

Tôi đoán nó trả về những thứ khác nhau tùy thuộc vào yêu cầu vì nó có một phiên phạm vi cho mỗi chuỗi trong nhóm và các phiên ở các trạng thái khác nhau? Có vẻ hơi kỳ lạ khi sa sẽ không nhận được dữ liệu mới sau một yêu cầu mới. Tôi hy vọng rằng tôi đang hiểu sai những gì autocommit = False đang làm
Nick Holden

Với autocommit=False, tôi tin rằng bạn nên gọi session.commit()khi yêu cầu hoàn thành (Tôi không quen thuộc với TurboGears, vì vậy hãy bỏ qua điều này nếu điều đó được xử lý cho bạn ở cấp khung). Bên cạnh việc đảm bảo rằng các thay đổi của bạn đã được đưa vào cơ sở dữ liệu, điều này sẽ hết hạn mọi thứ trong phiên. Giao dịch tiếp theo sẽ không bắt đầu cho đến lần sử dụng tiếp theo của phiên đó, vì vậy các yêu cầu trong tương lai trên cùng một chuỗi sẽ không thấy dữ liệu cũ.
dhaffey

10
Phong cách thay thế:session.execute(Foo.__table__.insert(), values)
Joril

6
Lưu ý rằng các phiên bản mới hơn của sqlalchemy có khả năng chèn hàng loạt: docs.sqlalchemy.org/en/latest/orm/…
Wayne Werner

18

Tôi thường làm điều đó bằng cách sử dụng 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
Bạn có chắc điều này làm việc? Nó không chỉ làm tương đương với việc nhập .addchúng vào phiên một lúc?
Alec

Điều đó sẽ phản trực quan với tên phương thức, tài liệu không đi vào chi tiết: Add the given collection of instances to this Session.Bạn có lý do gì để tin rằng nó không thực hiện chèn hàng loạt không?
reubano

3
Tôi không nghĩ rằng nó quá phản trực giác - trên thực tế, nó bổ sung tất cả những thứ bạn yêu cầu. Không có gì về việc thêm tất cả những thứ vào phiên có vẻ như nó ngụ ý những gì các câu lệnh SQL cơ bản được phát hành. Nhìn vào nguồn: github.com/zzzeek/sqlalchemy/blob/… trên thực tế nó dường như chỉ có .addtừng mục riêng lẻ.
Alec

Nó hoạt động tốt, so với bulk_save_objects(), với a flush(), chúng ta có thể lấy ID của đối tượng, nhưng bulk_save_objects()không thể (sự kiện với flush()được gọi).
coanor 19/09/18

14

Hỗ trợ trực tiếp đã được thêm vào SQLAlchemy kể từ phiên bản 0.8

Theo các tài liệu , connection.execute(table.insert().values(data))nên làm thủ thuật. (Lưu ý rằng điều này không giống như connection.execute(table.insert(), data)điều này dẫn đến nhiều lần chèn hàng riêng lẻ thông qua lệnh gọi đến executemany). Trên bất kỳ thứ gì ngoại trừ kết nối cục bộ, sự khác biệt về hiệu suất có thể rất lớn.


10

SQLAlchemy đã giới thiệu điều đó trong phiên bản 1.0.0:

Hoạt động hàng loạt - tài liệu SQLAlchemy

Với các thao tác này, giờ đây bạn có thể thực hiện chèn hoặc cập nhật hàng loạt!

Ví dụ: (nếu bạn muốn chi phí thấp nhất cho INSERT bảng đơn giản), bạn có thể sử dụng 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()

Hoặc, nếu bạn muốn, bỏ qua các loadmebộ và ghi trực tiếp từ điển vào dicts(nhưng tôi thấy việc loại bỏ tất cả các từ điển ra khỏi dữ liệu và tải lên danh sách các từ điển trong một vòng lặp sẽ dễ dàng hơn).


7

Câu trả lời của Piere là đúng nhưng một vấn đề là bulk_save_objectstheo mặc định không trả về khóa chính của các đối tượng, nếu điều đó là mối quan tâm của bạn. Đặt return_defaultsđể Truecó được hành vi này.

Tài liệu ở đây .

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
Cần phải thận trọng với lá cờ. Nó sẽ chèn từng đối tượng một cách tuần tự và hiệu suất đạt được có thể không có ở đó [1]. Trong trường hợp của tôi, hiệu suất bị giảm sút mà tôi nghi ngờ do chi phí. [1]: docs.sqlalchemy.org/en/13/orm/…
dhfromkorea

6

Tất cả các con đường đều dẫn đến Rome , nhưng một số con đường trong số đó băng qua núi, cần có phà nhưng nếu bạn muốn đến đó nhanh chóng thì chỉ cần đi đường cao tốc.


Trong trường hợp này, đường cao tốc sẽ sử dụng tính năng execute_batch () của psycopg2 . Tài liệu nói rằng nó là tốt nhất:

Việc triển khai hiện tại executemany()là (sử dụng cách nói cực kỳ từ thiện) không hoạt động đặc biệt. Các hàm này có thể được sử dụng để tăng tốc độ thực thi lặp đi lặp lại một câu lệnh dựa trên một tập các tham số. Bằng cách giảm số vòng quay của máy chủ, hiệu suất có thể đạt được mức độ lớn hơn so với việc sử dụng executemany().

Trong thử nghiệm của riêng tôi execute_batch()khoảng hai lần càng nhanh như executemany(), và cung cấp cho các tùy chọn để cấu hình PAGE_SIZE cho tinh chỉnh hơn nữa (nếu bạn muốn ép 2-3% cuối cùng của hiệu suất ra của người lái xe).

Tính năng tương tự có thể dễ dàng được bật nếu bạn đang sử dụng SQLAlchemy bằng cách đặt use_batch_mode=Truelàm tham số khi bạn khởi tạo công cụ bằngcreate_engine()


Lưu ý: psycopg2 của execute_valuesnhanh hơn so với psycopg2 của execute_batchkhi thực hiện chèn số lượng lớn!
Fierr

5

Đây là một cách:

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

Điều này sẽ chèn như thế này:

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

Tham khảo: Câu hỏi thường gặp về SQLAlchemy bao gồm các điểm chuẩn cho các phương thức cam kết khác nhau.


3

Câu trả lời tốt nhất mà tôi tìm thấy cho đến nay là trong tài liệu sqlalchemy:

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

Có một ví dụ hoàn chỉnh về tiêu chuẩn của các giải pháp khả thi.

Như được hiển thị trong tài liệu:

Bul_save_objects không phải là giải pháp tốt nhất nhưng hiệu suất của nó là chính xác.

Cách triển khai tốt thứ hai về khả năng đọc mà tôi nghĩ là với 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)]
    )

Bối cảnh của chức năng này được đưa ra trong bài viết tài liệu.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.