К содержимому

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

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

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

Нет советчика по индексам и рекомендаций по тюнингу. Планирует 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 нет: число наблюдают, а не выводят рассуждением.