问题:SQLAlchemy 'bulk_save_objects' vs 'add_all' 底层逻辑差异?

考虑以下三种使用 sqlalchemy ORM 插入对象的方法:

(1)

for obj in objects:
    session.add(obj)

(2)

session.add_all(objects)

(3)

session.bulk_save_objects(objects)

假设objects[]的长度是50000

  • 方法(1)是否形成并发送50000插入SQL查询?

  • 方法(2)是否只形成并发送1SQL查询?

  • 方法(3)是否只形成并发送1SQL查询?

我知道这三种方法的速度差别很大。但是关于底层实现细节有什么区别?

解答

(2) 基本上实现了作为 (1),如果 ORM 必须获取生成的值(例如主键),则两者都可能在刷新期间发出 50,000 次插入。如果这 50,000 个对象具有级联关系,它们甚至可能发出更多。

In [4]: session.add_all([Foo() for _ in range(5)])

In [5]: session.commit()
BEGIN (implicit)
INSERT INTO foo DEFAULT VALUES RETURNING foo.id
{}
... (repeats 3 times)
INSERT INTO foo DEFAULT VALUES RETURNING foo.id
{}
COMMIT

如果您事先提供主键和其他数据库生成的值,那么当参数匹配时,Session可以将单独的插入组合到单个“executemany”操作中。

In [8]: session.add_all([Foo(id=i) for i in range(5)])

In [9]: session.commit()
BEGIN (implicit)
INSERT INTO foo (id) VALUES (%(id)s)
({'id': 0}, {'id': 1}, {'id': 2}, {'id': 3}, {'id': 4})
COMMIT

如果您的 DB-API 驱动程序实现executemany()或等效使用允许它发出具有多个数据的单个语句的方法,那么它可以导致单个查询。例如,在启用executemany_mode='values'后,Postgresql 日志包含上述内容

LOG: statement: INSERT INTO foo (id) VALUES (0),(1),(2),(3),(4)

批量操作跳过大部分Session机制——例如持久化相关对象——以换取性能提升。例如,默认情况下,它不获取默认值,例如主键,这允许它尝试和批量更改操作和参数匹配的更少的“executemany”操作。

In [12]: session.bulk_save_objects([Foo() for _ in range(5)])
BEGIN (implicit)
INSERT INTO foo DEFAULT VALUES
({}, {}, {}, {}, {})

In [13]: session.commit()
COMMIT

它可能仍会发出多个语句,这再次取决于数据和正在使用的 DB-API 驱动程序。文档是一个很好的阅读。

使用psycopg2 快速执行助手启用上述在 Postgresql 日志中产生

LOG: statement: INSERT INTO foo DEFAULT VALUES;INSERT INTO foo DEFAULT VALUES;INSERT INTO foo DEFAULT VALUES;INSERT INTO foo DEFAULT VALUES;INSERT INTO foo DEFAULT VALUES

换句话说,多个语句已加入到发送到服务器的“单个”语句中。

所以,最终所有 3 的答案都是“视情况而定”,这当然看起来令人沮丧。

Logo

学AI,认准AI Studio!GPU算力,限时免费领,邀请好友解锁更多惊喜福利 >>>

更多推荐