Cheatsheet for queries. Long-form: Textbook Ch 5 .
Basic select
from sqlalchemy import select
# All columns
stmt = select(User)
# Specific columns
stmt = select(User.id, User.email)
# Distinct
stmt = select(User.email).distinct()
Execute
result = await session.execute(stmt)
result.scalars().all() # for select(User) → list[User]
result.scalars().first()
result.scalar_one() # exactly 1; raises 0 or 2+
result.scalar_one_or_none() # 0 or 1
result.all() # for multi-column → list[Row]
result.first()
result.one()
result.one_or_none()
result.mappings().all() # list[dict]
Filtering
stmt = (
select(User)
.where(User.email == "x")
.where(User.active == True) # AND
)
# OR / NOT
from sqlalchemy import or_, not_, and_
stmt = select(User).where(or_(User.role == "admin", User.id == 1))
stmt = select(User).where(not_(User.active))
# IN / NOT IN
stmt = select(User).where(User.id.in_([1, 2, 3]))
stmt = select(User).where(User.id.notin_([1, 2, 3]))
# LIKE / ILIKE
stmt = select(User).where(User.email.like("%@example.com"))
stmt = select(User).where(User.email.ilike("%@example.com")) # PG / MySQL
# Null / not null
stmt = select(User).where(User.deleted_at.is_(None))
stmt = select(User).where(User.deleted_at.is_not(None))
# Between
stmt = select(Event).where(Event.ts.between(t1, t2))
# Contains / startswith / endswith
stmt = select(Post).where(Post.title.contains("foo"))
stmt = select(Post).where(Post.title.startswith("foo"))
Order / limit / offset
stmt = (
select(User)
.order_by(User.created_at.desc(), User.id)
.limit(20)
.offset(40)
)
For cursor pagination instead of offset:
stmt = (
select(User)
.where(User.id > cursor)
.order_by(User.id)
.limit(20)
)
Joins
stmt = select(Post, User).join(User, Post.author_id == User.id)
# Via relationship (cleaner)
stmt = select(Post).join(Post.author)
# Outer join
stmt = select(Post).outerjoin(Comment).where(Comment.id == None)
# Aliasing
from sqlalchemy.orm import aliased
a = aliased(User); b = aliased(User)
stmt = select(a, b).join(b, a.referrer_id == b.id)
Aggregations
from sqlalchemy import func
# Count
total = await session.scalar(select(func.count(User.id)))
# Group by
stmt = (
select(User.role, func.count(User.id).label("n"))
.group_by(User.role)
)
# Having
stmt = (
select(User.role, func.count(User.id).label("n"))
.group_by(User.role)
.having(func.count(User.id) > 10)
)
# Aggregates
func.sum(...) func.avg(...) func.min(...) func.max(...)
Subqueries
sub = (
select(Post.author_id, func.count(Post.id).label("posts"))
.group_by(Post.author_id)
.subquery()
)
stmt = (
select(User, sub.c.posts)
.join(sub, User.id == sub.c.author_id)
)
CTEs
recent = (
select(Post)
.where(Post.created_at > datetime(2026, 1, 1))
.cte("recent_posts")
)
stmt = select(User, recent).join(recent, User.id == recent.c.author_id)
Recursive:
roots = (
select(Category)
.where(Category.parent_id == None)
.cte("cats", recursive=True)
)
ext = roots.union_all(
select(Category).join(roots, Category.parent_id == roots.c.id)
)
stmt = select(ext)
EXISTS
from sqlalchemy import exists
stmt = select(User).where(
select(Post).where(Post.author_id == User.id).exists()
)
Window functions
stmt = select(
Post.author_id,
Post.title,
func.row_number().over(
partition_by=Post.author_id,
order_by=Post.created_at.desc(),
).label("rn"),
)
Bulk UPDATE / DELETE
from sqlalchemy import update, delete
await session.execute(
update(User).where(User.email == "x").values(active=False)
)
await session.execute(
delete(User).where(User.id == 5)
)
await session.commit()
For ORM-managed updates (with relationships / events): fetch then mutate.
RETURNING
stmt = (
update(User)
.where(User.id == 1)
.values(name="x")
.returning(User.id, User.name)
)
result = await session.execute(stmt)
print(result.all())
INSERT
from sqlalchemy import insert
await session.execute(insert(User).values(email="[email protected]"))
await session.execute(insert(User), [{"email": ...}, ...]) # bulk
Upsert (Postgres / MySQL / SQLite have dialect-specific versions)
from sqlalchemy.dialects.postgresql import insert as pg_insert
stmt = pg_insert(User).values(email="[email protected]", name="Alice")
stmt = stmt.on_conflict_do_update(
index_elements=["email"],
set_={"name": stmt.excluded.name},
)
See Postgres cheatsheets for details.
text() raw SQL
from sqlalchemy import text
stmt = text("SELECT * FROM users WHERE email = :email")
result = await session.execute(stmt, {"email": "[email protected]"})
Always parameterize (:name), never f-string.
Inspect SQL
print(stmt.compile(engine, compile_kwargs={"literal_binds": True}))
Common mistakes
- Using legacy
session.query(...)— switch toselect(...)+session.execute(...). - SELECT * — wide tables waste bandwidth; specify columns.
resultwithoutscalars()for ORM → returns Row tuples.- Forgetting
commit()after bulk update/delete.
Read this next
If you want my query-builder helpers (cursor pagination, filtering by Pydantic model), it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .