Question

I have these two tables (the names have been pluralized for the sake of the example):

Table Locations:

idlocation varchar(12)
name       varchar(50)

Table Answers:

idlocation varchar(6)
question_number varchar(3)
answer_text1    varchar(300)
answer_text2    varchar(300)

This table can hold answers for multiple locations according a list of numbered questions that repeat on each of them.

What I am trying to do is to add up the values residing in the answer_text1 and answer_text2 columns, for each location available on the Locations table but for only an specific question and then output a value based on the result (1 or 0).

The query goes as follows using a nested table Answers to perform the SUM operation:

select
l.idlocation,    

'RESULT' = (
              case when (
                select 
                sum(cast(isnull(c.answer_text1,0) as int)) +
                sum(cast(isnull(c.answer_text2,0) as int)) 
                from Answers c
                where b.idlocation=c.idlocation and c.question_number='05'
              ) > 0 then
                           1
              else 
                           0
              end
            ) 

from Locations l, Answers b
where l.idlocation=b.idlocation and b.question_number='05'

In the table Answers I am saving sometimes a date string type of value for its field answer_text2 but on a different question number.

When I run the query I get the following error:

Conversion failed when converting the varchar value '27/12/2013' to data type int

I do have that value '27/12/2013' on the answer_text2 field but for a different question, so my filter gets ignored on the nested select statement after this: b.idlocation=c.idlocation, and it's adding up apparently more questions hence the error posted.

Update

According to Steve's suggested solution, I ended up implementing the filter to avoid char/varchar considerations into my SUM statement with a little variant:

Every possible not INT string value has a length greater than 2 ('00' to '99' for my question numbers) so I use this filter to determine when I am going to apply the cast.

'RESULT' = 
           case when (
          select sum( 
                          case when len(c.answer_text1) <= 2 then   
                             cast(isnull(c.answer_text1,'0') as int) 
                          else 
                             0 
                          end 
                         ) + 
               sum( 
                           case when len(c.answer_text2) <= 2 then  
                              cast(isnull(c.answer_text2,'0') as int) 
                           else 
                              0 
                           end 
                          )
        from Answers c 
                   where c.idlocation=b.idlocation and c.question_number='05' 
        ) > 0   
           then 
                1 
           else 
                0 
           end
Was it helpful?

Solution

This is an unfortunate result of how the SQL Server query processor/optimizer works. In my opinion, the SQL standard prohibits the calculation of SELECT list expressions before the rows that will contribute to the result set have been identified, but the optimizer considers plans that violate this prohibition.

What you're observing is an error in the evaluation of a SELECT list item on a row that is not in the result set of your query. While this shouldn't happen, it does, and it's somewhat understandable, because to protect against it in every situation would exclude many efficient query plans from consideration. The vast majority of SELECT expressions will never raise an error, regardless of data.

What you can do is try to protect against this with an additional CASE expression. To protect against strings with the '/' character, for example:

... SUM(CASE WHEN c.answer_text1 IS NOT NULL and c.answer_text1 NOT LIKE '%/%' THEN CAST(c.answer_text1 as int) ELSE 0 END)...

If you're using SQL Server 2012, you have a better option: TRY_CONVERT:

...SUM(COALESCE(TRY_CONVERT(int,c.answer_text1),0)...

In your particular case, the overall database design is flawed, because numeric information should be stored in number-type columns. (This, of course, may not be your fault.) So redesign is an option, putting integer answers in integer-type columns and non-integer answer_text elsewhere. A compromise, if you can't redesign the tables, that I think will work, is to add a persisted computed column with value TRY_CONVERT(int,c.answer_text1) (or its best equivalent, based on what you know about the actual data in the table - perhaps the integer value of only columns containing no non-digit character and having length less than 9).

OTHER TIPS

Your query appears correct enough, which means you have a Question 05 record with a datetime in either the answer_text1 or answer_text2 field.

Give this a shot to figure out which row has a date:

select *
from Answers 
where question_number='05'
  and (isdate(answer_text1) = 1 or isdate(answer_text2) = 1)

Furthermore, you could filter out any rows that have dates in them

where isdate(c.answer_text1) = 0
  and isdate(c.answer_text2) = 0
  and ...

Another option similar in nature to Steve's excellent answer is to filter your Answers table with a subquery like so:

select
        l.idlocation,    

        'RESULT' = (
                      case when (
                        select 
                        sum(cast(isnull(c.answer_text1,0) as int)) +
                        sum(cast(isnull(c.answer_text2,0) as int)) 
                        from (select answer_text1, answer_text2, idlocation from Answers where question_number ='05') c
                        where b.idlocation=c.idlocation
                      ) > 0 then
                                   1
                      else 
                                   0
                      end
                    ) 

        from Locations l, Answers b
        where l.idlocation=b.idlocation and b.question_number='05'

More generally, though, you could just have this query like this

select locations.idlocation, case when sum(case when is_numeric(answer_text1) then answer_text1 else 0 end) + sum(case when is_numeric(answer_text2) then answer_text2 else 0 end) > 0 then 1 else 0 end as RESULT  from locations
inner join answers on answers.idlocation = locations.idlocation
where answers.question_number ='05'
group by locations.idlocation

Which would produce the same result.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top