SQLAlchemy требует, чтобы запрос был псевдонимом, но псевдоним не используется в сгенерированном SQL - PullRequest
10 голосов
/ 08 мая 2019

У меня есть простой класс модели, представляющий битву между двумя персонажами:

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", казалось бы, не учитывался, но оказал существенное влияние на сгенерированный запрос.

1 Ответ

1 голос
/ 17 мая 2019

Ответы

SQLalchemy решает ввести его, несмотря на то, что ему явно не дано указание

Нет.Вы говорите ему использовать подзапрос в тот самый момент, когда вы звоните db.sesion.query(query) (хотя вы можете не знать об этом).Вместо этого используйте db.session.execute(query).

, почему добавление псевдонима препятствует созданию подвыбора и почему псевдоним не используется!Псевдоним "foo", по-видимому, не учитывался, но оказал существенное влияние на сгенерированный запрос.

Он сделал не , а используется .

Объяснение - введение

SQLAlchemy только что обманул вас.Я предполагаю, что вы использовали print(query), чтобы заглянуть под капот и понять, что не так - на этот раз не повезло, он не сказал вам всей правды.

Чтобы просмотреть реальный SQL, который был сгенерирован, включить функцию эха на в двигателе.Сделав это, вы обнаружите, что в действительности 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 as select over alias ) - строковое представление запроса или результат 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()) - с подзапросом и псевдонимом.

Делает ли этоиметь какое-либо отношение к сессии?Нет - вы можете проверить, преобразовав запрос в 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.statement является объектом sqlalchemy.sql.selectable.Select, а во втором query является sqlalchemy.sql.selectable.Alias объектом.

Это подчеркивает тот факт, что преобразование запроса вQuery объект с db.session.query(), заставляет компилятор выбрать другой маршрут в зависимости от измененного типа оператора.Мы можем видеть, что S , на самом деле, является псевдонимом, заключенным в select, используя:

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

Тот факт, что псевдоним отображается при переносе в операторе select (S ), создание подзапроса каким-то образом согласуется с документацией , которая описывает псевдоним как использование в операторе SELECT (но не как корень запроса):

Когда псевдоним создается из объекта Table, это приводит к тому, что таблица отображается как псевдоним AS таблицы в операторе SELECT.

Почему сначала был суб-выбор?

Давайте назовем запрос без .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'))

потому что псевдоним не имеет смысла, если нет последовательного выбора!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...