SQL Server CASE Statement Evaluate Expression Once - sql

I may be missing something obvious! Thanks in advance for any help.
I am trying to use a CASE statement in an inline SQL Statement. I only want to evaluate the expression once, so I am looking to put the expression in the CASE section, and then evaluate the result in each WHEN. Here is the example:
SELECT
MyTable.ColumnA,
CASE DateDiff(d, MyTable.MyDate, getDate())
WHEN <= 0 THEN 'bad'
WHEN BETWEEN 1 AND 15 THEN 'reasonable'
ELSE 'good'
END as MyCalculatedColumn,
MyTable.SomeOtherColumn
I know I can do this:
CASE
WHEN DateDiff(d, MyTable.MyDate, getDate()) <= 0 THEN 'bad'
WHEN DateDiff(d, MyTable.MyDate, getDate()) BETWEEN 1 AND 15 THEN 'reasonable'
ELSE 'good'
END
But in my first example, SQL does not seem to like this statement:
WHEN <= 0 THEN 'bad'
Note that the statement is inline with other SQL, so I can't do something like:
DECLARE #DaysDiff bigint
SET #DaysDiff = DateDiff(d, MyTable.MyDate, getDate())
CASE #DaysDiff
WHEN <= 0 THEN 'bad'
WHEN BETWEEN 1 AND 15 THEN 'reasonable'
ELSE 'good'
END
My actual DateDiff expression is much more complex and I only want to maintain its logic, and have it evaluated, only once.
Thanks again...

You can use apply for this purpose:
SELECT MyTable.ColumnA,
(CASE WHEN day_diff <= 0 THEN 'bad'
WHEN BETWEEN 1 AND 15 THEN 'reasonable'
ELSE 'good'
END) as MyCalculatedColumn,
MyTable.SomeOtherColumn
FROM MyTable CROSS APPLY
(VALUES (DateDiff(day, MyTable.MyDate, getDate()))) v(day_diff)
APPLY is a very handy way to add calculated values into a statement. Because they are defined in the FROM clause, they can be used in SELECT, WHERE, and GROUP BY clauses where column aliases would not be recognized.

I see your problem. CASE expression WHEN value can only do an equality check
You could try using a CTE (Common Table Expression), do everything except the case statement in the CTE and then put the CASE in the final SELECT at the end. I'm not sure whether it will prevent the expression being evaluated twice - thats kindof the optimisers problem, not yours (thats how I like to think about it)
WITH cteMyComplexThing AS(
SELECT MyTable.ColumnA,
DateDiff(d, MyTable.MyDate, getDate()) as ComplexThing,
MyTable.SomeOtherColumn
FROM MyTable
)
SELECT
ColumnA,
CASE
WHEN ComplexThing <= 0 THEN 'bad'
WHEN ComplexThing BETWEEN 1 AND 15 THEN 'reasonable'
ELSE 'good'
END as MyCalculatedColumn,
SomeOtherColumn
FROM cteMyComplexThing

The WHEN clause in a CASE statement needs both sides of the condition. <=0 cannot stand by itself.
CASE #DaysDiff
WHEN ? <= 0 THEN 'bad'
WHEN BETWEEN 1 AND 15 THEN 'reasonable'
ELSE 'good'
END

Related

Issue with group by while using case with aggregate functions

I need your advise to add group by for below piece of code. I tried couple of things but it is giving not a group by function error to me.
Select rec_id,
Case when is_eff=1 and min(date_dt) > dt1 and min(date_dt) <= (dt1 + 90)
Then 'Yes'
Case when is_eff <> 1 and impl_dt is not null and min(impl_dt) > dt1 and min(impl_dt) <= (dt1+90)
Then 'Yes'
Else 'No'
End as column
From Table
Group by rec_id
Error: Not a group by expression
Any suggestions on this please.
You type CASE WHEN THEN, CASE WHEN THEN,...
Syntax is different than what I found on oracle docs:
https://www.oracletutorial.com/oracle-basics/oracle-case/
Being CASE WHEN THEN, WHEN THEN, WHEN THEN,...
Also, fields you are not using aggregate functions on in your CASE should also be included in your group by clause.
Select rec_id,
Case when is_eff=1 and min(date_dt) > dt1 and min(date_dt) <= (dt1 + 90)
Then 'Yes'
when is_eff <> 1 and impl_dt is not null and min(impl_dt) > dt1 and min(impl_dt) <= (dt1+90)
Then 'Yes'
Else 'No'
End as column
From Table
Group by rec_id,dt1,is_eff,impl_dt

How to set flag for time interval in T-SQL

I have a query that has InsertDate and flag field. I need to set the flag to Y for the first 10 minutes, and every 12 hours after that. This is what I have so far, but hard-coded is not an ideal at all.
select InsertDate, case when DATEDIFF(MINUTE, MAX(InsertDate),
GETDATE()) <= 10 then 'Y' when DATEDIFF(MINUTE, MAX(InsertDate),
GETDATE()) = 720 then 'Y' when DATEDIFF(MINUTE, MAX(InsertDate),
GETDATE()) = 1440 then 'Y' Else 'N' end as flag
How do I update my syntax to get away from hard-coded? Thank you for any helps
Your query is not valid SQL to start with: it has no from clause, and having both insertdate and max(insertdate) in the select clause makes no sense.
Bottom line, I think you want modulo artithmetics:
case
when datediff(minute, insertdate, getdate()) <= 10 then 'y'
when datediff(minute, insertdate, getdate()) % 720 = 0 then 'y'
else 'n'
end as flag
<expr> % 720 = 0 reads as: <expr> is a multiple of 720.

GROUP BY with nested case expression - is there better way?

SQL server 2012
I am taking fees received and multiplying them by different factors based on how long the Client has been a Client. The group by clause is fairly straight forward. However, my select gets awkward when I want to use this criteria in different ways:
select mp.professionals
,case when sl.stmndate < dateadd(year, 3, m.qClientOpenDate) then 'New' else 'Old' end age -- straight forward
,case (case when sl.stmndate < dateadd(year, 3, m.qClientOpenDate) then 'New' else 'Old' end) -- nested case
when 'New' then sum(fees) * 0.5
when 'Old' then sum(fees) * 0.25
else 0
end Credit
,case (case when sl.stmndate < dateadd(year, 3, m.qClientOpenDate) then 'New' else 'Old' end) -- nested case
when 'New' then 'Welcome!'
when 'Old' then 'Thank you for being a long-time Client!'
end Greeting
from mattersprofessionals mp
inner join matters m on m.matters = mp.matters
inner join stmnledger sl on sl.matters = mp.matters
group by mp.professionals, case when sl.stmndate < dateadd(year, 3, m.qClientOpenDate) then 'New' else 'Old' end
I suppose I should mention this is a simplified version of my actual case statements.
I was hoping there was a way to group by sl.stmndate < dateadd(year, 3, m.qClientOpenDate) as a boolean or something so I don't have to do the nested case expressions.
I know I could do a sub-query on the basic case and then do more case expressions in the outer query. But that's just rearranging the same nested case concept.
When the values are calculated directly from the row I tend to use cross apply for this as it is more concise than adding a derived table/CTE whose only purpose is to define a column alias but still needs to project out the remaining columns and contain a FROM.
This is not an option for expressions that reference window functions or aggregate functions but will work fine here.
select mp.professionals
,ca.age -- straight forward
,case ca.age -- nested case
when 'New' then sum(fees) * 0.5
when 'Old' then sum(fees) * 0.25
else 0
end Credit
,case ca.age -- nested case
when 'New' then 'Welcome!'
when 'Old' then 'Thank you for being a long-time Client!'
end Greeting
from mattersprofessionals mp
inner join matters m on m.matters = mp.matters
inner join stmnledger sl on sl.matters = mp.matters
cross apply (select case when sl.stmndate < dateadd(year, 3, m.qClientOpenDate) then 'New' else 'Old' end) ca(age)
group by mp.professionals, ca.age
You can simplify the query a little by pre-computing the case expression in a subquery:
select
professionals,
age,
sum(fees) * case age
when 'New' then 0.5
when 'Old' then 0.25
else 0
end credit,
case age
when 'New' then 'Welcome'
when 'Old' then 'Thank you for being a long-time Client!'
end greeting
from (
select
mp.professionals,
case when sl.stmndate < dateadd(year, 3, m.qClientOpenDate)
then 'New'
else 'Old'
end age
from mattersprofessionals mp
inner join matters m on m.matters = mp.matters
inner join stmnledger sl on sl.matters = mp.matters
) t
group by professionals, age
Note: the else branch in the fees calculation is probably never reached (since age always takes either value 'New' or 'Old').
Technically I think the answer to my question is "No". There isn't a way to group by the boolean portion of a case statement so it can be used in various ways within the select.
I upvoted GMB's and Martin Smith's answers as they are helpful and informative. I am playing with the cross apply, a learning experience for sure.

How to write a T-SQL query which can return 0 rows as part of a case statement

I am trying to write a query which checks how many records arrived in the last x days so that I can send out an alert if no new records have arrived in that window.
To that end I want a query which will check the table and return either 1 row stating no files were detected if there is a problem or no rows if everything is ok. The reason I want there to be no rows is because the downstream program will treat any returned rows as an error having been detected and alert accordingly.
select 'Null check' as id,
case when
count(*) > 0 then NULL
else 'No files detected' end as Message
from TABLE where LASTUPDATEDATE > dateadd(d, -1, getdate())
This query works if an error is detected but not in the correct case as it still returns a row. How can I rewrite it so that it doesn't return anything? Thanks!
For performance reasons, I would recommend writing this as:
select 'Null check' as id, 'No files detected' as Message
from (select top (1) t.*
from table t
where lastupdatedate > dateadd(day, -1, getdate())
) t
having count(*) = 0;
This saves SQL Server from actually having to count a bunch of rows if when things are fine.
Seems to be a task for NOT EXISTS:
select 'Null check' as id, 'No files detected' as Message
where not exists
(
select *
from tab
where lastupdatedate > dateadd(day, -1, getdate())
)
The trick is to use having count(*) = 0
select 'Null check' as id, 'No files detected' as Message
from TABLE
where LASTUPDATEDATE > dateadd(d, -1, getdate())
having count(*) = 0
Demo
SQL Fiddle - count() = 0
SQL Fiddle - count() > 0
You can accomplish this with a CASE and subquery:
SELECT [Message] =
CASE
WHEN COALESCE(SELECT COUNT(*) FROM [TABLE] WHERE [LASTUPDATEDATE] > DATEADD(d, -1, GETDATE())), 0) > 0
THEN 'No files detected'
ELSE NULL
END ;

SQL Case Statement: Inside Where Clause

I have read some other Q&A about case statements inside the 'WHERE' clause, but I cannot truly understand how to use it. I will post below a snippet of the code. I believe I am ignorant of a fundamental principle concerning how to use a case statement, and this is why the code will not compile/run. I appreciate any help.
where i.status IN ('CR','L','O')
and i.FGCs > 0
and i.LastShpd > CAST(CONVERT(CHAR(11),DATEADD(DAY,-180,GETDATE()),113) AS datetime)
and (Case
When n.OnOrder IN ('0', '')
Then i.OnOrder = 0 or i.LastShpd < CAST(CONVERT(CHAR(11),DATEADD(DAY,-21,GETDATE()),113) AS datetime)))
End)
Order by i.LastShpd desc
To explain what I have above, I already got the appropriate 'SELECT' and 'FROM' statement. Now I am filtering the results based on those the shown variables (ecx LastShpd). What I wanted the case statement to do was: When n.OnOrder = 0, I want to keep only the rows where i.OnOrder = 0 or if i.LastShpd has been greater than 21 days.
I don't think you need a Case for this:
where i.status IN ('CR','L','O')
and i.FGCs > 0
and i.LastShpd > CAST(CONVERT(CHAR(11),DATEADD(DAY,-180,GETDATE()),113) AS datetime)
and (
(n.OnOrder IN ('0', '') and i.OnOrder = 0)
or i.LastShpd < CAST(CONVERT(CHAR(11),DATEADD(DAY,-21,GETDATE()),113) AS datetime)
)
Re-reading your question maybe is this other way:
where i.status IN ('CR','L','O')
and i.FGCs > 0
and i.LastShpd > CAST(CONVERT(CHAR(11),DATEADD(DAY,-180,GETDATE()),113) AS datetime)
and (
n.OnOrder Not IN ('0', '')
or i.OnOrder = 0
or i.LastShpd < CAST(CONVERT(CHAR(11),DATEADD(DAY,-21,GETDATE()),113) AS datetime)
)
When using a CASE within a WHERE clause, you still need to have both sides of the operation defined (i.e. [CASE CONDITION] = [SOMETHING]). This can get tricky depending on what you want to do, but the easiest thing is to have your case statement execute as a true/false type of condition so that you end up with [CASE] = 1 or [CASE] = 0.
In your case (accidental pun!):
where i.status IN ('CR','L','O')
and i.FGCs > 0
and i.LastShpd > CAST(CONVERT(CHAR(11),DATEADD(DAY,-180,GETDATE()),113) AS datetime)
and (Case
When n.OnOrder IN ('0', '') AND (i.OnOrder = 0 or i.LastShpd < CAST(CONVERT(CHAR(11),DATEADD(DAY,-21,GETDATE()),113) AS datetime))
THEN 1
ELSE 0
End) = 1
Of course as another answer has pointed out, a case is not really necessary in this particular instance. However, if you had more complex conditions this could be helpful.