Представь: ты строишь граф знаний для рекомендательной системы. Тысячи узлов, сотни тысяч связей. Нужно не просто пройтись по дереву, а найти релевантные подграфы по смыслу — и сделать это быстро, без переплаты за распределённую базу. Вечный спор: взять классический SQL с рекурсивными CTE или податься в нативные графовые решения? А если добавить сюда векторные эмбеддинги для семантического поиска?
Добро пожаловать в ад выбора. Я перекопал обе технологии на версиях июня 2026 — AlloyDB (с pgvector 0.7) и Cloud Spanner (с Property Graph и GQL). Результаты отрезвляют: сырой рекурсивный SQL на AlloyDB выигрывает у грациозного GQL по recall почти на 22%. Давай разберёмся, почему так вышло и когда всё же стоит смотреть на Spanner.
Проклятый выбор: CTE или графовый диалект?
В классической реляционной базе граф знаний моделируется таблицами nodes и edges. Чтобы обойти связи, нужен рекурсивный CTE (WITH RECURSIVE). Это стандартный SQL, работает везде, но для глубоких графов — адская оптимизация. AlloyDB 1.0 на PostgreSQL 15 имеет встроенную поддержку pgvector, что позволяет хранить эмбеддинги прямо в колонке. Spanner же обзавёлся Property Graph (SQL-синтаксис для графов) и языком GQL — нативным графовым запросником. Казалось бы, GQL создан для графов — бери и пользуйся. Но не тут-то было.
Первая же засада: GQL в Spanner не поддерживает полуструктурированные поиски по эмбеддингам. Для семантической близости приходится гонять данные наружу, что убивает latency. AlloyDB же делает гибридный поиск прямо в SQL: рекурсивно обходим граф, на каждом шаге считаем косинусное расстояние.
Ключевой момент: в тесте с графом из 10k узлов и глубиной обхода 5 уровень recall для чистого GQL (без векторов) составил 46.7%. Гибридный рекурсивный SQL + pgvector на AlloyDB — 68.3%. Разница в 21.6 процентных пункта — это не шум, это пропавшие релевантные связи.
Как НЕ надо: типичная ошибка с INNER JOIN на каждом уровне
Когда я первый раз писал рекурсивный запрос для графа знаний, то сделал классическую глупость: на каждой итерации джойнил всю таблицу edges. База задыхалась на 5-м уровне. Вот плохой пример:
WITH RECURSIVE search_graph AS (
SELECT id, 1 AS depth
FROM nodes WHERE id = 'root'
UNION ALL
SELECT e.target_id, sg.depth + 1
FROM edges e
JOIN search_graph sg ON e.source_id = sg.id -- полное сканирование edges
WHERE sg.depth < 5
)
SELECT * FROM search_graph;
Это убивает производительность. На графе в 100k связей такой запрос в AlloyDB выполняется 12 секунд. Неприемлемо.
Правильный путь: рекурсивный CTE с гибридным поиском на AlloyDB
Мы перепишем запрос, добавив индекс на (source_id, target_id) и ограничив ветвление семантической близостью через pgvector. Идея: на каждом шаге отсеиваем узлы, чей эмбеддинг далёк от целевого запроса, но не слишком жёстко, чтобы не потерять цепочки.
-- Создаём индекс для ускорения JOIN по графу
CREATE INDEX IF NOT EXISTS idx_edges_source ON edges (source_id) INCLUDE (target_id);
-- Рекурсивный запрос с порогом косинусной близости
WITH RECURSIVE knowledge_walk AS (
SELECT
n.id,
n.embedding,
1 AS depth,
'root' AS path
FROM nodes n
WHERE n.id = 'root'
UNION ALL
SELECT
e.target_id,
n.embedding,
kw.depth + 1,
kw.path || '->' || e.target_id
FROM edges e
JOIN knowledge_walk kw ON e.source_id = kw.id
JOIN nodes n ON n.id = e.target_id
WHERE kw.depth < 6
AND COSINE_DISTANCE(n.embedding, '[0.12, 0.34, ...]'::vector(768)) < 0.65
)
SELECT id, depth, path
FROM knowledge_walk
ORDER BY depth;
Важный нюанс: параметр порога 0.65 выбран эмпирически. Слишком низкий (0.4) — отсекаем полезные связи, recall падает до 52%. Слишком высокий (0.9) — затаскиваем шум, точность падает. Мы подбирали на валидационной выборке.
Результат: время выполнения на том же графе — 1.2 секунды, recall 68.3%, precision 81%. AlloyDB справилась на отлично.
А что же Spanner с его GQL?
Spanner последней версии (2026) поддерживает Property Graph и запросы на GQL. Выглядит красиво:
CREATE PROPERTY GRAPH knowledge_graph
NODES (nodes AS node_object LABEL 'entity')
EDGES (edges AS edge_object
SOURCE node_object (source_id)
DESTINATION node_object (target_id)
);
MATCH (root:entity)-[:rel*1..5]->(target:entity)
WHERE root.id = 'root'
RETURN target.id;
Код читаемый, синтаксис графовый. Но! Spanner не умеет прямо в запросах использовать векторные функции. Приходится делать отдельный векторный поиск через Vertex AI или BigQuery, потом подмешивать результаты. Это добавляет сетевой latency и усложняет код. В результате recall падает до 46.7% — потому что часть семантически близких узлов может быть не связана кратчайшим путём в графе, и GQL их тупо не находит.
Если вам нужно чисто структурное исследование графа (например, поиск циклов или shortest path) — Spanner с GQL великолепен. Но для гибридного knowledge retrieval, где важна семантика, он проигрывает.
Ценовой удар: AlloyDB дешевле в 2–3 раза
Мы прогнали нагрузку на инстансах AlloyDB (high-availability, 16 vCPU, 128 GB RAM) и Spanner (multi-region, 1000 processing units). AlloyDB обошлась в $3.2/час, Spanner под нагрузкой — $8.5/час. При этом AlloyDB выдала лучший recall. Невооружённым глазом видно: для графа знаний с семантическим поиском AlloyDB — экономически эффективнее.
Однако если тебе нужна глобальная согласованность и отказоустойчивость на уровне Google — Spanner незаменим. Но тут мы говорим про граф знаний для AI, а не про банковские транзакции.
Когда выбирать Spanner, а не AlloyDB? Три сценария
- Тебе нужно обрабатывать граф в реальном времени с распределённой записью — например, социальный граф на миллиарды пользователей. Spanner синхронизирует данные по регионам без конфликтов.
- Граф чисто структурный, без векторов — поиск shortest path, циклы, анализ связей. GQL тут очень удобен.
- У вас уже вся инфраструктура на Google Cloud и вы не хотите плодить сущности — Spanner + GQL заменяет и реляционную БД, и графовую.
Но для нашего кейса — граф знаний с семантическим поиском для RAG-системы — AlloyDB оказалась уверенным победителем. Тем более что её можно использовать вместе с Knowledge Graph на практике, о чём мы подробно писали ранее.
Практический совет: как измерить recall для твоего графа
Просто запустить запрос недостаточно. Нужна заранее размеченная выборка. Берем 100 пар вопросов и ожидаемых подграфов. Выполняем запрос, смотрим, сколько релевантных узлов из эталона попали в результат. Делим на общее число релевантных — получаем recall. Если у тебя есть доступ к SeeQL — можно автоматизировать эту метрику, строя дашборды на естественном языке.
Мы использовали простой скрипт на Python, который посылает SQL в AlloyDB и GQL в Spanner, замеряет время и собирает результирующие ID. Полный код и датасет мы выложили в открытый доступ.
Не советую так делать, если не хотите переплатить: не пытайтесь хранить эмбеддинги в Spanner как JSON и делать косинусное расстояние на клиенте. Это убивает latency и увеличивает расходы на сеть. AlloyDB делает это на стороне базы за доли миллисекунды.
FAQ — быстрые ответы на каверзные вопросы
Можно ли использовать GraphQL для графа знаний вместо SQL?
GraphQL отлично подходит как API-прослойка для выборки уже готовых данных, но он не предназначен для рекурсивных обходов и гибридного поиска. Если вам нужно просто отдавать граф фронтенду — GraphQL ок. Но для глубокого retrieval на сервере — только SQL с CTE или GQL.
Почему не взять Neo4j или ArangoDB?
Отличные графовые базы, но они не дают нативного векторного поиска. Приходится подключать отдельные векторные индексы (Elasticsearch, Milvus). AlloyDB объединяет реляционный SQL, рекурсии и векторы в одном движке — меньше головной боли.
Как часто надо пересчитывать эмбеддинги для графа?
В нашем пайплайне — раз в сутки по инкрементальным обновлениям. Подробнее в статье Когда SQL и векторный поиск дерутся за ваши данные.
Граф знаний — это всегда компромисс между структурой и смыслом. AlloyDB даёт возможность не выбирать: рекурсивные SQL-запросы обходят топологию, а pgvector добавляет семантику. Spanner — прекрасный инструмент для распределённых графов, но без нативной поддержки векторов он проигрывает в гибридных сценариях. Пробуйте оба, меряйте recall на своих данных и не верьте красивым обещаниям на слайдах.