TSQL делится на ноль, несмотря на отсутствие столбцов, содержащих 0 - PullRequest
9 голосов
/ 04 марта 2011

Я пытался понять, почему я получаю «деление на ноль» (Msg 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, но это а) не имеет смысла, imho иб) при попытке воспроизвести ошибку с тестовой настройкой все работает нормально (см. ниже):

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.

Ответы [ 4 ]

13 голосов
/ 05 марта 2011

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

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

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

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

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

Пол

4 голосов
/ 04 марта 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
1 голос
/ 04 марта 2011

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

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

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

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

0 голосов
/ 01 февраля 2012

Я столкнулся с той же проблемой.В моем случае 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
...