Как объяснить ошибку типов в рекурсивном CTE, если рекурсивная ветвь возвращает логически совместимые значения?
Рекурсивный CTE должен иметь одинаковое число столбцов в якорной и рекурсивной частях, а соответствующие столбцы должны иметь совместимые типы. Во многих СУБД тип результата определяется якорной частью, поэтому значение из рекурсивной ветви может быть логически подходящим, но не соответствовать объявленному типу без явного приведения.
Рекурсивные CTE появились как декларативный способ описывать обход иерархий, графов и транзитивных связей без ручного управления циклом в прикладном коде. Такой подход позволяет выразить начальный набор строк и правило получения следующего набора в рамках одного SQL-оператора.
Для этого СУБД должна заранее определить схему промежуточного результата: количество столбцов, их имена и типы. Иначе невозможно корректно подготовить план выполнения и объединять результаты разных итераций.
Рекурсивный CTE состоит из якорной части, которая формирует начальные строки, и рекурсивной части, которая добавляет строки на следующих итерациях. Эти части объединяются по правилам, близким к UNION ALL, поэтому их результаты должны быть структурно совместимы.
Ошибка может возникнуть до обработки данных: например, якорная часть объявляет глубину как целое число, а рекурсивная вычисляет десятичное значение. Даже если каждое конкретное значение можно было бы преобразовать, СУБД не обязана выполнять такое преобразование неявно.
Сначала СУБД анализирует форму результата рекурсивного CTE. Обычно имена столбцов берутся из списка имён CTE или из якорной части, а типы соответствующих столбцов определяются преимущественно якорной частью либо правилами конкретного диалекта.
Затем рекурсивная ветвь проверяется на совместимость с этой схемой. Недостаточно, чтобы значения были похожи по смыслу: важны точный тип, возможность неявного приведения, а в некоторых СУБД также точность, масштаб, длина, сортировка и другие свойства типа.
Надёжное решение — явно выбрать тип результата и привести к нему обе части. Например, если глубина действительно может быть дробной, это нужно выразить уже в якорной части:
Если дробная часть не имеет смысла, правильнее привести рекурсивное выражение к целому типу, предварительно определив допустимое округление или усечение. Молчаливое преобразование опасно потерей точности, переполнением и различиями между СУБД.
Точный алгоритм разрешения типов различается по диалектам SQL. Поэтому переносимый запрос не должен полагаться на неявные преобразования; тип результата следует задавать явно, особенно для чисел, строк разной длины, дат и пользовательских типов.
В отчёте по структуре организации рекурсивный CTE вычислял уровень подразделения. Якорная часть возвращала целое значение 1, но после изменения формулы рекурсивная часть стала использовать деление и возвращать десятичный тип. В одной СУБД запрос перестал компилироваться, а в другой неявное преобразование могло привести к потере дробной части.
Рассматривались два варианта. Можно было оставить целевой тип целым и явно округлять результат, но это искажало смысл метрики. Можно было объявить уровень десятичным в якорной части и явно привести рекурсивный результат к той же точности; этот вариант сохранил данные и сделал контракт CTE очевидным.
После явного задания типа запрос стал предсказуемым при переносе между средами, а проверка схемы результата выявила аналогичные проблемы в других рекурсивных запросах.
Не всегда. Если якорная часть уже определила несовместимый тип результата, приведение только рекурсивного выражения может не помочь или привести к нежелательному преобразованию. Безопаснее явно задать целевой тип в якорной части и привести к нему рекурсивную часть.
Нет, типовая схема результата устанавливается при подготовке запроса. Каждая итерация должна возвращать строки этой схемы; СУБД не выбирает новый тип в зависимости от фактических значений очередного шага. Поэтому ошибка обычно обнаруживается до выполнения или при компиляции плана.
Да. Например, преобразование десятичного значения в целое может молча отбросить дробную часть, а преобразование длинной строки — обрезать её или вызвать ошибку. Явное приведение устраняет неоднозначность типов, но не заменяет проверку диапазона, точности и смысла преобразования.