All jobs

Find Slow Database Queries in a Laravel App

Yes. An agent seeds your Laravel app in a sandbox, logs every query over 50 ms, and reports each with its Eloquent source, EXPLAIN ANALYZE and a fix.

Run this on RushIn a Project that has your repository.

What the agent does

This job measures instead of guessing. The agent clones the repository into its own cloud sandbox, installs the app with its real database engine, fills it with enough data for slow plans to show, visits the main pages, and records every query that takes longer than 50 ms. Each slow query is then traced back to the Eloquent or query builder code that issued it and explained with the database's own EXPLAIN ANALYZE.

In an app like BookStackApp/BookStack it reads composer.json (Laravel 13, PHP 8.3) and docker-compose.yml, which runs mysql:8.4; BookStack supports MySQL and MariaDB, so that is what the agent installs, and it notes that MySQL 8.4 has EXPLAIN ANALYZE as Postgres does. It runs composer install, the migrations, then the seeders the project already ships: DummyContentSeeder, which creates an editor and a viewer user, five small books and one book with 50 chapters and 200 pages, and LargeContentSeeder, which adds a book of 50 chapters with 100 pages each, 5,000 pages in all, and rebuilds the permission rows for it. It may scale those up further with the same factories when the plans still look fast.

Then it serves the app with php artisan serve and requests the main pages signed in as the seeded viewer, whose role is limited and therefore exercises permission filtering: the home page (HomeController::index), /books, the large book, one of its chapters, a page, and /search. It records queries two ways, Laravel's DB::listen with each query's time and a backtrace into app/, and MySQL's slow query log with long_query_time at 0.05, so a query that is slow in the database and one that is only slow through PHP are told apart. The usual suspects in BookStack are the whereHas('jointPermissions') subquery that PermissionApplicator::restrictEntityQuery adds to every list, the correlated book_slug subquery PageQueries::mergeBookSlugForSelect adds per page row, and the full book tree BookContents::getTree loads for chapter navigation.

For each slow query it runs EXPLAIN ANALYZE with the same bindings, tries the index or rewrite it proposes on its copy of the database, measures again and keeps both plans. It writes slow-queries.md and opens no pull request.

Start it with one API call

POST https://api.prix.dev/o/{org}/sessions with this body:

{
  "agent": "claude",
  "input": "Load this app in the sandbox with a realistic amount of seed data, exercise its main pages, log every query over 50 ms, and write slow-queries.md: each query, the code that issues it, its EXPLAIN ANALYZE, and the index or rewrite with the time after. Do not open a pull request.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
  1. Get an API key. In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
  2. Send the request. POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
  3. Read what came back. Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

What comes back

What comes back is slow-queries.md, attached to the session as an artifact. It starts with the setup: database engine and version, which seeders ran, the row counts per table, the user the pages were loaded as, and the pages visited. Then each slow query gets a section with the SQL and its bindings, the PHP file and line that issued it, how many times one page load ran it, the EXPLAIN ANALYZE output, the proposed index or rewrite (as a migration snippet or a changed query), and the plan and time after the change on the same data.

Every timing in the report comes from the sandbox on seeded data and is labelled that way; the point is the before and after on the same data, not a prediction of production numbers. The session's transcript keeps every command, seeder run and query log, so any number can be traced to where it was measured.

A reviewer checks each proposed index against the write load on that table and against production cardinality, which seeded data only approximates, and picks which changes to make. A follow-up to the same session can ask the agent to turn the chosen ones into a migration and a pull request, since its sandbox still has the seeded database.

Run it on every release that touches models or queries

Run it before a release goes out: a step in your release pipeline can send this request for the release branch, using .../tree/<branch> in resources, whenever the diff touches models, query classes or migrations. A regression in a query plan then shows up as a report before deploy rather than as an alert after.

Or run it on a schedule, once a week against the default branch. With a webhook URL on the API key, Rush POSTs run.completed when the report is ready, and your pipeline can attach it to the release.

The use case this job follows, with its automation and trigger: Investigate an incident.

Why run it on Rush?

Finding slow queries properly needs a database with enough data, a way to log what each page sends, and room to add indexes and drop them again. A Rush sandbox gives the agent all three: it installs the database, seeds it, measures and experiments on its own copy, and nothing it does reaches your local database, staging or production.

The report is evidence, not advice: each query has its source line, its plan before and after, and the measured times, with the full record in the session's transcript. Several apps, or several branches of one app, can be measured at once in separate runs, up to the key's concurrent run limit.

Frequently asked questions

My app uses Postgres. Does this work the same way?

Yes. The agent installs the engine your app uses, logs slow statements with log_min_duration_statement or pg_stat_statements on Postgres, and explains them with EXPLAIN (ANALYZE, BUFFERS). BookStack is the example here, and it runs on MySQL, so its report uses MySQL's plans.

What if my app has no seeders?

The agent writes seed data in the sandbox from your model factories, or from the schema when there are none, and says in the report how many rows of what it created. Say in the prompt if some tables need a realistic shape, such as many rows per tenant.

Will it touch my production database?

No. It never connects to your databases. Everything runs against the database it installs in its own sandbox.

Does it catch N+1 queries as well as slow ones?

Yes. Because it records every query per page load with a backtrace, it reports a query repeated once per row even when each run is fast, and suggests the eager load that removes it.