# Queries

Every statement a recorded request ran, with the time it took and where in the request it ran.

![The Queries page in the panel: statements ranked by total time, with their calls, p95 and the ones repeated in one request](/images/docs/queries.png)

## What turns it on

Queries are collected inside a [request](/docs/telemetry/requests), so `requests.track_requests` is the switch, and the application has to accept **Requests**. There is nothing else to enable.

The other side of that: a query run by a queue worker, an Artisan command or a scheduled task is not collected, because none of those are requests.

## What is sent

The statement as Laravel hands it over, which is the SQL with placeholders in it. Bindings are not sent, so the values your users typed never leave the server.

Two shapes are collapsed before sending, because both blow up the number of distinct statements for no benefit:

```sql
select * from users where id in (?, ?, ?)   ->   select * from users where id in (...?)
insert into logs values (?, ?), (?, ?)      ->   insert into logs values (...?)
```

What arrives is the normalised statement, its hash, the connection name, the duration in milliseconds, and the offset from the start of the request.

## What the panel does with it

Grouping is by that hash, so one statement is one row however many times it ran, across every request that ran it. From there:

* The slowest statements, by total time rather than by one unlucky run.
* The N+1 shapes: one statement repeated inside a single request, which is the signature of a missing eager load.
* The endpoints a statement runs on, which is usually the answer to where to fix it.

## Limits

`requests.max_queries` keeps a hundred statements per request. The counter is not capped, so a request that ran four thousand still reports four thousand, and it is the count that tells you to look.

## Query plans

A slow statement is slow for a reason, and the commonest one is that no index lets the database skip rows, so it reads the whole table on every run. Laravel cannot tell you that. The database can, if somebody asks it for the plan, and the client will ask on your behalf once you let it:

```env .env
LB_QUERY_EXPLAIN=true
```

Off by default, because it runs a statement of LaraBug's own against your database. What it runs, and when:

* **A plain `EXPLAIN`, never `EXPLAIN ANALYZE`.** The planner says what it would do without the query being executed.
* **Only on a slow `SELECT`.** Nothing that writes is ever explained.
* **At most once per statement per hour.** A statement that stays slow is explained again an hour later, so its plan keeps up with your schema.
* **On MySQL, MariaDB and Postgres.** Other connections are left alone.

No bindings or values are sent, and neither is the raw plan. What arrives is a summary of it: whether it reads the whole table, which table, how many rows the planner expects, and on MySQL and MariaDB the index it chose and the ones it considered. Postgres plans do not name those, so they arrive empty.

A plan rides on the request it was taken in, so it counts towards nothing on its own.

### Where it shows up

A statement's page shows its newest plan: full scan or not, the table, the estimated rows, the index used or the indexes that exist but were passed over, and when it was explained. A statement whose newest plan is a full scan is marked on the queries list.

### The missing index detector

With plans arriving, LaraBug opens a **Missing index** issue for a statement when all three of these hold:

* its newest plan in the last six hours is a full table scan,
* the planner expects that scan to read at least 10,000 rows,
* and the statement matters: it ran at least 100 times in those six hours, or spent at least a minute of database time.

The issue carries the statement, the table, the estimated rows, and the possible keys when there are any. That last one decides the fix. An index that exists but is not used wants the statement or the index looked at, often a function wrapped around the column or a type mismatch. No index at all wants one created.

It resolves itself when a newer plan shows the statement no longer reads the whole table. More often, a statement that got its index stops being slow, stops being explained, and the issue resolves a few hours after its last plan ages out of the window. The thresholds live in `config/detection.php` under `missing_index` and are set high on purpose until there are plans from real applications to tune them against.