【问题标题】:New/override SQLAlchemy operator compiler output新建/覆盖 SQLAlchemy 运算符编译器输出
【发布时间】:2020-01-09 18:49:46
【问题描述】:

编译in_ 表达式的默认 SQLAlchemy 行为对于非常大的列表是病态的,我想为运算符创建一个自定义的、更快的编译器。如果解决方案是一个新的运算符(即:in_list_)或者它是否覆盖了in_ 的默认编译器,这对应用程序来说并不重要。但是,我还没有找到任何关于如何具体执行此操作的文档。

subclassing guidelines for compilation extension 不包含任何关于运算符的内容,这表明这不是开始的地方。 documentation on redefining and creating new operators 专注于更改或创建新的运算符行为,但运算符的行为不是问题,只是编译器。

这是我正在尝试完成的一个非常无效的示例:

from sqlalchemy.types import TypeEngine

class in_list_(TypeEngine.Comparator):
  pass

@compiles(in_list_)
def in_list_impl(element, compiler, **kwargs):
  return "IN ('Now', 'I', 'can', 'inline', 'the', 'list')"

然后在一个表达式中:

select([mytable.c.x, mytable.c.y]).where(mytable.c.x.in_list_(long_list))

【问题讨论】:

标签: python sqlalchemy


【解决方案1】:

IN 用于非常 大型列表确实是病态的,您可能会更好地为using a temporary tableIN 提供子查询或连接。但问题是“如何覆盖特定运算符的编译器输出”。对于像INNOT IN 这样的二元运算符,您需要覆盖的是SQLAlchemy 如何处理BinaryExpressions 的编译:

from sqlalchemy.ext.compiler import compiles
from sqlalchemy.sql.elements import BinaryExpression
from sqlalchemy.sql.operators import in_op, notin_op

def visit_in_op_binary(compiler, binary, operator, **kw):
    return "%s IN %s" % (
        compiler.process(binary.left, **kw),
        compiler.process(binary.right, **{**kw, "literal_binds": True}))

def visit_notin_op_binary(compiler, binary, operator, **kw):
    return "%s NOT IN %s" % (
        compiler.process(binary.left, **kw),
        compiler.process(binary.right, **{**kw, "literal_binds": True}))

@compiles(BinaryExpression)
def compile_binary(binary, compiler, override_operator=None, **kw):
    operator = override_operator or binary.operator

    if operator is in_op:
        return visit_in_op_binary(
            compiler, binary, operator, override_operator=override_operator,
            **kw)

    if operator is notin_op:
        return visit_notin_op_binary(
            compiler, binary, operator, override_operator=override_operator,
            **kw)

    return compiler.visit_binary(binary, override_operator=override_operator, **kw)

请注意,简单地生成包含绑定参数的分组和子句列表的二进制表达式对于非常大的列表会花费大量时间,更不用说即使使用文字绑定也要编译所有内容,因此您可能不会观察到显着的性能提升.另一方面,许多实现对您可以在语句中使用多少个占位符/参数有限制,因此内联绑定允许这样的查询完全运行。

另一方面,如果您的列表确实符合您的实现设置的限制(Postgresql 似乎只受可用 RAM 的限制),您可能不需要任何具有最新 SQLAlchemy 的编译器解决方法; use expanding bind parameters instead:

In [15]: %%time
    ...: session.query(Foo).\
    ...:     filter(Foo.data.in_(range(250000))).\
    ...:     all()
    ...: 
CPU times: user 5.09 s, sys: 91.9 ms, total: 5.18 s
Wall time: 5.18 s
Out[15]: []

In [16]: %%time
    ...: session.query(Foo).\
    ...:     filter(Foo.data.in_(bindparam('xs', range(250000), expanding=True))).\
    ...:     all()
    ...: 
CPU times: user 310 ms, sys: 8.05 ms, total: 318 ms
Wall time: 317 ms
Out[16]: []

正如 cmets 中所述,在 1.4 版中,扩展 bindparam 将支持开箱即用的文字执行:

In [4]: session.query(Foo).\
   ...:     filter(Foo.data.in_(
   ...:         bindparam('xs', range(10), expanding=True, literal_execute=True))).\
   ...:     all()
2019-09-07 20:35:04,560 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2019-09-07 20:35:04,561 INFO sqlalchemy.engine.base.Engine SELECT foo.id AS foo_id, foo.data AS foo_data 
FROM foo 
WHERE foo.data IN (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
2019-09-07 20:35:04,561 INFO sqlalchemy.engine.base.Engine ()
Out[4]: []

【讨论】:

  • 很好地回答了我的问题,即使您和@zzzeek 正确指出,扩展绑定参数是一个更简单的解决方案。您的临时表答案也很有帮助。将解决方案转换为 MySQL 后,我运行了一些测试,基于临时表的查询运行时间与 bindparam 解决方案的列表大小约为 175k 整数的平衡点差不多。但我怀疑如果该列表需要显着增加,则有必要使用临时表。
【解决方案2】:

您可以做的一件事是使用原始 sql 并手动构建查询。但是,这是至关重要的,您必须使用 BINDS

一旦你承诺,你需要管理整个查询的变量,而不仅仅是 IN 列表位。所以你完全靠自己。经常这样做是不现实的,除非您有专门的、经过高度测试的实用程序功能,但它确实有效。而且也很快:我必须在 999 分块,因为 Oracle 没有超过,但 Postgresql 或 Oracle 都没有抱怨太多。而且,是的,这是在 SQLAlchemy (1.3.8) 下。

这里有一些示例代码。列表的位都是动态生成的,以 Postgresql 为目标。不幸的是,每个 RDBMS 都有自己的占位符格式和绑定变量。参考PEP249 paramstyles 了解更多详情。

生成的查询是什么样的:
qry = """select recname, objecttype
            from bme_mvprd
            where ignore = false
YOU HAVE TO BUILD THIS ?
            and objecttype 
in ( 
%(objecttypes_000__)s
, %(objecttypes_001__)s
, %(objecttypes_002__)s
, %(objecttypes_003__)s
)
...
"""

条件最初来自这个数组:[0, 1, 2, 4]

您作为绑定参数传递的内容如下所示:

以及绑定参数的样子,再次,Postgres 特定的:

(是的,你也需要生成这个)

sub = {
    'objecttypes_000__': 0,
    'objecttypes_001__': 1,
    'objecttypes_002__': 2,
    'objecttypes_003__': 4,
}

您必须execute(qry, sub) 才能使用绑定。

NO CEATING with execute(?qry % sub?) 可以在 Postgresql 中工作,但会让你回到 SQL 注入领域

Oracle 使用 :1, :2, :3 类型的占位符,这样会出错,但 Postgresql 使用 Python 类型的占位符,因此您需要非常小心,以免意外绕过参数绑定。

注意:大型IN SQL 操作有时可以替换为EXISTS 测试,如果可能的话,应该首选这些操作。我上面的 chunk-by-999 示例是因为除了首先填充临时表之外别无他法:这是一个示例,而不是最佳实践。

PPS:如果您的列表为空怎么办? I asked a question about that => answer = "... in (%(var001)s)...", {'var001':None} 但您根本不能添加IN (...)

【讨论】:

  • 我不会对此投反对票,但您绝对不需要使用绑定。事实上,这个问题的全部推动力是 SQLAlchemy 在构建 IN 表达式时自动生成要绑定的参数,一旦列表大小增长到成千上万,字符串构建参数所花费的时间开始变得很重要总运行时间。一切都很重要。
  • @wst 在谈论性能时,请带上数字。使用 mogrify(请参阅 stackoverflow.com/how-do-i-get-a-raw-compiled-sql-query-from-a-sqlalchemy-expression,我得到的平均时间是 a)解析到查询 + 绑定,然后 b)mogrify.execute。对于 10K 项目列表:0.3 秒。 100K 项目列表上的 3.5 秒。与实际的数据库执行不同,但无论如何都会保留这一点。不知道这与您的数字相比如何,并将其保留。硬件:MBP 2011
  • 数字与问题的范围无关。问题是询问如何扩展 SQLAlchemy 的一部分。我故意避免把它放在性能上,因为我已经知道如何解决性能问题。
  • 这是一个奇怪的声明,因为你一直在和我谈论现实。随便。
【解决方案3】:

完全披露,我不知道如何覆盖in_() 的编译器,而且我知道我有过度简化的风险,所以请善意接受这一点,但鉴于你上面的例子,我什至不会尝试。相反,只需创建一个辅助函数:

from sqlalchemy import Table, Column, String, MetaData
from sqlalchemy.sql import text


long_list = ['Now', 'I', 'can', 'inline', 'the', 'list']
tbl = Table("mytable", MetaData(), Column("x", String), Column("y", String))


def col_in_list(col, l):
    # do something safe to generate your in clause here.
    return text("IN ('Now', 'I', 'can', 'inline', 'the', 'list')")


if __name__ == "__main__":
    print(tbl.select().where(col_in_list(tbl.c.x, long_list)))

渲染:

SELECT mytable.x, mytable.y
FROM mytable
WHERE mytable.x IN ('Now','I','can','inline','the','list')

【讨论】:

  • 对不起,我将做一些我很少做的事情并投反对票。连接字符串是通往 SQL 注入的道路,也是 OWASP 的 10 大黑客来源。拒绝吧!我不在乎它是否是“你的变量”。在你的代码库中,尤其是作为一种通用方法,有一种方法可以吸引用户来源的变量,尤其是初级编码人员。 xkcd.com/327
  • @JLPeyret 如果没有外人注入的路径,这不是注入攻击面。使用相同的逻辑,人们可能会争辩说,许多 SQLAlchemy 特性同样是“不安全”的操作,尽管有更多的仪式。一个简单的事实是,SQL 上的任何语言抽象层都可能需要连接字符串。
  • @wst 我会恭敬地,但从根本上同意不同意你的观点。没有人打算将他们的代码暴露给外部攻击,但是使用不安全的代码,即使是在你的内部,也会促进这一点。字符串连接对于容纳 SQL 关键字和模式对象是必要的。仅此而已。超过这一点,使用绑定。额外的好处:使用字符串操作处理“None”值是困难,并且绑定做得很好。
  • @wst 编辑,因为我意识到你是 OP。如果你只做这一次,你的代码就会被大包围!!!!!!!!!不要在这里使用用户变量!!!!!!把它锁起来……也许吧。不好的做法。但这是你的代码。作为一般建议?太可怕了。
  • @JLPeyret 我试图说明我认为重写 IN 运算符的编译是笨拙的,所以我从答案中删除了动态生成的字符串。
猜你喜欢
  • 2021-12-09
  • 2010-12-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多