Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как сделать индекс в базе данных
Это техническое вопрос, который проверяет понимание оптимизации БД. Рассмотрю на примере PostgreSQL и SQLAlchemy.
Что такое индекс?
Индекс — это структура данных, которая ускоряет поиск по определённым полям. Без индекса БД выполняет полный скан таблицы (Sequential Scan), что медленно. С индексом — быстро находит нужные строки.
Без индекса: SELECT * FROM users WHERE email = 'john@example.com'
→ PostgreSQL проверяет ВСЕ 1 млн строк
→ Очень медленно
С индексом: SELECT * FROM users WHERE email = 'john@example.com'
→ PostgreSQL использует B-tree структуру
→ Находит за ~log(N) сравнений = очень быстро
1. Создание индекса в SQL
Простой индекс на одно поле
-- Создаём индекс на поле email
CREATE INDEX idx_users_email ON users(email);
-- Проверяем создание
\d users -- В PostgreSQL
Уникальный индекс
-- Гарантирует уникальность (как PRIMARY KEY, но гибче)
CREATE UNIQUE INDEX idx_users_email_unique ON users(email);
-- Теперь две записи с одинаковым email добавить нельзя
INSERT INTO users (email) VALUES ('john@example.com'); -- OK
INSERT INTO users (email) VALUES ('john@example.com'); -- ERROR
Составной индекс (на несколько полей)
-- Индекс на несколько колонок для queries типа:
-- SELECT * FROM orders WHERE user_id = ? AND status = ?
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
-- Этот индекс помогает:
SELECT * FROM orders WHERE user_id = 5 AND status = 'completed';
-- Но НЕ помогает:
SELECT * FROM orders WHERE status = 'completed'; -- status вторая колонка
Индекс на выражения
-- Индекс на функцию для регистронезависимого поиска
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
-- Теперь быстро работает:
SELECT * FROM users WHERE LOWER(email) = 'john@example.com';
Частичный индекс (условный)
-- Индекс только на активные пользователей
CREATE INDEX idx_users_active ON users(id) WHERE active = true;
-- Экономит место, т.к. не индексирует удалённых пользователей
SELECT * FROM users WHERE id = 5 AND active = true; -- Использует индекс
SELECT * FROM users WHERE id = 5 AND active = false; -- Полный скан
Удаление индекса
DROP INDEX idx_users_email;
2. Создание индекса через SQLAlchemy (ORM)
В определении модели
from sqlalchemy import Column, Integer, String, Index, Boolean
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
email = Column(String, unique=True) # Автоматически создаёт индекс
first_name = Column(String)
last_name = Column(String)
active = Column(Boolean, default=True)
# Простой индекс
__table_args__ = (
Index('idx_users_firstname', 'first_name'),
Index('idx_users_active', 'active'),
)
class Order(Base):
__tablename__ = 'orders'
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('users.id'))
status = Column(String)
created_at = Column(DateTime)
# Составной индекс
__table_args__ = (
Index('idx_orders_user_status', 'user_id', 'status'),
)
Индекс на выражение через SQLAlchemy
from sqlalchemy import func
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
email = Column(String)
# Индекс на LOWER(email)
__table_args__ = (
Index('idx_users_email_lower', func.lower(email)),
)
Частичный индекс через SQLAlchemy
from sqlalchemy import and_
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
email = Column(String)
active = Column(Boolean)
# Индекс только для активных пользователей
__table_args__ = (
Index(
'idx_users_email_active',
'email',
postgresql_where=and_(active == True)
),
)
3. Миграция с Alembic/Goose
Если используешь миграции (Goose)
-- migrations/0003_add_indexes.sql
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
CREATE INDEX idx_users_active ON users(id) WHERE active = true;
Откат (если нужно удалить индексы)
-- Downsql для Goose
DROP INDEX IF EXISTS idx_users_email;
DROP INDEX IF EXISTS idx_orders_user_status;
DROP INDEX IF EXISTS idx_users_active;
4. Как выбрать поля для индексов?
Поля, которые НУЖНО индексировать:
# 1. Поля в WHERE условиях (часто ищешь по ним)
SELECT * FROM users WHERE email = ? # Индексируй email
SELECT * FROM orders WHERE status = ? # Индексируй status
# 2. Поля в JOIN условиях (Foreign Keys)
SELECT * FROM orders
JOIN users ON orders.user_id = users.id # Индексируй user_id
# 3. Поля в ORDER BY (сортировка)
SELECT * FROM posts ORDER BY created_at # Индексируй created_at
# 4. Поля в GROUP BY
SELECT COUNT(*), category FROM products
GROUP BY category # Индексируй category
Поля, которые НЕ НУЖНО индексировать:
# 1. Boolean поля с неравномерным распределением
# Индекс на is_deleted не помогает, если 99% активные
# 2. Поля с мало уникальными значениями (low cardinality)
# Индекс на gender (M/F) бесполезен
# 3. Очень большие текстовые поля
# Индекс на описание занимает много места
# 4. Часто обновляемые поля
# Индекс нужно перестраивать при каждом обновлении
5. Анализ производительности
EXPLAIN ANALYZE — твой лучший друг
-- Без индекса
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'john@example.com';
-- Вывод (плохо):
-- Seq Scan on users (cost=0.00..1234.00 rows=1)
-- Filter: (email = 'john@example.com')
-- Planning Time: 0.1 ms
-- Execution Time: 45.2 ms <- МЕДЛЕННО!
-- С индексом
CREATE INDEX idx_users_email ON users(email);
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'john@example.com';
-- Вывод (хорошо):
-- Index Scan using idx_users_email on users
-- Index Cond: (email = 'john@example.com')
-- Planning Time: 0.1 ms
-- Execution Time: 0.5 ms <- БЫСТРО!
В Python коде
from sqlalchemy import event, create_engine
import logging
logger = logging.getLogger('sqlalchemy.engine')
logger.setLevel(logging.INFO)
# Включи SQL логирование для анализа
engine = create_engine(
'postgresql://user:pass@localhost/db',
echo=True # Выводит все SQL запросы
)
# Теперь видишь, какие запросы выполняются
6. Практический пример: оптимизация медленного приложения
from sqlalchemy import Column, Integer, String, DateTime, Index
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime
Base = declarative_base()
class BlogPost(Base):
__tablename__ = 'blog_posts'
id = Column(Integer, primary_key=True)
author_id = Column(Integer, ForeignKey('users.id'))
title = Column(String(255))
slug = Column(String(255), unique=True) # Автоматический индекс
content = Column(String)
published = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
# Индексы для типичных queries
__table_args__ = (
# Часто ищем по author_id
Index('idx_posts_author', 'author_id'),
# Часто ищем опубликованные посты
Index('idx_posts_published', 'published'),
# Часто сортируем по created_at
Index('idx_posts_created', 'created_at'),
# Часто: все посты автора, отсортированные по дате
Index('idx_posts_author_created', 'author_id', 'created_at'),
# Частичный индекс: только опубликованные
Index(
'idx_posts_published_created',
'created_at',
postgresql_where='published = true'
),
)
Типичные queries:
# Query 1: Все посты автора
author_posts = session.query(BlogPost).filter_by(author_id=5).all()
# Использует: idx_posts_author
# Query 2: Опубликованные посты, отсортированные по дате
recent_posts = session.query(BlogPost)\
.filter_by(published=True)\
.order_by(BlogPost.created_at.desc())\
.limit(10)
.all()
# Использует: idx_posts_published_created
# Query 3: Пост по slug (быстро благодаря unique)
post = session.query(BlogPost).filter_by(slug='my-post').first()
# Использует: автоматический индекс на slug
7. Лучшие практики
Не переусложняй:
# Плохо: индекс на каждое поле
class User(Base):
__table_args__ = (
Index('idx_id', 'id'), # Ненужно, уже PRIMARY KEY
Index('idx_name', 'name'), # Редко ищем по имени
Index('idx_phone', 'phone'), # Медленно обновляется
)
# Хорошо: индекс только на нужные поля
class User(Base):
__table_args__ = (
Index('idx_email', 'email'), # Часто ищем по email
Index('idx_active', 'active'), # Часто фильтруем
)
Проверяй использование индексов:
-- Какие индексы вообще используются?
SELECT schemaname, tablename, indexname
FROM pg_indexes
WHERE schemaname = 'public';
-- Есть ли неиспользуемые индексы?
SELECT i.relname AS index_name,
(SELECT COUNT(*) FROM pg_stat_user_indexes
WHERE indexrelname = i.relname AND idx_scan = 0) AS never_used
FROM pg_class i
WHERE i.relkind = 'i';
Резюме
Когда создавать индекс:
- На поля в WHERE, JOIN, ORDER BY, GROUP BY
- На Foreign Keys
- На поля, используемые для фильтрации
Типы индексов:
- Простой индекс (одно поле)
- Составной индекс (несколько полей)
- Уникальный индекс (UNIQUE)
- Частичный индекс (WHERE условие)
- Индекс на выражение (LOWER(email))
Инструменты для анализа:
- EXPLAIN ANALYZE
- pg_stat_user_indexes
- SQLAlchemy echo=True
Золотое правило: Создавай индекс только после анализа EXPLAIN, убедись, что это действительно нужно.