如何避免 SQL 中的“除以零”错误?

Posted

技术标签:

【中文标题】如何避免 SQL 中的“除以零”错误?【英文标题】:How to avoid the "divide by zero" error in SQL? 【发布时间】:2010-10-26 02:17:56 【问题描述】:

我有这个错误信息:

消息 8134,级别 16,状态 1,行 1 遇到除以零错误。

编写 SQL 代码的最佳方法是什么,这样我就再也不会看到这个错误消息了?

我可以执行以下任一操作:

添加 where 子句,使我的除数永远不为零

或者

我可以添加一个 case 语句,以便对零进行特殊处理。

是使用NULLIF 子句的最佳方式吗?

有没有更好的方法,或者如何执行?

【问题讨论】:

可能需要进行一些数据验证。 【参考方案1】:

为了避免“被零除”错误,我们将其编程如下:

Select Case when divisor=0 then null
Else dividend / divisor
End ,,,

但这里有一个更好的方法:

Select dividend / NULLIF(divisor, 0) ...

现在唯一的问题是记住 NullIf 位,如果我使用“/”键。

【讨论】:

一种更好的方法“选择除数 / nullif(divisor, 0) ...”如果除数为 NULL,则中断。 @Anderson 这根本不是真的。你确定你没有不小心使用IsNull 而不是NullIf 吗?自己试试吧! SELECT Value,1/NullIf(Value,0)FROM(VALUES(0),(5.0),(NULL))x(Value); 除非“中断”,否则您的意思是返回 NULL?您可以使用IsNullCoalesce 将其转换为您想要的任何内容。 @ErikE,这是真的......尝试运行......选择1/nullif(null,0)......你得到“NULLIF的第一个参数的类型不能是NULL常量因为必须知道第一个参数的类型。”使用“coalesce(FieldName,0)”来处理这个问题......例如选择 1/nullif(coalesce(null,0),0) @JohnJoseph 我不知道你是同意我的观点还是与我争论。 @JohnJoseph 仔细查看您遇到的错误。是的,SELECT 1 / NULLIF(NULL, 0) 失败,但这是因为NULLIF() 需要知道第一个参数的数据类型。这个更改后的示例运行良好:SELECT 1 / NULLIF(CAST(NULL AS INT), 0)。在现实生活中,您将向NULLIF() 提供表格列,而不是NULL 常量。由于表列具有已知的数据类型,这也可以正常工作:SELECT 1 / NULLIF(SomeNullableColumn, 0) FROM SomeTable【参考方案2】:

如果您想返回零,以防发生零偏差,您可以使用:

SELECT COALESCE(dividend / NULLIF(divisor,0), 0) FROM sometable

对于每个为零的除数,您将在结果集中得到一个零。

【讨论】:

一些基准测试表明 COALESCE 比 ISNULL 稍慢。但是,COALESCE 符合标准,因此更便携。 如果其他人没有立即明白为什么这样做,如果 d 为 0,NULLIF(d,0) 将返回 NULL。在 SQL 中,除以 NULL 返回 NULL。 Coalesce 将生成的 NULL 替换为 0。 @SQLGeorge 虽然我同意您的论点,但请注意,在某些情况下,人们更关心统计上的正确性而不是数学上的正确性。在使用统计函数的某些情况下,当除数为零时,0 甚至 1 都是可接受的结果。 有人可以向我解释为什么这很糟糕吗?如果我想计算一个百分比并且除数为零,我当然希望结果为零。 我认为@George 和@James/ Wilson 从根本上误解了所提出的问题。当然,在某些业务应用程序中返回“0”是合适的,即使从数学角度来看它在技术上并不正确。【参考方案3】:

当我尝试解决除以零时,这似乎是解决我的情况的最佳解决方案,这确实发生在我的数据中。

假设您想计算各个学校俱乐部的男女比例,但您发现以下查询失败,并在尝试计算指环王俱乐部的比例时出现被零除错误,即没有女人:

SELECT club_id, males, females, males/females AS ratio
  FROM school_clubs;

您可以使用函数NULLIF 来避免被零除。 NULLIF 比较两个表达式,如果相等则返回 null,否则返回第一个表达式。

将查询重写为:

SELECT club_id, males, females, males/NULLIF(females, 0) AS ratio
  FROM school_clubs;

任何数除以NULL 得到NULL,不会产生错误。

【讨论】:

是的,确实,这比获得如此多赞的其他答案更好。在您的解决方案中,您至少有一个 NULL,这表明您无法提供正确的结果。但是,如果将结果从 NULL 转换为零,那么您只会得到错误和误导性的结果。 顺便说一句,如果你想计算男女比例,那么我建议最好将其与总数进行比较,例如:select males/(males+females), females/(males+females)。这将为您提供俱乐部中男性和女性的百分比分布,例如 31% 的男性和 69% 的女性。【参考方案4】:

您也可以在查询的开头这样做:

SET ARITHABORT OFF 
SET ANSI_WARNINGS OFF

因此,如果您有类似 100/0 的内容,它将返回 NULL。我只对简单查询执行此操作,所以我不知道它将如何影响更长/复杂的查询。

【讨论】:

为我工作。就我而言,我必须在 WHERE 子句中使用除法操作。我确定没有零分隔符,因为当我注释 WHERE 时,结果中没有零值。但不知何故,查询优化器在过滤时确实除以零。 SET ARITHABORT OFF SET 和 ANSI_WARNINGS OFF 可以正常工作 - 经过 2 天的战斗,在 WHERE 子句中除以零。谢谢! 这个“感觉”很脏,但我喜欢它!在进行聚合和使用 CASE 语句的查询中需要它不是一个选项,因为那时我必须将该列添加到 GROUP BY 中,这完全改变了结果。使初始查询成为子选择,然后对外部查询执行 GROUP BY 也会更改结果,因为涉及除法。 好的,所以我仍然喜欢这个“解决方案”,但就像你们中的许多人可能觉得的那样,我觉得必须有一个“更清洁”的方式。如果我忘记重新启用警告怎么办?或者有人克隆了我的代码(这永远不会发生,对吗?)并且没有考虑警告?无论如何,看到了关于 NULLIF() 的其他答案。我知道 NULLIF() 但没有意识到除以 NULL 返回 NULL(我认为这将是一个错误)。所以...我选择了以下内容: ISNULL( (SUM(foo) / NULLIF(SUM(bar),0) ), 0) AS Avg 我不知道这个解决方案。我不确定我是否喜欢它,但有一天知道它可能会有用。非常感谢。 这是最简单的解决方案,但请注意它会损害性能。来自docs.microsoft.com/en-us/sql/t-sql/statements/…:“将 ARITHABORT 设置为 OFF 会对查询优化产生负面影响,从而导致性能问题。”【参考方案5】:

如果有除以零,您至少可以阻止查询中断并返回NULL

SELECT a / NULLIF(b, 0) FROM t 

但是,我会从不使用coalesce 将其转换为零,就像其他答案中显示的那样,它得到了很多赞成。这在数学意义上是完全错误的,甚至是危险的,因为您的应用程序可能会返回错误和误导性的结果。

【讨论】:

【参考方案6】:

编辑: 我最近对此有很多反对意见......所以我想我只是添加一个注释,这个答案是在问题经历它最近的编辑之前写的,其中返回 null 被突出显示为一个选项......似乎很容易接受。我的一些回答是针对诸如 cmets 中 Edwardo 的问题,他似乎主张返回 0。这就是我所反对的情况。

回答: 我认为这里有一个潜在的问题,即除以 0 是不合法的。这表明某些事情在根本上是错误的。如果你除以零,你正在尝试做一些在数学上没有意义的事情,所以你得到的任何数字答案都不会是有效的。 (在这种情况下使用 null 是合理的,因为它不是将在以后的数学计算中使用的值)。

因此,Edwardo 在 cmets 中询问“如果用户输入 0 会怎样?”,他主张应该可以得到 0 作为回报。如果用户在金额中输入零,并且您希望在他们这样做时返回 0,那么您应该在业务规则级别输入代码以捕获该值并返回 0...没有一些除以 0 = 的特殊情况0.

这是一个细微的区别,但很重要...因为下次有人调用您的函数并期望它做正确的事情时,它会做一些数学上不正确的时髦的事情,但只是处理特定的边缘情况以后很有可能咬人。你并没有真正除以 0...你只是对一个糟糕的问题返回了一个糟糕的答案。

想象一下我正在编写一些代码,但我把它搞砸了。我应该读取辐射测量比例值,但在一个我没有预料到的奇怪边缘情况下,我读取了 0。然后我将我的值放入你的函数中......你给我一个 0!万岁,无辐射!除了它真的在那里,只是我传递了一个错误的价值......但我不知道。我希望除法抛出错误,因为这是错误的标志。

【讨论】:

我不同意。您的业​​务规则永远不应该以非法计算而告终。如果你最终做了这样的事情,你的数据模型很可能是错误的。每当您遇到除以 0 时,您应该考虑数据是否应该为 NULL 而不是 0。 我不敢相信有人问我是否曾经“做过任何真正的编程?”因为我说要做对,而不是偷懒。 叹息 对不起,我不是故意冒犯你的。但是这个问题在很多常见的 LOB 应用程序中是完全有效的,并且用“除以 0 是不合法的”来回答它并不能增加恕我直言的价值。 @JackDouglas 对。这是您希望业务规则以特殊方式处理特殊情况的情况......但它不应该是返回空白的基础数学。这应该是商业规则。接受的答案返回一个空值,这是处理它的好方法。抛出异常也可以。在它进入 SQL 之前发现并处理它可以说是理想的。提供某种其他东西可以调用但返回数学上不正确的值的函数是不是要走的路,因为这种特殊情况可能不适用于其他调用者。 @JackDouglas 是的,这是一个很好的总结,我同意。最初,这个问题似乎被表述为“我能做些什么来隐藏这个错误”。从那时起,它已经发展。返回一个空值,他最终得出的答案,似乎是一个合理的回应。 (我强烈主张不要返回 0 或其他数字。)【参考方案7】:
SELECT Dividend / ISNULL(NULLIF(Divisor,0), 1) AS Result from table

通过使用 nullif() 捕获零,然后使用 isnull() 生成 null,您可以避免除以零错误。

【讨论】:

由于篇幅较长,建议您删除您的答案。请注意,最好对您的建议添加一个小解释 - 即使它看起来很简单;)【参考方案8】:

用零代替“除以零”是有争议的 - 但它也不是唯一的选择。在某些情况下,用 1 替换是(合理地)合适的。我经常发现自己在使用

 ISNULL(Numerator/NULLIF(Divisor,0),1)

当我查看分数/计数的变化时,如果我没有数据,则希望默认为 1。例如

NewScore = OldScore *  ISNULL(NewSampleScore/NULLIF(OldSampleScore,0),1) 

通常情况下,我实际上已经在其他地方计算了这个比率(尤其是因为它可以为低分母抛出一些非常大的调整因子。在这种情况下,我通常会控制 OldSampleScore 大于阈值;这然后排除零。但有时“黑客”是合适的。

【讨论】:

@N 梅森;是的,有时 1 是一个选项。但是,当您键入反斜杠时,您如何记住 ISNULL 部分? 抱歉 Henrik - 我不确定我是否理解这个问题。【参考方案9】:

不久前我写了一个函数来处理我的stored procedures:

print 'Creating safeDivide Stored Proc ...'
go

if exists (select * from dbo.sysobjects where  name = 'safeDivide') drop function safeDivide;
go

create function dbo.safeDivide( @Numerator decimal(38,19), @divisor decimal(39,19))
   returns decimal(38,19)
begin
 -- **************************************************************************
 --  Procedure: safeDivide()
 --     Author: Ron Savage, Central, ex: 1282
 --       Date: 06/22/2004
 --
 --  Description:
 --  This function divides the first argument by the second argument after
 --  checking for NULL or 0 divisors to avoid "divide by zero" errors.
 -- Change History:
 --
 -- Date        Init. Description
 -- 05/14/2009  RS    Updated to handle really freaking big numbers, just in
 --                   case. :-)
 -- 05/14/2009  RS    Updated to handle negative divisors.
 -- **************************************************************************
   declare @p_product    decimal(38,19);

   select @p_product = null;

   if ( @divisor is not null and @divisor <> 0 and @Numerator is not null )
      select @p_product = @Numerator / @divisor;

   return(@p_product)
end
go

【讨论】:

嗨,Ron,不错的解决方案,但它的数据类型有限(小数点后 4 位)并且我们的 @divisors 也可以为负数。你如何强制使用它? TIA Henrik Staun Poulsen 当时为了处理特定的问题场景,我很快就把它冲掉了。单一开发者应用程序,所以除了我的记忆之外,执行起来并不困难。 :-) 尽管有 print 语句,但它不是存储过程,而是标量 UDF。如果它是查询的一部分,这将在 MS-SQL 中杀死你。 我同意 Mark Sowul 的断言,即标量函数会引起疼痛。这是 T-SQL 中的一个糟糕建议,不要这样做!标量函数是性能破坏者!内联表值函数是 SQL Server 中唯一好的用户函数(可能除了可以很好执行的 CLR 函数)。 您可以将整个函数替换为表达式@numerator / NULLIF(@divisor, 0)。它的作用相同,而且速度更快。【参考方案10】:
    添加强制 Divisor 为非零的 CHECK 约束 向表单添加验证器,以便用户无法在此字段中输入零值。

【讨论】:

我开始越来越喜欢 CHECK 约束了。【参考方案11】:

对于更新 SQL:

update Table1 set Col1 = Col2 / ISNULL(NULLIF(Col3,0),1)

【讨论】:

嗨 Vijay,是的,这会起作用,但是......我会小心 ISNULL 部分,你最终会除以 NULL。我宁愿告诉用户结果未知,因为除数为零。 它在复杂的子查询中救了我,谢谢。【参考方案12】:

没有神奇的全局设置“关闭除以 0 异常”。操作必须抛出,因为 x/0 的数学含义与 NULL 的含义不同,所以不能返回 NULL。 我假设您正在处理显而易见的问题,并且您的查询具有应该消除除数为 0 的记录并且从不评估除数的条件。通常的“陷阱”比大多数开发人员期望的 SQL 表现得像过程语言并提供逻辑运算符短路,但它确实 NOT。我推荐你阅读这篇文章:http://www.sqlmag.com/Articles/ArticleID/9148/pg/2/2.html

【讨论】:

有这样一个“魔法全局设置”;SET ARITHABORT OFF。【参考方案13】:

这是一种可以除以零的情况。业务规则是,要计算库存周转率,您需要计算一段时间内的销售成本,然后将其年化。获得年化数字后,除以该期间的平均库存。

我正在计算三个月内发生的库存周转次数。我已经计算出我在三个月内销售的商品成本为 1,000 美元。年销售额为 4,000 美元(1,000 美元/3)*12。期初库存为 0。期末库存为 0。我现在的平均库存为 0。我的年销售额为 4000 美元,没有库存。这会产生无限数量的转弯。这意味着我所有的库存都被客户转换和购买。

这是如何计算库存周转的业务规则。

【讨论】:

是的,然后你就有了无限圈数。所以在这种情况下,如果你有除以零,那么你应该显示类似“#INF”的内容。 “期初库存为 0。期末库存为 0。我的平均库存现在为 0。” 您的计算是一个估计值。有时库存是正数,或者您无法运送/出售任何东西。如果您对 +∞ 结果不满意,请使用更好的平均库存估算值。【参考方案14】:
CREATE FUNCTION dbo.Divide(@Numerator Real, @Denominator Real)
RETURNS Real AS
/*
Purpose:      Handle Division by Zero errors
Description:  User Defined Scalar Function
Parameter(s): @Numerator and @Denominator

Test it:

SELECT 'Numerator = 0' Division, dbo.fn_CORP_Divide(0,16) Results
UNION ALL
SELECT 'Denominator = 0', dbo.fn_CORP_Divide(16,0)
UNION ALL
SELECT 'Numerator is NULL', dbo.fn_CORP_Divide(NULL,16)
UNION ALL
SELECT 'Denominator is NULL', dbo.fn_CORP_Divide(16,NULL)
UNION ALL
SELECT 'Numerator & Denominator is NULL', dbo.fn_CORP_Divide(NULL,NULL)
UNION ALL
SELECT 'Numerator & Denominator = 0', dbo.fn_CORP_Divide(0,0)
UNION ALL
SELECT '16 / 4', dbo.fn_CORP_Divide(16,4)
UNION ALL
SELECT '16 / 3', dbo.fn_CORP_Divide(16,3)

*/
BEGIN
    RETURN
        CASE WHEN @Denominator = 0 THEN
            NULL
        ELSE
            @Numerator / @Denominator
        END
END
GO

【讨论】:

我不喜欢您的解决方案,因为使用 UDF 会强制查询以单线程模式运行。我喜欢你的测试设置。我希望在我们所有的 UDF 中都有它。 对我来说这个解决方案完美而优雅 @Payedimaunt;是的,UDF 会导致非常优雅的代码。但它的表现并不好。这是“安眠药上的光标”。 :-) 也很难记住写 dbo.Divide 而不是普通的“/”【参考方案15】:

使用 where 子句过滤掉数据,以免得到 0 值。

【讨论】:

【参考方案16】:

有时,0 可能不合适,但有时 1 也不合适。有时,从 0 到 100,000,000 的跳跃被描述为 1% 或 100% 的变化也可能具有误导性。在这种情况下,100,000,000% 可能是合适的。这取决于您打算根据百分比或比率得出什么样的结论。

例如,销售量很小的商品从 2 到 4 销售,而销售量非常大的商品从销售 1,000,000 变为 2,000,000 对分析师或管理层来说可能意味着非常不同的事情,但两者都会作为 100 % 或 1 变化。

隔离 NULL 值可能比搜索一堆 0% 或 100% 与合法数据混合的行更容易。通常,分母中的 0 表示错误或缺失值,您可能不想仅仅为了让数据集看起来整洁而填写任意值。

CASE
     WHEN [Denominator] = 0
     THEN NULL --or any value or sub case
     ELSE [Numerator]/[Denominator]
END as DivisionProblem

【讨论】:

我发现问题是要记得做除法的时候要做的事情。如果您忘记添加 CASE 或 NULLIF,您将在 x 周后的周一早上收到支持案例。我讨厌那样。【参考方案17】:

这就是我修复它的方法:

IIF(ValueA != 0, Total / ValueA, 0)

它可以包含在更新中:

SET Pct = IIF(ValueA != 0, Total / ValueA, 0)

或者在一个选择中:

SELECT IIF(ValueA != 0, Total / ValueA, 0) AS Pct FROM Tablename;

想法?

【讨论】:

我和其他人发现用零替换“未知”是一种危险的解决方法。我更喜欢NULL。但困难的一点是要记住在所有分区上添加 iif 或 nullif!【参考方案18】:

当错误传播回调用程序时,您可以适当地处理错误(如果这是您想要的,则忽略它)。在 C# 中,SQL 中发生的任何错误都会抛出一个异常,我可以捕获然后在我的代码中进行处理,就像任何其他错误一样。

我同意 Beska 的观点,因为您不想隐藏错误。您可能不是在处理核反应堆,但通常隐藏错误是不好的编程习惯。这是大多数现代编程语言实现结构化异常处理以将实际返回值与错误/状态代码解耦的原因之一。当你做数学时尤其如此。最大的问题是您无法区分返回的正确计算的 0 或错误导致的 0。相反,返回的任何值都是计算值,如果出现任何问题,则会引发异常。这当然会根据您访问数据库的方式和使用的语言而有所不同,但您应该始终能够收到可以处理的错误消息。

try

    Database.ComputePercentage();

catch (SqlException e)

    // now you can handle the exception or at least log that the exception was thrown if you choose not to handle it
    // Exception Details: System.Data.SqlClient.SqlException: Divide by zero error encountered.

【讨论】:

我想我们都同意用 0 隐藏错误不是解决方案。我的建议是编写我们的代码,使每个“/”后跟一个“NULLIF”。这样,我的报告/核反应堆就不会单独存在,而是显示“NULL”而不是那个讨厌的错误消息 8134。当然,如果除数为 0 时我需要不同的进程,那么 Beska 和我同意。我们需要编写代码。每次写“/”时都很难做到。 “/NULLIF”也不是每次都做不到。【参考方案19】:

使用NULLIF(exp,0),但以这种方式 - NULLIF(ISNULL(exp,0),0)

NULLIF(exp,0) 会在 exp 为 null 时中断,但 NULLIF(ISNULL(exp,0),0) 不会中断

【讨论】:

如果 0 是零而不是 o,我无法获得 NULLIF(exp, 0)。尝试; DECLARE @i INT SELECT 1 / NULLIF(@i, 0) 看看你是否可以让它中断。

以上是关于如何避免 SQL 中的“除以零”错误?的主要内容,如果未能解决你的问题,请参考以下文章

clickhouse:如何避免 DB::Exception:除以零

在没有太多条件的情况下如何避免除以零?

T-SQL为啥除以零不会在order by子句中产生错误

运行脚本时,我在 SQL 中得到“除以零”

如何修复visual studio报表中我的表达式中除以零的错误?

如何修复'RuntimeWarning:在 double_scalars 中遇到的除以零'