О нас Руководства Проекты Контакты
Админка
пожалуйста подождите

Введение

Оптимизация производительности PostgreSQL критически важна для приложений, работающих в масштабе. Медленные запросы, отсутствующие индексы и некорректная конфигурация могут парализовать производительность приложения. Старшие инженеры должны понимать планирование запросов, стратегии индексирования и настройку конфигурации, чтобы поддерживать отзывчивость систем.

Анализ производительности запросов

EXPLAIN ANALYZE

EXPLAIN ANALYZE SELECT * FROM orders
WHERE user_id = 123
AND created_at > '2024-01-01'
ORDER BY created_at DESC
LIMIT 10;

Интерпретация вывода:

  • Seq Scan: Полное сканирование таблицы (часто плохо для больших таблиц)
  • Index Scan: Использование индекса (хорошо)
  • Index Only Scan: Лучший вариант — данные берутся только из индекса
  • Bitmap Heap Scan: Комбинирует несколько условий по индексам
  • actual time: Реальное время выполнения
  • rows: Фактически обработанные строки

Выявление медленных запросов

-- Включите логирование медленных запросов в postgresql.conf
-- log_min_duration_statement = 1000 -- Логировать запросы > 1 секунды
-- Найти выполняющиеся в данный момент запросы
SELECT pid, now() - query_start AS duration, state, query
FROM pg_stat_activity
WHERE state <> 'idle'
AND query NOT LIKE '% FROM pg_stat_activity %'
ORDER BY duration DESC;
-- Найти длительно выполняющиеся запросы
SELECT datname, pid, state, query, age(clock_timestamp(), query_start) AS age
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY age DESC;

Завершение длительно выполняющихся запросов

-- Отменить запрос (мягко)
SELECT pg_cancel_backend(pid);
-- Завершить подключение (принудительно)
SELECT pg_terminate_backend(pid);

Стратегии индексирования

Базовые индексы

-- Индекс по одному столбцу
CREATE INDEX idx_orders_user_id ON orders(user_id);
-- Составной индекс (порядок столбцов важен!)
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);
-- Уникальный индекс
CREATE UNIQUE INDEX idx_users_email ON users(email);

Частичные индексы

Индексируйте только строки, соответствующие условию:

-- Индексировать только активных пользователей
CREATE INDEX idx_active_users ON users(email)
WHERE status = 'active';
-- Индексировать только недавние заказы
CREATE INDEX idx_recent_orders ON orders(user_id, created_at)
WHERE created_at > '2024-01-01';

Индексы по выражениям

-- Индекс по email в нижнем регистре
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
-- Индекс по извлечённому году
CREATE INDEX idx_orders_year ON orders(EXTRACT(YEAR FROM created_at));

Покрывающие индексы (INCLUDE)

Добавляйте неиндексируемые столбцы, чтобы включить index-only scans:

CREATE INDEX idx_orders_user ON orders(user_id)
INCLUDE (total, status, created_at);
-- Теперь запрос может быть полностью удовлетворён за счёт индекса
SELECT total, status, created_at FROM orders WHERE user_id = 123;

Когда использовать разные типы индексов

Тип индексаСценарий использования
B-treeПо умолчанию, запросы на равенство и диапазоны
HashТолько равенство (редко лучше, чем B-tree)
GINПолнотекстовый поиск, JSONB, массивы
GiSTГеометрические данные, полнотекстовый поиск
BRINБольшие таблицы с естественным порядком

Индексирование JSONB

-- Индекс GIN для запросов на включение (containment) JSONB
CREATE INDEX idx_products_metadata ON products USING gin(metadata);
-- Запрос: WHERE metadata @> '{"category": "electronics"}'
-- Индексировать конкретный путь JSONB
CREATE INDEX idx_products_category ON products((metadata->>'category'));

Оптимизация запросов

Избегайте SELECT *

-- Плохо
SELECT * FROM orders WHERE user_id = 123;
-- Хорошо
SELECT id, total, status, created_at FROM orders WHERE user_id = 123;

Используйте LIMIT вместе с ORDER BY

-- Плохо: сортирует все строки
SELECT * FROM orders ORDER BY created_at DESC;
-- Хорошо: останавливается раньше благодаря индексу
SELECT * FROM orders ORDER BY created_at DESC LIMIT 10;

Эффективная пагинация

-- Плохо: OFFSET сканирует и отбрасывает строки
SELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 10000;
-- Хорошо: keyset pagination
SELECT * FROM orders
WHERE id < 12345 -- Последний ID с предыдущей страницы
ORDER BY id DESC
LIMIT 20;

Пакетные операции

-- Плохо: много мелких транзакций
INSERT INTO logs (message) VALUES ('log 1');
INSERT INTO logs (message) VALUES ('log 2');
-- ... ещё 1000
-- Хорошо: одна транзакция с несколькими строками
INSERT INTO logs (message) VALUES
('log 1'), ('log 2'), ('log 3'), ...;
-- Или используйте COPY для массовых загрузок
COPY logs(message) FROM '/path/to/data.csv' CSV;

Избегайте N+1 в коде приложения

-- Вместо того чтобы запрашивать users, а затем orders отдельно,
-- используйте JOIN или LATERAL JOIN
SELECT u.*, o.*
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.id IN (1, 2, 3);

Оптимизация PostGIS

Запросы к геометрии

-- Сохранить точку
INSERT INTO locations (name, coordinates)
VALUES ('Office', ST_SetSRID(ST_MakePoint(-73.935242, 40.730610), 4326));
-- Запрос как GeoJSON
SELECT ST_AsGeoJSON(ST_Transform(coordinates, 3857))
FROM locations;
-- Найти точки в пределах радиуса
SELECT * FROM locations
WHERE ST_DWithin(
coordinates::geography,
ST_MakePoint(-73.935, 40.730)::geography,
1000 -- Метры
);
-- Пространственный индекс (обязательно!)
CREATE INDEX idx_locations_geo ON locations USING gist(coordinates);

Настройка конфигурации

Настройки памяти

# postgresql.conf
# Разделяемая память для кэширования (25% RAM)
shared_buffers = 4GB
# Память на операцию (sort, hash)
work_mem = 256MB
# Память для операций обслуживания
maintenance_work_mem = 1GB
# Оценка кэша планировщиком запросов
effective_cache_size = 12GB

Настройки подключений

# Максимальное число подключений
max_connections = 200
# Пулинг подключений (используйте PgBouncer для лучшего масштабирования)

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

# Настройки WAL для нагрузок с преобладанием записи
wal_buffers = 64MB
checkpoint_completion_target = 0.9
max_wal_size = 4GB

Обслуживание таблиц

VACUUM и ANALYZE

-- Обновить статистику (помогает планировщику запросов)
ANALYZE orders;
-- Освободить место от удалённых строк
VACUUM orders;
-- Полная очистка (блокирует таблицу)
VACUUM FULL orders;
-- Настройки автоматического vacuum
-- autovacuum = on (по умолчанию)

REINDEX

-- Перестроить раздувшийся индекс
REINDEX INDEX idx_orders_user_id;
-- Перестроить все индексы таблицы
REINDEX TABLE orders;
-- CONCURRENTLY (без блокировки, PostgreSQL 12+)
REINDEX INDEX CONCURRENTLY idx_orders_user_id;

Статистика таблиц

-- Проверить размер таблицы
SELECT pg_size_pretty(pg_total_relation_size('orders'));
-- Проверить размер индекса
SELECT pg_size_pretty(pg_relation_size('idx_orders_user_id'));
-- Оценка раздувания таблицы
SELECT schemaname, tablename,
pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename)) as size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname || '.' || tablename) DESC;

Пулинг подключений

Конфигурация PgBouncer

# pgbouncer.ini
[databases]
mydb = host=localhost port=5432 dbname=mydb
[pgbouncer]
listen_port = 6432
listen_addr = *
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20

Мониторинг

Ключевые запросы

-- Статистика использования индексов
SELECT schemaname, tablename, indexname, idx_scan, idx_tup_read
FROM pg_stat_user_indexes
ORDER BY idx_scan ASC;
-- Неиспользуемые индексы (кандидаты на удаление)
SELECT schemaname, tablename, indexname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0 AND schemaname = 'public';
-- Паттерны доступа к таблицам
SELECT schemaname, relname, seq_scan, idx_scan,
n_tup_ins, n_tup_upd, n_tup_del
FROM pg_stat_user_tables
ORDER BY seq_scan DESC;
-- Коэффициент попаданий в кэш (должен быть > 99%)
SELECT
sum(heap_blks_read) as heap_read,
sum(heap_blks_hit) as heap_hit,
sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) as ratio
FROM pg_statio_user_tables;

Манипуляции с JSONB

Переименование свойств полей JSON

-- Пример: переименовать 'wrongname' в 'name' во вложенном JSONB
UPDATE products AS p
SET metadata = (
jsonb_set(
p.metadata::jsonb #- '{objectfield,wrongname}',
'{objectfield,name}',
p.metadata::jsonb #> '{objectfield,wrongname}'
)
)::json
WHERE p.metadata::jsonb ? 'objectfield'
AND p.metadata::jsonb #> '{objectfield}' ? 'wrongname';

Запросы к массивам JSONB

-- Найти элементы, где массив содержит конкретное значение
SELECT * FROM products
WHERE metadata->'tags' ? 'featured';
-- Запросить элементы вложенного массива
SELECT * FROM products
WHERE metadata @> '{"categories": [{"name": "electronics"}]}';
-- Агрегировать элементы массива JSONB
SELECT id, jsonb_array_elements(metadata->'variants') as variant
FROM products;

Анонимизация данных

Для сред разработки/staging анонимизируйте production-данные:

-- Анонимизировать пользовательские данные для использования вне production
UPDATE users SET
first_name = SUBSTR(MD5(RANDOM()::text), 1, 2 + FLOOR(3*RANDOM())::int),
last_name = SUBSTR(MD5(RANDOM()::text), 1, 4 + FLOOR(4*RANDOM())::int),
address = CONCAT(
SUBSTR(MD5(RANDOM()::text), 1, 5 + FLOOR(5*RANDOM())::int), ' ',
SUBSTR(MD5(RANDOM()::text), 1, 2 + FLOOR(2*RANDOM())::int), ', ',
FLOOR(100*RANDOM())::int, ', ',
SUBSTR(MD5(RANDOM()::text), 1, 2 + FLOOR(3*RANDOM())::int)
),
email = CONCAT(
SUBSTR(MD5(RANDOM()::text), 1, 5 + FLOOR(5*RANDOM())::int), '@',
SUBSTR(MD5(RANDOM()::text), 1, 2 + FLOOR(3*RANDOM())::int), '.test'
),
phone = CONCAT('+1', FLOOR(1000000000 + 8999999999*RANDOM())::bigint);

Поиск больших таблиц

Определите таблицы, потребляющие больше всего места:

SELECT
schemaname AS schema,
tablename AS table,
pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename)) AS total_size,
pg_size_pretty(pg_relation_size(schemaname || '.' || tablename)) AS table_size,
pg_size_pretty(pg_indexes_size(schemaname || '.' || tablename)) AS indexes_size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname || '.' || tablename) DESC
LIMIT 20;

Управление внешними ключами

Временное отключение для массовых операций

-- Отключить проверки внешних ключей (используйте с осторожностью!)
SET session_replication_role = 'replica';
-- Выполнить массовые операции
TRUNCATE TABLE orders CASCADE;
-- Или сложные миграции данных...
-- Снова включить проверки внешних ключей
SET session_replication_role = 'origin';

Чек-лист лучших практик

  • [ ] Индексируйте столбцы, используемые в WHERE, JOIN, ORDER BY
  • [ ] Используйте EXPLAIN ANALYZE для медленных запросов
  • [ ] Реализуйте пулинг подключений
  • [ ] Корректно настройте shared_buffers
  • [ ] Запускайте ANALYZE после массовых изменений данных
  • [ ] Отслеживайте и удаляйте неиспользуемые индексы
  • [ ] Используйте keyset pagination для больших наборов данных
  • [ ] Поддерживайте статистику в актуальном состоянии
  • [ ] Используйте индексы GIN для запросов к JSONB
  • [ ] Анонимизируйте данные в не-production средах

Заключение

Оптимизация производительности PostgreSQL требует понимания выполнения запросов, корректного индексирования и настройки конфигурации. Начните с EXPLAIN ANALYZE, чтобы выявить узкие места, добавьте подходящие индексы и настройте конфигурацию в зависимости от нагрузки. Регулярный мониторинг гарантирует, что производительность не будет деградировать со временем.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
Зетка Интерактив