【问题标题】:SQLAlchemy requires query to be aliased yet that alias is not used in the generated SQLSQLAlchemy 要求为查询设置别名,但在生成的 SQL 中未使用该别名
【发布时间】:2019-09-26 03:50:56
【问题描述】:

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

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> 来解决此问题 - 要求子选择别名的 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" 别名看似被忽略,但对生成的查询产生了重大影响。

【问题讨论】:

  • 只是猜测你第二个问题的后半部分 - 你的 alias() 是你查询的最后一个语句。根据文档, select() 对象上的别名创建了一个命名子查询 '(select ...) AS aliasname' 但在这种情况下 - 作为最后的语句 - 它被优化了,因为在这个查询“之外”没有任何东西甚至理论上引用它。如果您在 .alias() 之后添加一些引用“foo”的内容,它会出现吗?
  • 如果没有别名,您应该将核心 select() 语句传递给 Session.execute(),而不是 Session.query()。前者执行,而后者构造新查询(在 ORM 级别)。

标签: python sqlalchemy


【解决方案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 作为选择别名)是查询的字符串表示或str(query.compile()) 的结果。您可以调整它以使用 postgres 方言:

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

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

第二个查询(我们称它为 A as aliased)是在调用db.session.query(query).all() 时生成的。如果您只输入str(db.session.query(query)),您会看到我们得到了一个不同的查询(与Nquery.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 对象。

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

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

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

从 Table 对象创建 Alias 时,这具有在 SELECT 语句中将 table 呈现为 tablename AS aliasname 的效果。

为什么一开始会有子选择?

让我们将不带.alias('foo') 的查询命名为N(无别名),并在下面的伪代码中将其表示为n_query。因为当您调用db.session.query(n_query) 时它是sqlalchemy.sql.selectable.Select 类型,所以它创建了一个子查询,其方式与使用别名的情况大致相同。您可以通过以下方式验证我们是否在另一个选择中获得了选择:

>>> 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,别名就没用了!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-11-10
    • 2016-10-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多