Как реализовать проверку наличия потомков у узла в SQL
Что проверяют
Понимаете ли вы, что для ответа «есть ли потомки» рекурсия не нужна, и умеете ли написать запрос, который не читает лишнего. Следом обычно спрашивают, как вывести дерево целиком и как это ложится на код на Go без запроса на каждый узел.
Как отвечать
Если у узла есть хотя бы один потомок, значит есть и прямой ребёнок. Для модели «список смежности» (id, parent_id) достаточно запроса SELECT EXISTS (SELECT 1 FROM nodes WHERE parent_id = $1).
EXISTS останавливается на первой найденной строке, а COUNT(*) > 0 посчитает всех детей. Под запрос нужен индекс по parent_id: PostgreSQL сам индексирует первичный ключ, но не колонку, которая ссылается внешним ключом.
Для списка узлов флаг считают одним запросом: SELECT n.id, EXISTS (SELECT 1 FROM nodes c WHERE c.parent_id = n.id) AS has_children FROM nodes n.
В Go это db.QueryRowContext(ctx, q, id).Scan(&hasChildren) в database/sql или pool.QueryRow(ctx, q, id).Scan(&hasChildren) в pgx, результат EXISTS сканируется прямо в bool. Плейсхолдеры зависят от драйвера: $1 в PostgreSQL, ? в MySQL.
WITH RECURSIVE нужен, когда требуется всё поддерево или глубина. В PostgreSQL 14 и новее у него есть конструкция CYCLE, которая защищает от зацикливания на битых данных.
Если дерево читают намного чаще, чем меняют, назовите альтернативные модели:
- materialized path: путь вида
1.4.9в колонке, в PostgreSQL для этого есть типltreeс оператором<@; - nested sets: потомки есть, если
rgt - lft > 1, но вставка пересчитывает заметную часть таблицы; - closure table: отдельная таблица всех пар «предок, потомок», быстрые чтения ценой лишних строк.
Где спотыкаются
Пишут рекурсивный CTE ради проверки существования. Запрос обходит всё поддерево, хотя ответ известен после первой строки.
Проверяют флаг из Go в цикле по узлам, отдельным запросом на каждый. Это классическое N+1: на тысяче узлов тысяча обращений к базе.
Забывают про мягкое удаление: если у детей есть deleted_at, условие deleted_at IS NULL должно стоять и внутри EXISTS, иначе удалённый ребёнок продолжит делать узел родителем.