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

时间:2019-05-08 11:58:04

标签: python sqlalchemy

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

obj.render(exp)

我有一种构造CTE的方法,该CTE将战斗投射为一系列出场(每场战斗都有两次出场-胜利者和失败者):

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)

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

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

这将生成以下SQL:

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

当对SQLite数据库执行时,这工作得很好,但是对Postgres 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
)

此时有几件事要注意:

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

我的第一个问题是如何,尽管没有明确指示(据我所知),尽管SQLalchemy决定引入它,但我会对该子选择进行别名吗?

我发现该问题的解决方案是在查询中添加<alias>.<column>

.alias("foo")

促使以下SQL生成(一个怪异地解决了整个冗余子选择问题!)

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

我的第二个问题是为什么确实添加了别名阻止了子选择的创建 为什么是不使用别名的原因! 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 别名似乎被忽略了,但对生成的查询产生了实质性的影响。

1 个答案:

答案 0 :(得分:1)

答案

  

尽管没有明确指示SQLalchemy决定引入它

不是。您告诉它在调用db.sesion.query(query)的那一刻就使用子查询(尽管您可能不知道它)。请改用db.session.execute(query)

  

为什么添加别名会阻止子选择的创建,为什么不使用别名!似乎忽略了“ foo”别名,但对生成的查询有很大影响。

没有没有,并且已使用

说明-简介

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 select over alias )是查询的字符串表示形式或str(query.compile())的结果。您可以调整它以使用postgres方言:

dialect = postgresql.dialect()
str(query.compile(dialect=dialect))

并获得略有不同的结果,但仍然没有子查询。有趣,不是吗?仅供以后参考,query.compile与(简化)与调用dialect.statement_compiler(dialect, query, bind=None)

相同

调用db.session.query(query).all()时将生成第二个查询(别名为 A 。如果您只键入str(db.session.query(query)),则会看到我们得到了另一个查询(与 N query.compile()相比)-带有子查询和别名。

与会话有关吗?否-您可以通过将查询转换为Query对象,而不考虑会话信息来进行检查:

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

查看实施细节(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子类的构造函数,专门用于继承过程来满足您的方言需求;对于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对象。

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

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

别名在包裹在选择语句( S )中时会呈现别名,从而创建子查询的事实在某种程度上与the documentation一致,后者描述了别名在SELECT语句中的使用(但而不是查询的根):

  

从Table对象创建别名时,具有将表呈现为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>]

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

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

我可以更改此行为吗?

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

db.session.execute(n_query)

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

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

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

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

相关问题