🎯 GiST индексы: когда B-tree сдох, а GIN перебор
GiST (Generalized Search Tree) — индекс для нестандартной хуйни, которую обычные индексы не переваривают. Геоданные, диапазоны, полнотекст, кастомные типы.
Если данные не укладываются в "больше-меньше-равно", твой выбор.
🗺️ PostGIS: координаты не работают с B-tree
Задача: найти все кафе в радиусе 1км. B-tree на lat/lng = Seq Scan и пиздец.
Бенчмарк: 5M точек, радиус 1км от центра Москвы. B-tree квадрат: 180ms + ложные срабатывания. GiST круг: 12ms, точные результаты.
🐕 Первое сравнение: геометрия
Пёс ищет еду в квадратной комнате — проверяет все углы (B-tree квадрат). Умный пёс ищет в радиусе от своей миски — только актуальная зона (GiST круг). Второй находит быстрее и без лишнего.
📝 Полнотекстовый поиск: GiST vs GIN
GIN быстрее для поиска. GiST меньше весит и быстрее обновляется.
Когда GiST: частые INSERT/UPDATE (логи, новости, чаты), размер индекса критичен.
Когда GIN: поиск важнее вставок, read-heavy база.
"GIN всегда лучше для полнотекста"
Реальность: Новостной сайт, 500 статей/час. GIN обновление 120ms × 500 = 16 секунд CPU/час. GiST: 45ms = 6 секунд. Экономия 240 часов CPU/месяц =
"Геоданные сложные, используем два WHERE на lat/lng"
Реальность: Квадрат вместо круга = пользователь видит кафе в 5км, но не видит в 900м за углом. Конверсия -15%, потому что Иванов И.И. "упростил".
```sql
-- Бронирование переговорок:
CREATE TABLE bookings (
id SERIAL PRIMARY KEY,
room_id INT,
time_range tstzrange
);
CREATE INDEX idx_bookings_time ON bookings USING GIST (time_range);
-- Найти пересечения:
SELECT * FROM bookings
WHERE room_id = 5 AND time_range && tstzrange('2025-11-14 14:00', '2025-11-14 16:00');
-- EXCLUSION: физически запретить двойное бронирование
CREATE TABLE bookings (
room_id INT,
time_range tstzrange,
EXCLUDE USING GIST (room_id WITH =, time_range WITH &&)
);
-- База сама гарантирует непересечение, двойное бронирование невозможно
```
Спрашиваешь собаку "ты свободен 14:00-16:00?" B-tree проверяет начало и конец отдельно, путается. GiST понимает диапазоны целиком: пёс занят 15:00-17:00 = пересечение есть, слот занят. Логика правильная. 🚀 PostgreSQL 16: параллельное построение
До PG16: GiST строился в один поток. 100M строк = 40 минут.
PostgreSQL 16:
Прирост PG16: построение GiST +60-70% скорость, rebuild в 3-4 раза быстрее, VACUUM +40%.
🔥 Кастомные типы
B-tree = овчарка: "сидеть", "лежать", "охранять". Команды простые, работает быстро.
GiST = служебный пёс: поиск взрывчатки, наркотиков, людей в завалах. Сложнее тренировать, медленнее в базовых командах, но умеет то, что овчарка физически не может.
Пытаешься научить овчарку искать людей в завалах (геопоиск). Нужен специально обученный пёс.
Практический чеклист
Используй GiST:
✅ Геоданные (PostGIS): координаты, полигоны
✅ Диапазоны: tstzrange, daterange, int4range
✅ Полнотекст с частыми UPDATE
✅ EXCLUSION constraints
НЕ используй:
❌ Обычные числа/строки (B-tree быстрее)
❌ JSONB/массивы (GIN эффективнее)
❌ Полнотекст read-heavy (GIN быстрее)
Итого: GiST в PostgreSQL 16
PostGIS? GiST обязателен. Диапазоны? GiST. Полнотекст с частыми вставками? GiST. Все остальное? Смотри другие индексы.
В PG16: параллельное построение (+60%), обнови max_parallel_maintenance_workers с дефолтных 2 до числа cores.
GiST (Generalized Search Tree) — индекс для нестандартной хуйни, которую обычные индексы не переваривают. Геоданные, диапазоны, полнотекст, кастомные типы.
Если данные не укладываются в "больше-меньше-равно", твой выбор.
🗺️ PostGIS: координаты не работают с B-tree
Задача: найти все кафе в радиусе 1км. B-tree на lat/lng = Seq Scan и пиздец.
-- Не делай так (B-tree на координатах):
CREATE INDEX idx_fail ON locations(lat, lng);
SELECT * FROM locations
WHERE lat BETWEEN 55.74 AND 55.76 AND lng BETWEEN 37.60 AND 37.62;
-- Найдет квадрат, не круг. Кафе из соседнего района тоже попадут
-- Правильно (GiST + PostGIS):
CREATE EXTENSION postgis;
ALTER TABLE locations ADD COLUMN geom geography(POINT, 4326);
UPDATE locations SET geom = ST_SetSRID(ST_MakePoint(lng, lat), 4326);
CREATE INDEX idx_locations_geom ON locations USING GIST (geom);
SELECT * FROM locations
WHERE ST_DWithin(geom, ST_MakePoint(37.617635, 55.755826)::geography, 1000);
-- Execution: 12ms на 5M точек vs 4500ms Seq Scan
Бенчмарк: 5M точек, радиус 1км от центра Москвы. B-tree квадрат: 180ms + ложные срабатывания. GiST круг: 12ms, точные результаты.
🐕 Первое сравнение: геометрия
Пёс ищет еду в квадратной комнате — проверяет все углы (B-tree квадрат). Умный пёс ищет в радиусе от своей миски — только актуальная зона (GiST круг). Второй находит быстрее и без лишнего.
📝 Полнотекстовый поиск: GiST vs GIN
GIN быстрее для поиска. GiST меньше весит и быстрее обновляется.
CREATE TABLE articles (id SERIAL, title TEXT, content TEXT, tsv tsvector);
-- GIN: быстрый поиск
CREATE INDEX idx_gin ON articles USING GIN (tsv);
-- Размер: 450MB | Поиск: 8ms | INSERT: 120ms
-- GiST: быстрая вставка
CREATE INDEX idx_gist ON articles USING GIST (tsv);
-- Размер: 280MB (на 38% меньше!) | Поиск: 25ms | INSERT: 45ms
SELECT * FROM articles WHERE tsv @@ to_tsquery('russian', 'postgresql & индексы');
Когда GiST: частые INSERT/UPDATE (логи, новости, чаты), размер индекса критичен.
Когда GIN: поиск важнее вставок, read-heavy база.
"GIN всегда лучше для полнотекста"
Реальность: Новостной сайт, 500 статей/час. GIN обновление 120ms × 500 = 16 секунд CPU/час. GiST: 45ms = 6 секунд. Экономия 240 часов CPU/месяц =
"Геоданные сложные, используем два WHERE на lat/lng"
Реальность: Квадрат вместо круга = пользователь видит кафе в 5км, но не видит в 900м за углом. Конверсия -15%, потому что Иванов И.И. "упростил".
```sql
-- Бронирование переговорок:
CREATE TABLE bookings (
id SERIAL PRIMARY KEY,
room_id INT,
time_range tstzrange
);
CREATE INDEX idx_bookings_time ON bookings USING GIST (time_range);
-- Найти пересечения:
SELECT * FROM bookings
WHERE room_id = 5 AND time_range && tstzrange('2025-11-14 14:00', '2025-11-14 16:00');
-- EXCLUSION: физически запретить двойное бронирование
CREATE TABLE bookings (
room_id INT,
time_range tstzrange,
EXCLUDE USING GIST (room_id WITH =, time_range WITH &&)
);
-- База сама гарантирует непересечение, двойное бронирование невозможно
```
Спрашиваешь собаку "ты свободен 14:00-16:00?" B-tree проверяет начало и конец отдельно, путается. GiST понимает диапазоны целиком: пёс занят 15:00-17:00 = пересечение есть, слот занят. Логика правильная. 🚀 PostgreSQL 16: параллельное построение
До PG16: GiST строился в один поток. 100M строк = 40 минут.
PostgreSQL 16:
CREATE INDEX CONCURRENTLY idx_locations_geom ON locations USING GIST (geom);
-- Автоматически распараллеливает
-- 8 cores: 40 минут → 12 минут | 16 cores: 40 минут → 7 минут
SET max_parallel_maintenance_workers = 4; -- дефолт 2, можно до CPU cores
Прирост PG16: построение GiST +60-70% скорость, rebuild в 3-4 раза быстрее, VACUUM +40%.
🔥 Кастомные типы
-- Диапазоны дат:
CREATE INDEX idx_date_ranges ON events USING GIST (daterange(start_date, end_date));
-- IP адреса:
CREATE EXTENSION btree_gist;
CREATE INDEX idx_ip_ranges ON networks USING GIST (ip_range);
-- Массивы с пересечениями:
CREATE INDEX idx_tags ON articles USING GIST (tags gist__intbig_ops);
B-tree = овчарка: "сидеть", "лежать", "охранять". Команды простые, работает быстро.
GiST = служебный пёс: поиск взрывчатки, наркотиков, людей в завалах. Сложнее тренировать, медленнее в базовых командах, но умеет то, что овчарка физически не может.
Пытаешься научить овчарку искать людей в завалах (геопоиск). Нужен специально обученный пёс.
Практический чеклист
Используй GiST:
✅ Геоданные (PostGIS): координаты, полигоны
✅ Диапазоны: tstzrange, daterange, int4range
✅ Полнотекст с частыми UPDATE
✅ EXCLUSION constraints
НЕ используй:
❌ Обычные числа/строки (B-tree быстрее)
❌ JSONB/массивы (GIN эффективнее)
❌ Полнотекст read-heavy (GIN быстрее)
Итого: GiST в PostgreSQL 16
PostGIS? GiST обязателен. Диапазоны? GiST. Полнотекст с частыми вставками? GiST. Все остальное? Смотри другие индексы.
В PG16: параллельное построение (+60%), обнови max_parallel_maintenance_workers с дефолтных 2 до числа cores.