# Производительность

> Чтение планов, отпечатки запросов и работа, которую библиотека не делает.

Source: https://ormgo.vercel.app/ru/docs/performance/
Symbols: https://ormgo.vercel.app/api/orm.txt — the generated list of every exported name.

---
## Чего здесь нет

Нет советчика по индексам и рекомендаций по тюнингу. Планирует PostgreSQL, а решение о схеме или сервере требует всей нагрузки, а не одного запроса. Здесь только отчёты; рекомендаций нет.

## Explain

```go
plan, err := q.Explain(ctx)        // EXPLAIN — запрос не выполняется
plan, err := q.ExplainAnalyze(ctx) // EXPLAIN ANALYZE — выполняется, чтобы измерить
```

Имена различаются, потому что опасно различается поведение. `ExplainAnalyze` на `DELETE` удаляет.

```go
plan, _ := q.Explain(ctx)
fmt.Println(plan.TotalCost, plan.PlanRows)
for _, node := range plan.Walk() {
    if node.Type == "Seq Scan" && node.PlanRows > 10000 {
        // крупное последовательное сканирование — сообщено, а не продиагностировано
    }
}
```

## Диагностика без базы

```go
report, err := q.Diagnostics()
```

Только структура: сколько джойнов, есть ли `LIMIT`, коррелирован ли запрос. Соединение не нужно, поэтому это работает в юнит-тесте.

## Отпечатки

Отпечаток опознаёт *форму* запроса независимо от значений:

```go
fp, err := q.Fingerprint()
// v1:9f2c... — одинаков для любого набора аргументов
```

Два запроса, различающиеся только значениями, дают один отпечаток; различающиеся `LIMIT` — разные, потому что маленький лимит и есть то, из-за чего планировщик предпочитает индекс, который иначе бы проигнорировал. Группировка по «это один и тот же запрос» полезнее группировки по «выглядит похоже».

## Отчёт

```go
report, err := q.PerformanceReport(ctx)
```

План, форма и отпечаток вместе, всегда через обычный `EXPLAIN` и никогда через `ANALYZE`.

## Что быстро из-за формы API

**Связи загружаются пакетами.** Число запросов зависит от формы дерева, а не от количества строк. N+1 негде взяться, потому что нет ленивой загрузки.

**Сканирование без рефлексии.** Проекция читается в N типизированных локальных переменных одним `Scan`. Сущности — через сгенерированные метаданные.

**Дескрипторы разделяются, а не копируются.** Они доступны только для чтения, поэтому все запросы по таблице используют один набор.

**`Count` и `Exists` выбирают константу.** Строка на совпадение и никаких значений из неё: запрашивать колонки значило бы заставить сервер доставать и декодировать данные, которые никто не читает.

**Для массовой загрузки есть COPY.** `CopyFrom` и `CopyFromSeq` на порядок быстрее `INSERT`, а потоковая форма никогда не держит всю пачку.

## Измерение

В репозитории есть бенчмарки, которые компилируются и запускаются в CI, поэтому регрессия в работе между вызывающим и pgx проявится падением сборки, а не отсутствием сигнала:

```bash
go test -run '^$' -bench . -benchtime 10x ./...
```

## Разобранные примеры

### Прочитать план, прежде чем верить запросу

```go
q := db.Orders.Query().
    Where(Orders.CustomerID.Eq(id)).
    OrderBy(Orders.PlacedAt.Desc()).
    Limit(20)

plan, err := q.Explain(ctx)
fmt.Println(plan.TotalCost, plan.PlanRows)
```

`Explain` не выполняет оператор. `ExplainAnalyze` выполняет — поэтому у них
разные имена: запуск второго на `DELETE` удаляет.

### Группировка медленных запросов по форме

```go
fp, err := q.Fingerprint()
metrics.Observe(fp.String(), elapsed)
```

Два вызова с разными идентификаторами клиента дают один отпечаток; тот же запрос
с другим `LIMIT` — другой, потому что маленький лимит и заставляет планировщик
выбрать индекс, который иначе он пропустил бы.

### Проверка формы запроса в юнит-тесте

```go
report, err := q.Diagnostics()   // база не нужна
if report.Joins > 3 {
    t.Errorf("this grew a join nobody meant to add")
}
```

### Считать запросы, а не догадываться

```go
db := domain.New(orm.Traced(pool, counter))

_, _ = db.Customers.Query().With(Customers.Orders).All(ctx)
// counter увидел 2 оператора, сколько бы ни было строк
```

Трейсер — честный способ утверждать, что N+1 нет: число наблюдают, а не выводят
рассуждением.
