繁体   English   中英

SQLAlchemy要求查询为别名,但在生成的SQL中不使用别名

[英]SQLAlchemy requires query to be aliased yet that alias is not used in the generated SQL

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

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")

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

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)

此时有几点需要注意:

  1. 子选择是多余的,我们应该简单地使用子选择作为主选择语句。
  2. 您可以通过对子选择进行<alias>.<column>并使用主选择语句中的<alias>.<column>来解决此问题 - 需要在子选择上使用别名的Postgres在其他地方有详细记录。

我的第一个问题是,除非没有明确指示(据我所知),我将如何看待SQLalchemy决定引入它的这个子选择?

我发现问题的解决方案是在查询中添加.alias("foo")

query = query\
        ...\
        .alias("foo")

哪个casuse生成以下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决定引入它

它不是。 你告诉它在你调用db.sesion.query(query)使用子查询(虽然你可能没有意识到)。 请改用db.session.execute(query)

为什么添加别名会阻止创建子选择,为什么不使用别名! “foo”别名似乎被忽视但对生成的查询产生了实质性影响。

没有 ,它使用。

说明 - 介绍

SQLAlchemy只是欺骗了你。 我认为你一直在使用print(query)窥视引擎盖并理解错误 - 这次运气不好,它并没有告诉你全部真相。

要查看生成的真实SQL,请在引擎中启用echo功能 完成后,您会发现实际上,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作为select over alias )是查询的字符串表示形式或str(query.compile())的结果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)) ,你会看到我们得到一个不同的查询(与Nquery.compile() ) - 带有子查询和别名。

它与会话有什么关系吗? 否 - 您可以通过将查询转换为Query对象来检查,忽略会话信息:

from sqlalchemy.orm.query import Query
str(Query(query))

Query.__str__实现细节( Query.__str__ ),我们可以看到A的变化是:

context = Query(query)._compile_context()
str(context.statement.compile(bind=None))

context.statement.compile将尝试选择一种方言(在我们的例子中正确识别Postgres),然后以与S变体相同的方式执行语句:

dialect.statement_compiler(dialect, context.statement, bind=None)

提醒自己, S起源于:

dialect = postgresql.dialect()
str(dialect.statement_compiler(dialect, query, bind=None))

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

注意:我们可以采取A的捷径:

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对象。

这突出了这样一个事实,即使用db.session.query()Query转换为Query对象会导致编译器根据更改的语句类型采用不同的路由。 事实上,我们可以看到S是一个包含在select中的别名:

>>> context.statement._froms
[<sqlalchemy.sql.selectable.Alias at 0x7f7e2f4f7160; foo>]

在包装在select语句( S )中时呈现别名的事实,创建子查询在某种程度上与描述Alias在SELECT语句中使用的文档 (但不是查询的根)一致:

从Table对象创建Alias时,这会使表在SELECT语句中呈现为表名AS别名。

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

让我们将没有.alias('foo')的查询命名为N (无别名),并在下面的伪代码中将其表示为n_query 因为它的类型sqlalchemy.sql.selectable.Select当你调用db.session.query(n_query)它创造了几乎相同的方式一个子查询中使用别名的情况。 您可以验证我们在另一个选择内部选择了:

>>> Query(nquery).statement._froms
[<sqlalchemy.sql.selectable.Select at 0x7f7e1e26e668; Select object>]

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

我不确定为什么你显示的第一个查询有一个子查询可见 - 你可能会使用echo(或str(db.session(n_query))吗?

我可以改变这种行为吗?

当然! 只需执行以下查询:

db.session.execute(n_query)

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

这与执行别名查询完全相同:

db.session.execute(n_query.alias('foo'))

因为如果没有连续选择,别名没有用!

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM