SQLAlchemy“默认”与“server_default”性能
Posted
技术标签:
【中文标题】SQLAlchemy“默认”与“server_default”性能【英文标题】:SQLAlchemy "default" vs "server_default" performance 【发布时间】:2019-02-25 02:59:29 【问题描述】:在将 SQLAlchemy 与 PostgreSQL 一起使用时,使用 default
而不是 server_default
映射表列默认值是否有性能优势(或劣势)?
我的理解是default
(通常)在INSERT
中呈现表达式,而server_default
将表达式放在CREATE TABLE
语句中。似乎server_default
类似于直接在数据库中对默认值的典型处理,例如:
CREATE TABLE example (
id serial PRIMARY KEY,
updated timestamptz DEFAULT now()
);
...但我不清楚在INSERT
上处理默认值还是通过创建表更有效。
如果将下面示例中的每个default
参数更改为server_default
,行插入是否会有任何性能改进或下降?
from uuid import uuid4
from sqlalchemy import Column, Boolean, DateTime, Integer
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql import func
Base = declarative_base()
class Item(Base):
__tablename__ = 'item'
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
count = Column(Integer, nullable=False, default=0)
flag = Column(Boolean, nullable=False, default=False)
updated = Column(DateTime(timezone=True), nullable=False, default=func.now())
注意:到目前为止,我找到的关于何时使用 default
而不是 server_default
的最佳解释并未解决性能问题 (see Mike Bayer's SO answer on the subject)。我对该解释的过于简单的总结是,default
比server_default
更受青睐...
...所以问题是在default
和server_default
之间进行选择时是否应该考虑性能?
【问题讨论】:
【参考方案1】:不可能给你一个“这样更快”的答案,因为每个默认值表达式的性能在服务器和 Python 中都可能有很大差异。检索当前时间的函数与标量默认值的行为不同。
接下来,您必须意识到可以通过五种不同的方式提供默认值:
Client-side scalar defaults。一个固定值,例如0
或True
。该值在INSERT
语句中使用。
Client-side Python function。每次需要默认值时调用,生成要插入的值,从那里开始使用与标量默认值相同的方式。这些可以是context sensitive(可以访问当前执行上下文以及要插入的值)。
Client-side SQL expression;这会生成一个额外的SQL expression,然后用于查询并在服务器上执行以生成一个值。
Server-side DLL expression 是 SQL 表达式,然后存储在表定义中,因此也是架构的一部分。服务器使用这些来填充 INSERT
语句中省略的任何列的值,或者在 INSERT
或 UPDATE
语句中将列值设置为 DEFAULT
时。
Server-side implicit defaults or triggers,其中其他 DLL(例如触发器或特定数据库功能)为列提供默认值。
请注意,当涉及到确定默认值的 SQL 表达式时,无论是客户端 SQL 表达式、服务器端 DLL 表达式还是触发器,它与默认值表达式的数据库几乎没有区别来自。查询执行器需要知道如何为给定列生成值,一旦从 DML 语句或模式定义中解析出来,服务器仍然必须为每一行执行表达式。
在这些选项之间进行选择很少会仅基于性能,性能应该最多只是您考虑的多个方面之一。这里涉及到很多因素:
default
使用标量或 Python 函数直接生成 Python 默认值,然后在插入时将新值发送到服务器。 Python 代码可以在数据插入数据库之前访问默认值。
客户端 SQL 表达式、server_default
值和服务器端隐式默认值和触发器都让服务器生成默认值,如果您希望能够访问它,则必须由客户端获取相同的 SQLAlchemy 会话。在将对象插入数据库之前,您无法访问该值。
根据确切的查询和数据库支持,SQLAlchemy 可能必须进行额外 SQL 查询以在 INSERT
语句之前生成默认值,或者在之后运行单独的 SELECT
以获取默认值已插入。您可以控制何时发生这种情况(直接在插入时或在刷新后首次访问时,使用eager_defaults
mapper configuration)。
server_default
或附加到架构的其他默认值(例如触发器)可确保所有客户端都使用相同的默认值,而默认值在 Python 中实现其他平台无法访问。
在使用 PostgreSQL 时,SQLAlchemy 可以利用the RETURNING
clause for DML statements,它使客户端可以在一个步骤中访问服务器端生成的默认值。
因此,当使用 server_default
列默认值计算每一行的新值(不是标量值)时,您可以节省少量 Python 端时间,并节省少量网络带宽,因为您不是将该列的数据发送到数据库。数据库可能更快地创建相同的值,或者可能更慢;这在很大程度上取决于操作的类型。如果您需要从 Python 访问生成的默认值,在同一个事务中,您必须等待返回的数据流,由 SQLAlchemy 解析出来。然而,所有这些细节可能与插入或更新行发生的其他所有事情相比变得微不足道。
了解 ORM 不适合用于高性能批量行插入或更新;引用SQAlchemy Performance FAQ entry:
SQLAlchemy ORM 在同步对数据库的更改时使用工作单元模式。这种模式远远超出了简单的数据“插入”。它包括使用属性检测系统接收分配给对象的属性,该系统跟踪对象的更改,包括在标识映射中跟踪插入的所有行,其效果是对于每一行 SQLAlchemy 必须检索其“最后插入的 id”(如果尚未给出),并且还涉及根据需要对要插入的行进行扫描和排序以查找依赖项。对象还需要进行相当程度的簿记,以保持所有这些运行,对于大量的行来说,一次可能会花费大量的时间来处理大型数据结构,因此最好将它们分块。
基本上,工作单元是高度自动化的,目的是自动化将复杂对象图持久化到关系数据库中的任务,无需显式持久化代码,这种自动化是有代价的。
ORM 基本上不适用于高性能批量插入 - 这就是 SQLAlchemy 除了 ORM 之外还提供 Core 作为一流组件的全部原因。
由于像 SQLAlchemy 这样的 ORM 会带来高昂的开销,因此服务器端或 Python 端默认值之间的任何性能差异都会很快在 ORM 操作的噪音中消失。
因此,如果您担心大量插入或更新操作的性能,您可能希望对这些操作使用 bulk operations,并启用 psycopg2
batch execution helpers 以真正获得速度提升。在使用这些批量操作时,我希望服务器端默认值仅通过节省将行数据从 Python 移动到服务器的带宽来提高性能,但多少取决于默认值的确切性质。
如果批量操作之外的 ORM 插入和更新性能对您来说是个大问题,您需要测试您的特定选项。我将从 SQLAlchemy examples.performance
package 和 add your own test suite 开始使用两个模型,它们仅在单个 server_default
和 default
配置中有所不同。
【讨论】:
妙语,因为像 SQLAlchemy 这样的 ORM 具有高昂的开销,服务器端或 Python 端默认值之间的任何性能差异很快就会消失,巧妙地总结了所有这里有帮助的见解。特别是在选择default
或 server_default
时出现任何重大性能问题的可能性通常很低,并且考虑到默认值的性质、与访问默认值相关的需求以及最适合 SQLAlchemy 的应用程序需求ORM 或 Core,或两者的混合可能是做出该决定的最佳依据。【参考方案2】:
除了比较两者的表现之外,还有其他重要的事情
如果您需要将新列 create_at (Not Null)
添加到现有表 User
中,其中包含一些数据,default
将不起作用。
如果使用default
,在升级数据库的过程中会出现cannot insert Null value to the existing data in table的错误。如果你想维护你的数据,即使只是为了测试,这也会带来很大的麻烦。
当使用server_default
时,在升级数据库的过程中,数据库会将当前的日期时间值插入到所有以前的现有测试数据中。
所以在这种情况下,只有server_default
可以工作。
【讨论】:
或者,您可以在更改列约束之前通过迁移步骤填充这些列(例如,由 Alembic 提供)。以上是关于SQLAlchemy“默认”与“server_default”性能的主要内容,如果未能解决你的问题,请参考以下文章