pytest-queryguard#

A pytest plugin that fails tests with N+1 queries and enforces query budgets for SQLAlchemy 2, in sync, async and FastAPI code. On PostgreSQL 16 and newer, SQLite, MySQL and MariaDB it can also fail tests whose queries have no index to use.

ORM code that looks correct can send one query per row instead of one query per page. The tests pass, because a test database holds a handful of rows. In production the same endpoint sends hundreds of queries. pytest-queryguard watches the SQL each test sends, and when it sees the pattern it fails the test and points to the line that caused it.

queryguard: N+1 detected

  Order.customer was lazy-loaded 50 times
  first at app/orders.py:10 in list_orders
  kind: relationship-load  label: Order.customer
  sql: SELECT customer.id, customer.name FROM customer WHERE customer.id = ?
  fix: load it with the parent query, e.g.
       select(Order).options(joinedload(Order.customer))
pip install "pytest-queryguard>=0.4.0rc3"

It is on by default: there is nothing to import or configure. These pages describe 0.4.0rc3; the stable 0.3.0 (pip install pytest-queryguard) lacks most of the adoption features, see Getting started.

Where to go#

The path most projects take, one page each:

  1. Getting started: install, a first failing test and its fix, async code and FastAPI.
  2. A test failed: now what: read the report, check the finding, fix it or leave it out on purpose.
  3. CI and GitHub Action: findings as pull request annotations.
  4. Baseline: adopt it on an existing project; only new findings fail.
  5. Index check and index advisor: queries no index can serve, on PostgreSQL, SQLite and MySQL and MariaDB.

Reference: How detection works, Configuration, Budgets and timing, Ignoring findings. Before you adopt it: FAQ, Limitations, Comparison with other tools, Stability and versioning.

The source and the issue tracker are on GitHub.

Edit this page on GitHub