Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Индексы в БД: создание и использование
Индекс — это структура данных, которая ускоряет поиск и сортировку записей в БД. Рассмотрю как создавать и использовать индексы в PostgreSQL и как их описывать в Java приложениях.
1. Создание индекса в SQL (Миграция)
Простой индекс по одному столбцу
-- Миграция: 0001_create_users_table.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
username VARCHAR(100) NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
-- Индекс по email для быстрого поиска пользователя
CREATE INDEX idx_users_email ON users(email);
-- Индекс по created_at для сортировки по дате
CREATE INDEX idx_users_created_at ON users(created_at DESC);
Составной индекс (по нескольким столбцам)
-- Таблица заказов
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
status VARCHAR(50) NOT NULL,
created_at TIMESTAMP NOT NULL,
amount DECIMAL(10, 2),
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- Составной индекс: часто ищем заказы по user_id и status
CREATE INDEX idx_orders_user_id_status ON orders(user_id, status);
-- Индекс для диапазонного поиска
CREATE INDEX idx_orders_created_at ON orders(created_at DESC);
Индекс с условием (Partial Index)
-- Индекс только по активным заказам (для быстрого поиска)
CREATE INDEX idx_orders_active ON orders(user_id)
WHERE status = ACTIVE;
-- Индекс по удаленным записям (для мягкого удаления)
CREATE INDEX idx_users_deleted ON users(deleted_at)
WHERE deleted_at IS NOT NULL;
Уникальный индекс
-- Гарантирует уникальность, может быть несколько NULL
CREATE UNIQUE INDEX idx_users_email_unique ON users(email);
-- Полнотекстовый индекс (для поиска по тексту)
CREATE INDEX idx_articles_body_fts ON articles
USING GIN (to_tsvector(russian, body));
2. Описание индексов в JPA/Hibernate
Простой индекс
import jakarta.persistence.*;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
@Index(name = "idx_users_username") // Индекс на поле
private String username;
@Column(nullable = false)
private LocalDateTime createdAt;
// Getters, setters
}
Составной индекс
import jakarta.persistence.*;
@Entity
@Table(
name = "orders",
indexes = {
@Index(
name = "idx_orders_user_status",
columnList = "user_id,status",
unique = false
),
@Index(
name = "idx_orders_created_at",
columnList = "created_at DESC"
)
}
)
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private OrderStatus status;
@Column(nullable = false)
private LocalDateTime createdAt;
@Column(nullable = false)
private BigDecimal amount;
// Getters, setters
}
public enum OrderStatus {
ACTIVE, COMPLETED, CANCELLED
}
3. Миграции индексов (Goose)
Рекомендуемый подход для production:
-- migrations/0001_create_users_table.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
username VARCHAR(100) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Индексы в отдельной миграции
-- migrations/0002_add_users_indexes.sql
CREATE UNIQUE INDEX CONCURRENTLY idx_users_email
ON users(email);
CREATE INDEX CONCURRENTLY idx_users_username
ON users(username);
CREATE INDEX CONCURRENTLY idx_users_created_at
ON users(created_at DESC);
-- migrations/0003_create_orders_table.sql
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status VARCHAR(50) NOT NULL DEFAULT ACTIVE,
amount DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- migrations/0004_add_orders_indexes.sql
CREATE INDEX CONCURRENTLY idx_orders_user_id
ON orders(user_id);
CREATE INDEX CONCURRENTLY idx_orders_user_status
ON orders(user_id, status);
CREATE INDEX CONCURRENTLY idx_orders_created_at
ON orders(created_at DESC);
-- Индекс по активным заказам
CREATE INDEX CONCURRENTLY idx_orders_active
ON orders(user_id) WHERE status = ACTIVE;
4. Практические примеры в коде
Пример 1: Поиск пользователя по email
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Запрос использует индекс idx_users_email
Optional<User> findByEmail(String email);
}
SQL, который будет выполнен:
-- PostgreSQL использует индекс для быстрого поиска
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = ?;
-- Index Scan using idx_users_email
-- Index Cond: (email = user@example.com)
Пример 2: Поиск заказов с фильтрацией
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
// Использует составной индекс idx_orders_user_status
List<Order> findByUserIdAndStatus(Long userId, OrderStatus status);
// Использует индекс idx_orders_created_at
List<Order> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
}
Пример 3: Сложный запрос
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
// Использует idx_orders_user_status и idx_orders_created_at
@Query("""n SELECT o FROM Order o
WHERE o.user.id = :userId
AND o.status = :status
AND o.createdAt >= :startDate
ORDER BY o.createdAt DESC
""")
List<Order> findUserOrdersFiltered(
@Param("userId") Long userId,
@Param("status") OrderStatus status,
@Param("startDate") LocalDateTime startDate
);
}
5. Проверка использования индексов
EXPLAIN ANALYZE в PostgreSQL
-- Проверяем, использует ли запрос индекс
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = user@example.com;
-- Результат должен показать:
-- Index Scan using idx_users_email on users (cost=...)
-- Index Cond: (email = user@example.com)
Запрос для проверки индексов
-- Посмотреть все индексы таблицы
SELECT indexname, tablename
FROM pg_indexes
WHERE tablename = users;
-- Посмотреть неиспользуемые индексы
SELECT
schemaname,
tablename,
indexname,
idx_scan as "scans"
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
-- Посмотреть размер индекса
SELECT
indexname,
pg_size_pretty(pg_relation_size(indexrelname)) AS size
FROM pg_indexes
JOIN pg_class ON pg_class.relname = indexname
WHERE tablename = orders
ORDER BY pg_relation_size(indexrelid) DESC;
6. Типы индексов
| Тип | Использование | Пример |
|---|---|---|
| B-tree (default) | Основной индекс для поиска, сортировки | CREATE INDEX idx_email ON users(email) |
| Hash | Только для равенства (=) | CREATE INDEX idx_hash ON users USING HASH(status) |
| GIN | Полнотекстовый поиск, массивы | CREATE INDEX idx_fts ON articles USING GIN(to_tsvector(russian, body)) |
| GIST | Геопространственные данные | CREATE INDEX idx_geo ON locations USING GIST(coordinates) |
| BRIN | Большие таблицы с сортированными данными | CREATE INDEX idx_brin ON events USING BRIN(created_at) |
7. Best Practices
Правильно
-- ✅ Индексируем колонки, по которым часто ищем
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
-- ✅ Используем CONCURRENTLY для больших таблиц
CREATE INDEX CONCURRENTLY idx_articles_slug
ON articles(slug);
-- ✅ Составной индекс в порядке запроса
CREATE INDEX idx_orders_user_date
ON orders(user_id, created_at DESC);
-- ✅ Удаляем неиспользуемые индексы
DROP INDEX CONCURRENTLY idx_old_index;
Неправильно
-- ❌ Индекс на редко используемое поле
CREATE INDEX idx_user_middle_name ON users(middle_name);
-- ❌ Слишком много индексов на одной таблице
CREATE INDEX idx1 ON orders(user_id);
CREATE INDEX idx2 ON orders(status);
CREATE INDEX idx3 ON orders(created_at);
CREATE INDEX idx4 ON orders(amount);
-- Лучше: один составной индекс
-- ❌ Индекс на большой колонке текста
CREATE INDEX idx_description ON products(full_description);
-- Используй GIST или полнотекстовый поиск вместо этого
8. Когда нужны индексы
✅ Нужны индексы:
- Поля WHERE (в 99% случаев)
- Поля JOIN (внешние ключи)
- Поля ORDER BY (если таблица большая)
- Поля GROUP BY (если таблица большая)
- UNIQUE ограничения
❌ Не нужны индексы:
- Маленькие таблицы (< 1000 записей)
- Поля с низкой селективностью (boolean, статусы)
- Часто обновляемые поля
- Редко используемые поля
9. Мониторинг индексов
@Service
public class DatabaseHealthCheck {
@Scheduled(fixedRate = 3600000) // Каждый час
public void checkUnusedIndexes() {
// Находим неиспользуемые индексы
// Их нужно удалить, чтобы не замедлять UPDATE/INSERT
}
@Scheduled(fixedRate = 3600000)
public void checkIndexFragmentation() {
// Для больших индексов проверяем фрагментацию
// REINDEX может улучшить производительность
}
}
Выводы
- Индексируй WHERE, JOIN, ORDER BY — самые важные места
- Составные индексы часто эффективнее отдельных
- Проверяй с EXPLAIN ANALYZE — видишь, использует ли БД индекс
- Не переусложняй — каждый индекс замедляет INSERT/UPDATE
- Удаляй неиспользуемые — они только занимают место
- Для больших таблиц используй CONCURRENTLY при создании
- Регулярно мониторь — неиспользуемые индексы нужно удалять