views:

52

answers:

6

Hi all,

I have a query that looks like this, and which is getting a percentage:

SELECT SUM(convert(decimal(3,2),
          CASE WHEN Status_ID = 1 AND State_ID = 14 THEN 1 ELSE 0 END))/
          SUM(convert(decimal(3,2),CASE WHEN State_ID = 14 THEN 1 ELSE 0 END)) * 100
FROM SomeTable

This may return something like 59.77803 in a normal case. But it turns that there are (rightly) cases when the second SUM -- the denominator -- could be 0. Does anyone know of a way to account for this case and avoid a divide by 0 exception?

I'm using SQL Server 2005. Thanks.

A: 
SELECT SUM(convert(decimal(3,2),
          CASE WHEN Status_ID = 1 AND State_ID = 14 THEN 1 ELSE 0 END))/
          SUM(convert(decimal(3,2),CASE WHEN State_ID = 14 THEN 1 ELSE 0 END)) + 0.00000000001 * 100
FROM SomeTable
rdkleine
I'm going go ahead and say this seems hacky. But is it really the best way? I thought maybe an 'if' check or something.
Mega Matt
Sure its a bit hacky, there is a loss of 1 million percent. Alternately you should provide what the result must be when there is a divide by 0
rdkleine
+1  A: 

ask yourself what's the result you want to return when the denominator is 0?
then based on that fix the query accordingly.

there's no good built-in way to do it. It's a business logic thing.

Mladen Prajdic
+1  A: 

If I'm reading it right, this should work:

SELECT SUM(convert(decimal(3,2), 
          CASE WHEN Status_ID = 1 THEN 1 ELSE 0 END))/ 
          SUM(convert(decimal(3,2),1)) * 100 
FROM SomeTable 
WHERE State_ID = 14 
James Curran
+1. Right, if the numerator is going to be zero in case of StateID 14, then the denominator doesn't matter.
p.campbell
@James Unfortunately, this was a simplified problem. In reality, I've got a number of these SELECT statements, all from one table, and given a third condition (in a WHERE clause) that I didn't specify here. So giving State_ID in a WHERE clause loses the granularity I need. Thanks for the answer, though.
Mega Matt
A: 

To avoid divide by 0 exception you can use a function which accepts numerator,denominator and default value to pass when denominator is 0. And call that function to do division.

CREATE FUNCTION [dbo].[fn_divide] 
(
    @numerator DECIMAL(3,2),@denominator DECIMAL(3,2),@default DECIMAL(3,2)
)  
RETURNS DECIMAL(3,2) 

AS  BEGIN

 DECLARE @result DECIMAL(3,2)

 IF @denominator = 0
 BEGIN
    set @result=@default
 END
ELSE
 BEGIN
    set @result=@numerator/@denominator
 END 

  return @result
END
N30
that will add a little overhead to large result sets
KM
Scalar UDFs perform very poorly compared to inline code.
Cade Roux
A: 

The real question is what should the "answer" be when you try to divide by zero? I'd make it NULL, so try something like this:

DECLARE @x int

set @x=5
select 1.0/NULLIF(@x,0)

set @x=0
select 1.0/NULLIF(@x,0)

OUTPUT:

---------------------------------------
0.200000000000

(1 row(s) affected)


---------------------------------------
NULL

(1 row(s) affected)

your code would be:

SELECT SUM(convert(decimal(3,2), CASE
                                     WHEN Status_ID = 1 AND State_ID = 14 THEN 1 
                                     ELSE 0 
                                 END
                  )
          ) 
        /
        SUM(convert(decimal(3,2),CASE
                                     WHEN State_ID = 14 THEN 1 
                                     ELSE NULL   --<<<<<<<<<<<<force null if div by zero
                                 END
                   )
           )
        * 100
FROM SomeTable

I don't really understand the WHEN Status_ID = 1 AND State_ID = 14 THEN 1, but that is the OPs code.

KM
NULLs are excluded from SUM(), so I don't think that would fix it.
Cade Roux
@Cade Roux, if they are all NULL, then the entire SUM is NULL, if some some are NULL then those are eliminated, which is what is desired by the OP?
KM
@KM You're right, in this case there is no way to get a 0 out of the SUM. In general, you might need to wrap the SUM with a NULLIF(, 0) if there's still a way to get to a 0 sum.
Cade Roux
A: 

I would probably use a NULL to represent uncalculatable, but it depends upon your intent in your problem domain (code expanded to show what's going on):

SELECT SUM(convert(decimal(3,2),
                   CASE WHEN Status_ID = 1 AND State_ID = 14 THEN 1 ELSE 0 END)
          )
       /
       NULLIF(
           SUM(convert(decimal(3,2),
                       CASE WHEN State_ID = 14 THEN 1 ELSE 0 END)
           )
           , 0
       )
       * 100
FROM SomeTable

The result when there are no rows in the table so the denominator would be zero - gets converted to NULL so the whole expression ends up NULL.

That code can also be simplified a little (probably because it's already a simplified toy problem):

SELECT SUM( convert(decimal(3,2), CASE WHEN Status_ID = 1 THEN 1 ELSE 0 END) )
       /
       NULLIF( convert(decimal(3,2), COUNT(*)), 0 )
       * 100
FROM SomeTable
WHERE State_ID = 14

As an example of combining a number of different things into one query:

SELECT CASE WHEN State_ID = 14 THEN 'State_ID = 14'
            WHEN State_ID IN (1, 2, 3) THEN 'State_ID IN (1, 2, 3)'
            ELSE 'DEFAULT'
       END AS Category
       ,SUM(convert(decimal(3,2),
                   CASE WHEN Status_ID = 1 THEN 1 ELSE 0 END)
          )
       /
       convert(decimal(3,2), COUNT(*))
       * 100
FROM SomeTable
GROUP BY CASE WHEN State_ID = 14 THEN 'State_ID = 14'
            WHEN State_ID IN (1, 2, 3) THEN 'State_ID IN (1, 2, 3)'
            ELSE 'DEFAULT'
       END
Cade Roux
@Case You're right, it was a simplified problem. In reality, I've got a number of these SELECT statements, all from one table, and given a third condition that I didn't specify here. So giving State_ID in a WHERE clause loses the granularity I need. But, your first answer is probably what I'll try. True, that I'll need those null columns to be 0, but I may just need to loop through the dataset that's returned to set nulls to 0. More overhead, but not as hacky as adding 0.0000001 to the denominator.
Mega Matt
@Mega Matt you can wrap the outside NULL with ISNULL(, 0)
Cade Roux
@Mega Matt You may also be able to do this in fewer queries by using a group by and grouping by an expression like a CASE tree.
Cade Roux
@Mega Matt - Note that in the GROUP BY case (say you did GROUP BY State_ID), you would not get a row for 14 at all if there were no 14s. This is a little different than over the entire set WHERE State_ID = 14, which will always return one row.
Cade Roux
@Cade, I'm intrigued by the CASE tree idea, but I have to admit I'm not completely familiar with that. Do you have an example or can you point me somewhere that discusses what you're talking about? Thanks.
Mega Matt
@Mega Matt Updated my answer - I'm not sure what your different queries do, but if the categories are disjoint, then you can get it all in one set like that. If you need them all on one row instead of separate rows, you can then use a PIVOT technique.
Cade Roux