← Назад к вопросам
Была ли необходимость оптимизировать запросы
1.0 Junior🔥 181 комментариев
#SQL и базы данных#Опыт и проекты
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI21 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Оптимизация SQL запросов: реальный опыт
Краткий ответ
Да, регулярно и критически важно. В моей практике оптимизация запросов была необходима в 100% проектов. Начиная с маленьких ускорений (100ms → 50ms) и заканчивая критическими спасениями (5 минут → 2 секунды).
Реальный случай: аналитика в SaaS платформе
Проблема
Ситуация: Работал в SaaS компании с 50M+ записей в основной таблице (transactions)
Симптомы:
- Отчёт по конверсии по дням ("Conversion by Day") загружался 3-5 минут
- Пользователи ждали, refreshы дашбордов зависали
- Database CPU était 80%+ когда запускали аналитику
- Другие приложения страдали
Исходный запрос
-- ❌ ПЛОХО: 4 минуты
SELECT
DATE(created_at) as date,
COUNT(DISTINCT user_id) as unique_users,
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed,
COUNT(*) as total
FROM transactions
WHERE created_at >= '2024-01-01'
AND created_at < '2024-03-31'
GROUP BY DATE(created_at)
ORDER BY date;
Почему медленно:
- Нет индекса на created_at — полная таблица сканится (50M строк)
- DATE(created_at) — функция вычисляется для каждой строки
- DISTINCT user_id — требует сортировки для группировки
- Нет партиционирования — БД ищет везде
Анализ
Я использовал:
-- EXPLAIN ANALYZE показал:
Seq Scan on transactions (cost=0.00..10000000.00 rows=50000000) <- ПОЛНЫЙ скан!
Filter: (created_at >= '2024-01-01'...)
Planning Time: 2.1 ms
Execution Time: 285000.1 ms <- 4.75 минут!
Решение 1: Добавить индекс
-- Шаг 1: Создал индекс
CREATE INDEX idx_transactions_created_at
ON transactions(created_at);
-- Результат: 4 минуты → 45 секунд (улучшение в 5x)
-- Всё ещё медленно
Решение 2: Покрывающий индекс
-- Шаг 2: Индекс включает все нужные столбцы
CREATE INDEX idx_transactions_created_user_status
ON transactions(created_at DESC, user_id, status);
-- Результат: 45 секунд → 8 секунд (улучшение в 6x)
-- Index Only Scan теперь возможен!
Решение 3: Партиционирование
-- Шаг 3: Добавил партиционирование по дате (на будущее)
CREATE TABLE transactions_new (
id BIGINT,
user_id UUID,
created_at TIMESTAMP,
status VARCHAR,
amount DECIMAL(10, 2)
) PARTITION BY RANGE (created_at) (
PARTITION p_2024_01 VALUES FROM ('2024-01-01') TO ('2024-02-01'),
PARTITION p_2024_02 VALUES FROM ('2024-02-01') TO ('2024-03-01'),
PARTITION p_2024_03 VALUES FROM ('2024-03-01') TO ('2024-04-01'),
-- ...
);
-- Результат: 8 секунд → 2 секунды (улучшение в 4x)
-- Partition Pruning исключает ненужные партиции
Решение 4: Переформатирование запроса
-- ✅ ХОРОШО: 2 секунды, можно ещё лучше
SELECT
created_at::DATE as date,
COUNT(DISTINCT user_id) as unique_users,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
COUNT(*) as total
FROM transactions
WHERE created_at >= '2024-01-01'::TIMESTAMP
AND created_at < '2024-04-01'::TIMESTAMP
GROUP BY created_at::DATE
ORDER BY date;
-- Улучшения:
-- 1. DATE() → ::DATE (готовый cast)
-- 2. CASE WHEN → SUM(CASE ...) (проще для оптимизатора)
-- 3. Явные типы ('2024-01-01'::TIMESTAMP)
Решение 5: Materialized View (кеширование)
-- Если этот отчёт запрашивают часто, создал Materialized View
CREATE MATERIALIZED VIEW mv_daily_conversion AS
SELECT
created_at::DATE as date,
COUNT(DISTINCT user_id) as unique_users,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
COUNT(*) as total
FROM transactions
GROUP BY created_at::DATE;
-- Refresh раз в час:
REFRESH MATERIALIZED VIEW mv_daily_conversion;
-- Запрос теперь:
SELECT * FROM mv_daily_conversion WHERE date >= '2024-01-01';
-- Результат: < 100ms!
Финальный результат
| Решение | Время | Улучшение |
|---|---|---|
| Исходный | 4м 45с | baseline |
| + Простой индекс | 45с | 6x |
| + Покрывающий индекс | 8с | 30x |
| + Партиционирование | 2с | 140x |
| + Materialized View | 100ms | 2850x |
Другие примеры оптимизации
Пример 2: Когортный анализ (SLOW JOIN)
-- ❌ МЕДЛЕННО (2 минуты)
SELECT
u.user_id,
u.signup_date,
COUNT(DISTINCT t.transaction_id) as transactions,
SUM(t.amount) as revenue
FROM users u
LEFT JOIN transactions t ON u.user_id = t.user_id
WHERE u.signup_date >= '2024-01-01'
GROUP BY u.user_id, u.signup_date;
-- ✅ БЫСТРО (5 секунд) - агрегация отдельно
WITH user_agg AS (
SELECT
user_id,
COUNT(DISTINCT transaction_id) as transactions,
SUM(amount) as revenue
FROM transactions
GROUP BY user_id
)
SELECT
u.user_id,
u.signup_date,
COALESCE(ua.transactions, 0),
COALESCE(ua.revenue, 0)
FROM users u
LEFT JOIN user_agg ua ON u.user_id = ua.user_id
WHERE u.signup_date >= '2024-01-01';
Пример 3: Window Functions (OVER PARTITION BY)
-- ❌ МЕДЛЕННО (пять подзапросов)
SELECT
date,
daily_revenue,
(SELECT SUM(daily_revenue) FROM daily_stats d2 WHERE d2.date <= d1.date) as cumulative
FROM daily_stats d1;
-- ✅ БЫСТРО (window function)
SELECT
date,
daily_revenue,
SUM(daily_revenue) OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as cumulative
FROM daily_stats;
-- Результат: 30 секунд → 200ms (150x улучшение)
Практические советы по оптимизации
1. Всегда смотри EXPLAIN ANALYZE
EXPLAIN ANALYZE
SELECT ...
-- Ищешь:
-- - Seq Scan (полный скан) → нужен индекс
-- - Nested Loop (вложенные циклы) → кеширование
-- - Sort (сортировка) → индекс по сортируемому полю
-- - Hash (хеширование) → проверь join условие
2. Индексируй правильно
-- ❌ Не поможет
CREATE INDEX idx_name ON table(name);
-- Если в WHERE условие: created_at BETWEEN ... AND ...
-- ✅ Правильно
CREATE INDEX idx_created_at ON table(created_at);
3. Используй кеширование (Redis, Memcached)
# Python пример
import redis
cache = redis.Redis()
key = "analytics:daily_conversion:2024-03"
# Проверка кеша
result = cache.get(key)
if result:
return json.loads(result) # < 1ms
# Если нет кеша - вычисляем и сохраняем
result = db.query("SELECT ...") # 2 секунды
cache.setex(key, 3600, json.dumps(result)) # Кеш на час
return result
4. Партиционируй большие таблицы
-- Для таблиц > 100M строк
CREATE TABLE transactions (...)
PARTITION BY RANGE (YEAR_MONTH(created_at));
-- Partition Pruning исключит ненужные данные
5. Используй аналитические окна вместо подзапросов
-- ❌ Подзапрос (медленный)
SELECT user_id,
(SELECT COUNT(*) FROM events WHERE events.user_id = users.user_id) as event_count
FROM users;
-- ✅ Window function (быстрый)
SELECT user_id, COUNT(*) OVER (PARTITION BY user_id) as event_count
FROM events;
Мониторинг на Production
Что я мониторил
-- Медленные запросы (PostgreSQL)
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
WHERE mean_exec_time > 1000 -- > 1 секунды
ORDER BY mean_exec_time DESC;
-- Большие таблицы
SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size
FROM pg_tables
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC
LIMIT 10;
-- Неиспользуемые индексы
SELECT schemaname, tablename, indexname
FROM pg_stat_user_indexes
WHERE idx_scan = 0;
Итог
Оптимизация была критически важна потому что:
- Performance impact: дашборды загружались в 3-5 минут → 2 секунды
- Cost impact: меньше CPU/RAM нужно серверам
- User experience: пользователи видели результаты сразу
- Scalability: могли добавлять больше пользователей без деградации
Моя рекомендация: Всегда профилируй (EXPLAIN ANALYZE) перед оптимизацией. Не оптимизируй "на глаз" — оптимизируй где реально медленно.