Обнаружено деление TSQL на ноль, несмотря на отсутствие столбцов, содержащих 0

Я пытался понять, почему я получаю «деление на ноль» (сообщение 8134) с моим SQL-запросом, но я должен что-то упустить. Я хотел бы знать, почему для конкретного случая ниже, я не ищу NULLIF, CASE WHEN... или подобные, поскольку я уже знаю о них (и, конечно, могу использовать их в ситуации, как показано ниже).

У меня есть оператор SQL с вычисляемым столбцом, похожим на

SELECT
    TotalSize,
    FreeSpace,
    (FreeSpace / TotalSize * 100)
FROM
    tblComputer
...[ couple of joins ]...
WHERE
    SomeCondition = SomeValue

Выполнение этого оператора приводит к ошибкам с вышеупомянутыми сообщениями об ошибках, что само по себе не является проблемой - очевидно, что TotalSize вполне может быть 0 и, следовательно, вызывать ошибку.

Теперь я не понимаю, что у меня нет строк, в которых столбец TotalSize равен 0, когда я комментирую вычисляемый столбец, я дважды проверил, что это не так.

Затем я подумал, что по какой-то причине вычисление столбца будет выполняться для всего набора результатов до фактической фильтрации с условиями предложения where, но это а) не имело бы смысла имхо и б) когда при попытке воспроизвести ошибку на тестовой установке все работает нормально (см. ниже):

INSERT INTO tblComputer (ComputerName, IsServer) VALUES ('PC0001',1)
INSERT INTO tblComputer (ComputerName, IsServer) VALUES ('PC0002',1)
INSERT INTO tblComputer (ComputerName, IsServer) VALUES ('PC0003',1)
INSERT INTO tblComputer (ComputerName, IsServer) VALUES ('PC0004',0)
INSERT INTO tblComputer (ComputerName, IsServer) VALUES ('PC0005',1)
INSERT INTO tblComputer (ComputerName, IsServer) VALUES ('PC0006',0)
INSERT INTO tblComputer (ComputerName, IsServer) VALUES ('PC0007',1)

INSERT INTO tblHDD (ComputerID, TotalSize, FreeSpace) VALUES (1,100,21)
INSERT INTO tblHDD (ComputerID, TotalSize, FreeSpace) VALUES (2,100,10)
INSERT INTO tblHDD (ComputerID, TotalSize, FreeSpace) VALUES (3,100,55)
INSERT INTO tblHDD (ComputerID, TotalSize, FreeSpace) VALUES (4,0,10)
INSERT INTO tblHDD (ComputerID, TotalSize, FreeSpace) VALUES (5,100,23)
INSERT INTO tblHDD (ComputerID, TotalSize, FreeSpace) VALUES (6,100,18)
INSERT INTO tblHDD (ComputerID, TotalSize, FreeSpace) VALUES (7,100,11)

-- This statement does not throw an error as apparently the row for ComputerID 4 
-- is filtered out before computing the (FreeSpace / TotalSize * 100)
SELECT 
TotalSize,
FreeSpace,
(FreeSpace / TotalSize * 100)
FROM 
tblComputer
JOIN
tblHDD ON
tblComputer.ID = tblHDD.ComputerID
WHERE
IsServer = 1

Я совсем запутался и хотел бы знать, в чем причина.

Приветствуются любые идеи или указатели в правильном направлении, заранее спасибо

Обновить

Спасибо за ваш вклад, но, к сожалению, я, кажется, не приближаюсь к корню проблемы. Мне удалось немного сократить оператор, и теперь у меня есть случай, когда я могу выполнить его без ошибок, если удалить одно JOIN (мне это понадобится для дополнительных столбцов в выводе, которые я временно удалил).

Я не понимаю, почему использование JOIN приводит к ошибке, разве стандартный INNER JOIN не должен всегда возвращать то же количество строк или меньше, но никогда не больше?

Рабочий код

SELECT 
TotalSize,
FreeSpace
((FreeSpace / TotalSize) * 100)
FROM 
MyTable1
INNER JOIN 
MyTable2 ON
MyTable1.ID = MyTable2.Table1ID
WHERE 
SomeCondition

Ошибка, вызывающая код

SELECT 
TotalSize,
FreeSpace
((FreeSpace / TotalSize) * 100)
FROM 
MyTable1
INNER JOIN 
MyTable2 ON
MyTable1.ID = MyTable2.Table1ID
-- This JOIN causes "divide by zero encountered" error
INNER JOIN 
MyTable3 ON
MyTable2.ID = MyTable3.Table2ID
WHERE 
SomeCondition

Я также попытал счастья, используя курсор и перебирая результат построчно, но в этом случае не возникло ошибки (независимо от того, какой из двух приведенных выше операторов я пробовал).

Извините за беспорядочный отступ кода, почему-то кажется, что правильное форматирование не применяется.

G.


person Gorgsenegger    schedule 04.03.2011    source источник


Ответы (4)


SQL — декларативный язык; вы пишете запрос, который логически описывает желаемый результат, но оптимизатор должен создать физический план. Этот физический план может не иметь большого отношения к письменной форме запроса, потому что оптимизатор не просто переупорядочивает «шаги», полученные из текстовой формы запроса, он может применять более 300 различных преобразований, чтобы найти эффективную стратегию выполнения.

Оптимизатор имеет значительную свободу в переупорядочивании выражений, объединений и других конструкций логических запросов. Это означает, что вы не можете, вообще говоря, полагаться на какую-либо письменную форму запроса, чтобы принудительно оценить одну вещь перед другой. В частности, переписывание, предложенное Ливеном, не заставляет вычислять предикат предложения WHERE перед выражением. Оптимизатор может, в зависимости от оценки стоимости, решить вычислить выражение там, где это кажется наиболее эффективным. В некоторых случаях это может даже означать, что выражение вычисляется более одного раза.

Первоначальный вопрос рассматривал эту возможность, но отвергал ее как «не имеющую особого смысла». Тем не менее, продукт работает именно так: если SQL Server оценит, что объединение уменьшит размер набора настолько, что это удешевит вычисление выражения для результата объединения, он может это сделать.

Общее правило состоит в том, чтобы никогда не зависеть от конкретного порядка вычисления, чтобы избежать таких вещей, как ошибки переполнения или деления на ноль. В этом примере можно использовать оператор CASE для проверки делителя нуля — пример защитного программирования.

Свобода оптимизатора переупорядочивать вещи является фундаментальным принципом его дизайна. Вы можете найти случаи, когда это приводит к нелогичному поведению, но в целом преимущества намного перевешивают недостатки.

Павел

person Community    schedule 05.03.2011
comment
Спасибо за объяснение, которое подтверждает то, что я начал предполагать в течение последних нескольких дней, и то, что было предложено Ливеном в комментариях выше. Так как я могу выбрать только один ответ, а он был более вовлечен в дело, я выбрал его, но еще раз спасибо за вашу помощь. - person Gorgsenegger; 08.03.2011
comment
+1. @Gorgsenegger - ответ SQLkiwi точен, а также отвечает, что не так с моим собственным предположением о попытке заставить оптимизатор сначала оценить предложение where. Я благодарю вас за вотум доверия, но вы действительно должны отметить это как принятый ответ. - person Lieven Keersmaekers; 08.03.2011
comment
@Lieven, ладно, если для тебя это не проблема, все равно спасибо ;-) - person Gorgsenegger; 08.03.2011
comment
легко ли указать в спецификации SQL, что это так? На самом деле я только что задал аналогичный вопрос, и некоторые люди, кажется, думают, что стандарт говорит, что вы должны сначала оценить предложение where. (грубый парафраз) stackoverflow.com/questions/5249415/ - person Peter Recore; 09.03.2011

Основные шаги, которые SQL Server использует для обработки одного оператора SELECT, включают следующий

  1. Синтаксический анализатор просматривает инструкцию SELECT и разбивает ее на логические единицы, такие как ключевые слова, выражения, операторы и идентификаторы.
  2. Дерево запросов, иногда называемое деревом последовательности, строится с описанием логических шагов, необходимых для преобразования исходных данных в формат, требуемый результирующим набором.
  3. Оптимизатор запросов анализирует различные способы доступа к исходным таблицам. Затем он выбирает последовательность шагов, которая дает самые быстрые результаты при использовании меньшего количества ресурсов. Дерево запросов обновляется, чтобы записать именно эту последовательность шагов. Окончательная оптимизированная версия дерева запросов называется планом выполнения.
  4. Реляционный механизм начинает выполнение плана выполнения. По мере обработки шагов, требующих данных из базовых таблиц, реляционная машина запрашивает, чтобы машина хранения передала данные из наборов строк, запрошенных из реляционной машины.
  5. Реляционный механизм обрабатывает данные, возвращенные из механизма хранения, в формат, определенный для набора результатов, и возвращает набор результатов клиенту.

Моя интерпретация вещей заключается в том, что нет нет гарантии, что ваше предложение where будет оценено до оценки вычисляемого столбца для всех строк.

Вы можете проверить это предположение, изменив свой запрос, как показано ниже, и заставив предложение where оцениваться перед вычислением.

SELECT
    TotalSize,
    FreeSpace,
    (FreeSpace / TotalSize * 100)
FROM (
  SELECT
      TotalSize,
      FreeSpace,
  FROM
      tblComputer
  ...[ couple of joins ]...
  WHERE
      SomeCondition = SomeValue
  ) t
person Lieven Keersmaekers    schedule 04.03.2011
comment
+1 Ага. Вы можете поместить скаляр вычисления до или после фильтра. Я помню, что видел хороший ответ Ремуса Русану об этом, но не могу найти его сейчас. - person Martin Smith; 04.03.2011
comment
Спасибо за ваш ответ, я обновил свой исходный пост, чтобы отразить мои последние выводы. К сожалению, я до сих пор не нашел ответа/причины проблемы... - person Gorgsenegger; 04.03.2011
comment
@Gorgsenegger, ответ заключается в том, что (почти) полностью зависит от оптимизатора, как и когда он решит оценивать вычисления. Используя вашу небольшую выборку, он решает оценить вычисление после оценки предложения where. В вашем оригинале все наоборот. Вы пытались изменить запрос, как я предложил? - person Lieven Keersmaekers; 04.03.2011
comment
@Lieven, да, я пытался поместить часть с условием в качестве подзапроса в from, но все равно получаю ошибку. - person Gorgsenegger; 04.03.2011
comment
Это то, чего я не ожидал... Не могли бы вы опубликовать план? - person Lieven Keersmaekers; 04.03.2011
comment
+1. Недавно меня укусили подобным образом. Мой select содержал выражение, которое преобразует varchar в datetime, что-то вроде cast(string_date as varchar). В то же время я использовал isdate(string_date) = 0 в качестве фильтра. И что ты думаешь? Я получил ошибку о неудачном преобразовании в дату и время. Я избавился от него только после того, как убедился, что все строки в выбранном наборе строк были правильными датами. Получается, что иногда select оценивается одновременно с where. - person Andriy M; 04.03.2011
comment
@Lieven, вы действительно хотите, чтобы я опубликовал весь план выполнения в виде XML-файла (около 500 строк)? Я могу сделать это, если вы хотите, но я хочу убедиться, что это именно то, о чем вы просили, прежде чем я заполню эту тему таким количеством строк ;-) - person Gorgsenegger; 08.03.2011
comment
@Gorgsenegger, если нет другой альтернативы ‹g›, но теперь, когда вы смогли сократить случай до двух простых запросов, первое, что я бы сделал, это сравнил оба плана выполнения и попытался работать оттуда. - person Lieven Keersmaekers; 08.03.2011
comment
Подводя итог всему в этой ветке, и после сравнения планов выполнения, мне кажется, как вы уже указали в своем первом комментарии выше (а также как упомянул SQLkiwi ниже), порядок выполнения и фильтрации произвольный и может изменение, основанное на вещах, которые оптимизатор принимает во внимание и которые мы (люди) можем не понять без дальнейшего изучения. Я выберу ваш ответ, так как я не могу выбрать два, и до сих пор вы приложили больше усилий к этой теме. Спасибо за вашу помощь, на этот раз я определенно узнал кое-что о SQL ;-) - person Gorgsenegger; 08.03.2011

Какие строки возвращаются при запуске:

SELECT
   TotalSize
FROM
   tblComputer
   ...[ couple of joins ]...
WHERE
   SomeCondition = SomeValue
   and ((TotalSize * 100) = 0)

Это может дать вам представление о том, как SQL Server оценивает (TotalSize * 100) как ноль.

Еще одна идея: есть ли что-то в вашем операторе where, что также может быть проблемой?
Вы предполагаете, что это TotalSize, но это может быть где-то еще.

person Bravax    schedule 04.03.2011
comment
Спасибо за предложение. Это не помогло мне в моем случае здесь, но стоит иметь в виду, так как это может быть полезно когда-нибудь позже :-) - person Gorgsenegger; 04.03.2011

Я столкнулся с той же проблемой. В моем случае значения NULL были приемлемы, поэтому я смог исправить это следующим образом:

Select Expression1 / Expression2 -- Caused Division By 0
Select Expression1 / NULLIF(Expression2,0) -- Causes result to be NULL

Если вам нужна другая обработка, вы можете обернуть все выражение в функцию ISNULL следующим образом:

Select ISNULL(Expression1 / NULLIF(Expression2,0)-5) -- Returns -5 instead of null or divide by 0
person Bytemaster    schedule 31.01.2012
comment
Спасибо, но, как я сказал в своем вопросе, я искал причину, а не способ обойти это, используя ваши предложения. - person Gorgsenegger; 05.02.2012
comment
Не знаю, как я пропустил это при первом прочтении. Другой ответ в значительной степени охватывает это - это зависит от плана выполнения, сгенерированного во время выполнения. Я только что столкнулся с этим, когда я пропустил это в одном из своих представлений - долгое время, но кто-то, наконец, придумал запрос против него, который изменил план настолько, чтобы вызвать ошибку. - person Bytemaster; 02.03.2012