I have a simple model class that represents a battle between two characters:
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)
I have a method which constructs a CTE which projects the battles into a series of appearences (each battle has two appearences - the winner and the loser):
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")
I then have a query which utilises this view to determine the characters which have seen the most battles:
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()
This generates the following 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
)
This works perfectly fine when executing against a SQLite database, but when executing against a Postgres SQL database the following error is given:
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)
There are a few things to notice at this point:
- The sub-select is redundant, we should simply using the sub-select as the main select statement.
- You could resolve this by aliasing the sub-select and using
<alias>.<column>
in the main select statement - Postgres requiring an alias on the sub-selects is well documented elsewhere.
My first question is how would I alias this sub-select seeing that SQLalchemy decides to introduce it despite not being explicitly instructed to (as far as I can tell)?
I found a solution to the problem was to add .alias("foo")
to the query:
query = query\
...\
.alias("foo")
Which casuses the following SQL to be generated (one that weirdly resolved the whole redundant sub-select issue as well!):
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
My second question is 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.