← Назад к вопросам
Что нужно делать когда нагрузка на бд становится слишком большая?
2.7 Senior🔥 211 комментариев
#SQL и базы данных#Архитектура и проектирование
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Что делать когда нагрузка на БД становится слишком большая?
Это одна из ключевых задач Data Engineer. Когда БД перегружена, нужна система-матическая диагностика и применение скейлирующих решений в правильном порядке.
Шаг 1: Диагностика (измерение проблемы)
Сначала понимаешь, ЧТО перегружено и ПОЧЕМУ.
1.1. Какой ресурс исчерпан?
-- PostgreSQL: посмотреть основные метрики
SELECT
datname as database,
numbackends as active_connections,
tup_inserted + tup_updated + tup_deleted as mutations_per_sec,
tup_fetched as reads_per_sec
FROM pg_stat_database
WHERE datname = 'production'
ORDER BY datname;
-- MySQL: текущие процессы
SHOW PROCESSLIST;
-- Redis: память
INFO memory
-- Результат: used_memory, maxmemory, evicted_keys (если много evictions)
1.2. Какие запросы медленные?
-- PostgreSQL: slow query log
SELECT
query,
calls,
total_time / calls as avg_time_ms,
mean_exec_time,
max_exec_time
FROM pg_stat_statements
WHERE query NOT LIKE '%pg_stat_statements%'
ORDER BY mean_exec_time DESC
LIMIT 10;
-- Результат: обычно видишь 1-2 query которые занимают 80% времени
1.3. Метрики мониторинга
Установи мониторинг ДО кризиса:
# Prometheus metrics для отслеживания
from prometheus_client import Gauge, Counter
db_connections = Gauge('db_connections_active', 'Active DB connections')
db_query_time = Gauge('db_query_time_seconds', 'Query execution time')
db_errors = Counter('db_errors_total', 'Total DB errors')
db_cache_hits = Counter('db_cache_hits_total', 'Cache hits')
# Установка алертов
# Alert if connections > 80% of max_connections
# Alert if query_time > 5 seconds (P99)
# Alert if cache_hit_ratio < 90%
Шаг 2: Быстрые исправления (первая помощь)
Это делаешь СРАЗУ, пока диагностируешь:
2.1. Останови bad queries
-- Найти долгоживущий запрос
SELECT
pid,
query,
state,
EXTRACT(EPOCH FROM (now() - query_start)) as duration_seconds
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY duration_seconds DESC;
-- Если query зависла на 1 час — убей его
SELECT pg_terminate_backend(12345); -- pid из результата выше
2.2. Увеличь connection pool
# Если проблема в недостаточном количестве connections
# Временно увеличь:
# SQLAlchemy
engine = create_engine(
'postgresql://...',
poolsize=50, # было 20
max_overflow=20, # было 10
)
# Django
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'CONN_MAX_AGE': 600, # держи connection дольше
'OPTIONS': {'connect_timeout': 10}
}
}
2.3. Очисти кэш/буферы
-- PostgreSQL: очисти statement cache
DISCART PLANS; -- забудь compiled queries
-- MySQL
FLUSH QUERY CACHE;
FLUSH TABLES;
Шаг 3: Оптимизация запросов (среднесрочно)
Это то, что реально помогает.
3.1. Добавь индексы
Проверь, какие индексы отсутствуют:
-- PostgreSQL: посмотри seq scans (плохо!)
SELECT
relname as table_name,
seq_scan,
idx_scan,
seq_scan - idx_scan as extra_seq_scans
FROM pg_stat_user_tables
WHERE seq_scan > idx_scan
ORDER BY extra_seq_scans DESC
LIMIT 10;
-- Результат: если table_name: orders, seq_scan: 10M, idx_scan: 100
-- Значит запросы не используют индексы
-- Добавь индекс на часто используемые колонки
CREATE INDEX CONCURRENTLY idx_orders_user_date
ON orders(user_id, created_at DESC);
-- CONCURRENTLY = не блокирует таблицу при создании индекса
3.2. Переписи медленные запросы
-- Плохо: LEFT JOIN + группировка создаёт temporary table
SELECT
u.user_id,
COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
LEFT JOIN payments p ON o.id = p.order_id
LEFT JOIN items i ON o.id = i.order_id
GROUP BY u.user_id;
-- Это: 1M users × 100M orders = медленно
-- Хорошо: подзапрос с aggregation перед JOIN
SELECT
u.user_id,
order_stats.order_count
FROM users u
LEFT JOIN (
SELECT user_id, COUNT(*) as order_count
FROM orders
GROUP BY user_id
) order_stats ON u.id = order_stats.user_id;
-- Это: 1M users × 1M unique user_ids в subquery = быстрее
3.3. Партиционирование
-- Если таблица очень большая (500M+ rows)
-- Партиципируй по дате
CREATE TABLE orders_partitioned (
id BIGSERIAL,
user_id INT,
created_at TIMESTAMP,
amount DECIMAL
) PARTITION BY RANGE (created_at);
-- Автоматически создаёт партиции
CREATE TABLE orders_2024_q1 PARTITION OF orders_partitioned
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
-- Преимущество: запросы читают только нужные партиции
-- SELECT WHERE created_at > '2024-03-01' читает только Q1 2024
Шаг 4: Масштабирование БД (medium term)
Когда оптимизация не помогает.
4.1. Вертикальное масштабирование (bigger machine)
❌ Минусы:
- CPU 8 cores → 16 cores = 2x cost
- RAM 32GB → 128GB = 4x cost
- Даже не гарантирует 2x производительность (из-за contention)
- Есть потолок (не можешь бесконечно масштабировать single server)
✅ Плюсы:
- Просто: просто купи мощнее сервер
- Быстро: часов 2-3 на миграцию
4.2. Горизонтальное масштабирование (read replicas)
Проблема: основной сервер перегружен чтением
Решение: read replicas
Структура:
Primary (writes) ← чтение + запись
├─ Replica 1 (read-only) ← только чтение
├─ Replica 2 (read-only) ← только чтение
└─ Replica 3 (read-only) ← только чтение
# PostgreSQL: создать replica
# На primary:
PG_HBA_CONF: host replication replica 192.168.1.10/32 trust
# На replica:
RESTORE_COMMAND = 'cp /archive/%f "%p"'
standby_mode = 'on'
primary_conninfo = 'host=192.168.1.1 port=5432 user=replication'
# В приложении:
from sqlalchemy import create_engine
write_db = create_engine('postgresql://primary...')
read_db = create_engine('postgresql://replica1...') # для SELECT
def get_orders(user_id):
# читаешь с replica
return read_db.execute(f"SELECT * FROM orders WHERE user_id = {user_id}")
4.3. Шардирование (split data)
Когда БД растёт и горизонтальное не помогает.
Проблема: 100M users в одной таблице, индексы не помогают
Решение: шардирование (разбить на несколько БД)
Логика:
shard_id = hash(user_id) % num_shards
В приложении:
shards = [
db1 (users 0-25M),
db2 (users 25M-50M),
db3 (users 50M-75M),
db4 (users 75M-100M),
]
user_id = 12345678
shard = 12345678 % 4 # shard 2
get_user(12345678) → connects to db2
Шаг 5: Caching Layer (быстрый результат)
Не все запросы нужно выполнять в БД.
5.1. Query result caching
import redis
import json
from functools import wraps
redis_client = redis.Redis(host='localhost', port=6379)
def cache_query(ttl_seconds=3600):
"""Кэшируй результаты запроса в Redis"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Генерируем ключ кэша
cache_key = f"{func.__name__}:{json.dumps(args)}:{json.dumps(kwargs)}"
# Проверяем кэш
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
# Если не в кэше, выполняем query
result = func(*args, **kwargs)
# Сохраняем результат в кэш
redis_client.setex(cache_key, ttl_seconds, json.dumps(result))
return result
return wrapper
return decorator
# Использование
@cache_query(ttl_seconds=3600)
def get_user_stats(user_id):
query = "SELECT COUNT(*) as order_count FROM orders WHERE user_id = %s"
return db.execute(query, [user_id])
# Первый вызов: читает из БД
stats1 = get_user_stats(123)
# Следующие вызовы (в течение часа): из Redis
stats2 = get_user_stats(123) # быстро!
5.2. Materialized views
-- Создай материализованное представление (snapshot таблицы)
CREATE MATERIALIZED VIEW user_stats_mv AS
SELECT
user_id,
COUNT(*) as order_count,
SUM(amount) as total_spent,
MAX(created_at) as last_order_date
FROM orders
GROUP BY user_id;
-- Пересчитывай раз в час
REFRESH MATERIALIZED VIEW CONCURRENTLY user_stats_mv;
-- В запросах используй view вместо table
SELECT * FROM user_stats_mv WHERE user_id = 123;
-- Это instant (не нужно GROUP BY на 100M rows)
Шаг 6: Offload нетрадиционных нагрузок
Не всё должно быть в OLTP БД.
6.1. Отправь analytics в Data Warehouse
# Плохо: каждый аналитический запрос нагружает production БД
db.execute("SELECT user_id, COUNT(*) FROM orders GROUP BY user_id")
# Хорошо: синхронизируй данные в Data Warehouse
# nightly sync
- Extract from production PostgreSQL
- Load to BigQuery/Snowflake
- Аналитики работают с BigQuery
# Production БД не страдает
6.2. Используй NoSQL для specific usecases
# PostgreSQL: для ACID транзакций
book_order() # ACID, consistency важна
# MongoDB: для гибкой схемы
log_event({"user_id": 123, "event": "click", ...}) # NoSQL OK
# ElasticSearch: для full-text search
search_products("laptop computer") # ES, не PostgreSQL
# Redis: для real-time data
increment_view_count(product_id) # Redis, очень быстро
Checklist: Действия при перегрузке БД
Немедленно (минуты):
- Определить узкие места (slow query log)
- Убить зависшие запросы
- Увеличить connection pool
- Перезагрузить приложение (если есть утечка соединений)
Короткосрочно (часы):
- Добавить индексы на часто используемые колонки
- Переписать 1-2 критичных slow query
- Включить query caching (Redis)
- Создать materialized view для популярных агрегаций
Среднесрочно (дни):
- Реплицировать БД для чтения
- Микс-грейд оборудования (вертикальное масштабирование)
- Поднять separate Data Warehouse для analytics
Долгосрочно (недели/месяцы):
- Архитектурные изменения (микросервисы, CQRS)
- Шардирование по потребностям
- Полная переиндексация и опимизация
Вывод
Когда нагрузка на БД становится большой:
- Диагностика — понять что сломалось
- Быстрые фиксы — kill bad queries, add connections
- Оптимизация — индексы, переписать запросы
- Масштабирование — replicas, sharding, bigger machine
- Кэширование — Redis, materialized views
- Offload — analytics в DW, NoSQL для specific cases
Порядок важен: сначала оптимизация (дешево), потом масштабирование (дорого).