11

我一直试图理解为什么我的 SQL 查询会出现“除以零”(Msg 8134),但我一定遗漏了一些东西。我想知道下面具体情况的原因,我不是在寻找NULLIFCASE WHEN...或者我已经知道的类似情况(当然可以在下面的情况下使用它们)。

我有一个 SQL 语句,其计算列类似于

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

使用上述错误消息运行此语句会出错,这本身并不是问题 - 显然很TotalSize可能是 0,因此会导致错误。

现在我不明白的是,TotalSize当我将计算列注释掉时,我没有任何列为 0 的行,我仔细检查了情况并非如此。

然后我认为由于某种原因,列计算将在使用 where 子句的条件实际过滤之前对整个结果集执行,但是这 a) 没有意义 imho 和 b) 在尝试使用 a 重现错误时测试设置一切正常(见下文):

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

4 回答 4

14

SQL 是一种声明性语言;您编写的查询在逻辑上描述了您想要的结果,但由优化器来生成物理计划。这个物理计划可能与查询的书面形式没有太大关系,因为优化器不会简单地对从查询的文本形式派生的“步骤”进行重新排序,它可以应用 300 多种不同的转换来找到有效的执行策略。

优化器有相当大的自由来重新排序表达式、连接和其他逻辑查询结构。这意味着您通常不能依靠任何书面查询表格来强制在另一件事之前评估一件事。特别是,Lieven 给出的重写不会强制在表达式之前计算 WHERE 子句谓词。优化器可能会根据成本估算决定在看起来最有效的地方评估表达式。在某些情况下,这甚至可能意味着表达式被多次计算。

最初的问题考虑了这种可能性,但拒绝了它,因为它“没有多大意义”。然而,这就是产品的工作方式——如果 SQL Server 估计连接将减小集合大小足以降低计算连接结果的表达式的成本,那么它可以自由地这样做。

一般规则是永远不要依赖特定的评估顺序来避免溢出或被零除错误之类的事情。在此示例中,将使用 CASE 语句来检查零除数 - 防御性编程的示例。

优化器对事物重新排序的自由是其设计的基本原则。您可以找到导致反直觉行为的案例,但总体而言,好处远大于坏处。

保罗

于 2011-03-05T10:27:22.003 回答
4

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
于 2011-03-04T09:43:41.283 回答
1

运行时返回哪些行:

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

这可能会为您提供有关 SQL 服务如何评估 (TotalSize * 100) 为零的线索。

另一个想法,您的 where 语句中是否有任何问题也可能是问题所在?
您假设它是 TotalSize,但它可能在其他地方。

于 2011-03-04T09:15:05.927 回答
0

I was running into the same issue. In my case NULLs were acceptable so I was able to fix it this way:

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

If you need other handling, you can wrap the entire expression in an ISNULL function like this:

Select ISNULL(Expression1 / NULLIF(Expression2,0)-5) -- Returns -5 instead of null or divide by 0
于 2012-01-31T20:54:37.120 回答