
Тормоза в базе бьют по бизнесу: пользователи дольше ждут, конверсия падает, инфраструктура дорожает. Но «оптимизировать всё» — плохая стратегия. Нужна прицельная работа по запросам, которые реально влияют на опыт пользователей и счета за серверы.
Оптимизировать стоит, если:
Измерения нужны до и после изменений.
-- Включается в postgresql.conf или через ALTER SYSTEM (после — reload/restart при необходимости):
ALTER SYSTEM SET log_min_duration_statement = '200ms';
ALTER SYSTEM SET log_checkpoints = on;
ALTER SYSTEM SET log_lock_waits = on;
ALTER SYSTEM SET log_temp_files = 0; -- логируем внешние сортировки
-- Расширение для статистики часто/долго исполняемых запросов
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements'; -- нужен перезапуск
ALTER SYSTEM SET pg_stat_statements.max = 20000;
ALTER SYSTEM SET pg_stat_statements.track = all;
После перезапуска проверяем топ «тяжёлых» запросов:
SELECT
query,
calls,
round(total_exec_time/1000, 2) AS total_sec,
round(mean_exec_time, 2) AS mean_ms,
round((total_exec_time/calls), 2) AS avg_ms,
rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;
CREATE EXTENSION IF NOT EXISTS auto_explain;
ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements,auto_explain';
ALTER SYSTEM SET auto_explain.log_min_duration = '200ms';
ALTER SYSTEM SET auto_explain.log_analyze = on;
ALTER SYSTEM SET auto_explain.log_buffers = on;
SELECT pg_reload_conf();
Минимальный набор для диагностики:
Команда для чтения конкретного запроса:
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT ...
Смотрите на:
Универсальный тип по умолчанию. Подходит для равенств, диапазонов, сортировок. Допускает составные, покрывающие (INCLUDE) и частичные индексы.
GIN эффективен для множеств/документов. С операторным классом gin_trgm_ops ускоряет LIKE/ILIKE с подстановками. Для полнотекстового — GIN по tsvector.
Хорош для пространственных данных, интервалов, «похоже на» (fuzzy), но требует аккуратной настройки.
Очень компактный индекс, который хранит «сводки» по блокам. Идеален, когда значения коррелируют с физическим порядком (логи, метрики по времени).
Запрос продукта: показать последние оплаченные заказы за 7 дней, отсортировать по дате.
-- Пример запроса
SELECT id, user_id, total, created_at
FROM orders
WHERE status = 'paid' AND created_at >= now() - interval '7 days'
ORDER BY created_at DESC
LIMIT 50;
Симптомы в плане: Seq Scan по orders + Sort с проливом на диск.
Решение: частичный покрывающий индекс с направленным порядком по дате, чтобы избежать сортировки и лишнего чтения.
-- Индекс создаётся без простоя записи
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_orders_paid_created_at_desc
ON orders (created_at DESC)
INCLUDE (id, user_id, total)
WHERE status = 'paid';
Почему так:
Проверка плана:
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, user_id, total, created_at
FROM orders
WHERE status = 'paid' AND created_at >= now() - interval '7 days'
ORDER BY created_at DESC
LIMIT 50;
Ожидаем: Index Only Scan Backward по idx_orders_paid_created_at_desc, без Sort.
Запрос: искать товары по названию «iphone» или «чехол».
Наивно:
SELECT id, name
FROM products
WHERE name ILIKE '%iphone%'
LIMIT 20;
Это заставит читать всю таблицу. Фикс с триграммами:
-- Разово
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- Индекс для подстрочного поиска
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_products_name_trgm
ON products USING GIN (name gin_trgm_ops);
-- Теперь запрос использует GIN и работает за миллисекунды
SELECT id, name
FROM products
WHERE name ILIKE '%iphone%'
ORDER BY similarity(name, 'iphone') DESC
LIMIT 20;
Если нужен полнотекстовый поиск с учётом формы слова (русский язык):
-- Полнотекст
ALTER TABLE products ADD COLUMN IF NOT EXISTS name_tsv tsvector;
-- Обновляем tsvector и делаем триггер для актуальности
UPDATE products
SET name_tsv = to_tsvector('russian', coalesce(name,''));
CREATE FUNCTION products_tsv_update() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
NEW.name_tsv := to_tsvector('russian', coalesce(NEW.name,''));
RETURN NEW;
END;
$$;
CREATE TRIGGER products_tsv_update_trg
BEFORE INSERT OR UPDATE ON products
FOR EACH ROW EXECUTE FUNCTION products_tsv_update();
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_products_name_tsv
ON products USING GIN (name_tsv);
-- Запрос
SELECT id, name
FROM products
WHERE name_tsv @@ plainto_tsquery('russian', 'чехол для iphone')
ORDER BY ts_rank(name_tsv, plainto_tsquery('russian', 'чехол для iphone')) DESC
LIMIT 20;
Таблица логов растёт километр в день. Сканы «за месяц» мучительны.
Шаги:
-- BRIN-индекс
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_logs_created_at_brin
ON logs USING BRIN (created_at) WITH (pages_per_range = 128);
-- Партиционирование по диапазону дат
CREATE TABLE IF NOT EXISTS logs_partitioned (
id bigserial PRIMARY KEY,
created_at timestamptz NOT NULL,
user_id bigint,
payload jsonb
) PARTITION BY RANGE (created_at);
-- Ежемесячные партиции
CREATE TABLE IF NOT EXISTS logs_2026_04
PARTITION OF logs_partitioned FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_logs_2026_04_brin
ON logs_2026_04 USING BRIN (created_at) WITH (pages_per_range = 128);
-- Пример запроса с отсечением партиций и использованием BRIN
EXPLAIN (ANALYZE, BUFFERS)
SELECT date_trunc('day', created_at) AS d, count(*)
FROM logs_partitioned
WHERE created_at >= now() - interval '30 days'
GROUP BY 1
ORDER BY 1;
На больших данных такой подход сокращает чтение на порядки.
-- Точечно для одной сессии/задачи, не глобально
SET work_mem = '128MB';
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_example ON t (col1, col2);
-- Удаление тоже без «стопа мира»
DROP INDEX CONCURRENTLY IF EXISTS idx_example;
SELECT * FROM pg_stat_progress_create_index;
SET LOCAL statement_timeout = '5min';
REINDEX INDEX CONCURRENTLY idx_example;
Подсказка: умножьте сэкономленные миллисекунды на количество запросов в пике — получите выигрыш в окнах продаж и маркетинговых кампаниях.
Главное: оптимизация — это не магия, а понятный цикл «измерь → пойми план → добавь/исправь индекс → проверь → внедри». Пара правильных индексов и переписанный WHERE часто дают больший эффект, чем новый сервер. И это прямые деньги: меньше инфраструктуры, выше конверсия, спокойнее релизы.