Skip to main content

query-patterns

Use when the user asks about "SQLAlchemy queries", "filter records", "SQLAlchemy select", "session.scalars", "join tables", "aggregate query", "order by", "pagination", "N+1 query problem", "eager loading", "SQLAlchemy session", "bulk insert", "convert Model.query to select() 2.0 style", "upgrade to SQLAlchemy 2.1", "filter queries by current user", "owner-scoped queries", or needs patterns for querying data with SQLAlchemy.

Source facts

Repository
Agents-Store/cursor-plugins
Last source activity
October 3, 2026 at 23:16
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
2 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
query-patterns
description
Use when the user asks about "SQLAlchemy queries", "filter records", "SQLAlchemy select", "session.scalars", "join tables", "aggregate query", "order by", "pagination", "N+1 query problem", "eager loading", "SQLAlchemy session", "bulk insert", "convert Model.query to select() 2.0 style", "upgrade to SQLAlchemy 2.1", "filter queries by current user", "owner-scoped queries", or needs patterns for querying data with SQLAlchemy.
# SQLAlchemy Query Patterns Production patterns for querying, filtering, and optimizing database access in the SQLAlchemy 2.0 style: build a statement with `select()`, run it with `session.scalars()` / `session.execute()`. Every example runs unchanged on SQLAlchemy 2.0 and 2.1. The examples use the models from the `model-patterns` skill (`User`, `Client`, `Appointment`, `Spending`, `Tag`) and a `session` that is a `sqlalchemy.orm.Session`. In Flask-SQLAlchemy use `db.session` and `db.select` (see Flask-SQLAlchemy Shortcuts below). Reading or migrating `Model.query` / `session.query()` code? See [Legacy 1.x style](../api-reference/references/legacy-1x-style.md). ## Engine and Session ```python from sqlalchemy import create_engine from sqlalchemy.orm import Session, sessionmaker engine = create_engine("sqlite:///app.db") # URL from an environment variable in real code SessionLocal = sessionmaker(engine) with SessionLocal() as session: # closes the session on exit ... ``` `create_engine` once per process; one short-lived `Session` per unit of work (a request, a job). A `Session` is not thread-safe and not meant to be shared. ## Basic Queries ```python from sqlalchemy import select # Get by primary key: returns the object or None, and uses the identity map user = session.get(User, 1) # All rows of an entity users = session.scalars(select(User)).all() # Filter by keyword equality (single entity, no joins) vip_clients = session.scalars(select(Client).filter_by(status="vip")).all() # First match or None client = session.scalars(select(Client).where(Client.email == "test@example.com")).first() # Exactly one row: raises NoResultFound / MultipleResultsFound otherwise user = session.scalars(select(User).where(User.email == "ann@example.com")).one() ``` `session.scalars(stmt)` unwraps the single entity column of each row. Use `session.execute(stmt)` when the statement returns several columns or entities, and read `Row` objects. | Method | Returns | |--------|---------| | `.all()` | list of every row | | `.first()` | first row or `None` (no error if more rows exist) | | `.one()` | the only row; raises if there are zero or several | | `.one_or_none()` | the only row or `None`; raises if several | | `session.scalar(stmt)` | first column of the first row, or `None` | Selecting columns instead of entities returns `Row` objects (tuple-like, with attribute access): ```python rows = session.execute(select(Client.id, Client.name).where(Client.status == "vip")).all() for client_id, name in rows: print(client_id, name) as_dicts = session.execute(select(Client.id, Client.name)).mappings().all() ``` ## Filtering ```python import datetime as dt from sqlalchemy import and_, not_, or_, select # Equality and inequality select(Client).where(Client.status == "vip") select(Client).where(Client.status != "cancelled") # Several where() arguments are ANDed select(Appointment).where( Appointment.user_id == 1, Appointment.date == dt.date(2026, 3, 1), Appointment.status == "on plan", ) # LIKE (case-sensitive) and ILIKE (case-insensitive) select(Client).where(Client.name.like("Ann%")) select(Client).where(Client.name.ilike("%anna%")) # User-supplied search text: autoescape stops "%" and "_" acting as wildcards search_term = "50%" select(Client).where(Client.name.icontains(search_term, autoescape=True)) # IN select(Client).where(Client.status.in_(["vip", "regular"])) # IS NULL / IS NOT NULL select(Client).where(Client.birthday.is_(None)) select(Client).where(Client.birthday.is_not(None)) # Ranges select(Appointment).where(Appointment.date.between(dt.date(2026, 3, 1), dt.date(2026, 3, 31))) select(Appointment).where(Appointment.date >= dt.date(2026, 3, 1), Appointment.date < dt.date(2026, 4, 1)) # OR, explicit AND, NOT select(Client).where(or_(Client.status == "vip", Client.status == "regular")) select(Client).where(and_(Client.status == "vip", or_(Client.phone.is_(None), Client.email.is_(None)))) select(Client).where(not_(Client.status == "new")) # Relationship comparison: EXISTS under the hood select(Client).where(Client.appointments.any(Appointment.status == "done")) select(Appointment).where(Appointment.client.has(Client.status == "vip")) ``` For a half-open date range prefer `>= start, < next_start` over `between`, which includes both ends. ## Ordering ```python # Ascending (default) select(Client).order_by(Client.name) # Descending, several keys select(Appointment).order_by(Appointment.date.desc(), Appointment.time.desc()) select(Client).order_by(Client.status, Client.name) # NULLs last (supported by PostgreSQL and SQLite 3.30+) select(Client).order_by(Client.birthday.asc().nulls_last()) ``` Add a unique key (`Client.id`) as the last sort key when paginating: without a total order, rows can repeat or vanish between pages. ## Aggregation ```python from sqlalchemy import func # Count count = session.scalar(select(func.count()).select_from(Client).where(Client.user_id == 1)) # Sum; SUM over zero rows is NULL, so coalesce it total = session.scalar( select(func.coalesce(func.sum(Spending.amount), 0)).where(Spending.user_id == 1) ) # Average avg_price = session.scalar(select(func.avg(Appointment.price))) # Group by spending_by_category = session.execute( select(Spending.category, func.sum(Spending.amount).label("total")) .where(Spending.user_id == 1) .group_by(Spending.category) .order_by(func.sum(Spending.amount).desc()) ).all() for category, total in spending_by_category: print(category, total) # HAVING busy_clients = session.execute( select(Appointment.client_id, func.count().label("visits")) .group_by(Appointment.client_id) .having(func.count() > 1) ).all() ``` ## Joins ```python # Navigating a relationship issues a lazy-load query per object (see Eager Loading) appointments = session.scalars(select(Appointment).where(Appointment.user_id == 1)).all() for appt in appointments: print(appt.client.name) # Join along a relationship: returns the entities of both sides today = dt.date(2026, 3, 1) rows = session.execute( select(Appointment, Client) .join(Appointment.client) .where(Appointment.date == today) ).all() for appt, client in rows: print(client.name, appt.time) # Explicit ON clause select(Appointment, Client).join(Client, Appointment.client_id == Client.id) # Left outer join: clients with or without appointments select(Client, Appointment).join(Client.appointments, isouter=True) # Clients with no appointments at all select(Client).outerjoin(Client.appointments).where(Appointment.id.is_(None)) ``` After a join, qualify filters with the entity (`.where(Client.status == "vip")`). `filter_by()` is shorthand for one entity: SQLAlchemy 2.0 applies it to the last joined entity, 2.1 searches every entity in the FROM clause and raises `AmbiguousColumnError` when a name such as `id` exists in several. ## Eager Loading (Avoid N+1) ```python from sqlalchemy.orm import joinedload, selectinload # Many-to-one: one query with a JOIN appointments = session.scalars( select(Appointment).options(joinedload(Appointment.client)).where(Appointment.user_id == 1) ).all() # Collections: a second query, SELECT ... WHERE client_id IN (...) clients = session.scalars( select(Client).options(selectinload(Client.appointments)).where(Client.user_id == 1) ).all() # joinedload on a collection repeats parent rows: unique() is required clients = session.scalars( select(Client).options(joinedload(Client.tags)).where(Client.user_id == 1) ).unique().all() ``` Rule of thumb: `joinedload` for many-to-one, `selectinload` for collections. Nested paths chain: `selectinload(Client.appointments).joinedload(Appointment.client)`. Detect N+1 by logging statements (`create_engine(url, echo=True)`, or `logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)`), or make a lazy load fail loudly: ```python from sqlalchemy.orm import raiseload # Any relationship not eagerly loaded raises InvalidRequestError instead of querying appointments = session.scalars( select(Appointment).options(joinedload(Appointment.client), raiseload("*")) ).all() ``` ## Pagination ```python # Offset pagination: simple, but slows down on deep pages page, per_page = 2, 20 stmt = select(Client).where(Client.user_id == 1).order_by(Client.id) clients = session.scalars(stmt.limit(per_page).offset((page - 1) * per_page)).all() total = session.scalar(select(func.count()).select_from(Client).where(Client.user_id == 1)) # Keyset pagination: pass the last id of the previous page last_seen_id = 100 next_page = session.scalars( select(Client).where(Client.user_id == 1, Client.id > last_seen_id).order_by(Client.id).limit(per_page) ).all() ``` In Flask-SQLAlchemy, `db.paginate(select_stmt, page=..., per_page=..., error_out=False)` returns a `Pagination` object (below). ## Session Operations (CRUD) ```python from sqlalchemy.exc import IntegrityError # Create client = Client(name="Anna", phone="+380501234567", user_id=1) session.add(client) session.commit() # Update: change attributes on a loaded object client.status = "vip" session.commit() # Delete session.delete(client) session.commit() # Several objects session.add_all([Client(name="Ben", user_id=1), Client(name="Cleo", user_id=1)]) session.commit() # Roll back on error: a failed flush leaves the session unusable until rollback() try: session.add(User(name="Dup", email="ann@example.com")) session.commit() except IntegrityError: session.rollback() raise ``` Prefer a transaction block: it commits when the block ends and rolls back if an exception leaves it. ```python with SessionLocal() as session, session.begin(): session.add(Client(name="Dina", user_id=1)) ``` ### Bulk Operations For thousands of rows, send dictionaries instead of building objects. ORM-enabled `insert()`, `update()` and `delete()` run as single statements (the `where` clause uses the model attributes): ```python from sqlalchemy import delete, insert, update # Bulk INSERT: executemany, no object per row session.execute( insert(Client), [{"user_id": 1, "name": "Eve"}, {"user_id": 1, "name": "Finn"}], ) # INSERT ... RETURNING: new primary keys without a second query (PostgreSQL, SQLite 3.35+, MariaDB) new_ids = session.scalars( insert(Client).returning(Client.id), [{"user_id": 1, "name": "Gus"}, {"user_id": 1, "name": "Hana"}], ).all() # Bulk UPDATE by primary key: every dictionary carries the key session.execute(update(Client), [{"id": new_ids[0], "status": "vip"}]) # UPDATE and DELETE by criteria session.execute(update(Client).where(Client.status == "new").values(status="regular")) session.execute(delete(Appointment).where(Appointment.status == "cancelled")) session.commit() ``` A bulk `DELETE` does not run ORM cascades; delete through the session when `cascade="all, delete-orphan"` must apply. ## Subqueries ```python # Clients with at least one done appointment: a select() goes straight into in_()
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub