Back to Blog
Python

Python SQLAlchemy WHERE, ORDER BY, GROUP BY, and Joins

python sqlalchemy where order by group by and joins: Learn how to build SQLAlchemy queries with WHERE, ORDER BY, GROUP BY, and JOINs using the ORM and Core APIs, with...

SQLAlchemyORMSQL QueriesDatabasePython
Illustration of SQLAlchemy query building with WHERE, ORDER BY, GROUP BY, and JOIN clauses represented as connected blocks.

When you need to filter, sort, group, or combine rows in a relational database, SQLAlchemy provides a Pythonic layer over SQL. The core operations—WHERE, ORDER BY, GROUP BY, and JOINs—map to query methods that behave predictably once you understand how they compose. This article covers python sqlalchemy where order by group by and joins in the ORM and Core APIs, with code you can adapt directly.

Building a Query with WHERE in SQLAlchemy ORM

In SQLAlchemy 2.0, the ORM uses the select() function to construct queries. The where() method applies filtering conditions, equivalent to SQL's WHERE clause. Consider two models, Author and Book, with a one-to-many relationship:

from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship from sqlalchemy import ForeignKey, String, Integer class Base(DeclarativeBase): pass class Author(Base): __tablename__ = "authors" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(100)) books: Mapped[list["Book"]] = relationship(back_populates="author") class Book(Base): __tablename__ = "books" id: Mapped[int] = mapped_column(primary_key=True) title: Mapped[str] = mapped_column(String(200)) year: Mapped[int] = mapped_column(Integer) author_id: Mapped[int] = mapped_column(ForeignKey("authors.id")) author: Mapped["Author"] = relationship(back_populates="books")

To fetch books published after 2020, you write:

from sqlalchemy import select from sqlalchemy.orm import Session stmt = select(Book).where(Book.year > 2020) with Session(engine) as session: books = session.scalars(stmt).all()

The where() method accepts multiple conditions, which are combined with AND by default. For OR conditions, use or_() from sqlalchemy:

from sqlalchemy import or_ stmt = select(Book).where( or_(Book.year > 2020, Book.title.like("%Python%")) )

Each call to where() adds another AND condition, so stmt.where(A).where(B) is equivalent to stmt.where(A, B).

Ordering Results with order_by()

order_by() sorts the result set. You can sort by one or more columns, with ascending order as the default. To sort descending, use the desc() function or the column's .desc() method:

from sqlalchemy import desc stmt = select(Book).order_by(Book.year.desc(), Book.title)

This returns books newest first, then alphabetically by title within the same year. In the ORM, you can also order by a related column using the relationship path:

stmt = select(Book).join(Book.author).order_by(Author.name)

When combining where() and order_by(), the order of method calls does not affect the SQL—SQLAlchemy builds the statement internally. However, for readability, place where() before order_by().

Grouping and Aggregates with group_by()

group_by() groups rows that share a common value, typically used with aggregate functions like count(), sum(), or avg(). The func object from sqlalchemy provides these functions. For example, to count books per author:

from sqlalchemy import func stmt = ( select(Author.name, func.count(Book.id)) .join(Book, Author.id == Book.author_id) .group_by(Author.id) )

In SQLAlchemy 2.0, you must include all non-aggregated columns in group_by(). Grouping by Author.id is sufficient if it's the primary key, because the ORM knows other columns are functionally dependent. When using Core tables, you may need to group by every selected column.

group_by() can also be combined with having() to filter groups after aggregation:

stmt = ( select(Author.name, func.count(Book.id)) .join(Book, Author.id == Book.author_id) .group_by(Author.id) .having(func.count(Book.id) > 5) )

This returns only authors with more than five books.

Joining Tables in SQLAlchemy ORM

Joins combine rows from multiple tables based on a condition. The ORM's join() method accepts a target model and an explicit ON clause, or it can infer the condition from a configured relationship. Using the relationship defined earlier:

stmt = select(Book).join(Book.author)

This performs an inner join. For a left outer join, use outerjoin():

stmt = select(Author, Book).outerjoin(Book, Author.id == Book.author_id)

If you need to join on a condition that isn't a relationship, pass the ON clause explicitly:

stmt = select(Book).join(Author, Book.author_id == Author.id)

When selecting multiple entities, session.execute() returns rows that can be unpacked:

with Session(engine) as session: rows = session.execute(stmt).all() for author, book in rows: print(author.name, book.title)

Combining WHERE, ORDER BY, GROUP BY, and JOINs

These clauses compose naturally in a single statement. For example, to list authors with more than three books published after 2015, ordered by book count descending:

stmt = ( select(Author.name, func.count(Book.id).label("book_count")) .join(Book, Author.id == Book.author_id) .where(Book.year > 2015) .group_by(Author.id) .having(func.count(Book.id) > 3) .order_by(desc("book_count")) )

Note that order_by() can reference a column label defined in the select(). This works because SQLAlchemy translates the label to the underlying expression. When using desc() on a string, it expects a column reference; here the label is resolved correctly.

You can also chain where() before or after join(); the order doesn't change the SQL semantics. SQLAlchemy will place the WHERE and JOIN clauses correctly in the generated SQL.

Performance Considerations for Filtering and Joining

Query performance depends on how the database executes the generated SQL. For large tables, ensure that columns used in where(), join(), and order_by() are indexed. For example, Book.year and Book.author_id should have indexes if they appear in filters or joins frequently. SQLAlchemy does not create indexes automatically; you must define them in the model or migration.

Another performance concern is eager loading versus lazy loading. When you select Author and access author.books, SQLAlchemy by default issues a separate query for each author's books (the N+1 problem). To avoid this, use selectinload() or joinedload() in the statement:

from sqlalchemy.orm import selectinload stmt = select(Author).options(selectinload(Author.books))

This fetches all books in one additional query. For joins that filter on related columns, a single join is often more efficient than a subquery, but measure with your database's query planner.

Common Pitfalls with Column Ambiguity and Null Handling

When joining tables that have columns with the same name, SQLAlchemy may raise an ambiguous column error. Always qualify column references with the model or table object. For example, use Book.id instead of id.

Null handling also matters. where(Book.year > 2020) excludes rows where year is NULL. If you need to include them, add an explicit or_(Book.year > 2020, Book.year.is_(None)). Similarly, order_by() places NULLs first in ascending order on most databases; to control this, use nullsfirst() or nullslast() from sqlalchemy.

Grouping with NULLs can also produce unexpected results. In SQL, NULL values are considered equal for grouping, so all rows with NULL in the grouped column end up in one group. Be aware of this when grouping by nullable columns.

Finally, when using group_by() with order_by(), ensure that the ordering column is either part of the group or an aggregate. Otherwise the database may reject the query or return nondeterministic results. SQLAlchemy does not enforce this; the database does.

By understanding how these clauses interact, you can write queries that are both correct and maintainable. Start with a simple statement and add clauses incrementally, verifying the generated SQL with str(stmt) or session.execute(stmt).context.statement when debugging.

python sqlalchemy where order by group by and joins: Practic | RYUSLOG DEV