GiST держит дерево бounding-boxes, которые могут перекрываться. Это компромисс: можно складывать любые объекты, но поиск иногда спускается в несколько веток. Для многих типов данных это нормально (range types, geo).
Но есть категория данных, где перекрытие — фундаментальная проблема:
- Иерархические строки: URL
https://a.example.com/users/123— естественно разбивается по префиксу. Если делать GiST с overlap, дерево будет деградировать. - Точки в плоскости с фрактальным распределением: 90% точек в Москве, 10% размазаны по миру. Балансированное дерево не вырастет.
- Сети IP (
inet,cidr): тоже префиксное.
Для них в Postgres есть
Главное отличие: непересекающееся разбиение
В SP-GiST каждое значение принадлежит ровно одному поддереву. Это значит:
- Дерево может быть несбалансированным.
- При поиске мы спускаемся по строго одной ветке, без ветвления.
- Узлы дерева могут иметь разный fanout (число детей): в зависимости от того, как тип данных решит разбить пространство в этой точке.
Каждый узел — символ. Слово 'cat' идёт по пути c -> a -> t. Слово 'car' разделяется на уровне 'a', где есть выбор между 't' (cat) и 'r' (car). Узел может иметь сколько угодно детей.
В GiST все эти слова попали бы в bounding boxes по символьному диапазону [a-z], перекрывающиеся в большой степени. В SP-GiST разбиение детерминировано: каждый символ — отдельная ветка.
Operator classes из коробки
PostgreSQL поставляет несколько SP-GiST opclass:
text_ops— prefix tree для текста. Хорошо ищетLIKE 'foo%',text_pattern_ops.inet_ops— для IP-сетей (inet,cidr). Идеально подходит дляWHERE ip <<= '10.0.0.0/8'(subnet contains).point_ops— quadtree для точек в плоскости. Альтернатива GiSTpoint_ops.box_ops/range_ops— другие geometric и range типы.kd_point_ops— k-d tree для точек, обычно эффективнее quadtree при равномерном распределении.
Пример: префиксный поиск по строкам
Создаём таблицу URLs и наполняем 30K строк типа 'https://*.example.com/path'. SP-GiST с text_ops подходит для prefix-LIKE поиска.
SP-GiST с text_ops vs B-tree с text_pattern_ops. Сравним на префиксе. Оба умеют LIKE 'prefix%'; разница в форме дерева.
На реалистично «префиксных» данных SP-GiST часто компактнее B-tree, потому что общий префикс хранится один раз, а в B-tree повторяется в каждом ключе (хотя deduplication в B-tree сильно сглаживает эту разницу с Postgres 13+).
План запроса префиксного поиска. EXPLAIN на индексе SP-GiST: Index Scan по началу строки, не Seq Scan.
Пример: IP-префиксы
Один из самых сильных кейсов SP-GiST — inet с inet_ops. Запросы вида «какая подсеть содержит этот IP» обслуживаются мгновенно.
ip_ranges с подсетями. SP-GiST на inet идеально ложится на иерархию CIDR. Запрос: какая subnet содержит этот IP.
Оператор >>= означает «содержит или равен». SP-GiST использует структуру CIDR (каждый бит — узел) и спускается строго к нужной ветке.
SP-GiST vs GiST: когда что
| Признак | GiST | SP-GiST |
|---|---|---|
| Перекрытие веток | Может быть | Нет, строго раздельные |
| Балансировка | Стремится к балансу | Может быть несбалансирован |
| Multi-column | Поддерживается | Только с расширением (spgist_btree style) |
| Concurrent insert | Хорошо | Хуже (split может блокировать) |
| Best fit | Range, geo (равномерное распределение) | Prefix, иерархия, фрактальное распределение |
Практическое правило: если у тебя естественно префиксные или естественно иерархические данные (URL, путь к файлу, телефонный номер, CIDR) — SP-GiST скорее всего выиграет. Если данные равномерные и многомерные (координаты домов) — GiST.
Подводные камни
-
Глубина без баланса. В худшем случае (вырожденные данные — например, 10K строк, каждая длиной 1000 символов, отличающиеся последним байтом) SP-GiST trie вырастает в очень глубокое дерево. Поиск становится медленным, page reads растут. Обычно это решается префиксным сжатием внутри узла (Patricia trie вместо обычного trie), что SP-GiST для text_ops умеет.
-
Высокая стоимость split. При концентрированных вставках в одну ветку приходится часто перестраивать локальную часть дерева. Это написано в коде менее эффективно, чем split в B-tree.
-
Меньше распространён. У SP-GiST меньшее покрытие opclass, чем у B-tree или GiST. Если твой тип данных не имеет встроенного SP-GiST opclass — реализовать его руками возможно, но это серьёзная инженерная работа.
Quadtree для точек: альтернатива GiST point_ops
Для типа point доступны и gist_point_ops (GiST с R-tree-подобной структурой), и spgist_point_ops / spgist_kd_point_ops (SP-GiST quadtree / k-d tree). Какой выбрать?
- На равномерных данных (плотность ~постоянная по плоскости): GiST R-tree обычно выигрывает на 5-10% — bounding boxes складываются хорошо.
- На кластерных/фрактальных (90% точек в одной зоне): SP-GiST quadtree выигрывает существенно, потому что плотная зона раскладывается в глубокое поддерево, а пустые зоны не занимают ничего.
- На равномерных с KNN-запросом (find nearest 10 points): GiST поддерживает
<->оператор изначально, SP-GiST — да, но с Postgres 12+; проверь версию.
Пишем свой opclass: что внутри
Если у тебя есть кастомный тип данных (например, телефонный номер с иерархией код страны / код города / номер) и хочется индекса — SP-GiST даёт пять callback-функций:
- config — параметры (тип ключа узла, тип листа).
- choose — куда добавить новое значение при insert (в какое поддерево, нужно ли split-нуть узел).
- picksplit — как разделить переполненный узел: какие значения куда.
- inner_consistent — при поиске, в какие дочерние узлы спускаться.
- leaf_consistent — проверка финального совпадения на листе.
Это меньше, чем у GiST (там 7 callbacks), и более «декларативно». Если ты пишешь расширение и думаешь про индекс — SP-GiST часто проще.
Multi-column через SP-GiST
Из коробки SP-GiST не поддерживает multi-column ключи — это известное ограничение. Workaround: сделай expression index на конкатенацию или составить кастомный тип. Альтернатива — построить два отдельных индекса и положиться на bitmap AND в EXPLAIN, что в большинстве случаев достаточно эффективно.
-- Не работает напрямую:
-- CREATE INDEX ON urls USING SPGIST (domain, path);
-- Workaround: expression
CREATE INDEX urls_combined_spgist
ON urls USING SPGIST ((domain || '|' || path));
Краткий путеводитель: какой индекс выбрать
| Сценарий | Лучший выбор |
|---|---|
| Равенство по короткому скаляру (id, status) | B-tree |
| Equality по длинному UUID/token | Hash или B-tree (UNIQUE — только B-tree) |
| JSONB поиск по ключу-значению | GIN (jsonb_path_ops для @>) |
| Массив text[] @> ARRAY[…] | GIN |
| Full-text по tsvector, read-heavy | GIN |
| Full-text по tsvector, write-heavy | GiST |
| Range types (tstzrange &&) | GiST |
| Geo (точки, polygons) | GiST + PostGIS |
| Time-series по timestamp, append-only | BRIN |
| Sequential ID на огромной таблице | BRIN |
| URL префикс, LIKE ‘prefix%‘ | SP-GiST text_ops |
| LIKE ‘%substring%‘ | GIN pg_trgm |
| IP-подсети, subnet containment | SP-GiST inet_ops |
| KNN — ближайшие точки | GiST <-> |
Эта таблица — приближение. В реальной системе нужно смотреть EXPLAIN ANALYZE на конкретной нагрузке.
Чек-лист
- SP-GiST — фреймворк для индексов с неперекрывающимся пространственным разбиением.
- Дерево может быть несбалансированным — отлично для данных с естественной иерархией.
- Встроенные opclass:
text_ops(trie),inet_ops(CIDR),point_ops(quadtree),kd_point_ops(k-d tree),range_ops,box_ops. - Хорош для: URL/префиксного поиска, IP-сетей, фрактальных точек, телефонных номеров.
- Плох для: данных без иерархической структуры, multi-column ключей, высокого concurrent insert.
- GiST vs SP-GiST: GiST разрешает overlap (универсальнее, но иногда медленнее на поиске), SP-GiST — нет (специализированнее, быстрее на подходящих данных).
- Тип данных может реализовать свой opclass — пишутся 5 callback-функций.