2. PostgreSQL: Индексы
Индексы — это структуры данных, которые ускоряют поиск записей в таблице. PostgreSQL поддерживает несколько типов индексов для разных сценариев использования.
Типы индексов
Заголовок раздела «Типы индексов»graph TD A[PostgreSQL Indexes] --> B[B-Tree] A --> C[Hash] A --> D[GIN] A --> E[GiST] A --> F[BRIN] A --> G[SP-GiST]
B --> B1[Default, универсальный] C --> C1[Только равенство] D --> D1[JSONB, массивы, full-text] E --> E1[Геометрия, диапазоны] F --> F1[Большие таблицы] G --> G1[Неравномерные данные]B-Tree: Универсальный индекс
Заголовок раздела «B-Tree: Универсальный индекс»B-Tree — это индекс по умолчанию, подходит для большинства случаев.
Когда использовать B-Tree
Заголовок раздела «Когда использовать B-Tree»- Поиск по равенству:
WHERE id = 10 - Диапазоны:
WHERE created_at > '2024-01-01' - Сортировка:
ORDER BY username - Префиксный поиск:
WHERE email LIKE 'john%'
Создание B-Tree индекса
Заголовок раздела «Создание B-Tree индекса»-- Простой индексCREATE INDEX idx_users_email ON users(email);
-- Составной индекс (порядок важен!)CREATE INDEX idx_users_status_created ON users(status, created_at);
-- Уникальный индексCREATE UNIQUE INDEX idx_users_username ON users(username);
-- Частичный индекс (только активные пользователи)CREATE INDEX idx_active_usersON users(email)WHERE status = 'active';
-- Индекс с сортировкойCREATE INDEX idx_posts_created_descON posts(created_at DESC NULLS LAST);Пример использования
Заголовок раздела «Пример использования»-- Запрос использует индексEXPLAIN ANALYZESELECT * FROM users
-- Составной индекс: эффективенSELECT * FROM usersWHERE status = 'active' AND created_at > NOW() - INTERVAL '1 day';
-- Неэффективно: индекс не используется (обратный порядок)SELECT * FROM usersWHERE created_at > NOW() - INTERVAL '1 day' AND status = 'active';Hash: Только для равенства
Заголовок раздела «Hash: Только для равенства»Hash индексы оптимизированы для операций =, но не поддерживают диапазоны и сортировку.
-- Создание Hash индексаCREATE INDEX idx_users_api_key ON users USING HASH(api_key);
-- ЭффективноSELECT * FROM users WHERE api_key = 'abc123xyz';
-- НЕ использует индексSELECT * FROM users WHERE api_key > 'abc';⚠️ Важно: Hash индексы раньше не логировались в WAL (до PostgreSQL 10), теперь безопасны для использования.
GIN: Для массивов и JSONB
Заголовок раздела «GIN: Для массивов и JSONB»GIN (Generalized Inverted Index) идеален для полнотекстового поиска, JSONB и массивов.
JSONB индексы
Заголовок раздела «JSONB индексы»-- Таблица с JSONBCREATE TABLE products ( id SERIAL PRIMARY KEY, name VARCHAR(100), attributes JSONB);
-- GIN индекс для JSONBCREATE INDEX idx_products_attributes ON products USING GIN(attributes);
-- Вставка данныхINSERT INTO products (name, attributes) VALUES('Laptop', '{"brand": "Dell", "ram": 16, "tags": ["business", "portable"]}'),('Phone', '{"brand": "Apple", "storage": 128, "tags": ["mobile", "5G"]}');
-- Поиск по ключуSELECT * FROM productsWHERE attributes ? 'brand';
-- Поиск по значениюSELECT * FROM productsWHERE attributes @> '{"brand": "Dell"}';
-- Поиск в массивеSELECT * FROM productsWHERE attributes->'tags' ? 'portable';Массивы
Заголовок раздела «Массивы»CREATE TABLE articles ( id SERIAL PRIMARY KEY, title TEXT, tags TEXT[]);
-- GIN индекс для массиваCREATE INDEX idx_articles_tags ON articles USING GIN(tags);
INSERT INTO articles (title, tags) VALUES('PostgreSQL Guide', ARRAY['database', 'sql', 'tutorial']),('Node.js Best Practices', ARRAY['javascript', 'nodejs', 'backend']);
-- Поиск статей с тегом 'database'SELECT * FROM articles WHERE tags @> ARRAY['database'];
-- Поиск статей с любым из теговSELECT * FROM articles WHERE tags && ARRAY['sql', 'nodejs'];GiST: Геометрия и диапазоны
Заголовок раздела «GiST: Геометрия и диапазоны»GiST (Generalized Search Tree) для пространственных данных и диапазонов.
-- Установка PostGIS для геоданныхCREATE EXTENSION postgis;
CREATE TABLE locations ( id SERIAL PRIMARY KEY, name VARCHAR(100), coordinates GEOGRAPHY(POINT));
-- GiST индекс для геоданныхCREATE INDEX idx_locations_coordinatesON locations USING GIST(coordinates);
-- Поиск в радиусе 1000 метровSELECT nameFROM locationsWHERE ST_DWithin( coordinates, ST_MakePoint(37.7749, -122.4194)::geography, 1000);
-- ДиапазоныCREATE TABLE bookings ( id SERIAL PRIMARY KEY, room_id INT, period TSRANGE);
CREATE INDEX idx_bookings_period ON bookings USING GIST(period);
-- Поиск пересеченийSELECT * FROM bookingsWHERE period && tsrange('2024-01-01', '2024-01-10');BRIN: Для больших таблиц
Заголовок раздела «BRIN: Для больших таблиц»BRIN (Block Range Index) — компактный индекс для больших отсортированных таблиц.
-- Таблица с логами (миллионы записей)CREATE TABLE logs ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMP NOT NULL, message TEXT);
-- BRIN индекс (очень маленький размер)CREATE INDEX idx_logs_created ON logs USING BRIN(created_at);
-- Эффективен для диапазоновSELECT * FROM logsWHERE created_at BETWEEN '2024-01-01' AND '2024-01-31';Плюсы BRIN:
- Очень маленький размер (1-2% от B-Tree)
- Быстрое создание и обновление
Минусы:
- Требует физической сортировки данных
- Менее точный, чем B-Tree
Мониторинг индексов
Заголовок раздела «Мониторинг индексов»-- Размер индексовSELECT schemaname, tablename, indexname, pg_size_pretty(pg_relation_size(indexname::regclass)) as sizeFROM pg_indexesWHERE schemaname = 'public'ORDER BY pg_relation_size(indexname::regclass) DESC;
-- Неиспользуемые индексыSELECT schemaname, tablename, indexname, idx_scan, idx_tup_read, idx_tup_fetchFROM pg_stat_user_indexesWHERE idx_scan = 0ORDER BY pg_relation_size(indexname::regclass) DESC;
-- Эффективность использования индексаSELECT schemaname, tablename, indexname, idx_scan, pg_size_pretty(pg_relation_size(indexname::regclass)) as sizeFROM pg_stat_user_indexesORDER BY idx_scan ASC;TypeScript пример с индексами
Заголовок раздела «TypeScript пример с индексами»import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL,});
// Поиск с использованием B-Tree индексаasync function findUserByEmail(email: string) { const result = await pool.query( 'SELECT * FROM users WHERE email = $1', [email] ); return result.rows[0];}
// JSONB поиск с GIN индексомasync function findProductsByBrand(brand: string) { const result = await pool.query( 'SELECT * FROM products WHERE attributes @> $1', [JSON.stringify({ brand })] ); return result.rows;}
// Поиск по массиву с GIN индексомasync function findArticlesByTags(tags: string[]) { const result = await pool.query( 'SELECT * FROM articles WHERE tags && $1', [tags] ); return result.rows;}
// Анализ использования индексаasync function explainQuery(query: string, params: any[]) { const result = await pool.query( `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${query}`, params ); console.log(JSON.stringify(result.rows[0], null, 2));}💡 Best Practices
Заголовок раздела «💡 Best Practices»- Составные индексы: Помещайте селективные колонки первыми
- Частичные индексы: Индексируйте только нужные строки (WHERE clause)
- Покрывающие индексы: Добавляйте INCLUDE для избежания обращения к таблице
- Мониторинг: Регулярно проверяйте неиспользуемые индексы
- EXPLAIN ANALYZE: Всегда проверяйте планы запросов
⚠️ Частые ошибки
Заголовок раздела «⚠️ Частые ошибки»- Создание индексов на каждую колонку (overhead на INSERT/UPDATE)
- Игнорирование порядка колонок в составных индексах
- Не использование частичных индексов для фильтрации
- Забывают обновлять статистику (
ANALYZE)
Следующий урок: Триггеры в PostgreSQL →