Working with mapped class attributes for querying, filtering, and comparisons
Queryable attributes are the mapped class attributes that provide the interface for constructing query expressions in SQLAlchemy. Understanding how to use these attributes is essential for building effective queries.
Every mapped class attribute (column or relationship) in SQLAlchemy is actually a QueryableAttribute that provides SQL expression capabilities. These attributes support Python operators and methods that translate to SQL.
from sqlalchemy import selectfrom models import User# User.name is a QueryableAttributestmt = select(User).where(User.name == "Alice")# User.age is also a QueryableAttribute stmt = select(User).where(User.age > 21)
from sqlalchemy import select# IN clausestmt = select(User).where(User.id.in_([1, 2, 3, 4, 5]))# SQL: WHERE users.id IN (1, 2, 3, 4, 5)# NOT IN clausestmt = select(User).where(User.status.not_in(["deleted", "banned"]))# SQL: WHERE users.status NOT IN ('deleted', 'banned')# BETWEENstmt = select(User).where(User.age.between(18, 65))# SQL: WHERE users.age BETWEEN 18 AND 65# IN with subquerysubq = select(Order.user_id).where(Order.total > 1000)stmt = select(User).where(User.id.in_(subq))
String-based columns have additional methods for pattern matching.
Pattern Matching
Contains/Starts/Ends
Case Sensitivity
from sqlalchemy import select# LIKE (case-sensitive on most databases)stmt = select(User).where(User.name.like("A%"))# SQL: WHERE users.name LIKE 'A%'# ILIKE (case-insensitive)stmt = select(User).where(User.email.ilike("%@GMAIL.COM"))# SQL: WHERE users.email ILIKE '%@gmail.com'# NOT LIKEstmt = select(User).where(User.name.not_like("test%"))# NOT ILIKE stmt = select(User).where(User.email.notilike("%spam%"))
Wildcard patterns:
% matches any sequence of characters
_ matches any single character
# Contains substringstmt = select(User).where(User.description.contains("developer"))# SQL: WHERE users.description LIKE '%developer%'# Starts withstmt = select(User).where(User.name.startswith("Dr."))# SQL: WHERE users.name LIKE 'Dr.%'# Ends withstmt = select(User).where(User.email.endswith("@company.com"))# SQL: WHERE users.email LIKE '%@company.com'
These convenience methods are equivalent to using like() with appropriate wildcards but are more readable.
from sqlalchemy import func# Case-insensitive comparison using func.lower()stmt = select(User).where( func.lower(User.email) == func.lower("Alice@Example.COM"))# Alternative: use ilike for simple patternsstmt = select(User).where(User.email.ilike("alice@example.com"))# Regular expressions (PostgreSQL)stmt = select(User).where(User.name.regexp_match(r"^[A-Z][a-z]+$"))
Relationship attributes work differently than column attributes.
from sqlalchemy import selectfrom models import User, Address# Has relationship - check if collection is non-emptystmt = select(User).where(User.addresses.any())# SQL: WHERE EXISTS (SELECT 1 FROM addresses WHERE addresses.user_id = users.id)# Has with conditionstmt = select(User).where( User.addresses.any(Address.email_address.like("%@gmail.com")))# Has not - check if collection is emptystmt = select(User).where(~User.addresses.any())
Use .any() for one-to-many or many-to-many relationships
# Bad - can't use index on User.emailstmt = select(User).where(func.lower(User.email) == "alice@example.com")# Better - normalize data at write time and query directlystmt = select(User).where(User.email == "alice@example.com")# Or create a functional index on lower(email) in PostgreSQL
3
Use appropriate operators
Some operators are more efficient than others:
# Efficient - can use indexstmt = select(User).where(User.name.startswith("A"))# Less efficient - can't use index effectively stmt = select(User).where(User.name.contains("alice"))# Most efficient for exact matchesstmt = select(User).where(User.name == "Alice")