← Назад к вопросам

Как найти узкое место в работе БД?

2.0 Middle🔥 201 комментариев
#DevOps и инфраструктура#Базы данных (SQL)

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Поиск узких мест в работе БД

Найдение и устранение узких мест в базе данных — критически важный навык для оптимизации приложений. Существует систематический подход к диагностике проблем.

1. Анализ медленных запросов

PostgreSQL: Включаем логирование медленных запросов

-- Проверяем текущие параметры
SHOW log_min_duration_statement;

-- Включаем логирование запросов, которые выполняются дольше 1 сек
ALTER SYSTEM SET log_min_duration_statement = 1000;  -- миллисекунды
ALTER SYSTEM SET log_statement = 'all';  -- логируем все запросы

SELECT pg_reload_conf();  -- Перезагружаем конфиг без перезагрузки

-- Проверяем логи
tail -f /var/log/postgresql/postgresql.log

MySQL: Медленный лог запросов

-- Включаем slow query log
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;  -- 1 секунда
SET GLOBAL log_queries_not_using_indexes = 'ON';

-- Смотрим статистику
SHOW VARIABLES LIKE '%slow%';

2. EXPLAIN и EXPLAIN ANALYZE

PostgreSQL: Детальный анализ плана выполнения

-- Простой EXPLAIN
EXPLAIN
SELECT u.id, u.name, COUNT(o.id) as orders_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > NOW() - INTERVAL '30 days'
GROUP BY u.id, u.name;

-- EXPLAIN ANALYZE — выполняет запрос и показывает реальные цифры
EXPLAIN ANALYZE
SELECT u.id, u.name, COUNT(o.id) as orders_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > NOW() - INTERVAL '30 days'
GROUP BY u.id, u.name;

-- Результат показывает:
-- Seq Scan vs Index Scan — полный скан таблицы vs использование индекса
-- Планируемые vs реальные rows — если число строк сильно отличается, нужна статистика
-- Время выполнения в миллисекундах

Что ищем в плане выполнения:

  • Seq Scan на большой таблице — нужен индекс
  • Nested Loop с большим числом строк — может быть медленно
  • Hash Join vs Merge Join — проверяем есть ли индексы на колонках связи
  • Filter — условие применяется после скана, может замедлить

3. Профилирование запросов в коде Python

import time
from django.db import connection
from django.db.models import Prefetch

# Django ORM профилирование
def get_users_with_orders():
    start = time.time()
    
    # ПЛОХО: N+1 query problem
    users = User.objects.all()  # 1 запрос
    for user in users:          # N дополнительных запросов
        orders = user.orders.all()
    
    print(f'Time: {time.time() - start:.2f}s')
    print(f'Queries: {len(connection.queries)}')
    for query in connection.queries:
        print(query['sql'])

# ХОРОШО: Используем select_related и prefetch_related
def get_users_with_orders_optimized():
    users = User.objects.prefetch_related('orders').all()  # 2 запроса вместо N+1
    return users

# select_related для ForeignKey (JOIN)
users = User.objects.select_related('profile').all()  # 1 запрос с JOIN

# prefetch_related для ManyToMany и Reverse ForeignKey
users = User.objects.prefetch_related(
    'orders',
    'orders__items'
).all()  # 3 запроса, но никаких N+1

4. Индексы: создание и анализ

-- Найти наиболее медленные запросы
SELECT 
    query,
    calls,
    total_time,
    mean_time,
    max_time
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;

-- Проверить есть ли индекс на колонке
SELECT 
    schemaname,
    tablename,
    indexname,
    indexdef
FROM pg_indexes
WHERE tablename = 'users';

-- Создаём индекс
CREATE INDEX idx_users_created_at ON users(created_at DESC);
CREATE INDEX idx_orders_user_id_status ON orders(user_id, status);  -- Составной индекс
CREATE INDEX idx_users_email ON users(LOWER(email));  -- Индекс на функцию

-- Проверяем использование индексов
EXPLAIN
SELECT * FROM users WHERE created_at > NOW() - INTERVAL '30 days';

-- Удаляем неиспользуемые индексы
SELECT 
    schemaname,
    tablename,
    indexname,
    idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;

DROP INDEX idx_unused;

5. Анализ размера таблиц и индексов

-- Размер таблиц PostgreSQL
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 20;

-- Размер индексов
SELECT 
    schemaname,
    tablename,
    indexname,
    pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_stat_user_indexes
ORDER BY pg_relation_size(indexrelid) DESC;

-- Кэширование: какой процент данных кэширован
SELECT 
    schemaname,
    tablename,
    heap_blks_read,
    heap_blks_hit,
    ROUND(heap_blks_hit::float / (heap_blks_hit + heap_blks_read) * 100, 2) AS cache_hit_ratio
FROM pg_statio_user_tables
ORDER BY heap_blks_read DESC;

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

-- PostgreSQL: Обновляем статистику (анализатор использует её для плана)
ANALYZE users;
ANALYZE;  -- Все таблицы

-- Проверяем конфиг автоматического анализа
SHOW autovacuum_analyze_scale_factor;
SHOW autovacuum_vacuum_scale_factor;

-- MySQL: Анализируем таблицы
ANALYZE TABLE users;
ANALYZE TABLE orders;

7. Проблема N+1 запросов

# ПЛОХО: N+1 query
users = User.objects.all()
for user in users:  # Если 1000 users, 1000 запросов!
    print(user.profile.bio)  # Каждый доступ — отдельный запрос

# ХОРОШО: Используем select_related
users = User.objects.select_related('profile').all()  # 1 запрос с JOIN
for user in users:
    print(user.profile.bio)  # Данные уже загружены

# ПЛОХО: Для ManyToMany
users = User.objects.all()
for user in users:
    orders = user.orders.all()  # Каждый раз запрос

# ХОРОШО
users = User.objects.prefetch_related('orders').all()  # 2 запроса
for user in users:
    orders = user.orders.all()  # Данные из памяти

# Используем Prefetch для более сложных случаев
from django.db.models import Prefetch

prefer_active = Prefetch(
    'orders',
    queryset=Order.objects.filter(status='active')
)
users = User.objects.prefetch_related(prefer_active).all()

8. Кэширование результатов

import hashlib
from django.core.cache import cache

def get_expensive_data(user_id):
    # Генерируем ключ кэша
    cache_key = f'user_data:{user_id}'
    
    # Проверяем кэш
    data = cache.get(cache_key)
    if data:
        return data
    
    # Если нет — выполняем дорогой запрос
    data = User.objects.select_related('profile').prefetch_related('orders').get(id=user_id)
    
    # Сохраняем в кэш на 1 час
    cache.set(cache_key, data, timeout=3600)
    
    return data

# Или используем @cache_page для представлений
from django.views.decorators.cache import cache_page

@cache_page(60 * 5)  # 5 минут
def user_detail(request, user_id):
    user = get_expensive_data(user_id)
    return render(request, 'user_detail.html', {'user': user})

9. Batch операции вместо циклов

# ПЛОХО: сотни запросов
for user_id in user_ids:
    user = User.objects.get(id=user_id)
    user.is_active = True
    user.save()

# ХОРОШО: один запрос
User.objects.filter(id__in=user_ids).update(is_active=True)

# Batch insert
users = [
    User(name='User1', email='user1@example.com'),
    User(name='User2', email='user2@example.com'),
    User(name='User3', email='user3@example.com'),
]
User.objects.bulk_create(users, batch_size=1000)

# Batch update
users = User.objects.filter(status='inactive')
for user in users:
    user.status = 'active'
    user.updated_at = timezone.now()

User.objects.bulk_update(users, ['status', 'updated_at'], batch_size=1000)

10. Мониторинг в продакшене

# Инструменты мониторинга
# 1. Django Debug Toolbar — для разработки
DEBUG = True  # Only in development!

# 2. django-silk — профилирование запросов
# 3. New Relic, DataDog — для продакшена
# 4. Prometheus + Grafana — метрики

# Пример метрики с Prometheus
from prometheus_client import Histogram, Counter

query_duration = Histogram(
    'db_query_duration_seconds',
    'Database query duration',
    buckets=(0.1, 0.5, 1.0, 2.5, 5.0)
)

query_count = Counter('db_queries_total', 'Total database queries')

# Используем
with query_duration.time():
    users = User.objects.all()
    query_count.inc()

11. Лучшие практики оптимизации

  • Профилируйте перед оптимизацией — measure first
  • Используйте EXPLAIN ANALYZE для каждого медленного запроса
  • Создавайте индексы на колонках в WHERE, JOIN, ORDER BY
  • Избегайте N+1 запросов — используйте select/prefetch_related
  • Кэшируйте дорогие операции — Redis, Memcached
  • Batch операции — bulk_create, bulk_update вместо циклов
  • Обновляйте статистику — ANALYZE таблиц регулярно
  • Мониторьте в продакшене — New Relic, DataDog
  • Читайте логи медленных запросов — они вам помогут
  • Тестируйте на реальных данных — производительность может отличаться

Систематический подход к оптимизации БД даёт наибольший результат.