Производительность
Чтение планов, отпечатки запросов и работа, которую библиотека не делает.
Чего здесь нет#
Нет советчика по индексам и рекомендаций по тюнингу. Планирует PostgreSQL, а решение о схеме или сервере требует всей нагрузки, а не одного запроса. Здесь только отчёты; рекомендаций нет.
Explain#
plan, err := q.Explain(ctx) // EXPLAIN — запрос не выполняется
plan, err := q.ExplainAnalyze(ctx) // EXPLAIN ANALYZE — выполняется, чтобы измеритьИмена различаются, потому что опасно различается поведение. ExplainAnalyze на DELETE удаляет.
plan, _ := q.Explain(ctx)
fmt.Println(plan.TotalCost, plan.PlanRows)
for _, node := range plan.Walk() {
if node.Type == "Seq Scan" && node.PlanRows > 10000 {
// крупное последовательное сканирование — сообщено, а не продиагностировано
}
}Диагностика без базы#
report, err := q.Diagnostics()Только структура: сколько джойнов, есть ли LIMIT, коррелирован ли запрос. Соединение не нужно, поэтому это работает в юнит-тесте.
Отпечатки#
Отпечаток опознаёт форму запроса независимо от значений:
fp, err := q.Fingerprint()
// v1:9f2c... — одинаков для любого набора аргументовДва запроса, различающиеся только значениями, дают один отпечаток; различающиеся LIMIT — разные, потому что маленький лимит и есть то, из-за чего планировщик предпочитает индекс, который иначе бы проигнорировал. Группировка по «это один и тот же запрос» полезнее группировки по «выглядит похоже».
Отчёт#
report, err := q.PerformanceReport(ctx)План, форма и отпечаток вместе, всегда через обычный EXPLAIN и никогда через ANALYZE.
Что быстро из-за формы API#
Связи загружаются пакетами. Число запросов зависит от формы дерева, а не от количества строк. N+1 негде взяться, потому что нет ленивой загрузки.
Сканирование без рефлексии. Проекция читается в N типизированных локальных переменных одним Scan. Сущности — через сгенерированные метаданные.
Дескрипторы разделяются, а не копируются. Они доступны только для чтения, поэтому все запросы по таблице используют один набор.
Count и Exists выбирают константу. Строка на совпадение и никаких значений из неё: запрашивать колонки значило бы заставить сервер доставать и декодировать данные, которые никто не читает.
Для массовой загрузки есть COPY. CopyFrom и CopyFromSeq на порядок быстрее INSERT, а потоковая форма никогда не держит всю пачку.
Измерение#
В репозитории есть бенчмарки, которые компилируются и запускаются в CI, поэтому регрессия в работе между вызывающим и pgx проявится падением сборки, а не отсутствием сигнала:
go test -run '^$' -bench . -benchtime 10x ./...Разобранные примеры#
Прочитать план, прежде чем верить запросу#
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 удаляет.
Группировка медленных запросов по форме#
fp, err := q.Fingerprint()
metrics.Observe(fp.String(), elapsed)Два вызова с разными идентификаторами клиента дают один отпечаток; тот же запрос
с другим LIMIT — другой, потому что маленький лимит и заставляет планировщик
выбрать индекс, который иначе он пропустил бы.
Проверка формы запроса в юнит-тесте#
report, err := q.Diagnostics() // база не нужна
if report.Joins > 3 {
t.Errorf("this grew a join nobody meant to add")
}Считать запросы, а не догадываться#
db := domain.New(orm.Traced(pool, counter))
_, _ = db.Customers.Query().With(Customers.Orders).All(ctx)
// counter увидел 2 оператора, сколько бы ни было строкТрейсер — честный способ утверждать, что N+1 нет: число наблюдают, а не выводят рассуждением.