There are different types of subquery in SQL and SQLAlchemy. In this blog, we are going to talking about:
- Subquery
- CTE (Common Table Expresssion)
- Scalar Subquery & Non-Scalar Subquery
- Correlated Subquery
- Set Result As Subquery
- Exsits Subquery
- Relationship As Subquery
Scalar & Non-Scalar Subquery
Concepts
Based on words in SQLAlchemy Docs:
A scalar subquery is a subquery that returns exactly zero or one row and exactly one column. The subquery is then used in the COLUMNS or WHERE clause of an enclosing SELECT statement.
Scalar query is different than a regular (or, non-scalar) subquery in that it is not used in the FROM clause.
In conclusions:
Scalar: Return exactly one thing. Used in COLUMNS
or WHERE
Non-scalar: Return a selectable, usually used in FROM
Construct Scalar Subquery
As we may all already known that Select.subquery()
could create non-scalar subquery. If we want to create a scalar subquery, we need to use another method: Select.scalar_subquery()
Correlated Subquery
Concept
Correlated subquery is a scalar subquery that related to the table of it's enclosing select:
select user_table.uid where (
select count(addresses.aid)
where addresses.uid = user_table.uid
) > 1;
This query will return user id of whom that have more than one addresses. Note this subquery is correlated to the outer selection, since the where condition uses user_table.uid
which comes from outer table.
SQLAlchemy Auto Correlation
In some simple cases, SQLAlchemy will take care of correlations:
TODO
Set Operations As Subquery
Set Operations Explained
SQLAlchemy allows construct subquery from a CompoundSelect
which is what we got after set operations of several selectables.
from sqlalchemy import union_all
# several selectables.
# selectables usually created using select()
stmt1: Select = ...
stmt2: Select = ...
# get a CompoundSelect
union_selection: CompoundSelect = union_all(stmt1, stmt2, ...)
CompoundSelect To Subquery
It's extremly simple to convert a CompoundSelect
object to a useable non-scalar subquery: Just use .subquery()
method:
union_subquery = union_selection.subquery()
Fetching ORM Entities
However, using .subquery()
alone doesn't returns an ORM class instance since SQLAlchemy don't know what schemes that query returns.
To bind the subquery to an ORM entity, we could exploit aliased()
feature:
some_user = aliased(User, union_subquery)
This usage of
aliased(<orm_class>, <sub_query>)
could be used anywhere not just in this case.
A Quicker Way
SQLAlchemy provides a shortcut called select(<orm_class>).from_statement(<statements>)
, if we don't need to add any other criteria after this, we could do something like below:
select(User).from_statement(union_selection)
Note that in this case, we use CompoundSelect
object directly and there's no need to convert it into the subquery.
ORM Relationship Subquery
We could use relationships we defined in ORM entities to construct scalar subquerys that used in WHERE
clauses.
Most of the times, we would use something like r.any()
, r.has()
or some Python Comparators.
Any
Relationship.any()
is used at one-to-many side, limiting result of enclosing select.
Consider we want to select user that has no contact info, one way is to use relationship subquery:
select(User).where(~User.addresses.any())
~
is Bitwise Not operator in Python, here is used to indicateNOT
in SQL.
Also, we could select user that has email addresses end with google@com
:
select(User).where(User.addresses.any(Address.address.like('%@google.com')))
Has
Relationship.has()
could be considered the complementary version of has()
, working at many-to-one side:
For example, we could select all addresses rows that associated to user that named bob
.
select(Address).where(Address.user.has(User.name == 'bob'))
== & != & contains()
The any()
and has()
both worked with ORM Entities in class level. We also have some tools in instances level:
user = session.get(User, 2) # get user with primary key 2
select(Address).where(Address.user == user)
address = Address(...)
select(User).where(User.addresses.contains(address))
No comment