CASE Statement in WHERE Clause SQL Server - sql

I'm having trouble sorting this out. I want to see the fiscal quarters for Date_Received. When the #ReviewPeriodQuarter = 1 then I want the Date_Received months 10,11,12. If #ReviewPeriodQuarter = 2 then I want the Date_Received months 1,2,3 etc. SQL Server doesn't like the BETWEEN part of this. Thanks
DECLARE #ReviewPeriodQuarter Int
SELECT * FROM Table
WHERE
MONTH(Date_Received) =
CASE
WHEN #ReviewPeriodQuarter = 1 THEN BETWEEN 10 AND 12
WHEN #ReviewPeriodQuarter = 2 THEN BETWEEN 1 AND 3
WHEN #ReviewPeriodQuarter = 3 THEN BETWEEN 4 AND 6
WHEN #ReviewPeriodQuarter = 4 THEN BETWEEN 7 AND 9
END

I would, pesonally, move the ranges to outside the SELECT entirely, and then just use a simple WHERE:
DECLARE #MonthStart int,
#MonthEnd int;
SELECT #MonthStart = CASE #ReviewPeriodQuarter WHEN 1 THEN 10,
WHEN 2 THEN 1
WHEN 3 THEN 4
WHEN 4 THEN 7
END,
#MonthEnd = CASE #ReviewPeriodQuarter WHEN 1 THEN 12,
WHEN 2 THEN 3
WHEN 3 THEN 6
WHEN 4 THEN 9
END;
SELECT *
FROM dbo.[Table]
WHERE MONTH(Date_Received) BETWEEN #MonthStart AND #MonthEnd;
Note that this still won't be SARGable though, due to the use of MONTH(Date_Received) in the WHERE. I must admit, needing rows from a table for specific months, regardless of year, is a little odd. If that is your true requirement you might be better off "investing" in a Calendar Table you can JOIN to, and then just having a WHERE on the calendar table's CalendarMonth column; which would be SARGable.

You can either do it with more parameters like Larnu or you can use your original method but tweaked
DECLARE #ReviewPeriodQuarter INT
SELECT *
FROM Table
WHERE MONTH(Date_Received) BETWEEN
CASE
WHEN #ReviewPeriodQuarter = 1 THEN 10
WHEN #ReviewPeriodQuarter = 2 THEN 1
WHEN #ReviewPeriodQuarter = 3 THEN 4
WHEN #ReviewPeriodQuarter = 4 THEN 7
END
AND
CASE
WHEN #ReviewPeriodQuarter = 1 THEN 12
WHEN #ReviewPeriodQuarter = 2 THEN 3
WHEN #ReviewPeriodQuarter = 3 THEN 6
WHEN #ReviewPeriodQuarter = 4 THEN 9
END

How about using values()?
SELECT *
FROM Table t JOIN
(VALUES (1, 10, 12),
(2, 1, 3),
(3, 4, 6),
(4, 7, 9)
) v(ReviewPeriodQuarter, lo, hi)
ON MONTH(t.Date_Received) BETWEEN v.lo AND v.hi AND
v.ReviewPeriodQuarter = #ReviewPeriodQuarter;
Or even more simply:
WHERE DATEPART(quarter, DATEADD(MONTH, -3, t.Date_Received)) = #ReviewPeriodQuarter
In other words, you don't need conditional logic to specify each quarter. You can just use date arithmetic.

Related

TSQL - dates overlapping - number of days

I have the following table on SQL Server:
ID
FROM
TO
OFFER NUMBER
1
2022.01.02
9999.12.31
1
1
2022.01.02
2022.02.10
2
2
2022.01.05
2022.02.15
1
3
2022.01.02
9999.12.31
1
3
2022.01.15
2022.02.20
2
3
2022.02.03
2022.02.25
3
4
2022.01.16
2022.02.05
1
5
2022.01.17
2022.02.13
1
5
2022.02.05
2022.02.13
2
The range includes the start date but excludes the end date.
The date 9999.12.31 is given (comes from another system), but we could use the last day of the current quarter instead.
I need to find a way to determine the number of days when the customer sees exactly one, two, or three offers. The following picture shows the method upon id 3:
The expected results should be like (without using the last day of the quarter):
ID
# of days when the customer sees only 1 offer
# of days when the customer sees 2 offers
# of days when the customer sees 3 offers
1
2913863
39
0
2
41
0
0
3
2913861
24
17
4
20
0
0
5
19
8
0
I've found this article but it did not enlighten me.
Also I have limited privileges that is I am not able to declare a variable for example so I need to use "basic" TSQL.
Please provide a detailed explanation besides the code.
Thanks in advance!
The following will (for each ID) extract all distinct dates, construct non-overlapping date ranges to test, and will count up the number of offers per range. The final step is to sum and format.
The fact that the start dates are inclusive and the end dates are exclusive while sometimes non-intuitive for the human, actually works well in algorithms like this.
DECLARE #Data TABLE (Id INT, FromDate DATETIME, ToDate DATETIME, OfferNumber INT)
INSERT #Data
VALUES
(1, '2022-01-02', '9999-12-31', 1),
(1, '2022-01-02', '2022-02-10', 2),
(2, '2022-01-05', '2022-02-15', 1),
(3, '2022-01-02', '9999-12-31', 1),
(3, '2022-01-15', '2022-02-20', 2),
(3, '2022-02-03', '2022-02-25', 3),
(4, '2022-01-16', '2022-02-05', 1),
(5, '2022-01-17', '2022-02-13', 1),
(5, '2022-02-05', '2022-02-13', 2)
;
WITH Dates AS ( -- Gather distinct dates
SELECT Id, Date = FromDate FROM #Data
UNION --(distinct)
SELECT Id, Date = ToDate FROM #Data
),
Ranges AS ( --Construct non-overlapping ranges (The ToDate = NULL case will be ignored later)
SELECT ID, FromDate = Date, ToDate = LEAD(Date) OVER(PARTITION BY Id ORDER BY Date)
FROM Dates
),
Counts AS ( -- Calculate days and count offers per date range
SELECT R.Id, R.FromDate, R.ToDate,
Days = DATEDIFF(DAY, R.FromDate, R.ToDate),
Offers = COUNT(*)
FROM Ranges R
JOIN #Data D ON D.Id = R.Id
AND D.FromDate <= R.FromDate
AND D.ToDate >= R.ToDate
GROUP BY R.Id, R.FromDate, R.ToDate
)
SELECT Id
,[Days with 1 Offer] = SUM(CASE WHEN Offers = 1 THEN Days ELSE 0 END)
,[Days with 2 Offers] = SUM(CASE WHEN Offers = 2 THEN Days ELSE 0 END)
,[Days with 3 Offers] = SUM(CASE WHEN Offers = 3 THEN Days ELSE 0 END)
FROM Counts
GROUP BY Id
The WITH clause introduces Common Table Expressions (CTEs) which progressively build up intermediate results until a final select can be made.
Results:
Id
Days with 1 Offer
Days with 2 Offers
Days with 3 Offers
1
2913863
39
0
2
41
0
0
3
2913861
24
17
4
20
0
0
5
19
8
0
Alternately, the final select could use a pivot. Something like:
SELECT Id,
[Days with 1 Offer] = ISNULL([1], 0),
[Days with 2 Offers] = ISNULL([2], 0),
[Days with 3 Offers] = ISNULL([3], 0)
FROM (SELECT Id, Offers, Days FROM Counts) C
PIVOT (SUM(Days) FOR Offers IN ([1], [2], [3])) PVT
ORDER BY Id
See This db<>fiddle for a working example.
Find all date points for each ID. For each date point, find the number of overlapping.
Refer to comments within query
with
dates as
(
-- get all date points
select ID, theDate = FromDate from offers
union -- union to exclude any duplicate
select ID, theDate = ToDate from offers
),
cte as
(
select ID = d.ID,
Date_Start = d.theDate,
Date_End = LEAD(d.theDate) OVER (PARTITION BY ID ORDER BY theDate),
TheCount = c.cnt
from dates d
cross apply
(
-- Count no of overlapping
select cnt = count(*)
from offers x
where x.ID = d.ID
and x.FromDate <= d.theDate
and x.ToDate > d.theDate
) c
)
select ID, TheCount, days = sum(datediff(day, Date_Start, Date_End))
from cte
where Date_End is not null
group by ID, TheCount
order by ID, TheCount
Result :
ID
TheCount
days
1
1
2913863
1
2
39
2
1
41
3
1
2913861
3
2
29
3
3
12
4
1
20
5
1
19
5
2
8
To get to the required format, use PIVOT
dbfiddle demo

Count of people by hour

I need some help working out how many people were on site for each hour.
The data looks like this
Id Roomid, NumPeople, Starttime, Closetime.
1 1 4 2018/10/03 09:06 2018/10/03 12:43
2 2 8 2018/10/03 10:16 2018/10/03 13:12
3 1 6 2018/10/03 13:02 2018/10/03 15:01
What I need out is the max count of people during the hour, each hour
Time | PeoplePresent
9 4
10 12
11 12
12 12
13 14
14 6
15 6
Getting the count of people as the arrived is simple enough, but I can’t think where to start to get the presence for each hour. Can anyone suggest a strategy for this. I ok with the simple SQL stuff but I’m certain this requires some advanced SQL functions.
Tested the following in SQL Server 2008 R2:
You can use a recursive CTE to build the list of hours, including the row id and NumPeople values. Then you can sum them together to get your final output. I put together the following test data based on the question.
CREATE TABLE #times
(
Id int
, Roomid INT
, NumPeople INT
, Starttime DATETIME
, Closetime DATETIME
)
INSERT INTO #times
(
Id
,Roomid
,NumPeople
,Starttime
,Closetime
)
VALUES
(1, 1, 4 , '2018/10/03 09:06', '2018/10/03 12:43')
,(2, 2, 8, '2018/10/03 10:16', '2018/10/03 13:12')
,(3, 1, 6, '2018/10/03 13:02', '2018/10/03 15:01')
;WITH recursive_CTE (id, startHour, currentHour, diff, NumPeople) AS
(
SELECT
Id
,startHour = DATEPART(HOUR, t.Starttime)
,currentHour = DATEPART(HOUR, t.Starttime)
,diff = DATEDIFF(HOUR, Starttime, Closetime)
,t.NumPeople
FROM #times t
UNION ALL
SELECT
r.id
,r.startHour
,r.currentHour + 1
,r.diff
,r.NumPeople
FROM recursive_CTE r
WHERE r.currentHour < startHour + diff
)
SELECT
Time = currentHour
,PeoplePresent = SUM(NumPeople)
FROM recursive_CTE
GROUP BY currentHour
DROP TABLE #times
Query results:
Time PeoplePresent
9 4
10 12
11 12
12 12
13 14
14 6
15 6

Count last 10 data for each minute

I'm trying to write a SQL Query that has to return how many transactions were made in the last 10 minutes for each minute. Like:
1 minute ago : 2 transactions
2 minutes ago : 1 transaction
...
9 minutes ago : 3 transactions
10 minutes ago : 4 transactions
I was trying to do this query:
DECLARE #N int = 10;
DECLARE #NOW DATETIME = GETDATE();
WITH numbers( num ) AS (
SELECT 1 UNION ALL
SELECT 1 + num FROM numbers WHERE num < #N )
SELECT num AS minute,
(
SELECT COUNT(*) AS RESULTS
FROM [ApiTransactions]
WHERE [DateUtc] > DATEADD(year, -1, #NOW)
GROUP BY DATEPART(minute, DATEADD(minute, -num, #NOW))
)
FROM numbers;
I still don't know if the logic is right. What I know is that I'm receiving the error:
Each GROUP BY expression must contain at least one column that is not an outer reference.
Why I'm having this error? Is there a better way to do the query?
You don't need a numbers table for this, unless you need to fill in times with no transactions. I would start with this:
SELECT DATEADD(minute, DATEDIFF(minute, 0, DateUtc), 0) as the_minute, COUNT(*)
FROM ApiTransactions
WHERE DateUtc > DATEADD(minute, -10, DateUtc)
GROUP BY DATEADD(minute, DATEDIFF(minute, 0, DateUtc), 0)
ORDER BY the_minute;
You can use this one:
SELECT DATEDIFF(minute,[DateUtc],GETDATE()) as minutes_ago,
COUNT(*) tran_count
FROM ApiTransactions
WHERE DATEDIFF(second,[DateUtc],GETDATE()) <= 600 -- 10 minutes ago
GROUP BY DATEDIFF(minute,[DateUtc],GETDATE())
Output:
minutes_ago tran_count
0 2
1 3
2 10
3 15
4 4
5 9
6 12
7 6
8 13
9 4
10 2
I managed to solve my problem in two different ways:
DECLARE #N int = 10;
DECLARE #NOW DATETIME = GETDATE();
WITH numbers( num ) AS (
SELECT 1 UNION ALL
SELECT 1 + num FROM numbers WHERE num < #N )
SELECT num-1 AS minute,
(
SELECT COUNT(*) AS RESULTS
FROM [ApiTransactions]
WHERE [DateUtc] > DATEADD(minute, -num, #NOW) AND [DateUtc] < DATEADD(minute, -num+1, #NOW)
)
FROM numbers;
that has as output:
minutes_ago amount
0 4 (most recents. still being updated)
1 0
2 2
3 3
4 1
5 2
6 1
7 2
8 1
9 3
and with:
DECLARE #NOW DATETIME = GETDATE();
SELECT CONVERT(int, DATEDIFF(second, [DateUtc], #NOW)/60) as TimePassed, COUNT([TypeCode]) as Amount
FROM [ApiTransactions]
WHERE [DateUtc] >= DATEADD (minute, -10 , #NOW)
GROUP BY CONVERT(int, DATEDIFF(second, [DateUtc], #NOW)/60)
ORDER BY CONVERT(int, DATEDIFF(second, [DateUtc], #NOW)/60)
that has as output:
minutes_ago amount
0 4 (most recents. still being updated)
2 2
3 3
4 1
5 2
6 1
7 2
8 1
9 3

DATEDIFF excluding summer months

We are running reports for a seasonal business, with expected lulls during the summer months. For some metrics, we'd essentially like to pretend that those months don't even exist.
Thus consider the default behavior of:
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-06-01') -- answer = 1
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-07-01') -- 2
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-08-01') -- 3
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-09-01') -- 4
We want to ignore June and July, so we would like those answers to look like this:
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-06-01') -- answer = 1
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-07-01') -- 1
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-08-01') -- 1
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-09-01') -- 2
What is the easiest way to accomplish this? I'd like a pure SQL solution, rather than something using TSQL, but writing a custom function such as NOSUMMER_DATEDIFF could also work.
Also, keep in mind the reports will span multiple years, so the solution should be able to handle that.
If you are only interested month differences, then I would suggest a trick here. Count the number of months since some date 0, but ignore the summer months. For example:
'2015-05-01' --> 2015 * 10 + 5 = 20155
'2015-06-01' --> 2015 * 10 + 6 = 20156
'2015-07-01' --> 2015 * 10 + 6 = 20156
'2015-08-01' --> 2015 * 10 + 6 = 20156
'2015-09-01' --> 2015 * 10 + 7 = 20157
This is a fairly easy calculation:
select (case when month(date2) <= 6 then year(date2) * 10 + month(date2)
when month(date2) in (7, 8) then year(date2) * 10 + 6
else year(date2) * 10 + (month(date2) - 2)
end)
For the difference:
select ((case when month(date2) <= 6 then year(date2) * 10 + month(date2)
when month(date2) in (7, 8) then year(date2) * 10 + 6
else year(date2) * 10 + (month(date2) - 2)
end) -
(case when month(date1) <= 6 then year(date1) * 10 + month(date1)
when month(date1) in (7, 8) then year(date1) * 10 + 6
else year(date1) * 10 + (month(date1) - 2)
end)
)
To able to achieve that, you have to "split" dates ranges to an "array" of dates for every single range of dates. CTE might be helpful in this case.
See:
--your table which holds dates ranges
DECLARE #dates TABLE(id INT IDENTITY(1,1), dFrom DATE, dTo DATE)
INSERT INTO #dates (dFrom, dTo)
VALUES('2015-05-01', '2015-06-01'),
('2015-05-01', '2015-07-01'),
('2015-05-01', '2015-08-01'),
('2015-05-01', '2015-09-01')
--summer month table
DECLARE #summermonths TABLE(summMonth INT)
INSERT INTO #summermonths(summMonth)
VALUES(6), (7)
--here Common Table Expressions is in action to "split" dates ranges to an array of dates for every single date range
;WITH CTE AS
(
SELECT id, DATEADD(MM, 0, dFrom) AS ndFrom, dTo, CASE WHEN MONTH(DATEADD(MM, 0, dFrom)) = 6 OR MONTH(DATEADD(MM, 0, dFrom)) = 7 THEN 0 ELSE 1 END AS COfMonth
FROM #dates
WHERE DATEADD(MM, 1, dFrom)<=dTo
UNION ALL
SELECT id, DATEADD(MM, 1, ndFrom) AS ndFrom, dTo, CASE WHEN MONTH(DATEADD(MM, 1, ndFrom)) = 6 OR MONTH(DATEADD(MM, 1, ndFrom)) = 7 THEN 0 ELSE 1 END AS COfMonth
FROM CTE
WHERE DATEADD(MM, 1, ndFrom)<=dTo
)
SELECT t1.id, t2.dFrom, t2.dTo, SUM(t1.COfMonth) AS MyDateDiff
FROM CTE AS t1 INNER JOIN #dates AS t2 ON t1.id = t2.id
GROUP BY t1.id, t2.dFrom , t2.dTo
Result:
id dFrom dTo MyDateDiff
1 2015-05-01 2015-06-01 1
2 2015-05-01 2015-07-01 1
3 2015-05-01 2015-08-01 2
4 2015-05-01 2015-09-01 3 --not 2, because of 5, 8, 9
Got it?
Note: a solution might be differ in case of dFrom and dTo is not the first date of month.

Combining three similar sql queries into a single result

I have a table say (TimeValue) something like this
Time Value Owner
======================
1 10 A
2 20 B
3 30 C
4 40 A
5 50 B
6 60 C
7 70 A
8 80 B
Now I have three sql statement like the followings
select owner, value as 'First sql' from TimeValue where time >= 1 and time <= 3 group by owner
select owner, value as 'Second sql' from TimeValue where time >= 4 and time <= 6 group by owner
select owner, value as 'Third sql' from TimeValue where time >= 7 and time <= 9 group by owner
Now these are the result of the above sql
1.
Value Owner
=================
10 A
20 B
30 C
2.
Value Owner
=================
40 A
50 B
60 C
3
Value Owner
===============
70 A
80 B
90 C
My question, what would be the sql statement if I want the following result??
Owner First SQL Second SQL Third SQL
==================================================
A 10 40 70
B 20 50 80
C 30 60 90
Thank you in advance
One way is to use a CASE statement, like this:
SELECT
owner
, MAX(CASE WHEN time >= 1 AND time <= 3 THEN value ELSE NULL END) AS FIRST
, MAX(CASE WHEN time >= 4 AND time <= 6 THEN value ELSE NULL END) AS SECOND
, MAX(CASE WHEN time >= 7 AND time <= 9 THEN value ELSE NULL END) AS THIRD
FROM TimeValue
GROUP BY owner
Note that you need an aggregating function, such as MAX, around value. In fact, many RDBMS engines would not even take SQL without an aggregating function around it.
SQL FIDDLE
select owner,
SUM(case when (time >= 1 and time <= 3) then value end) as 'FirstSQL',
SUM(case when (time >= 4 and time <= 6) then value end) as 'SecondSQL',
SUM(case when (time >= 7 and time <= 9) then value end) as 'ThirdSQL'
from TimeValue
group by owner
base on your example you should use SUM instead of MAX
The same idea with dasblinkenlight but different approach and I agree on aggregating function, such as MAX or SUM
SELECT
owner,
MAX(IF(time between 1 and 3, value,NULL) ) as first,
MAX(IF(time between 4 and 6, value,NULL) ) as sec,
MAX(IF(time between 7 and 9, value,NULL) ) as third
FROM TimeValue
GROUP BY owner
you can also use self-joins:
select
tv0.owner
, tv0.value sql1
, tv1.value sql2
, tv2.value sql3
from timevalue tv0
inner join timevalue tv1 on tv1.owner = tv0.owner
inner join timevalue tv2 on tv2.owner = tv0.owner
where tv0.time between 1 and 3
and tv1.time between 4 and 6
and tv2.time between 7 and 9