SqlAlchemy 2.0 中文文档(四十四)(1)https://developer.aliyun.com/article/1563068
行流式传输
若要启用无特定分区大小的服务器端游标,可以使用 Connection.execution_options.stream_results
选项,这与 Connection.execution_options.yield_per
相似,可以在 Connection
对象或语句对象上调用。
当使用 Connection.execution_options.stream_results
选项传递的 Result
对象被直接迭代时,行会内部使用默认的缓冲方案进行获取,首先缓冲一小部分行,然后在每次提取时增加越来越大的缓冲区,直到预配置的 1000 行的限制。此缓冲区的最大大小可以通过 Connection.execution_options.max_row_buffer
执行选项受到影响:
with engine.connect() as conn: with conn.execution_options(stream_results=True, max_row_buffer=100).execute( text("select * from table") ) as result: for row in result: print(f"{row}")
虽然Connection.execution_options.stream_results
选项可以与Result.partitions()
方法结合使用,但应向Result.partitions()
传递特定的分区大小,以避免获取整个结果。通常,在设置使用Result.partitions()
方法时,使用Connection.execution_options.yield_per
选项更为简单。
另请参阅
使用 Yield Per 获取大型结果集 - 在 ORM 查询指南中
Result.partitions()
Result.yield_per()
## 模式名称的翻译
为支持将常见的表集分布到多个模式中的多租户应用程序,可以使用Connection.execution_options.schema_translate_map
执行选项,以重新配置一组Table
对象,使其在不进行任何更改的情况下呈现不同的模式名称。
给定一个表:
user_table = Table( "user", metadata_obj, Column("id", Integer, primary_key=True), Column("name", String(50)), )
此Table
的“模式”,由Table.schema
属性定义,为 None
。 Connection.execution_options.schema_translate_map
可以指定所有模式为 None
的Table
对象实际上将模式呈现为 user_schema_one
:
connection = engine.connect().execution_options( schema_translate_map={None: "user_schema_one"} ) result = connection.execute(user_table.select())
上述代码将在数据库上调用如下形式的 SQL:
SELECT user_schema_one.user.id, user_schema_one.user.name FROM user_schema_one.user
也就是说,模式名称被替换为我们翻译后的名称。该映射可以指定任意数量的目标->目标模式:
connection = engine.connect().execution_options( schema_translate_map={ None: "user_schema_one", # no schema name -> "user_schema_one" "special": "special_schema", # schema="special" becomes "special_schema" "public": None, # Table objects with schema="public" will render with no schema } )
Connection.execution_options.schema_translate_map
参数影响从 SQL 表达式语言生成的所有 DDL 和 SQL 构造,这些构造是从Table
或Sequence
对象派生的。它不会影响通过text()
构造使用的文本字符串 SQL,也不会影响通过Connection.execute()
传递的纯字符串。
该特性仅在以下情况下生效,即模式名称直接来源于Table
或Sequence
的名称;它不会影响直接传递字符串模式名称的方法。按照这种模式,它在MetaData.create_all()
或MetaData.drop_all()
等方法执行的“可以创建”/“可以删除”检查中生效,并且在给定Table
对象的情况下使用表反射时生效。然而,它不会影响Inspector
对象上存在的操作,因为模式名称是显式传递给这些方法的。
提示
要在 ORMSession
中使用模式翻译功能,请在Engine
级别设置此选项,然后将该引擎传递给Session
。Session
为每个事务使用一个新的Connection
:
schema_engine = engine.execution_options(schema_translate_map={...}) session = Session(schema_engine) ...
警告
在没有扩展的情况下使用 ORMSession
时,模式翻译功能仅支持每个 Session 的单个模式翻译映射。如果在每个语句的基础上提供了不同的模式翻译映射,则它不会起作用,因为 ORMSession
不考虑用于单个对象的当前模式翻译值。
要在多个 schema_translate_map
配置中使用单个 Session
,可以使用 Horizontal Sharding 扩展。请参阅 Horizontal Sharding 中的示例。## SQL 编译缓存
新功能:1.4 版本中新增了 SQLAlchemy 具有透明查询缓存系统,大大降低了在 Core 和 ORM 中将 SQL 语句构造转换为 SQL 字符串所涉及的 Python 计算开销。请参阅 Transparent SQL Compilation Caching added to All DQL, DML Statements in Core, ORM 中的介绍。
SQLAlchemy 包含了一个全面的 SQL 编译器缓存系统,以及其 ORM 变体。该缓存系统在 Engine
内部是透明的,并提供了对于给定的 Core 或 ORM SQL 语句的 SQL 编译过程,以及为该语句组装结果获取机制的相关计算,只会对具有相同结构的语句对象执行一次,而对于具有相同结构的引擎的“已编译缓存”中的所有其他语句对象,在特定结构保持在引擎的“已编译缓存”中的持续时间内。所谓“具有相同结构的语句对象”,通常对应于在函数内构造的 SQL 语句,并且每次运行该函数时都会构建该语句:
def run_my_statement(connection, parameter): stmt = select(table) stmt = stmt.where(table.c.col == parameter) stmt = stmt.order_by(table.c.id) return connection.execute(stmt)
上述语句将生成类似于 SELECT id, col FROM table WHERE col = :col ORDER BY id
的 SQL 语句,注意,虽然 parameter
的值是一个普通的 Python 对象,比如一个字符串或一个整数,但该语句的字符串 SQL 形式不包括该值,因为它使用了绑定参数。在 connection.execute()
调用的范围内,上述 run_my_statement()
函数的后续调用将使用缓存的编译结构,以提高性能。
注意
需要注意的是,SQL 编译缓存仅缓存传递给数据库的 SQL 字符串,而不是查询返回的数据。它绝对不是数据缓存,也不会影响返回特定 SQL 语句的结果,也不意味着与提取结果行相关联的内存使用。
虽然 SQLAlchemy 从早期的 1.x 系列就有了一个基本的语句缓存,并且还在 ORM 中提供了“Baked Query”扩展,但这两个系统都需要高度特殊的 API 使用才能使缓存生效。从 1.4 版本开始的新缓存完全是自动的,无需改变编程风格即可生效。
该缓存会自动使用,无需进行任何配置更改,也不需要任何特殊步骤来启用它。以下各节详细介绍了缓存的配置和高级使用模式。
配置
缓存本身是一个名为 LRUCache
的类似于字典的对象,它是 SQLAlchemy 的内部字典子类,跟踪特定键的使用情况,并在缓存大小达到某个阈值时特性地执行定期的“修剪”步骤,移除最近最少使用的项目。此缓存的大小默认为 500,并且可以使用 create_engine.query_cache_size
参数进行配置:
engine = create_engine( "postgresql+psycopg2://scott:tiger@localhost/test", query_cache_size=1200 )
缓存的大小可以增长到给定大小的 150%,然后再减少到目标大小。因此,1200 大小的缓存可以增长到 1800 大小,然后被修剪到 1200。
缓存的大小基于每个引擎渲染的每个唯一 SQL 语句条目。从 Core 和 ORM 生成的 SQL 语句被平等对待。DDL 语句通常不会被缓存。为了确定缓存的作用,引擎日志将包括有关缓存行为的详细信息,下一节将对其进行描述。
使用日志估算缓存性能
上述 1200 大小的缓存实际上相当大。对于小型应用程序,100 大小可能足够。为了估算缓存的最佳大小,假设目标主机上存在足够的内存,缓存的大小应基于可能为使用的目标引擎渲染的唯一 SQL 字符串数量。查看此的最便捷方法是使用 SQL 回显,可以通过使用 create_engine.echo
标志直接启用,也可以通过使用 Python 日志记录;有关日志配置的背景,请参阅配置日志部分。
作为示例,我们将检查以下程序产生的日志记录:
from sqlalchemy import Column from sqlalchemy import create_engine from sqlalchemy import ForeignKey from sqlalchemy import Integer from sqlalchemy import select from sqlalchemy import String from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship from sqlalchemy.orm import Session Base = declarative_base() class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) data = Column(String) bs = relationship("B") class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(ForeignKey("a.id")) data = Column(String) e = create_engine("sqlite://", echo=True) Base.metadata.create_all(e) s = Session(e) s.add_all([A(bs=[B(), B(), B()]), A(bs=[B(), B(), B()]), A(bs=[B(), B(), B()])]) s.commit() for a_rec in s.scalars(select(A)): print(a_rec.bs)
运行时,每个记录的 SQL 语句将在传递的参数左侧包括一个带括号的缓存统计徽章。我们可能看到的四种消息类型如下所示:
[原始 SQL]
- 使用Connection.exec_driver_sql()
方法的驱动程序或最终用户发出的原始 SQL - 不适用缓存[无键]
- 语句对象是一个不被缓存的 DDL 语句,或者语句对象包含不可缓存的元素,例如用户定义的结构或任意大的 VALUES 子句。[在 X 秒内生成]
- 该语句是一个缓存未命中,必须编译,然后存储在缓存中。生成编译结构花费了 X 秒。数字 X 将是很小的分数秒。[cached since Xs ago]
- 该语句是缓存命中,无需重新编译。该语句自 X 秒前起已存储在缓存中。数字 X 将与应用程序运行时间和语句缓存时间成比例,例如,对于一个 24 小时的时间段,X 将为 86400。
每个徽章的详细描述如下。
上述程序中我们看到的第一条语句是 SQLite 方言检查“a”和“b”表是否存在:
INFO sqlalchemy.engine.Engine PRAGMA temp.table_info("a") INFO sqlalchemy.engine.Engine [raw sql] () INFO sqlalchemy.engine.Engine PRAGMA main.table_info("b") INFO sqlalchemy.engine.Engine [raw sql] ()
对于上述两个 SQLite PRAGMA 语句,徽章显示为[raw sql]
,这表示驱动程序直接将 Python 字符串通过Connection.exec_driver_sql()
发送到数据库。这些语句不适用于缓存,因为它们已经以字符串形式存在,而且由于 SQLAlchemy 不提前解析 SQL 字符串,对将返回的结果行的类型也一无所知。
我们看到的下一条语句是 CREATE TABLE 语句:
INFO sqlalchemy.engine.Engine CREATE TABLE a ( id INTEGER NOT NULL, data VARCHAR, PRIMARY KEY (id) ) INFO sqlalchemy.engine.Engine [no key 0.00007s] () INFO sqlalchemy.engine.Engine CREATE TABLE b ( id INTEGER NOT NULL, a_id INTEGER, data VARCHAR, PRIMARY KEY (id), FOREIGN KEY(a_id) REFERENCES a (id) ) INFO sqlalchemy.engine.Engine [no key 0.00006s] ()
对于每个语句,徽章显示为[no key 0.00006s]
。这表示这两个特定语句,由于 DDL 导向的CreateTable
构造未生成缓存键,因此未发生缓存。DDL 构造通常不参与缓存,因为它们通常不会被重复执行第二次,而且 DDL 也是数据库配置步骤,性能并不那么关键。
[no key]
徽章之所以重要,是因为它可以用于生成可缓存的 SQL 语句,除了某些当前不可缓存的特定子构造。这些包括未定义缓存参数的自定义用户定义 SQL 元素,以及生成任意长且不可重现的 SQL 字符串的某些构造,主要示例包括Values
构造以及使用Insert.values()
方法进行“多值插入”时。
到目前为止,我们的缓存仍然是空的。下一条语句将被缓存,一个段落看起来像:
INFO sqlalchemy.engine.Engine INSERT INTO a (data) VALUES (?) INFO sqlalchemy.engine.Engine [generated in 0.00011s] (None,) INFO sqlalchemy.engine.Engine INSERT INTO a (data) VALUES (?) INFO sqlalchemy.engine.Engine [cached since 0.0003533s ago] (None,) INFO sqlalchemy.engine.Engine INSERT INTO a (data) VALUES (?) INFO sqlalchemy.engine.Engine [cached since 0.0005326s ago] (None,) INFO sqlalchemy.engine.Engine INSERT INTO b (a_id, data) VALUES (?, ?) INFO sqlalchemy.engine.Engine [generated in 0.00010s] (1, None) INFO sqlalchemy.engine.Engine INSERT INTO b (a_id, data) VALUES (?, ?) INFO sqlalchemy.engine.Engine [cached since 0.0003232s ago] (1, None) INFO sqlalchemy.engine.Engine INSERT INTO b (a_id, data) VALUES (?, ?) INFO sqlalchemy.engine.Engine [cached since 0.0004887s ago] (1, None)
在上面,我们实际上看到了两个唯一的 SQL 字符串;"INSERT INTO a (data) VALUES (?)"
和"INSERT INTO b (a_id, data) VALUES (?, ?)"
。由于 SQLAlchemy 对所有文字值使用绑定参数,即使这些语句为不同对象重复多次,由于参数是分开的,实际的 SQL 字符串保持不变。
注意
上述两个语句是由 ORM 工作单元生成的,实际上会将这些语句缓存在每个映射器的本地缓存中。但是机制和术语是相同的。下面的章节禁用或使用备用字典缓存某些(或全部)语句将描述用户代码如何基于每个语句使用备用缓存容器。
我们看到的每个这两个语句的第一次出现的缓存徽章是[generated in 0.00011s]
。这表示该语句不在缓存中,在 0.00011 秒内编译成字符串,然后被缓存。当我们看到[generated]
徽章时,我们知道这意味着发生了缓存未命中。对于特定语句的第一次出现,这是可以预料的。然而,如果长时间运行的应用程序通常一遍又一遍地使用相同系列的 SQL 语句,但是观察到了大量新的[generated]
徽章,那可能意味着create_engine.query_cache_size
参数设置得太小。当从缓存中驱逐了被缓存的语句,因为 LRU 缓存修剪了较少使用的项目时,当下次使用它时,它将显示[generated]
徽章。
对于每个这两个语句的后续出现,我们看到的缓存徽章看起来像是[cached since 0.0003533s ago]
。这表示该语句在缓存中被找到,并且最初放入缓存中 0.0003533 秒前。需要注意的是,虽然[generated]
和[cached since]
徽章都指的是秒数,但它们表示的含义不同;在[generated]
的情况下,该数字是编译该语句所需的大致时间,并且会是一个极小的时间量。在[cached since]
的情况下,这是语句在缓存中存在的总时间。对于运行了六个小时的应用程序,该数字可能会显示[cached since 21600 seconds ago]
,这是个好事。看到“cached since”的数字很高表明这些语句很长时间以来都没有发生缓存未命中。即使应用程序运行了很长时间,但是经常出现“cached since”的低数字的语句可能表明这些语句太频繁地发生了缓存未命中,并且可能需要增加create_engine.query_cache_size
参数。
我们的示例程序然后执行了一些 SELECT 查询,在这些查询中,我们可以看到“generated”然后“cached”的相同模式,对于“a”表的 SELECT 以及“b”表的后续惰性加载也是如此:
INFO sqlalchemy.engine.Engine SELECT a.id AS a_id, a.data AS a_data FROM a INFO sqlalchemy.engine.Engine [generated in 0.00009s] () INFO sqlalchemy.engine.Engine SELECT b.id AS b_id, b.a_id AS b_a_id, b.data AS b_data FROM b WHERE ? = b.a_id INFO sqlalchemy.engine.Engine [generated in 0.00010s] (1,) INFO sqlalchemy.engine.Engine SELECT b.id AS b_id, b.a_id AS b_a_id, b.data AS b_data FROM b WHERE ? = b.a_id INFO sqlalchemy.engine.Engine [cached since 0.0005922s ago] (2,) INFO sqlalchemy.engine.Engine SELECT b.id AS b_id, b.a_id AS b_a_id, b.data AS b_data FROM b WHERE ? = b.a_id
根据我们上面的程序,完整运行显示总共缓存了四个不同的 SQL 字符串。这表明缓存大小为四足以。这显然是一个极小的大小,500 的默认大小可以放心使用。
缓存使用了多少内存?
前面的章节详细介绍了一些技术,用于检查create_engine.query_cache_size
是否需要增加。我们如何知道缓存大小不太大?我们可能希望将create_engine.query_cache_size
设置为不超过某个数字的原因是,我们的应用程序可能会使用非常大量的不同语句,例如一个从搜索 UX 动态构建查询的应用程序,如果过去 24 小时运行了十万条不同的查询并且它们都被缓存,我们不希望我们的主机因为内存不足而运行失败。
很难测量 Python 数据结构占用了多少内存,但是通过使用top
来测量内存增长,当连续添加了 250 个新语句时,建议一个中等核心语句大约占用 12K,而一个小型 ORM 语句大约占用 20K,包括为 ORM 获取结果的结构,这些结构对于 ORM 来说要大得多。
禁用或使用替代字典缓存一些(或全部)语句
内部使用的缓存称为LRUCache
,但这基本上只是一个字典。可以使用Connection.execution_options.compiled_cache
选项作为执行选项的一部分,通过在语句上设置执行选项,在Engine
或Connection
上设置执行选项,或者在使用 SQLAlchemy-2.0 样式调用 ORM Session.execute()
方法时设置执行选项,使用任何字典作为任何语句系列的缓存。例如,要运行一系列 SQL 语句并将它们缓存到特定字典中:
my_cache = {} with engine.connect().execution_options(compiled_cache=my_cache) as conn: conn.execute(table.select())
SQLAlchemy ORM 使用上述技术在单元工作“flush”过程中保留每个 mapper 的缓存,这些缓存与Engine
上配置的默认缓存分开,以及用于某些关系加载器查询。
使用该参数将缓存禁用,方法是发送一个值为None
:
# disable caching for this connection with engine.connect().execution_options(compiled_cache=None) as conn: conn.execute(table.select()) ```### 第三方方言的缓存 缓存功能要求方言的编译器生成的 SQL 字符串可以安全地用于许多语句调用,给定一个特定的缓存键,该键与该 SQL 字符串相关联。这意味着语句中的任何字面值,例如 SELECT 的 LIMIT/OFFSET 值,不能在方言的编译方案中硬编码,因为编译后的字符串将无法重用。SQLAlchemy 支持使用`BindParameter.render_literal_execute()`方法渲染绑定参数,该方法可以应用于现有的 `Select._limit_clause` 和 `Select._offset_clause` 属性,由自定义编译器实现,后文将对此进行说明。 由于有许多第三方方言,其中许多可能会从 SQL 语句中生成字面值,而不使用更新的“字面执行”功能,因此自 SQLAlchemy 1.4.5 起,为方言添加了一个名为`Dialect.supports_statement_cache`的属性。此属性在运行时直接在特定方言类上检查其是否存在,即使它已经存在于超类上,因此,即使第三方方言是现有可缓存的 SQLAlchemy 方言的子类,例如`sqlalchemy.dialects.postgresql.PGDialect`,仍必须显式包含此属性以启用缓存。该属性应仅在方言经过必要的修改并测试过使用不同参数的编译 SQL 语句的可重用性后才能**启用**。 对于所有不支持此属性的第三方方言,其日志将指示`方言不支持缓存`。 当一个方言已经针对缓存进行了测试,特别是 SQL 编译器已更新为不直接在 SQL 字符串中渲染任何字面值 LIMIT / OFFSET 时,方言作者可以如下应用该属性: ```py from sqlalchemy.engine.default import DefaultDialect class MyDialect(DefaultDialect): supports_statement_cache = True
该标志也需要应用于所有方言子类:
class MyDBAPIForMyDialect(MyDialect): supports_statement_cache = True
新版本 1.4.5 中新增了Dialect.supports_statement_cache
属性。
典型的方言修改案例如下。
示例:使用后编译参数渲染 LIMIT / OFFSET
举例来说,假设一个方言重写了SQLCompiler.limit_clause()
方法,该方法为 SQL 语句生成“LIMIT / OFFSET”子句,像这样:
# pre 1.4 style code def limit_clause(self, select, **kw): text = "" if select._limit is not None: text += " \n LIMIT %d" % (select._limit,) if select._offset is not None: text += " \n OFFSET %d" % (select._offset,) return text
以上例程将Select._limit
和Select._offset
整数值呈现为嵌入在 SQL 语句中的字面整数。这是对于不支持在 SELECT 语句的 LIMIT/OFFSET 子句中使用绑定参数的数据库的常见要求。然而,在初始编译阶段内呈现整数值与缓存直接不兼容,因为Select
对象的限制和偏移整数值不是缓存键的一部分,因此许多具有不同限制/偏移值的Select
语句将无法正确呈现值。
以上代码的更正是将字面整数移入 SQLAlchemy 的后编译设施,这将使字面整数在初始编译阶段之外渲染,而是在执行时在语句发送到 DBAPI 之前。这在编译阶段使用BindParameter.render_literal_execute()
方法访问,同时使用Select._limit_clause
和Select._offset_clause
属性,它们表示 LIMIT/OFFSET 作为完整的 SQL 表达式,如下所示:
# 1.4 cache-compatible code def limit_clause(self, select, **kw): text = "" limit_clause = select._limit_clause offset_clause = select._offset_clause if select._simple_int_clause(limit_clause): text += " \n LIMIT %s" % ( self.process(limit_clause.render_literal_execute(), **kw) ) elif limit_clause is not None: # assuming the DB doesn't support SQL expressions for LIMIT. # Otherwise render here normally raise exc.CompileError( "dialect 'mydialect' can only render simple integers for LIMIT" ) if select._simple_int_clause(offset_clause): text += " \n OFFSET %s" % ( self.process(offset_clause.render_literal_execute(), **kw) ) elif offset_clause is not None: # assuming the DB doesn't support SQL expressions for OFFSET. # Otherwise render here normally raise exc.CompileError( "dialect 'mydialect' can only render simple integers for OFFSET" ) return text
上述方法将生成一个编译后的 SELECT 语句,看起来像:
SELECT x FROM y LIMIT __[POSTCOMPILE_param_1] OFFSET __[POSTCOMPILE_param_2]
在上述情况下,__[POSTCOMPILE_param_1]
和__[POSTCOMPILE_param_2]
指示符将在语句执行时填充其相应的整数值,此时 SQL 字符串已从缓存中检索出来。
在适当进行类似上述更改后,Dialect.supports_statement_cache
标志应设置为True
。强烈建议第三方方言使用dialect 第三方测试套件,该套件将断言具有 LIMIT/OFFSET 的 SELECT 操作是否正确呈现和缓存。
另请参阅
为什么升级到 1.4 和/或 2.x 后我的应用程序变慢? - 在常见问题解答部分 ### 使用 Lambda 在语句生成中获得显著的速度提升
深度炼金术
除非在非常性能密集的场景下,否则这种技术通常是非必要的,并且适用于有经验的 Python 程序员。虽然相当简单,但涉及到元编程概念,对于初学者的 Python 开发者来说并不合适。Lambda 方法可以在稍后以最小的努力应用于现有代码。
Python 函数通常表示为 lambda,可以用来生成基于 lambda 函数本身的 Python 代码位置以及 lambda 内部的闭包变量可缓存的 SQL 表达式。 其基本原理是允许缓存不仅是 SQL 表达式构造的 SQL 字符串编译形式,这是 SQLAlchemy 在未使用 lambda 系统时的正常行为,而且也是 SQL 表达式构造本身在 Python 中的组合,这也具有一定的 Python 开销。
lambda SQL 表达式功能可用作性能增强功能,并且也可以选择在 with_loader_criteria()
ORM 选项中使用,以提供通用 SQL 片段。
概要
Lambda 语句是使用 lambda_stmt()
函数构造的,该函数返回 StatementLambdaElement
的实例,它本身是一个可执行的语句构造。 使用 Python 的加法运算符 +
,或者 StatementLambdaElement.add_criteria()
方法添加附加修饰符和条件到对象中,该方法允许更多的选项。
假设在 Python 中的一个封闭函数或方法内调用了 lambda_stmt()
构造函数,以便在应用程序中多次使用,以便在第一次执行之后的后续执行可以利用已缓存的编译 SQL。 当 lambda 在 Python 的封闭函数内构造时,它也将受到闭包变量的影响,这对整个方法非常重要:
from sqlalchemy import lambda_stmt def run_my_statement(connection, parameter): stmt = lambda_stmt(lambda: select(table)) stmt += lambda s: s.where(table.c.col == parameter) stmt += lambda s: s.order_by(table.c.id) return connection.execute(stmt) with engine.connect() as conn: result = run_my_statement(some_connection, "some parameter")
在上面,用于定义 SELECT 语句结构的三个 lambda
可调用对象只被调用一次,并且生成的 SQL 字符串被缓存在引擎的编译缓存中。 从那时起,run_my_statement()
函数可以被调用任意次数,而其中的 lambda
可调用对象不会被调用,只用作缓存键来检索已经编译的 SQL。
注意
当 lambda 系统未被使用时,已经存在 SQL 缓存,这一点很重要。 lambda 系统只是在每个 SQL 语句调用时通过缓存 SQL 构建本身和使用更简单的缓存键来添加额外的工作减少层。
Lambda 的快速指南
在 lambda SQL 系统中,重点是确保为 lambda 生成的缓存键与其将产生的 SQL 字符串永远不会不匹配。LambdaElement
及其相关对象将运行并分析给定的 lambda,以便计算在每次运行时应该如何对其进行缓存,尝试检测任何潜在问题。基本指南包括:
- 任何类型的语句都受支持 - 虽然预期
select()
构造是lambda_stmt()
的主要用例,但 DML 语句如insert()
和update()
同样可用:
def upd(id_, newname): stmt = lambda_stmt(lambda: users.update()) stmt += lambda s: s.values(name=newname) stmt += lambda s: s.where(users.c.id == id_) return stmt with engine.begin() as conn: conn.execute(upd(7, "foo"))
- ORM 支持的用例直接得到支持 -
lambda_stmt()
可以完全适应 ORM 功能,并直接与Session.execute()
一起使用:
def select_user(session, name): stmt = lambda_stmt(lambda: select(User)) stmt += lambda s: s.where(User.name == name) row = session.execute(stmt).first() return row
- 绑定参数会自动适应 - 与 SQLAlchemy 之前的“烘焙查询”系统相比,lambda SQL 系统会自动适应成为 SQL 绑定参数的 Python 文字值。这意味着即使一个给定的 lambda 只运行一次,每次运行时生成的绑定参数的值也会从 lambda 的 闭包 中提取出来:
>>> def my_stmt(x, y): ... stmt = lambda_stmt(lambda: select(func.max(x, y))) ... return stmt >>> engine = create_engine("sqlite://", echo=True) >>> with engine.connect() as conn: ... print(conn.scalar(my_stmt(5, 10))) ... print(conn.scalar(my_stmt(12, 8))) SELECT max(?, ?) AS max_1 [generated in 0.00057s] (5, 10) 10 SELECT max(?, ?) AS max_1 [cached since 0.002059s ago] (12, 8) 12
- 在上面的例子中,
StatementLambdaElement
从每次调用my_stmt()
时生成的 lambda 的 闭包 中提取了x
和y
的值;这些值被替换为参数的值,并缓存到 SQL 结构中。 - 最好的情况是 lambda 在所有情况下产生相同的 SQL 结构 - 避免在 lambda 内部使用条件语句或自定义可调用对象,因为这可能会根据输入产生不同的 SQL;如果一个函数可能会有条件地使用两个不同的 SQL 片段,则使用两个单独的 lambda:
# **Don't** do this: def my_stmt(parameter, thing=False): stmt = lambda_stmt(lambda: select(table)) stmt += lambda s: ( s.where(table.c.x > parameter) if thing else s.where(table.c.y == parameter) ) return stmt # **Do** do this: def my_stmt(parameter, thing=False): stmt = lambda_stmt(lambda: select(table)) if thing: stmt += lambda s: s.where(table.c.x > parameter) else: stmt += lambda s: s.where(table.c.y == parameter) return stmt
- 如果 lambda 表达式不能生成一致的 SQL 结构,则可能会发生各种故障,并且有些故障目前无法轻松检测到。
- 不要在 lambda 中使用函数来生成绑定值 - 绑定值跟踪方法要求 SQL 语句中要使用的实际值在 lambda 的闭包中本地存在。如果值是从其他函数生成的,则这是不可能的,并且如果尝试这样做,
LambdaElement
通常会引发错误:
>>> def my_stmt(x, y): ... def get_x(): ... return x ... ... def get_y(): ... return y ... ... stmt = lambda_stmt(lambda: select(func.max(get_x(), get_y()))) ... return stmt >>> with engine.connect() as conn: ... print(conn.scalar(my_stmt(5, 10))) Traceback (most recent call last): # ... sqlalchemy.exc.InvalidRequestError: Can't invoke Python callable get_x() inside of lambda expression argument at <code object <lambda> at 0x7fed15f350e0, file "<stdin>", line 6>; lambda SQL constructs should not invoke functions from closure variables to produce literal values since the lambda SQL system normally extracts bound values without actually invoking the lambda or any functions within it.
- 在上述示例中,如果需要,
get_x()
和get_y()
的使用应该发生在 lambda 的外部,并分配给一个本地闭包变量:
>>> def my_stmt(x, y): ... def get_x(): ... return x ... ... def get_y(): ... return y ... ... x_param, y_param = get_x(), get_y() ... stmt = lambda_stmt(lambda: select(func.max(x_param, y_param))) ... return stmt
- 避免在 lambda 内部引用非 SQL 结构,因为默认情况下它们是不可缓存的 - 这个问题涉及到
LambdaElement
如何从语句内的其他闭包变量创建缓存键。为了提供最佳的缓存键准确性保证,lambda 闭包中的所有对象都被认为是重要的,并且默认情况下不会假设它们适用于缓存键。因此,以下示例还将引发一个相当详细的错误消息:
>>> class Foo: ... def __init__(self, x, y): ... self.x = x ... self.y = y >>> def my_stmt(foo): ... stmt = lambda_stmt(lambda: select(func.max(foo.x, foo.y))) ... return stmt >>> with engine.connect() as conn: ... print(conn.scalar(my_stmt(Foo(5, 10)))) Traceback (most recent call last): # ... sqlalchemy.exc.InvalidRequestError: Closure variable named 'foo' inside of lambda callable <code object <lambda> at 0x7fed15f35450, file "<stdin>", line 2> does not refer to a cacheable SQL element, and also does not appear to be serving as a SQL literal bound value based on the default SQL expression returned by the function. This variable needs to remain outside the scope of a SQL-generating lambda so that a proper cache key may be generated from the lambda's state. Evaluate this variable outside of the lambda, set track_on=[<elements>] to explicitly select closure elements to track, or set track_closure_variables=False to exclude closure variables from being part of the cache key.
- 上述错误表明
LambdaElement
不会假设传入的Foo
对象在所有情况下都会保持相同的行为。它也不会假设默认情况下可以使用Foo
作为缓存键;如果将Foo
对象用作缓存键,如果有许多不同的Foo
对象,这将用重复信息填充缓存,并且还会对所有这些对象保持长期引用。
解决上述情况的最佳方法是不在 lambda 内部引用foo
,而是在外部引用它:
>>> def my_stmt(foo): ... x_param, y_param = foo.x, foo.y ... stmt = lambda_stmt(lambda: select(func.max(x_param, y_param))) ... return stmt
- 在某些情况下,如果 lambda 的 SQL 结构保证不会根据输入更改,则传递
track_closure_variables=False
,这将禁用除了用于绑定参数的闭包变量之外的任何追踪:
>>> def my_stmt(foo): ... stmt = lambda_stmt( ... lambda: select(func.max(foo.x, foo.y)), track_closure_variables=False ... ) ... return stmt
- 还有一种选项可以将对象添加到元素中,显式形成缓存键的一部分,使用
track_on
参数;使用此参数允许特定值作为缓存键,并且还将阻止考虑其他闭包变量。这对于构造的 SQL 的一部分源自某种上下文对象的情况非常有用,该对象可能具有许多不同的值。在下面的示例中,SELECT 语句的第一部分将禁用对foo
变量的跟踪,而第二部分将显式跟踪self
作为缓存键的一部分:
>>> def my_stmt(self, foo): ... stmt = lambda_stmt( ... lambda: select(*self.column_expressions), track_closure_variables=False ... ) ... stmt = stmt.add_criteria(lambda: self.where_criteria, track_on=[self]) ... return stmt
- 使用
track_on
意味着给定对象将长期存储在 lambda 的内部缓存中,并且只要缓存不清除这些对象(默认情况下使用 1000 个条目的 LRU 方案)就会具有强引用。
缓存键生成
为了理解与 lambda SQL 构造相关的一些选项和行为,了解缓存系统是有帮助的。
SQLAlchemy 的缓存系统通常通过生成表示构造中所有状态的结构来从给定的 SQL 表达式构造中生成缓存键:
>>> from sqlalchemy import select, column >>> stmt = select(column("q")) >>> cache_key = stmt._generate_cache_key() >>> print(cache_key) # somewhat paraphrased CacheKey(key=( '0', <class 'sqlalchemy.sql.selectable.Select'>, '_raw_columns', ( ( '1', <class 'sqlalchemy.sql.elements.ColumnClause'>, 'name', 'q', 'type', ( <class 'sqlalchemy.sql.sqltypes.NullType'>, ), ), ), # a few more elements are here, and many more for a more # complicated SELECT statement ),)
上述键存储在基本上是字典的缓存中,值是一个结构,其中包括 SQL 语句的字符串形式,本例中是短语“SELECT q”。我们可以观察到,即使对于一个极短的查询,缓存键也是相当冗长的,因为它必须表示关于正在呈现和潜在执行的一切可能变化的内容。
相比之下,lambda 构造系统创建了一种不同类型的缓存键:
>>> from sqlalchemy import lambda_stmt >>> stmt = lambda_stmt(lambda: select(column("q"))) >>> cache_key = stmt._generate_cache_key() >>> print(cache_key) CacheKey(key=( <code object <lambda> at 0x7fed1617c710, file "<stdin>", line 1>, <class 'sqlalchemy.sql.lambdas.StatementLambdaElement'>, ),)
上面,我们看到的缓存键比非 lambda 语句的要短得多,而且甚至不需要产生select(column("q"))
构造本身;Python lambda 本身包含一个名为__code__
的属性,该属性引用一个 Python 代码对象,在应用程序的运行时是不可变和永久的。
当 lambda 还包括闭包变量时,在这些变量引用 SQL 构造(例如列对象)的常规情况下,它们将成为缓存键的一部分;如果它们引用将绑定参数的文字值,则它们将放置在缓存键的一个单独元素中:
>>> def my_stmt(parameter): ... col = column("q") ... stmt = lambda_stmt(lambda: select(col)) ... stmt += lambda s: s.where(col == parameter) ... return stmt
上述StatementLambdaElement
包括两个 lambda,两者都引用col
闭包变量,因此缓存键将表示这两个片段以及column()
对象:
>>> stmt = my_stmt(5) >>> key = stmt._generate_cache_key() >>> print(key) CacheKey(key=( <code object <lambda> at 0x7f07323c50e0, file "<stdin>", line 3>, ( '0', <class 'sqlalchemy.sql.elements.ColumnClause'>, 'name', 'q', 'type', ( <class 'sqlalchemy.sql.sqltypes.NullType'>, ), ), <code object <lambda> at 0x7f07323c5190, file "<stdin>", line 4>, <class 'sqlalchemy.sql.lambdas.LinkedLambdaElement'>, ( '0', <class 'sqlalchemy.sql.elements.ColumnClause'>, 'name', 'q', 'type', ( <class 'sqlalchemy.sql.sqltypes.NullType'>, ), ), ( '0', <class 'sqlalchemy.sql.elements.ColumnClause'>, 'name', 'q', 'type', ( <class 'sqlalchemy.sql.sqltypes.NullType'>, ), ), ),)
缓存键的第二部分已检索到将在调用语句时使用的绑定参数:
>>> key.bindparams [BindParameter('%(139668884281280 parameter)s', 5, type_=Integer())]
有关带有性能比较的“lambda”缓存的一系列示例,请参阅性能示例中的“short_selects”测试套件。 ## “对于 INSERT 语句的插入多个值”行为
2.0 版本中的新功能:参见除 MySQL 外所有后端现已实现的优化 ORM 批量插入以了解更改的背景,包括示例性能测试
提示
insertmanyvalues 功能是一个透明可用的性能特性,不需要用户干预即可按需进行。本节描述了该特性的架构以及如何测量其性能并调整其行为以优化 ORM 使用的批量 INSERT 语句的速度。
随着更多的数据库增加了对 INSERT…RETURNING 的支持,SQLAlchemy 在处理需要获取服务器生成值的 INSERT 语句的方式上发生了重大变化,其中最重要的是服务器生成的主键值,它们允许在后续操作中引用新行。特别是,在 ORM 中,这种情况长期以来一直是一个重大的性能问题,因为它依赖于能够检索服务器生成的主键值,以便正确填充 标识映射。
随着 SQLite 和 MariaDB 最近对 RETURNING 的支持,SQLAlchemy 不再需要依赖于大多数后端的单行限制 cursor.lastrowid 属性;除了 MySQL 外,现在可以在所有 SQLAlchemy 包含的 后端使用 RETURNING。剩下的性能限制是,cursor.executemany() DBAPI 方法不允许获取行,对于大多数后端来说,这个问题已经通过放弃使用 executemany()
,改为重构单个 INSERT 语句来容纳大量行,并在单个语句中使用 cursor.execute()
调用来解决。这种方法源自于 psycopg2
DBAPI 的 psycopg2 快速执行辅助功能,SQLAlchemy 在最近的发布系列中逐渐增加了对其的支持。
当前支持
该功能对所有支持 RETURNING 的 SQLAlchemy 后端都已启用,但对于 Oracle,除了 cx_Oracle 和 OracleDB 驱动程序提供其自己的等效功能外,其他均支持。该功能通常在使用 Insert.returning()
方法的 Insert
结构与 executemany 执行配合使用时发生,即当将字典列表传递给 Connection.execute()
或 Session.execute()
方法的 Connection.execute.parameters
参数时(以及 asyncio 和 Session.scalars()
等简写方法下的等效方法)。在使用诸如 Session.add()
和 Session.add_all()
等方法添加行时,它也在 ORM 工作单元 过程中发生。
对于 SQLAlchemy 包含的方言,支持或等效支持目前如下:
- SQLite - 支持 SQLite 版本 3.35 及以上
- PostgreSQL - 所有支持的 Postgresql 版本(9 及以上)
- SQL Server - 所有支持的 SQL Server 版本 [1]
- MariaDB - 支持 MariaDB 版本 10.5 及以上
- MySQL - 不支持,没有 RETURNING 功能
- Oracle - 支持使用本地 cx_Oracle / OracleDB API 执行多行 OUT 参数的 RETURNING,支持所有支持的 Oracle 版本 9 及以上。这不是与“executemanyvalues”相同的实现,但具有相同的使用模式和等效的性能优势。
在版本 2.0.10 中更改:
禁用该功能
要禁用给定后端的“insertmanyvalues”功能,以及 create_engine()
中的 create_engine.use_insertmanyvalues
参数作为 False
传递给 Engine
:
engine = create_engine( "mariadb+mariadbconnector://scott:tiger@host/db", use_insertmanyvalues=False )
该功能也可以通过将 Table.implicit_returning
参数传递为 False
来禁止为特定的 Table
对象隐式使用:
t = Table( "t", metadata, Column("id", Integer, primary_key=True), Column("x", Integer), implicit_returning=False, )
有可能想要针对特定表禁用 RETURNING 的原因是为了解决特定后端的限制。
批处理模式操作
该特性有两种操作模式,根据每个方言、每个Table
自动透明选择。一种是批处理模式,通过重写 INSERT 语句的形式:
INSERT INTO a (data, x, y) VALUES (%(data)s, %(x)s, %(y)s) RETURNING a.id
转换成“批处理”形式,如下所示:
INSERT INTO a (data, x, y) VALUES (%(data_0)s, %(x_0)s, %(y_0)s), (%(data_1)s, %(x_1)s, %(y_1)s), (%(data_2)s, %(x_2)s, %(y_2)s), ... (%(data_78)s, %(x_78)s, %(y_78)s) RETURNING a.id
当上述语句针对输入数据的子集(“批处理”)进行组织时,其大小由数据库后端确定,并且每个批次的参数数量与已知的语句大小/参数数量相对应。该特性然后针对每个输入数据批次执行一次 INSERT 语句,直到所有记录都被消耗完毕,将每个批次的 RETURNING 结果连接成一个单个大行集,可以从单个Result
对象中获取。
这种“批处理”形式允许使用更少的数据库往返进行许多行的 INSERT,并且已经证明在大多数支持的后端上可以实现显著的性能提升。
SqlAlchemy 2.0 中文文档(四十四)(3)https://developer.aliyun.com/article/1563070