SQL Server Nested CASE and OR in WHERE statement - sql-server

I have been trying all day to figure out how to (properly) move a nested IIF statement from Access to SQL Server.
The query needs to evaluate a simple NULL/-1/1 (null/yes/no) combobox. If it is blank, it should bring back all records. If YES (-1) then return Demog.[LT Due Date] <= GETDATE(). If NO (1) then Demog.[LT Due Date] >= GETDATE().
Here is the ACCESS SQL that works perfectly:
SELECT Demog.ID, Demog.[Long Term Date]
FROM Demog
WHERE (
(iif(Forms!Demog_Entry!cbox_LTPD=-1,Demog.[Long Term Date]<=Date(),
(iif(Forms!Demog_Entry!cbox_LTPD=0,Demog.[Long Term
Date]>=Date(),isnull(Forms!Demog_Entry!cbox_LTPD))))));
And here is one of the SQL Server Codes I've tried: It evaluates the Null correctly, but if anything else it only returns Demog.[LT Due Date] <= GETDATE().
Declare #LTPD as Bit
Set #LTPD = -1
SELECT Demog.ID, Demog.[LT Due Date]
FROM Demog
WHERE (1=(CASE WHEN #LTPD Is Null THEN 1 ELSE 0 END) OR Demog.[LT Due Date] <= GETDATE())
I have also tried doing a combination of CASE and OR statements. Here is what I have, but again, I just can't get it to evaluate all 3 states correctly.
WHERE (1=(CASE WHEN #LTPD Is Null THEN 1 ELSE 0 END) OR
(#LTPD=-1 AND Demog.[LT Due Date] <= GETDATE()) OR
(#LTPD=1 AND Demog.[LT Due Date] >= GETDATE()))
I also tried using IIF but could not make it work no matter what I did. I'm assuming IIF works in the SELECT statement but not the WHERE statement?

you might just need some ORs
select Demog.ID, Demog.[Long Term Date]
from Demog
where (#LTPD IS NULL)
or (#LTPD = -1 AND Demog.[Long Term Date]<=GetDate())
or (#LTPD = 1 AND Demog.[Long Term Date]>=GetDate())

Related

SQL Server : multiple Select queries

I am trying to show sum(grand_total) where is_loyal = 1 and sum(grand_total) where is_loyal = 0 together in the same result screen (grand_total column is in the same table for both). I have tried subqueries, join and case when but no luck so far. Is there a way to calculate and show the results together?
The only result I can find by subqueries is below, no matter how I change where clause gives me the same data. If I change my query a bit it throws error message
Msg 512, Level 16, State 1, Line 155
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
Result
With conditional aggregation:
select date,
sum(case when loyal = 1 then grand_total end) [Total Loyalty],
sum(case when loyal = 0 then grand_total end) [Total Spent non-loyalty]
from tablename
group by date
use case when like this:
select
sum(case when loyal = 1 then 1 else 0 end) as grand_total_Loyal,
sum(case when loyal = 0 then 1 else 0 end) as grand_total_noneloyal
A simple method uses arithmetic:
select sum(loyal * grandtotal) as loyal_grandtotal,
sum( (1 - loyal) * grandtotal) as notloyal_grandtotal
from t
where loyal in (1, 0);
The more general solution is using a case expression. However, if you have 0/1 flags they fit very easily into arithmetic calculations -- on reason why they are preferred over, say, string flags.

Apply Different WHERE clause depending on value of one field

i'm trying to build a query in which I need to apply 2 different where clauses, depending on the value of Current Month. In this case, I need to show data from the last 2 years, only of the months before the current month:
Example 1:
Current Date is: 01-01-2017
Need to show data from:
01/2015; 02/2015; 03/2015; 04/2015; 05/2015; 06/2015;
07/2015; 08/2015; 09/2015; 10/2015; 11/2015; 12/2015;
01/2016; 02/2016; 03/2016; 04/2016; 05/2016; 06/2016;
07/2016; 08/2016; 09/2016; 10/2016; 11/2016; 12/2016.
Example 2:
Current Date is: 01-03-2017
Need to show data from: 01/2016; 02/2016; 01/2017; 02/2017.
So I built the following query:
SELECT *
FROM TABLE1
WHERE
CASE MONTH(GETDATE())
WHEN 1
THEN YEAR(Data)>=YEAR(GETDATE())-2 and YEAR(data)<YEAR(GETDATE())
ELSE YEAR(Data)>=YEAR(GETDATE())-1 and YEAR(data)<=YEAR(data) and MONTH(data)<MONTH(GETDATE())
END
I'm getting an error.
Can you please help me?
Thank you.
Your syntax is incorrect for sure. THEN is not a logical expression - it is supposed to return value. So you can't write logical expression in THEN/ELSE blocks as you have attempted to. Instead you might try something like:
WHERE
#date >= CASE WHEN a=b THEN '20150101' ELSE '20160202' END
Another thing is: conversions and functions in predicate are very bad for performance. When working with dates you might want to prepare filter predicate before the query when possible, e.g.:
declare
#date_begin date,
#date_end date
set #date_end = DATEADD(..., #arg_date)
set #date_begin = DATEADD(YEAR, -2, #date_end)
select ...
where date between #date_begin and #date_end
in your case it could be something like:
declare
#arg_date DATE = GETDATE(),
#date_begin DATE,
#date_end DATE,
#max_month INT
set #max_month = MONTH(#date)
if #max_month = 1
begin
set #date_end = DATEADD(dd, 1-DATEPART(dy, #arg_date), #arg_date) /* first day of year */
set #date_begin = dateadd(YY, -2, #date_end)
end
else
begin
set #date_end = #arg_date
set #date_begin = dateadd(YY, -1, DATEADD(dd, 1-DATEPART(dy, #date_end), #date_end)) /* first day of year_begin */
end
SELECT *
FROM TABLE1 t
WHERE t.date >= #date_begin and t.date < #date_end
AND (#max_month = 1 OR MONTH(t.date) < #max_month)
another (a better) way is to prepare #periods table variable, put each (date_begin, date_end) pair you need into it and join with TABLE1 - you'll get rid of all function calls from within WHERE clause.
You should realize: you know exactly which periods of each year you need in the result set. There is nothing to compute from stored TABLE1->date column. Just filter it with precomputed date intervals. Don't convert or modify date column - it is already ready to use. Merely apply appropriate filters. MONTH(date) <= 3 is date <= 20170331. Don't torture left part - prepare appropriate right part of such predicates.
The easiest way would be something like:
SELECT *
FROM TABLE1
WHERE
(YEAR(Data)>=YEAR(GETDATE())-2 and YEAR(data)<YEAR(GETDATE()) AND MONTH(GETDATE()) = 1)
OR (YEAR(Data)>=YEAR(GETDATE())-1 and MONTH(data)<MONTH(GETDATE()) and MONTH(GETDATE()) <> 1)
(Note I removed the superfluous and YEAR(data)<=YEAR(data).).
Personally I prefer (and I think it's generally advised) AND/OR logic to a CASE in a WHERE clause.
The error with your CASE statement is caused by the fact that CASE returns an atomic value. It cannot be used in the same way as if in procedural languages.
You can't swap in additional statements to your where clause using case statements. Instead, you need to resolve the case to an equality:
select *
from Table1
where case month(getdate()) -- You want to avoid using functions on fields in your WHERE claises, as this can reduce performance.
when 1 then case when Data >= dateadd(year,datediff(year,0,getdate())-2,0)
and Data < dateadd(year,datediff(year,0,getdate()),0)
then 1 -- Data rows the meet the criteria will return 1.
else 0 -- Data rows that do not will return 0.
end
else case when (Data >= dateadd(year,datediff(year,0,getdate())-1,0)
and Data < dateadd(m,datediff(m,0,getdate())-12,0)
)
or (Data >= dateadd(year,datediff(year,0,getdate()),0)
and Data < dateadd(m,datediff(m,0,getdate()),0)
)
then 1
else 0
end
end = 1 -- Then limit the results to only those rows that returned a 1.
In your specific instance however, this can be simplified to a standard or:
select *
from Table1
where (month(getdate()) = 1
and Data >= dateadd(year,datediff(year,0,getdate())-2,0)
and Data < dateadd(year,datediff(year,0,getdate()),0)
)
or (month(getdate()) <> 1
and (Data >= dateadd(year,datediff(year,0,getdate())-1,0)
and Data < dateadd(m,datediff(m,0,getdate())-12,0)
)
or (Data >= dateadd(year,datediff(year,0,getdate()),0)
and Data < dateadd(m,datediff(m,0,getdate()),0)
)
)
Note the use of brackets above to separate out the logical tests. Where a Data row meets either one of those criteria it will be returned in your query.

Convert Access query to SQL Server Query

I have a MS Access query and I want to convert in SQL Server query, any help will be greatly appreciated.
SELECT
dbo_Employees.*,
(SELECT top 1 dbo_attendance.attend_date
FROM dbo_Attendance
WHERE dbo_attendance.ID_Employee=dbo_attendance.ID_Employee
and dbo_attendance.attend_date > dbo_attendance.attend_date
order by dbo_attendance.attend_date asc) AS NextDate,
IIf(IsNull(NextDate),Now(),Nextdate) AS next123,
Next123-dbo_attendance.attend_date AS difference,
dbo_attendance.attend_date,
IIf(dbo_attendance.attend_date+90<Next123,1,0) AS Day90Credit,
IIf(dbo_attendance.attend_date+90<Next123,dbo_attendance.attend_date+90,dbo_attendance.attend_date+365) AS CreditDate,
IIf((Day90Credit=0 And CreditDate<Now()) Or Day90Credit=1,1,0) AS TotalCredit
FROM dbo_attendance, dbo_Employees
WHERE (((dbo_Employees.Employee_ID)=[dbo_attendance].[ID_Employee]));
In sql server (and most every other RDBMS) we use CASE statements instead of iif(). The structure is pretty simple CASE WHEN <condition> THEN <value if true> ELSE <value if false> END.
Changing your iif() over to CASE will be the bulk of the switch over. The first iif() however is better represented as a COALESCE() which allows a list of fields or values. Coalesce will grab the first Non-Null value/field from the list for that record.
The other things that have to be switched is the Date logic. In SQL Server you use DATEADD() to add days (or other date parts like year and month) to a date. you use DATEDIFF() to subtract two dates to get a date part (like Days or Months or Years).
SELECT dbo_Employees.*,
(
SELECT TOP 1 dbo_attendance.attend_date
FROM dbo_Attendance
WHERE dbo_attendance.ID_Employee = dbo_attendance.ID_Employee
AND dbo_attendance.attend_date > dbo_attendance.attend_date
ORDER BY dbo_attendance.attend_date ASC
) AS NextDate,
COALESCE(NextDate, GETDATE()) AS next123,
datediff(day, dbo_attendance.attend_date, COALESCE(NextDate, GETDATE())) AS difference,
dbo_attendance.attend_date,
CASE
WHEN DATEADD(DAY, 90, dbo_attendance.attend_date) < COALESCE(NextDate, GETDATE())
THEN 1
ELSE 0
END AS Day90Credit,
CASE
WHEN DATEADD(DAY, 90, dbo_attendance.attend_date) < COALESCE(NextDate, GETDATE())
THEN dateAdd(DAY, 90, dbo_attendance.attend_date)
ELSE DATEADD(DAY, 365, dbo_attendance.attend_date)
END AS CREDITDATE,
CASE
WHEN (
Day90Credit = 0
AND CreditDate < GETDATE()
)
OR DATEADD(DAY, 90, dbo_attendance.attend_date) < COALESCE(NextDate, GETDATE())
THEN 1
ELSE 0
END AS TotalCredit
FROM dbo_attendance,
dbo_Employees
WHERE dbo_Employees.Employee_ID = [dbo_attendance].[ID_Employee];
Lastly... I can't remember how this works in SQL server since it's been a while since I was in the environment, but you might have to switch instances of dbo_ to dbo.. Your server will cry foul and let you know anyhow.
You can try CTE (Common Table Expressions) in Sql Server for complex calculations, see this link: https://technet.microsoft.com/en-us/library/ms190766(v=sql.105).aspx
I refactored part of your query as below, proceed adding your calculations under WITH block:
WITH Emp_CTE (ID_Employee, attend_date)
AS
(
SELECT emp.*,
(SELECT TOP 1 att.attend_date FROM dbo_Attendance AS att
WHERE att.ID_Employee = emp.ID_Employee
AND att.attend_date > emp.attend_date
ORDER BY att.attend_date ASC) AS [NextDate]
FROM dbo_Employees
)
SELECT ISNULL(NextDate, GETDATE()) AS [next123],
ISNULL(NextDate, GETDATE()) - att.attend_date AS [difference]
FROM Emp_CTE;

Convert IIF() Access statement to SQL Server

I am trying to convert a access code into sql 2008. But just found out sql 2008 doesn't support the IIF statement! Here are two ways that I am trying to rewrite my query, I know I am messing up with the syntax:
select distinct (IIf(IsNull([dbo_TASK]![act_start_date])
,IIf(IsNull([dbo_TASK]![restart_date]),[dbo_TASK]![target_start_date],[dbo_TASK]![restart_date]),[dbo_TASK]![act_start_date]) AS [Estimated Start Date] from dbo.task
ATTEMPT1:
if dbo.task.act_start_date=null
then
if(dbo.task.restart_date=null)
then dbo.task.target_start_date
else dbo.task.restart_date
else dbo.task.act_start_date
ATTEMP2:
select (case when dbo.task.act_start=null then
(case when dbo.task.restart_date=null
then (dbo.task.target_start_date)
else dbo.task.restart_date
end)
else (dbo.task.act_start_date)
end) from dbo.task
Your query was very close. When checking if a value is equal to null you use Is Null not =null
So if you implement that you can use the following:
select distinct
case
when [act_start_date] is null
then
case
when [restart_date] is null
then [target_start_date]
else [restart_date]
else [act_start_date]
end AS [Estimated Start Date]
from dbo.task
Or even easier you can use COALESCE() which will return the first non-null value:
select distinct
coalesce([act_start_date], [restart_date], [target_start_date]) as [Estimated Start Date]
from dbo.task

SQL Server Function WIthin Case Statement

select top 10 *, case
when datediff(day,DateOfSale, getDate()) > 5 then '5'
when datediff(day,DateOfSale, getDate()) > 10 then '10'
... (more datediff clauses)
...
...
else '20'
end as jack
from Foo
Is SQL Server smart enough to evaluate the datediff function call once within the case statement and then use that value for every when clause? Or is the function is getting called 'n' times, where 'n' is the amount of when clauses?
It's hard to see how SQL Server could evaluate the call once. The call has a column as parameter and so has to be evaluated for every row.
Thus, your condition is better written like:
when DateOfSale < dateadd(day, -5, getdate()) then '5'
In this case the difference is small. Date calculations are cheap.
The classic example where the function call does matter is a where condition on a table with an index on the date column. For example, YourTable with an index on (dt). This query would allow an index to be used:
select * from YourTable where dt < dateadd(day, -5, getdate())
While this query would not:
select * from YourTable where datediff(day, DateOfSale, getDate()) > 5
It's puzzling that so many answers are mentioning indexes. Indeed, DATEDIFF is not SARGable, but that's completely irrelevant here as CASE WHEN doesn't cause the query optimizer in SQL Server to consider index usage (other than trying to find a covering scannable path). The candidacy of DATEDIFF-involved expressions for index pathing is completely irrelevant to this question, as far as I can tell.
It's pretty easy to demonstrate that SQL Server does, indeed, stop evaluating predicates inside CASE statements once the first true predicate is found.
To demonstrate that fact, let's cook up some sample data:
CREATE TABLE Diffy (SomeKey INTEGER NOT NULL IDENTITY(1,1), DateOfSale DATE);
DECLARE #ThisOne AS DATE;
SET #ThisONe = '2012-01-01';
WHILE #thisONe < '2013-01-01'
BEGIN
INSERT INTO Diffy (DateOfSale) VALUES(#ThisOne);
SET #ThisOne = DateAdd(d, 1, #ThisOne);
END;
Then, let's SELECT it in the pattern of the original question. Note that the original question specifies a TOP 10 clause without an ORDER BY clause, so the values we actually get back are random. But if we add a clause to the CASE that would poison evaluation, we can see what's happening:
SELECT TOP 10 *, CASE
WHEN datediff(day,DateOfSale, getDate()) > 5 then '5'
WHEN datediff(day,DateOfSale, getDate()) > 10 then '10'
WHEN 1/0 > 1then 'boom'
ELSE '20' END
AS Jack
FROM Diffy;
Note that if we ever evaluated 1/0 > 1, then we'd expect something like a 'Divide by zero error encountered.'. However, running this query against my server yields ten rows, all with '5' in the Jack column.
If we take away the TOP 10, sure enough we get some rows and then get the Divide by zero error. Thus, we can safely conclude that SQL Server is doing early exit evaluation on the CASE statement.
On top of it, the documentation also tells us so:
The CASE statement evaluates its conditions sequentially and stops with the first condition whose condition is satisfied.
Perhaps the question is meant to ask if the common DATEDIFF() subexpression is hoisted from all the CASE statements, computed once, and then evaluated within each predicate's context. By observing the output of SET SHOWPLAN_TEXT ON, I think we can conclude that's not the case:
|--Compute Scalar(DEFINE:([Expr1004]=CASE WHEN datediff(day,CONVERT_IMPLICIT(datetimeoffset(7),[Scratch3].[dbo].[Diffy].[DateOfSale],0),CONVERT_IMPLICIT(datetimeoffset(3),getdate(),0))>(5) THEN '5' ELSE CASE WHEN datediff(day,CONVERT_IMPLICIT(datetimeoffset(7),[Scratch3].[dbo].[Diffy].[DateOfSale],0),CONVERT_IMPLICIT(datetimeoffset(3),getdate(),0))>(10) THEN '10' ELSE CASE WHEN (1)/(0)>(1) THEN 'boom' ELSE '20' END END END))
|--Index Scan(OBJECT:([Scratch3].[dbo].[Diffy].[DiffyHelper]))
From that, we can conclude that the structure of this query means that DATEDIFF() is evaluated for each row and for each predicate, so O(rows * predicates) calls, at worst. That causes some CPU load for the query, but DATEDIFF() isn't quite that expensive and shouldn't be much of a concern. If, in practice, it turns out to be causing a performance problem, there are ways to manually hoist the computation from the query. For example, DATEDIFF() on the date-relative side of the comparison.
Sure, but not in your case (the expression is based on a table column value that changes for each row), but in any event, don't execute the datediff on the table column value, run a dateadd on the predicate (comparison) value so your query can still use any existing index on DateOfSale...
select top 10 *,
case When DateOfSale < dateadd(day, -20, getDate()) then '20'
When DateOfSale < dateadd(day, -15, getDate()) then '15'
When DateOfSale < dateadd(day, -10, getDate()) then '10'
When DateOfSale < dateadd(day, -5, getDate()) then '5'
else '20' end jack
from Foo

Resources