В фильтре сначала проверяют, что делитель не равен нулю, а затем выполняют деление. Гарантирует ли SQL такой порядок вычисления условий?
Нет. SQL не гарантирует вычисление условий фильтра слева направо и не обязан применять одно условие как защиту от ошибки в другом. Оптимизатор может изменить порядок предикатов, поэтому проверку делителя следует встраивать непосредственно в выражение деления, например через NULLIF.
SQL является декларативным языком: запрос описывает требуемый результат, а не последовательность действий процессора. Это позволяет СУБД выбирать разные планы выполнения, применять индексы, переставлять предикаты и сокращать объём обрабатываемых данных.
Такой подход появился для автоматической оптимизации запросов без изменения их логического результата. Но он означает, что порядок записи отдельных условий не является надёжным механизмом управления побочными эффектами или предотвращения ошибок.
Предположим, фильтр должен выбрать строки, где знаменатель не равен нулю и результат деления превышает порог. Интуитивно можно ожидать, что сначала будет выполнена проверка знаменателя, а деление произойдёт только для безопасных строк.
Однако оптимизатор вправе начать с другого предиката, вычислить выражения заранее или использовать индекс. Если среди данных есть нулевой знаменатель, запрос может завершиться ошибкой деления, даже несмотря на наличие защитного условия.
Безопаснее сделать само деление определённым для нулевого знаменателя:
NULLIF(denominator, 0) возвращает NULL, если знаменатель равен нулю. Деление на NULL даёт NULL, а сравнение NULL > 10 имеет значение UNKNOWN; такая строка не проходит фильтр WHERE.
Важно различать логический порядок обработки запроса и физический порядок вычислений. Логически WHERE определяет отбираемые строки, но SQL не обещает последовательное физическое вычисление предикатов внутри этого условия.
Перестановка условий через AND обычно не должна менять логический результат, но может менять наличие ошибки, стоимость выполнения и использование индексов. Поэтому нельзя полагаться на запись более дешёвого или защитного условия слева.
Альтернативой может быть условное выражение CASE, возвращающее результат деления только для допустимого знаменателя. Для простого деления NULLIF обычно короче и яснее, но при необходимости вернуть специальное значение вместо NULL может подойти CASE.
Следует учитывать особенности конкретной СУБД: оптимизатор иногда вычисляет константные выражения ещё на этапе планирования. Поэтому даже визуально недостижимое опасное выражение не всегда безопасно. Надёжный принцип — не помещать потенциально ошибочную операцию в запрос без встроенной защиты.
В таблице финансовых показателей хранились числитель и знаменатель, причём старые строки могли иметь нулевой знаменатель. Разработчик добавил в WHERE проверку ненулевого знаменателя перед условием с делением, но после появления нового плана выполнения запрос начал периодически завершаться ошибкой.
Рассматривались два варианта. Можно было принудительно влиять на план или порядок вычисления средствами конкретной СУБД, но это ухудшало переносимость и связывало решение с реализацией оптимизатора. Можно было предварительно очищать данные, однако это не защищало запрос от будущих некорректных строк.
Выбрали NULLIF непосредственно в знаменателе. Запрос перестал зависеть от порядка проверки условий, строки с нулевым знаменателем корректно исключались, а оптимизатор сохранял возможность выбирать эффективный план.
Меняет ли скобочная группировка условий порядок их вычисления?
Нет. Скобки меняют структуру логического выражения и его результат при смешении операторов, но не превращают SQL в императивную последовательность вычислений. Даже отдельная группа условий не обязана вычисляться раньше другой, если СУБД может сохранить логический результат иным способом.
Всегда ли значение UNKNOWN в результате сравнения означает ошибку?
Нет. В SQL действует трёхзначная логика: результатом сравнения с NULL обычно становится UNKNOWN, а WHERE оставляет только строки с результатом TRUE. Поэтому выражение с безопасным делением, вернувшим NULL, обычно просто исключает строку из результата, а не завершает запрос ошибкой.
Можно ли использовать функцию, которая сама выбрасывает ошибку, в одном из условий как будто она защищена другим условием?
Нельзя полагаться на такую защиту. Пользовательская или встроенная функция может быть вычислена оптимизатором раньше ожидаемого момента, а её свойства, например детерминированность и стоимость, могут влиять на план. Потенциально ошибочный вызов нужно сделать безопасным на уровне аргументов или заменить выражением, явно обрабатывающим проблемные значения.