Use result of Case statement in another Case statement - sql-server

I have quite a long SELECT query but I have pasted the relevant part here.
I need to use the result of the of my CASE statement to use in another CASE statement. I'm doing this in SQL Server.
Would be very grateful for help.
SELECT
CompanyContact.Name AS CompanyName,
CASE
WHEN SUBSTRING(HeadLease.TenantBreakNotice, LEN(HeadLease.TenantBreakNotice), 1) = 'M'
THEN CONVERT(VARCHAR(10), DATEADD(DD, -365 / (12 / SUBSTRING(HeadLease.TenantBreakNotice, 1, LEN(HeadLease.TenantBreakNotice) - 1)), HeadLease.TenantBreakDate), 103)
WHEN SUBSTRING(HeadLease.TenantBreakNotice, LEN(HeadLease.TenantBreakNotice), 1) = 'Y'
THEN CONVERT(VARCHAR(10), DATEADD(DD, -365 * (SUBSTRING(HeadLease.TenantBreakNotice, 1, LEN(HeadLease.TenantBreakNotice) - 1)), HeadLease.TenantBreakDate), 103)
ELSE HeadLease.TenantBreakNotice
END AS [TenantBreakNotice], <-- I need this to be used in the case statement below.
CASE
WHEN [TenantBreakNotice] < CONVERT(varchar(10), getdate(), 103)
THEN 'Expiry'
WHEN [TenantBreakNotice] IS NULL
THEN 'Expiry'
ELSE 'Break'
END AS [LeaseEventType]
FROM
HeadLease

You cannot use a column alias in the same select where it is defined. The usual solution is to repeat the logic (hard to maintain), use a subquery, or CTE. SQL Server offers another elegant solution:
SELECT hl.Name AS CompanyName, v.TenantBreakNotice,
(CASE WHEN v.TenantBreakNotice < CONVERT(varchar(10), getdate(), 103) THEN 'Expiry'
WHEN TenantBreakNotice IS NULL THEN 'Expiry'
ELSE 'Break'
END) AS [LeaseEventType]
FROM HeadLease hl OUTER APPLY
(VALUES (CASE WHEN SUBSTRING(hl.TenantBreakNotice, LEN(hl.TenantBreakNotice), 1) = 'M'
THEN CONVERT(VARCHAR(10), DATEADD(DAY, -365/(12/SUBSTRING(hl.TenantBreakNotice, 1, LEN(hl.TenantBreakNotice) -1)), hl.TenantBreakDate), 103)
WHEN SUBSTRING(hl.TenantBreakNotice, LEN(hl.TenantBreakNotice), 1) = 'Y'
THEN CONVERT(VARCHAR(10), DATEADD(DAY, -365*(SUBSTRING(hl.TenantBreakNotice,1, LEN(hl.TenantBreakNotice)-1)), hl.TenantBreakDate), 103)
ELSE hl.TenantBreakNotice
END) v(TenantBreakNotice);
Of course, the logic is incorrect, because you are comparing dates as strings. However, that is something you need to figure out yourself. Don't convert dates to strings for date operations. And, you should output the results as YYYY-MM-DD so the formats are unambiguous.

As #Juergen pointed out, you can't do exactly what you want, but you could compute the first CASE expression in a subquery, and then use it an outer wrapping query:
WITH cte AS (
SELECT
Name AS CompanyName,
CASE WHEN SUBSTRING(HeadLease.TenantBreakNotice,LEN(HeadLease.TenantBreakNotice), 1) = 'M'
THEN CONVERT(VARCHAR(10), DATEADD(DD,-365/(12/SUBSTRING(HeadLease.TenantBreakNotice,1,LEN(HeadLease.TenantBreakNotice)-1)),HeadLease.TenantBreakDate), 103)
WHEN SUBSTRING(HeadLease.TenantBreakNotice,LEN(HeadLease.TenantBreakNotice),1) = 'Y'
THEN CONVERT(VARCHAR(10), DATEADD(DD,-365*(SUBSTRING(HeadLease.TenantBreakNotice,1,LEN(HeadLease.TenantBreakNotice)-1)),HeadLease.TenantBreakDate), 103)
ELSE HeadLease.TenantBreakNotice
END AS [TenantBreakNotice]
FROM HeadLease
)
SELECT
Name,
TenantBreakNotice,
CASE WHEN TenantBreakNotice < CONVERT(varchar(10), getdate(), 103)
THEN 'Expiry'
WHEN TenantBreakNotice IS NULL THEN 'Expiry'
ELSE 'Break'
END AS [LeaseEventType]
FROM cte;

Use CTEs (common table expressions). In CTEs you can refer to the columns from the previous CTE, so you can split the CASE logic like you would like.
Example:
WITH
CTE_1 AS
(
SELECT
*
,CASE
WHEN SUBSTRING(HeadLease.TenantBreakNotice,LEN(HeadLease.TenantBreakNotice),1) = 'M'
THEN CONVERT(VARCHAR(10), DATEADD(DD,-365/(12/SUBSTRING(HeadLease.TenantBreakNotice,1,LEN(HeadLease.TenantBreakNotice)-1)),HeadLease.TenantBreakDate), 103)
WHEN SUBSTRING(HeadLease.TenantBreakNotice,LEN(HeadLease.TenantBreakNotice),1) = 'Y'
THEN CONVERT(VARCHAR(10), DATEADD(DD,-365*(SUBSTRING(HeadLease.TenantBreakNotice,1,LEN(HeadLease.TenantBreakNotice)-1)),HeadLease.TenantBreakDate), 103)
ELSE
HeadLease.TenantBreakNotice
END AS [TenantBreakNotice]
...
),
CTE_2 AS
(
SELECT
*
,CASE
WHEN [TenantBreakNotice] < CONVERT(varchar(10),getdate(),103) THEN 'Expiry'
WHEN [TenantBreakNotice] IS NULL THEN 'Expiry'
ELSE 'Break'
END AS [LeaseEventType]
FROM
CTE_1
)
SELECT * FROM CTE_2

You can move the first case expression into your from by using a derived table/subquery like so:
select
cc.Name as CompanyName
, convert(varchar(10),hl.[TenantBreakNotice],103) as TenantBreakNotice
, case
when hl.[TenantBreakNotice] < getdate() then 'Expiry'
when hl.[TenantBreakNotice] is null then 'Expiry'
else 'Break'
end as [LeaseEventType]
from (
select *,
case
when substring(HeadLease.TenantBreakNotice,len(HeadLease.TenantBreakNotice),1) = 'M'
then dateadd(day,-365/(12/substring(HeadLease.TenantBreakNotice,1,len(HeadLease.TenantBreakNotice)-1)),HeadLease.TenantBreakDate)
when substring(HeadLease.TenantBreakNotice,len(HeadLease.TenantBreakNotice),1) = 'Y'
then dateadd(day,-365*(substring(HeadLease.TenantBreakNotice,1,len(HeadLease.TenantBreakNotice)-1)),HeadLease.TenantBreakDate)
else HeadLease.TenantBreakNotice
end as [TenantBreakNotice]
from HeadLease
) as hl
inner join CompanyContact cc
on cc....
Notes:
Don't compare strings as dates, this is going to lead to problems; especially with the style you specified (103).
Bad Habits to Kick : Using shorthand with date/time operations - Aaron Bertrand
Bad habits to kick : mis-handling date / range queries - Aaron Bertrand

Related

Subquery can return 1 column only

The purpose of this query would be to do a calculation based on if today's date is a weekday or a weekend and use it after with Union All, however I am getting
Msg 116 error:
Only one expression can be specified in the select list when the subquery is not introduced with EXISTS.
It looks like that I can return only 1 column with this:
My query:
SELECT (SELECT CASE WHEN DATENAME(weekday, GETDATE()) IN (N'Saturday', N'Sunday')
THEN
(SELECT --WeekEND
'X' AS Table_name
,CAST(MAX(date_1) as date) AS max_date
,DATEDIFF(DD,(CAST(MAX(date_1)as date)),GETDATE()) as NrOfDays
,CASE
WHEN DATEDIFF(DD,(CAST(MAX(date_1)as date)),GETDATE()) <= 3 THEN 'good'
ELSE 'bad'
END AS Status
FROM [Table_1])
ELSE
(SELECT --WeekDAY
'X' AS Table_name
,CAST(MAX(date_1) as date) AS max_date
,DATEDIFF(DD,(CAST(MAX(date_1)as date)),GETDATE()) as NrOfDays
,CASE
WHEN DATEDIFF(DD,(CAST(MAX(date_1)as date)),GETDATE()) <= 1 THEN 'good'
ELSE 'bad'
END AS Status
FROM [Table_1])
END
)
Same issue with the following just a simplified version (if I delete the 'good', 'bad' part it works):
SELECT (SELECT CASE WHEN DATENAME(weekday, GETDATE()) IN (N'Saturday', N'Sunday') THEN
(SELECT 'Weekend', 'good')
ELSE
(SELECT 'Weekday', 'bad')
END
)
You are trying to return two columns from your subquery, into a single column of your main query.
Instead, you need to make two (very similar) subqueries. One for each column in your main query.
SELECT(
SELECT(
CASE WHEN DATENAME(weekday, GETDATE()) IN (N'Saturday', N'Sunday') THEN
(SELECT 'Weekend')
ELSE
(SELECT 'Weekday')
END AS Column1,
CASE WHEN DATENAME(weekday, GETDATE()) IN (N'Saturday', N'Sunday') THEN
(SELECT 'good')
ELSE
(SELECT 'bad')
END AS Column2
)
)
There are other (better) ways to do this, depending upon your expected output. But this is probably the easiest/simplest to understand.
Also, not that it really matters but you have a lot of SELECTs in your query that you don't really need. The above can be simplified to:
SELECT
CASE WHEN DATENAME(weekday, GETDATE()) IN (N'Saturday', N'Sunday') THEN
'Weekend'
ELSE
'Weekday'
END AS Column1,
CASE WHEN DATENAME(weekday, GETDATE()) IN (N'Saturday', N'Sunday') THEN
'good'
ELSE
'bad'
END AS Column1
I don't know what your "Status" column is really doing, so I can't optimise it safely for you. But I think this should do what you want:
SELECT
'X' AS Table_name,
CAST(MAX(date_1) as date) AS max_date,
DATEDIFF(DD,(CAST(MAX(date_1)as date)), GETDATE()) as NrOfDays,
CASE WHEN DATENAME(weekday, GETDATE()) IN (N'Saturday', N'Sunday') THEN
CASE WHEN DATEDIFF(DD,(CAST(MAX(date_1)as date)), GETDATE()) <= 3 THEN
'good'
ELSE
'bad'
END
ELSE
CASE WHEN DATEDIFF(DD,(CAST(MAX(date_1)as date)), GETDATE()) <= 1 THEN
'good'
ELSE
'bad'
END
END AS Status
FROM [Table_1]

Sql query problem while joining the query to another query

I am running the below query in sql but it is giving the below error.
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
What I have tried :
SELECT b.Service_Name,
c.Service_Type,
Application_No,
Reg_No,
Student_Name,
(SELECT CONVERT(char(10),
dt + (SELECT COUNT(1)
FROM Holiday_list
WHERE Date_Fmt BETWEEN School_update AND dt),
103) AS cnt
FROM (SELECT CASE WHEN Service_TypeID = '1' THEN (School_update + 30) ELSE (School_update + 5) END AS dt
FROM Application_Status) a ) AS Nxt_date,
DATEDIFF(DAY, School_update, GETDATE()) AS Day_Count,
Created_Date,
School_Code,
CASE WHEN Payment_Status = 'Y' THEN 'PAID' WHEN Payment_Status = 'N' THEN 'NOT PAID' END AS Payment_Status
FROM Application_Status a,
MST_Service b,
MST_ServiceType c,
KSEEBMASTERS.dbo.MST_SCHOOL s,
MST_Division d
WHERE a.Service_ID = b.Service_ID
AND s.SCM_SCHOOL_CODE COLLATE Latin1_General_CI_AI = a.School_Code
AND s.DIST_CODE COLLATE Latin1_General_CI_AI = d.DistrictCode
AND a.Service_TypeID = c.Type_ID
AND d.DivisionCode = 'ED'
AND Payment_Status = 'Y'
AND school_status = 'Y'
AND Div_Status = 'N';
But the problem is in the below query while joining the query to another query.
SELECT CONVERT(char(10),
dt + (SELECT COUNT(*)
FROM Holiday_list
WHERE Date_Fmt BETWEEN School_update AND dt),
103) AS cnt
FROM (SELECT CASE WHEN Service_TypeID = '1' THEN (School_update + 30) ELSE (School_update + 5) END AS dt,
School_update
FROM Application_Status) a;
Here Application_Status is sone table with Column name School_Update and Holiday_List is another table with column name Date_Fmt.
The answer is in the error message: if you nest a query to get a field, that query must return one and only one row.
So your issue is there in the last query that you posted, but not in nested query SELECT COUNT(*) FROM Holiday_list (...) because it's an aggregate query.
Maybe it's just that SELECT CASE WHEN Service_TypeID = '1' THEN (School_update + 30) ELSE (School_update + 5) END AS dt, School_update FROM Application_Status returns more then one row and therefore also the query that contains it.

Rolling 3 Years of data with desired format

Start Date = 2016-03-01
End Date = 2019-02-15
I need the query/Function which should retrive the below result based on start and end date. I have a query to frame the result .But i need some query/function to retrive result set in 5 to 10 sec with performance
You mean like
SELECT * FROM table WHERE [start date]>='2016-03-01' AND [end date]<='2019-02-15'
Am I missing something? This seems too simple to ask as a question
If your problem is performance perhaps consider indexing start and end date columns, and if you're only getting a couple other values from the table include those in the index too.
CREATE INDEX IX_table_startdate_enddate
ON schema.table ([start date], [end date])
INCLUDE (othercolumn1, othercolumn2);
It means that queries like:
SELECT othercolumn1, othercolumn2 FROM table
WHERE [start date]>='2016-03-01' AND [end date]<='2019-02-15'
Can be answered from the index without having to connect the index to the table to pull the desired data
If you still can't wrong enough out of it after that perhaps you have a design flaw in your app; pilling a db for a massive amount of data in a tight interval could be solved another way like only emitting events when data actually changes
It's just occurred to me you're probably looking for a query to generate that result set from nothing. In sqlserver we can use a recursive cte:
DECLARE #dateFrom DATE = '2016-03-01'
DECLARE #dateTo DATE = '2019-02-15'
with d as (
SELECT #datefrom as m
UNION ALL
SELECT DATEADD(MONTH,1,reqDate)
FROM d
WHERE DATEADD(MONTH,1,reqDate) <= #dateto
),
SELECT
CONCAT(
DATENAME(MONTH, #dateFrom), ' ',
CASE WHEN MONTH(d.m) < MONTH(#dateFrom) THEN YEAR(d.m) - 1 ELSE YEAR(d.m) END, '-',
DATENAME(MONTH, #dateTo), ' ',
CASE WHEN MONTH(d.m) < MONTH(#dateFrom) THEN YEAR(d.m) ELSE YEAR(d.m) +1 END
) as range,
MONTH(d.m) as month,
d.m as startdate,--do not use spaces in column names
CASE WHEN #dateTo < EOMONTH(d.m) then #dateTo ELSE EOMONTH(d.m) END as enddate --or dateadd 1 month then dateadd -1 day if you don't have eomonth
FROM d
OPTION(MAXRECURSION 0);
Same format with your result try it.
DECLARE #StartDate DATETIME, #EndDate DATETIME
SET #StartDate = '2016-03-01'
SET #EndDate = '2038-02-15'
;WITH CTEs AS
(
SELECT #StartDate as [Start Date]
UNION ALL
SELECT DATEADD(MONTH,1,[Start Date])
FROM CTEs WHERE DATEADD(MONTH,1,[Start Date]) <= #EndDate
)
SELECT
CONCAT(
DATENAME(MONTH, #StartDate), ' ',
CASE WHEN MONTH([Start Date]) < MONTH(#StartDate) THEN YEAR([Start Date]) - 1 ELSE YEAR([Start Date]) END, '-',
DATENAME(MONTH, #EndDate), ' ',
CASE WHEN MONTH([Start Date]) < MONTH(#StartDate) THEN YEAR([Start Date]) ELSE YEAR([Start Date]) + 1 END
) AS [Range],
MONTH([Start Date]) AS [Month],
CONVERT(VARCHAR(10),[Start Date],101) AS [Start Date],
CONVERT(VARCHAR(10),(CASE WHEN [Start Date] <> DATEFROMPARTS(YEAR(#EndDate),MONTH(#EndDate),1)
THEN EOMONTH([Start Date])
ELSE #EndDate
END),101) AS [End Date]
FROM CTEs
OPTION(MAXRECURSION 0);

Using Max with CTE and CASE T-SQL

I am trying to use MAX() to select the most recent placement date within our database, and use Table_CTE so I can then select and filter between the dates desired.
BEGIN
DECLARE #Rangetill DATE, #Rangefrom DATE
SET #rangefrom = DATEADD(day, -50, GETDATE())
SET #Rangetill = DATEADD(day, -90, GETDATE());
WITH Table_CTE (Name, ID, Rangefrom, Rangetill, StatusID, Statusdate) AS
(
SELECT
PE.Personname + ' ' + PE.Surname [Name],
A.ApplicantId,
#rangefrom [Expiry warning from],
#rangetill [Expiry warning till],
A.Statusid,
selected = CASE
WHEN P.EndDate IS NOT NULL AND P.EndDate > A.StatusDate
THEN CONVERT(DATE, P.EndDate, 103)
ELSE CONVERT(DATE, A.StatusDate, 103)
END
FROM
Applicants AS A
LEFT JOIN
Person AS PE ON A.ApplicantId = PE.PersonID
LEFT JOIN
Placements AS P on A.applicantid = P.Applicantid
)
SELECT *
FROM Table_CTE
WHERE table_cte.Statusdate BETWEEN #Rangetill AND #Rangefrom
AND (Table_CTE.StatusID = 58 OR Table_CTE.statusid = 63)
ORDER BY Name DESC
END
The above selects the right information but also selects duplicate applicants with placement end dates (p.enddate) as they could have been placed more than once. The WHERE clause also limits the most recent enddate to within the range provided by the Variables and as there needs to be a log there will be multiple end dates. so my solution or idea would be to uses a max() within the Case or CTE Select. However I am not sure how to use or work with Max() in this case.
In this case I would like to check and return the Max(p.enddate) if it exists and store that in the statusdate of Table_CTE.
Is this possible and is it the best way to provide this information in a stored procedure?
In the CTE would be more efficient but this is easier
SELECT c.[Name], max(c.Statusdate)
FROM Table_CTE c
WHERE c.Statusdate Between #Rangetill and #Rangefrom
AND c.StatusID in (58, 63)
group by c.[Name]
Add the other columns on your own
Declare
#Rangetill date,
#Rangefrom date
SET #rangefrom = DATEADD(day, -50, GETDATE())
SET #Rangetill = DATEADD(day, -90, GETDATE());
With Table_CTE ( ID, Rangefrom, Rangetill, Statusdate)
AS (
Select
A.ApplicantId
, #rangefrom [Expiry warning from]
, #rangetill [Expiry warning till]
, selected = CASE
WHEN max(P.EndDate) IS NOT NULL AND max(P.EndDate) > max(A.StatusDate)
THEN max(CONVERT(DATE, P.EndDate, 103))
ELSE max(CONVERT(DATE, A.StatusDate, 103))
END
FROM Applicants AS A
LEFT JOIN Person AS PE ON A.ApplicantId = PE.PersonID
LEFT JOIN Placements AS P on A.applicantid = P.Applicantid
GROUP BY A.ApplicantId
)
SELECT
PE.PersonName + ' ' + PE.Surname [NAME]
, A.ApplicantId
, Table_CTE.ID
, Table_CTE.Statusdate
, #Rangefrom [Range from]
, #Rangetill [Range till]
FROM Table_CTE
LEFT JOIN Applicants AS A ON A.ApplicantId = Table_CTE.ID
LEFT JOIN Person as PE on PE.PersonID = A.ApplicantId
WHERE table_cte.Statusdate Between #Rangetill and #Rangefrom
AND (A.StatusID = 58 or A.statusid = 63 )
Order by PE.PersonName + ' '+ PE.Surname desc
END
Really messy way around things but I got my solution by removing all but the variables and ID from CTE so I could select Max(DATE) on both A.statusdate and P.EndDate.
By doing this I could group by A.ApplicantID and rejoin specific tables outside of the CTE in order to get Applicant Name and Status ID back into my results set.
Thank you for your help everyone.

SQL Server conditional query

I have a table for attendance entries looking like this :
I need a query to export the following format :
which present the Check-in and Check-out timings using British/French format (103)
I tried the following query :
SELECT
UserID,
(SELECT MIN(checktime)
FROM [FingerPrint].[dbo].[CHECKINOUT] I
WHERE CONVERT(VARCHAR(10), i.checktime, 111) = CONVERT(VARCHAR(10), p.checktime, 111)
AND i.userid = p.userid),
(SELECT MAX(checktime)
FROM [FingerPrint].[dbo].[CHECKINOUT] I
WHERE CONVERT(VARCHAR(10), i.checktime, 111) = CONVERT(VARCHAR(10), p.checktime, 111)
AND i.userid = p.userid)
FROM
[FingerPrint].[dbo].[CHECKINOUT] p
GROUP BY
p.checktime, p.UserID
Basically I need a query to select the minimum time (check-in) and maximum time (check-out) for each day using the export format above, yet when there is no value for check-in and check-out, then query should return (null) for time.
So basically you start with something like this:
SELECT UserId,
CAST(CheckTime As Date) As CheckDate,
MIN(CheckTime) As CheckIn,
MAX(CheckTime) As CheckOut
FROM [FingerPrint].[dbo].[CHECKINOUT]
GROUP BY UserId, CAST(CheckTime As Date)
For older versions of sql server (2005, 2000), you can use convert to char(10) to isolate the date part of the datetime column:
SELECT UserId,
CONVERT(char(10), CheckTime, 102) As CheckDate, /*Convert datetime format to Date*/
MIN(CheckTime) As CheckIn,
MAX(CheckTime) As CheckOut
FROM [FingerPrint].[dbo].[CHECKINOUT]
GROUP BY UserId, CONVERT(char(10), CheckTime, 102)
see fiddle here
Then you need to figure out what you want to display if the user have only one record for a day.
Also, what happens on night shifts, when a user checks in on one date, and checks out on the other date?
A better solution would be to add a bit column to specify if the record is for check in or for check out.
Update
Using case you can check if the current time is before or after whatever time of day you want to set up as the delimiter between check in and check out. In this example I've used 12 pm.
SELECT UserId,
CONVERT(char(10), CheckTime, 102) As CheckDate, /*Convert datetime format to Date*/
CASE WHEN MIN(CheckTime) <> MAX(CheckTime) THEN
MIN(CheckTime)
ELSE
CASE WHEN MIN(CheckTime) < DATEADD(Hour, 12, CONVERT(Datetime, CONVERT(char(10), CheckTime, 102), 102)) THEN
NULL
ELSE
MIN(CheckTime)
END
END As CheckIn,
CASE WHEN MIN(CheckTime) <> MAX(CheckTime) THEN
MAX(CheckTime)
ELSE
CASE WHEN MAX(CheckTime) > DATEADD(Hour, 12, CONVERT(Datetime, CONVERT(char(10), CheckTime, 102), 102)) THEN
NULL
ELSE
MAX(CheckTime)
END
END As CheckOut
FROM [CHECKINOUT]
GROUP BY UserId, CONVERT(char(10), CheckTime, 102)
Here is the relevant fiddle.

Resources