Диагностика проблем, неиспользуемые индексы, типичные ошибки и best practices.
Правильная диагностика проблем с индексами экономит часы простоя. Этот раздел научит находить и устранять проблемы производительности.
SELECT
schemaname,
relname as table_name,
indexrelname as index_name,
idx_scan as index_scans,
idx_tup_read as tuples_read,
idx_tup_fetch as tuples_fetched,
pg_size_pretty(pg_relation_size(indexrelid)) as size
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
AND idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;Интерпретация:
idx_scan = 0 — индекс ни разу не использовалсяsize — сколько места занимает зряSELECT
indexrelname,
idx_scan,
idx_tup_read,
idx_tup_fetch,
CASE
WHEN idx_scan > 0 THEN idx_tup_read / idx_scan
ELSE 0
END as avg_tuples_per_scan
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
ORDER BY idx_scan ASC;Интерпретация:
-- Удаление без блокировок (production)
DROP INDEX CONCURRENTLY IF EXISTS idx_unused;
-- Удаление нескольких индексов
DO $$
DECLARE
r record;
BEGIN
FOR r IN (
SELECT indexrelname
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
AND idx_scan = 0
AND indexrelname NOT LIKE '%_pkey' -- Не трогать PRIMARY KEY
) LOOP
EXECUTE 'DROP INDEX CONCURRENTLY IF EXISTS ' || r.indexrelname;
RAISE NOTICE 'Dropped index: %', r.indexrelname;
END LOOP;
END$$;SELECT
indexrelname,
pg_size_pretty(pg_relation_size(indexrelid)) as size,
idx_scan,
pg_relation_size(indexrelid) / NULLIF(idx_scan, 0) as bytes_per_scan
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
ORDER BY pg_relation_size(indexrelid) DESC;-- Установка расширения (требует суперпользователя)
CREATE EXTENSION IF NOT EXISTS pgstattuple;
-- Анализ индекса
SELECT * FROM pgstatindex('idx_users_email');Результат:
| Столбец | Описание |
|---|---|
leaf_fragmentation | Процент фрагментации листовых узлов |
avg_leaf_density | Средняя плотность заполнения (должна быть ~90%) |
leaf_count | Количество листовых страниц |
WITH bloat_info AS (
SELECT
indexrelname,
pg_relation_size(indexrelid) as index_size,
(pgstatindex(indexrelname::text)).leaf_fragmentation as frag,
(pgstatindex(indexrelname::text)).avg_leaf_density as density
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
)
SELECT
indexrelname,
pg_size_pretty(index_size) as size,
round(frag::numeric, 2) as fragmentation_percent,
round(density::numeric, 2) as avg_density_percent,
CASE
WHEN frag > 50 THEN 'CRITICAL'
WHEN frag > 20 THEN 'WARNING'
ELSE 'OK'
END as status
FROM bloat_info
ORDER BY frag DESC;-- Перестроение индекса (блокирует таблицу)
REINDEX INDEX idx_users_email;
-- Перестроение без блокировок (PostgreSQL 12+)
REINDEX INDEX CONCURRENTLY idx_users_email;
-- Альтернатива: pg_repack (требует расширения)
-- pg_repack --index=idx_users_email --dbname=mydb-- Базовый анализ
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT * FROM orders
WHERE user_id = 123 AND status = 'pending';Ключевые метрики:
Actual Time — реальное время выполненияRows — фактическое количество строкBuffers — чтения из буфера (hit = память, read = диск)Planning Time — время на планирование запросаExecution Time — время выполнения-- Требуется расширение pg_stat_statements
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Топ-10 медленных запросов
SELECT
query,
calls,
round(total_exec_time::numeric / 1000, 2) as total_time_sec,
round(mean_exec_time::numeric, 2) as avg_time_ms,
rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
-- Топ-10 самых частых запросов
SELECT
query,
calls,
round(mean_exec_time::numeric, 2) as avg_time_ms
FROM pg_stat_statements
ORDER BY calls DESC
LIMIT 10;-- EXPLAIN в формате JSON для визуализаторов
EXPLAIN (FORMAT JSON, ANALYZE)
SELECT * FROM orders WHERE user_id = 123;
-- Используйте https://explain.dalibo.com/ или https://explain.depesz.com/-- ❌ Плохо: индексы на каждой колонке
CREATE INDEX idx_users_name ON users(name);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_phone ON users(phone);
CREATE INDEX idx_users_created ON users(created_at);
-- ... ещё 10 индексов
-- ✅ Хорошо: индексы для частых запросов
CREATE INDEX idx_users_email ON users(email); -- Частый поиск по email
CREATE INDEX idx_users_created ON users(created_at DESC); -- Сортировка-- ❌ Плохо: дублирование функциональности
CREATE INDEX idx_user_id ON orders(user_id);
CREATE INDEX idx_user_status ON orders(user_id, status);
-- Первый индекс избыточен!
-- ✅ Хорошо: только составной
CREATE INDEX idx_user_status ON orders(user_id, status);
-- Покрывает и запросы только по user_id-- ❌ Плохо: индекс не используется
SELECT * FROM users WHERE LOWER(email) = 'test@example.com';
-- ✅ Хорошо: индекс по выражению
CREATE INDEX idx_lower_email ON users(LOWER(email));-- ❌ Плохо: низкая селективность первой
CREATE INDEX idx_status_user ON orders(status, user_id);
-- status имеет 4 значения, user_id — миллионы
-- ✅ Хорошо: высокая селективность первой
CREATE INDEX idx_user_status ON orders(user_id, status);-- ❌ Плохо: NULL не индексируются в некоторых типах индексов
SELECT * FROM users WHERE deleted_at IS NULL;
-- ✅ Хорошо: частичный индекс для NULL
CREATE INDEX idx_not_deleted ON users(created_at)
WHERE deleted_at IS NULL;-- ❌ Плохо: выборка всех строк
SELECT * FROM logs ORDER BY created_at DESC;
-- ✅ Хорошо: ограничение результата
SELECT * FROM logs ORDER BY created_at DESC LIMIT 100;# ❌ Плохо: N+1 запросов
users = db.query("SELECT * FROM users")
for user in users:
orders = db.query("SELECT * FROM orders WHERE user_id = ?", user.id)
# ✅ Хорошо: один запрос с JOIN
results = db.query("""
SELECT u.*, o.*
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
""")SELECT
indexrelname,
idx_scan as scans,
idx_tup_read as read,
idx_tup_fetch as fetched,
CASE
WHEN idx_scan > 0 THEN round(
(idx_tup_fetch::numeric / idx_tup_read) * 100, 2
)
ELSE 0
END as hit_ratio_percent
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
ORDER BY idx_scan DESC;Интерпретация:
hit_ratio близок к 100% — индекс эффективенhit_ratio низкий — много «холостых» чтенийSELECT
indexrelname,
pg_size_pretty(pg_relation_size(indexrelid)) as size,
idx_scan,
idx_tup_fetch,
CASE
WHEN idx_scan = 0 THEN 'UNUSED'
WHEN idx_scan < 100 THEN 'LOW'
WHEN idx_scan < 1000 THEN 'MEDIUM'
ELSE 'HIGH'
END as usage_level
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
ORDER BY pg_relation_size(indexrelid) DESC;-- Проверка последнего VACUUM/ANALYZE
SELECT
relname,
last_vacuum,
last_autovacuum,
last_analyze,
last_autoanalyze,
n_dead_tup,
n_live_tup,
round((n_dead_tup::numeric / NULLIF(n_live_tup, 0)) * 100, 2) as dead_ratio_percent
FROM pg_stat_user_tables
WHERE schemaname = 'public'
ORDER BY n_dead_tup DESC;Рекомендация: dead_ratio > 20% — требуется VACUUM.
CREATE OR REPLACE VIEW index_health AS
SELECT
schemaname,
relname as table_name,
indexrelname as index_name,
pg_size_pretty(pg_relation_size(indexrelid)) as size,
idx_scan,
idx_tup_read,
idx_tup_fetch,
CASE
WHEN idx_scan = 0 THEN 'UNUSED'
WHEN idx_scan < 100 THEN 'LOW_USAGE'
ELSE 'OK'
END as status,
indexdef
FROM pg_stat_user_indexes psu
JOIN pg_indexes pi USING (indexrelname)
WHERE schemaname = 'public';-- Проверка для alert-системы
SELECT
indexrelname,
pg_size_pretty(pg_relation_size(indexrelid)) as wasted_size
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
AND idx_scan = 0
AND pg_relation_size(indexrelid) > 10 * 1024 * 1024; -- > 10 MB#!/bin/bash
# weekly_index_report.sh
psql -d mydb -c "
SELECT
indexrelname,
pg_size_pretty(pg_relation_size(indexrelid)) as size,
idx_scan,
CASE WHEN idx_scan = 0 THEN '⚠️ UNUSED' ELSE '✅' END as status
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
ORDER BY idx_scan ASC, pg_relation_size(indexrelid) DESC;
" | mail -s "Weekly Index Report" admin@example.compg_stat_user_indexes — ищите неиспользуемые индексыEXPLAIN (ANALYZE, BUFFERS) — для всех медленных запросовpg_stat_statements — для поиска медленных запросовenable_seqscan = off — в productionidx_scan = 0)| Расширение | Описание |
|---|---|
pg_stat_statements | Статистика выполнения запросов |
pgstattuple | Точная оценка bloat и фрагментации |
pg_trgm | Trigram-индексы для нечёткого поиска |
pg_repack | Перестроение без блокировок (альтернатива REINDEX) |
hypopg | Гипотетические индексы для тестирования |
-- Установка
CREATE EXTENSION IF NOT EXISTS hypopg;
-- Создание гипотетического индекса
SELECT hypopg_create_index('CREATE INDEX idx_test ON users(email)');
-- Проверка плана с гипотетическим индексом
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
-- Удаление
SELECT hypopg_drop_index(indexrelid) FROM hypopg_index;Курс завершён! Вы изучили:
pg_stat_user_indexes, pg_stat_statementsВопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.