草庐IT

python - SQLAlchemy 要求为查询设置别名,但在生成的 SQL 中未使用该别名

coder 2023-08-18 原文

我有一个简单的模型类,代表两个角色之间的战斗:

class WaifuPickBattle(db.Model):
    """Table which represents a where one girl is chosen as a waifu."""

    __tablename__ = "waifu_battles"
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
    date = db.Column(db.DateTime, nullable=False)
    winner_name = db.Column(db.String, nullable=False)
    loser_name = db.Column(db.String, nullable=False)

我有一种构建 CTE 的方法,该方法将战斗转换到一系列外观中(每场战斗都有两次外观 - 赢家和输家):

def get_battle_appearences_cte():
    """Create a sqlalchemy subquery of the battle appearences."""
    wins = select([
        WaifuPickBattle.date,
        WaifuPickBattle.winner_name.label("name"),
        expression.literal_column("1").label("was_winner"),
        expression.literal_column("0").label("was_loser")
    ])
    losses = select([
        WaifuPickBattle.date,
        WaifuPickBattle.loser_name.label("name"),
        expression.literal_column("0").label("was_winner"),
        expression.literal_column("1").label("was_loser")
    ])
    return wins.union_all(losses).cte("battle_appearence")

然后我有一个查询,它利用这个 View 来确定看过最多战斗的角色:

def query_most_battled_waifus():
    """Find the waifus with the most battles in a given date range."""
    appearence_cte = get_battle_appearences_cte()
    query = \
        select([
            appearence_cte.c.name,
            func.sum(appearence_cte.c.was_winner).label("wins"),
            func.sum(appearence_cte.c.was_loser).label("losses"),
        ])\
        .group_by(appearence_cte.c.name)\
        .order_by(func.count().desc())\
        .limit(limit)
    return db.session.query(query).all()

这将生成以下 SQL:

WITH battle_appearence  AS
(
    SELECT
        waifu_battles.date AS date,
        waifu_battles.winner_name AS name,
        1 AS was_winner,
        0 AS was_loser
    FROM waifu_battles
    UNION ALL
    SELECT
        waifu_battles.date AS date,
        waifu_battles.loser_name AS name,
        0 AS was_winner,
        1 AS was_loser
    FROM waifu_battles
)
SELECT
    name AS name,
    wins AS wins,
    losses AS losses
FROM
(
    SELECT
        battle_appearence.name AS name,
        sum(battle_appearence.was_winner) AS wins,
        sum(battle_appearence.was_winner) AS losses
    FROM battle_appearence
    GROUP BY battle_appearence.name
    ORDER BY count(*) DESC
)

这在对 SQLite 数据库执行时非常有效,但在对 Postgres SQL 数据库执行时,会出现以下错误:
sqlalchemy.exc.ProgrammingError: (psycopg2.errors.SyntaxError) subquery in FROM must have an alias

LINE 6: FROM (SELECT battle_appearence.name AS name, count(battle_ap... ^ HINT: For example, FROM (SELECT ...) [AS] foo.

[SQL: WITH battle_appearence AS (SELECT waifu_battles.date AS date, waifu_battles.winner_name AS name, 1 AS was_winner, 0 AS was_loser FROM waifu_battles UNION ALL SELECT waifu_battles.date AS date, waifu_battles.loser_name AS name, 0 AS was_winner, 1 AS was_loser FROM waifu_battles) SELECT name AS name, wins AS wins, losses AS losses FROM (SELECT battle_appearence.name AS name, count(battle_appearence.was_winner) AS wins, count(battle_appearence.was_winner) AS losses FROM battle_appearence GROUP BY battle_appearence.name ORDER BY count(*) DESC)] (Background on this error at: http://sqlalche.me/e/f405)

此时有几点需要注意:
  • 子选择是多余的,我们应该简单地使用子选择作为主选择语句。
  • 您可以通过为子选择取别名并使用 <alias>.<column> 来解决此问题。在主选择语句中 - Postgres 需要在子选择上使用别名在别处有详细记录。

  • 我的第一个问题是 怎么样看到 SQLalchemy 决定引入它,尽管没有明确指示(据我所知),我会为这个子选择取别名吗?

    我发现问题的解决方案是添加 .alias("foo")到查询:
    query = query\
            ...\
            .alias("foo")
    

    这导致生成以下 SQL(奇怪地解决了整个冗余子选择问题的一个!):

    WITH battle_appearence  AS
    (
        SELECT
            waifu_battles.date AS date,
            waifu_battles.winner_name AS name,
            1 AS was_winner,
            0 AS was_loser
        FROM waifu_battles
        UNION ALL
        SELECT
            waifu_battles.date AS date,
            waifu_battles.loser_name AS name,
            0 AS was_winner,
            1 AS was_loser
        FROM waifu_battles
    )
    SELECT
        battle_appearence.name,
        sum(battle_appearence.was_winner) AS wins,
        sum(battle_appearence.was_winner) AS losses
    FROM battle_appearence
    GROUP BY battle_appearence.name
    ORDER BY count(*) DESC
    

    我的第二个问题是 为什么添加别名是否会阻止创建子选择和 为什么是别名没用! "foo"别名似乎被忽略了,但对生成的查询产生了重大影响。

    最佳答案

    答案

    SQLalchemy decides to introduce it despite not being explicitly instructed to



    它不是。您正在告诉它在您调用 db.sesion.query(query) 的那一刻使用子查询(虽然你可能不知道)。使用 db.session.execute(query)反而。

    why did adding the alias prevent the sub-select from being created and why is the alias not used! The "foo" alias was seemingly disregarded yet had a substantial effect on the generated query.



    确实如此 不是 用过的。

    说明-介绍

    SQLAlchemy 只是欺骗了你。我想你一直在使用 print(query)偷看引擎盖下并了解问题所在 - 这次运气不好,它并没有告诉你全部真相。

    查看生成的真实SQL,turn the echo functionality on在发动机中。完成后,您会发现实际上 sqlalchemy 生成了以下查询:
    WITH battle_appearence AS 
    (
        SELECT
            waifu_battles.date AS date,
            waifu_battles.winner_name AS name,
            1 AS was_winner,
            0 AS was_loser 
        FROM waifu_battles
        UNION ALL
        SELECT
            waifu_battles.date AS date,
            waifu_battles.loser_name AS name,
            0 AS was_winner,
            1 AS was_loser 
        FROM waifu_battles
    )
    SELECT foo.name AS foo_name, foo.wins AS foo_wins, foo.losses AS foo_losses 
    FROM (
        SELECT
            battle_appearence.name AS name,
            sum(battle_appearence.was_winner) AS wins,
            sum(battle_appearence.was_loser) AS losses 
        FROM battle_appearence
        GROUP BY battle_appearence.name
        ORDER BY count(*) DESC
        LIMIT ?
    )
    AS foo
    

    两个查询都可以正常工作(我声称真正使用过的查询 - 上面 - 以及您在答案末尾给出的查询)。让我们先深入探讨一下 - 为什么这些不同?

    如何调试查询以及为什么您看到的内容不同

    您看到的查询(我们称之为 S 作为选择替代别名)是查询的字符串表示或 str(query.compile()) 的结果.您可以调整它以使用 postgres 方言:
    dialect = postgresql.dialect()
    str(query.compile(dialect=dialect))
    

    并得到稍微不同的结果,但仍然没有子查询。很有趣,不是吗?仅供引用,query.compile (简化)与调用 dialect.statement_compiler(dialect, query, bind=None) 相同

    第二个查询(我们称之为 A 作为别名)是在调用 db.session.query(query).all() 时生成的。 .如果您只输入 str(db.session.query(query)) ,你会看到我们得到了一个不同的查询(与 N query.compile() 相比) - 带有子查询和别名。

    跟session有关系吗?否 - 您可以通过将查询转换为 Query 来检查这一点对象,忽略 session 信息:
    from sqlalchemy.orm.query import Query
    str(Query(query))
    

    查看实现细节( Query.__str__ )我们可以看到 发生了什么一个 是:
    context = Query(query)._compile_context()
    str(context.statement.compile(bind=None))
    
    context.statement.compile将尝试选择一种方言(在我们的例子中正确识别 Postgres),然后以与 相同的方式执行该语句。小号 变体:
    dialect.statement_compiler(dialect, context.statement, bind=None)
    

    提醒自己,小号 源于:
    dialect = postgresql.dialect()
    str(dialect.statement_compiler(dialect, query, bind=None))
    

    这暗示我们,在上下文中,有一些东西会改变语句编译器的行为。 dialect.statement_compiler 有什么用做,?它是 SQLCompiler 的子类的构造函数, 专门在继承过程中匹配您的方言需求;对于 Postgres,它应该是 PGCompiler .

    注意:我们可以为 走捷径一个 :
    dialect.statement_compiler(dialect, Query(query).statement, bind=None)
    

    让我们比较编译对象的状态。这可以通过访问 __dict__ 轻松完成。编译器的属性:
    with_subquery = dialect.statement_compiler(dialect, context.statement, bind=None)
    no_subquery = dialect.statement_compiler(dialect, query, bind=None)
    from deepdiff import DeepDiff 
    DeepDiff(sub.__dict__, nosub.__dict__, ignore_order=True)
    

    重要的是,语句的类型发生了变化。这并不意外,因为在第一种情况下,context.statementsqlalchemy.sql.selectable.Select对象,而在后者中 querysqlalchemy.sql.selectable.Alias目的。

    这突出了将查询转换为 Query 的事实。对象 db.session.query() , 导致编译器根据语句的更改类型采取不同的路线。我们可以看到小号 事实上,它是一个包裹在选择中的别名,使用:
    >>> context.statement._froms
    [<sqlalchemy.sql.selectable.Alias at 0x7f7e2f4f7160; foo>]
    

    别名在包含在 select 语句( S )中时呈现的事实,创建子查询在某种程度上与 the documentation 一致它将别名描述为在 SELECT 语句中使用(但不是作为查询的根):

    When an Alias is created from a Table object, this has the effect of the table being rendered as tablename AS aliasname in a SELECT statement.



    为什么首先有一个子选择?

    让我们在没有 .alias('foo') 的情况下命名查询如 电话 (无别名)并在下面的伪代码中表示为 n_query .因为它是类型 sqlalchemy.sql.selectable.Select当您拨打 db.session.query(n_query) 时它以与使用别名的情况大致相同的方式创建了一个子查询。您可以使用以下命令验证我们是否在另一个选择中获得了一个选择:
    >>> Query(nquery).statement._froms
    [<sqlalchemy.sql.selectable.Select at 0x7f7e1e26e668; Select object>]
    

    您现在应该很容易看到,在选择中包含选择意味着在使用 db.session.query(n_query) 查询数据库时始终创建了子选择。 .

    我不确定为什么您显示的第一个查询有一个可见的子查询 - 您是否有可能使用过 echo (或 str(db.session(n_query)) 当时?

    我可以改变这种行为吗?

    当然!只需执行您的查询:
    db.session.execute(n_query)
    

    然后(如果您按照上面的说明启用了 echo),您将看到发出相同的查询(正如您在最后发布的那样)。

    这与执行别名查询完全相同:
    db.session.execute(n_query.alias('foo'))
    

    因为如果没有连续的select,别名就没有用了!

    关于python - SQLAlchemy 要求为查询设置别名,但在生成的 SQL 中未使用该别名,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56040284/

    有关python - SQLAlchemy 要求为查询设置别名,但在生成的 SQL 中未使用该别名的更多相关文章

    1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

      我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

    2. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

      我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

    3. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

      类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

    4. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

      很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

    5. ruby - 在 Ruby 中使用匿名模块 - 2

      假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

    6. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

      我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

    7. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

      关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

    8. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

      我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

    9. ruby - ECONNRESET (Whois::ConnectionError) - 尝试在 Ruby 中查询 Whois 时出错 - 2

      我正在用Ruby编写一个简单的程序来检查域列表是否被占用。基本上它循环遍历列表,并使用以下函数进行检查。require'rubygems'require'whois'defcheck_domain(domain)c=Whois::Client.newc.query("google.com").available?end程序不断出错(即使我在google.com中进行硬编码),并打印以下消息。鉴于该程序非常简单,我已经没有什么想法了-有什么建议吗?/Library/Ruby/Gems/1.8/gems/whois-2.0.2/lib/whois/server/adapters/base.

    10. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

      我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

    随机推荐