SQL - How do split data by date into different monthly 'pots'? - sql

New to sql and having trouble figuring the following, though probably straightforward for more experienced users. I have a table of outstanding monies which I need to divide up into monthly pots; the table has the following columns:
Name, amount_due, date_due.
I need to divide the info by 'date_due' into 5 different monthly pots, the current month, last month, 2 months old, 3 months old, > 3 months old.

I assume you're after summary data that performs a SUM for each month, as opposed to showing all the rows sorted by month.
name , amount_due, date_due
Alice , 100, 2017-09-10
Bob , 500, 2017-07-03
Charlie, 300, 2017-07-02
Dan , 150, 2017-04-01
Eve , 200, 2017-01-01
Faith , 50, 2017-09-13
This query converts the date_due value into a "month" value (while still retaining date or datetime type information), then sums them by each month:
Output:
sum_amount_due, month_due
150, 2017-09-01
800, 2017-07-01
150, 2017-04-01
200, 2017-01-01
SQL:
SELECT
SUM( amount_due ) AS sum_amount_due,
DATEADD( month, DATEDIFF( month, 0, date_due ), 0 ) AS month_due
FROM
your_table
GROUP BY
DATEADD( month, DATEDIFF( month, 0, date_due ), 0 )
ORDER BY
month_due
This query does not handle dates older than 3 months specially, so for that we need to change the month_due expression to return '2001-01-01' for dates older than 3 months:
Output:
sum_amount_due, month_due
150, 2017-09-01
800, 2017-07-01
350, 2000-01-01
SQL:
SELECT
SUM( amount_due ) AS sum_amount_due,
CASE
WHEN date_due < DATEADD( month, GETDATE(), -3 ) THEN '2000-01-01'
ELSE DATEADD( month, DATEDIFF( month, 0, date_due ), 0 )
END AS month_due
FROM
your_table
GROUP BY
CASE
WHEN date_due < DATEADD( month, GETDATE(), -3 ) THEN '2000-01-01'
ELSE DATEADD( month, DATEDIFF( month, 0, date_due ), 0 )
END
ORDER BY
month_due
Due to how SQL works, you need to repeat the get-month expression in both the SELECT and GROUP BY clauses.
This can be made slightly syntactically simpler by using a subquery to identify records older than 3 months:
SELECT
SUM( amount_due ) AS sum_amount_due,
CASE
WHEN month_due_3_months THEN '2000-01-01'
ELSE month_due
END AS month_due
FROM
(
SELECT
amount_due,
DATEADD( month, DATEDIFF( month, 0, date_due ), 0 ) AS month_due,
CASE
WHEN DATEADD( month, DATEDIFF( month, 0, date_due ), 0 ) < DATEADD( month, GETDATE(), -3 ) THEN 1
ELSE 0
END AS month_due_3_months
FROM
your_table
) AS all_months
GROUP BY
CASE
WHEN month_due_3_months THEN '2000-01-01'
ELSE month_due
END AS month_due
ORDER BY
month_due
The syntactic complexity is due to some constraints inherent in the SQL language:
The SELECT sub-clause ("the projection") is evaluated after the FROM AND GROUP BY sub-clauses, so you cannot reference aliased expressions in GROUP BY: you must either repeat them or specify them in a subquery.
SQL does not have a Get month as date value function, surprisingly, you must use DATEADD( month, DATEDIFF( month, 0, #dateValue ), 0 ).
Do not use GETMONTH or DATEPART because it returns only the month component and disregards the year value, so it will incorrectly group rows from different years that share the same month.
There is no ternary operator in SQL, only the more verbose CASE WHEN x THEN y ELSE z END construct (though there is COALESCE, NULLIF, and ISNULL but those are special-cases).

I assumed all periods are cumulative, if not you need to modify each condition. Let me know if this works as expected or need modification.
select name
,sum( case when datediff(day,getdate(),date_due)<day(date_due) then amount_due end) 'current month'
,sum( case when datediff(month,getdate(),date_due)<=1 then amount_due end) 'last month'
,sum( case when datediff(month,getdate(),date_due)<=2 then amount_due end) 'last two month'
,sum( case when datediff(month,getdate(),date_due)<=3 then amount_due end) 'last three month'
,sum( case when datediff(month,getdate(),date_due)>3 then amount_due end) 'more than three month'
from monies
group by name

Use case to determine the due_date between months of due
SELECT name,
amount_due,
due_date,
CASE WHEN CAST(due_date AS DATETIME) BETWEEN DATEADD(mm, -1, GETDATE()) AND DATEADD(mm, 0, GETDATE())
THEN 'this month'
WHEN CAST(due_date AS DATETIME) BETWEEN DATEADD(mm, -2, GETDATE()) AND DATEADD(mm, -1, GETDATE())
THEN 'last month'
WHEN CAST(due_date AS DATETIME) BETWEEN DATEADD(mm, -3, GETDATE()) AND DATEADD(mm, -2, GETDATE())
THEN '2 months old'
WHEN CAST(due_date AS DATETIME) BETWEEN DATEADD(mm, -4, GETDATE()) AND DATEADD(mm, -3, GETDATE())
THEN '3 months old'
WHEN CAST(due_date AS DATETIME) < DATEADD(mm, -4, GETDATE())
THEN '> 3 months old'
END month_of_due

Related

SQL Server Group a promotion by last 24 hours, last week and last month and sort by week descending

I'm trying to look at how successful different promotions have been in the last 24 hours, week and month. To get the amount by promotion for the last 24 hours I've used this code but I don't understand how to get another two columns for the last week and the last month. And then finally I want to order it by the amount in the last week descending. I want to be able to run this query at any point during the month. Please help me.
SELECT Promotion
, Sum(Amount) AS Last_24
FROM dbo.CustomerPayment
WHERE Started >= DATEADD(day, - 1, GETDATE())
GROUP
BY Promotion
You can do it in a single query :
SELECT Promotion
, Sum(CASE WHEN Started >= DATEADD(day, -1, GETDATE()) THEN Amount ELSE 0 END) AS Last_24
, Sum(CASE WHEN Started >= DATEADD(day, -7, GETDATE()) THEN Amount ELSE 0 END) AS Last_Week
, Sum(Amount) AS Last_Month
FROM dbo.CustomerPayment
WHERE Started >= DATEADD(day, - 31, GETDATE())
GROUP
BY Promotion
ORDER BY Last_Week DESC
Note that this part :
WHERE Started >= DATEADD(day, - 31, GETDATE())
as te be clarified following your own interpretation of "Last Month" concept.
Use conditional aggregation -- that is, move the conditions to the select:
SELECT Promotion,
SUM(case when Started >= DATEADD(day, - 1, GETDATE()) then Amount end) AS Last_1_day,
SUM(case when Started >= DATEADD(day, - 7, GETDATE()) then Amount end) AS Last_7_day,
. . .
FROM dbo.CustomerPayment
GROUP BY Promotion;
One possible issue, though. GETDATE() -- despite its name -- returns a time component to the date. I suspect that you might actually want to treat this as a date, not a datetime:
SELECT Promotion,
SUM(case when Started >= DATEADD(day, - 1, CONVERT(DATE, GETDATE())) then Amount end) AS Last_1_day,
SUM(case when Started >= DATEADD(day, - 7, CONVERT(DATE, GETDATE())) then Amount end) AS Last_7_day,
. . .
FROM dbo.CustomerPayment
GROUP BY Promotion;
I was looking for month / week. Not 7 / 30 days.
If you wish those, just use variables to have that query readable.
declare #monthstart date,
#weekstart date
;
select #monthstart=datefromparts(year(current_timestamp),month(current_timestamp),1)
select cast(DATEADD(d,1-DATEPART(WEEKDAY,current_timestamp),CURRENT_TIMESTAMP) as date) as Sunday,
cast(DATEADD(d,2-case when DATEPART(WEEKDAY,current_timestamp)=1 then 8 else DATEPART(WEEKDAY,current_timestamp) end,CURRENT_TIMESTAMP) as date) as Monday
;

Average Counts by Hour by Day of Week

This question helped get me part of the way there:
SELECT
[Day],
[Hour],
[DayN],
AVG(Totals) AS [Avg]
FROM
(
SELECT
w = DATEDIFF(WEEK, 0, ForDateTime),
[Day] = DATENAME(WEEKDAY, ForDateTime),
[DayN] = DATEPART(WEEKDAY, ForDateTime),
[Hour] = DATEPART(HOUR, ForDateTime),
Totals = COUNT(*)
FROM
#Visit
GROUP BY
DATEDIFF(WEEK, 0, ForDateTime),
DATENAME(WEEKDAY, ForDateTime),
DATEPART(WEEKDAY, ForDateTime),
DATEPART(HOUR, ForDateTime)
) AS q
GROUP BY
[Day],
[Hour],
[DayN]
ORDER BY
DayN;
How could this be changed so rather than showing the average by Hour, e.g. 9, 10, 11, 12, etc. It shows it by 09:30-10:30,10:30-11:30,11:30-12:30,12:30-13:30 all the way up to 23:30.
A simple approach is to offset ForDateTime by 30 minutes. Basically you just need to replace every occurence of ForDateTime with dateadd(minute, 30, ForDateTime) in the query.
In the resultset, Hour 9 gives you the timeslot from 8:30 to 9:30, and so on.

How can I select the past seven days and its corresponding week in the past year

How can I select
The past week
Its corresponding days in the year before
This is needed for a dashboard, I would like to show a chart with results from the past seven days. It displays green if our call-center handles 98% of their phone calls within a certain time-span, red if we go over 98%. As a reference I would like to create a chart below with the corresponding seven days in the year before. This is challenging, because weekdays really influence the workload. That means I can't compare a Tuesday with a Sunday or Monday.
For instance, today is Saturday 21st Dec 2019, I would like to report the following timespans:
2019-12-13 00:00:00 -> 2019-12-20 23:59:59
and
2018-12-14 00:00:00 -> 2018-12-21 23:59:59
I made the following code (used within a select statement):
case when cs.ReachedAt between (getdate() - 7) and getdate() then 1 else 0 end as Is_PastWeek
case when cs.ReachedAt between (convert(datetime, convert(varchar(50), convert(date, dateadd(d, -1, dateadd(wk, -52, getdate())))) + ' 23:59:59')) and (convert(datetime, convert(varchar(50), convert(date, dateadd(d, -8, dateadd(wk, -52, getdate())))) + ' 00:00:00')) then 1 else 0 end as Is_SameWeekLastYear
It works, but isn't perfect. I just select the corresponding weekday in the same week as 52 weeks ago. Which means I sometimes end up selecting a matching weekday, but not the nearest. How can I do this better?
EDIT
To clarify what I mean by "picking the nearest corresponding weekday in the year before", i made the following example:
with cte1 as (
select row_number() over (order by (select 1)) - 1 as incrementor
from master.sys.columns sc1
cross join master.sys.columns sc2
), cte2 as (
select dateadd(day, cte1.incrementor, '2000-01-01') as generated_date
from cte1
where dateadd(day, cte1.incrementor, '2000-01-01') < getdate()
), cte3 as (
select convert(date, generated_date) as generated_date
, convert(date, getdate()) as now_date
from cte2
), cte4 as (
select *
, convert(date, dateadd(YEAR, -1, now_date)) as year_back
from cte3
)
select now_date
, generated_date
from cte4
where 1=1
and datepart(week, year_back) = datepart(week, generated_date)
and datepart(DW, year_back) = datepart(DW, generated_date)
This will result in:
For the grey values, I would rather take the weekday of one week later. That way I pick "the nearest corresponding weekday in the year before".
Please note that the above is an example to show what I mean, my ultimate goal is to start with this date, select the whole week before... And all (if possible) neatly within a where clause.
The expression datepart(week, getdate()) will deliver you the calendar week. With this, you can go further.
This is too long for a comment.
What difference does it make? If you are looking for the past week, just look at the same 7 days from the previous year. In one case the week might start on a Tuesday and in the other on a Wednesday. But in both cases, each weekday occurs once.
The logic would be:
where cs.ReachedAt >= datefromparts(year(getdate() - 7) - 1, month(getdate() - 7), day(getdate() - 7) and
cs.ReachedAt < datefromparts(year(getdate()), month(getdate()), day(getdate()))
The logic for the current year:
where cs.ReachedAt >= convert(date, getdate() - 7) and
cs.ReachedAt < convert(date, getdate())

Roll weekend counts into monday counts

I have a query like this:
select date, count(*)
from inflow
where date >= dateadd(year, -2, getdate())
group by date
order by date
I need to exclude Saturday and Sunday dates, and instead add their counts into the following Monday. What would be the best way to do this? Do I need to exclude Saturday, Sunday, and Mondays, then add them on with a join to a different query? The query above is a simplified version, this is a relatively big query, so I need to keep efficiency in mind.
Well, this is a somewhat brute-force approach:
select date,
(case when datename(weekday, date) = 'Monday')
then cnt + cnt1 + cnt2
else cnt
end) as cnt
from (select date, count(*) as cnt,
lag(count(*), 1, 0) over (order by date) as prev_cnt,
lag(count(*), 2, 0) over (order by date) as prev_cnt2
from inflow
where date >= dateadd(year, -2, getdate())
group by date
) d
where datename(weekday, date) not in ('Saturday', 'Sunday')
order by date;
Note: This is assuming English-language settings so the datename() logic works.
An alternative method without subqueries;
select v.dte, count(*) as cnt
from inflow i cross apply
(values (case when datename(weekday, i.date) = 'Saturday'
then dateadd(day, 2, i.date)
when datename(weekday, i.date) = 'Sunday'
then dateadd(day, 1, 9.date)
else i.date
end)
) v.dte
where i.date >= dateadd(year, -2, getdate())
group by v.dte
order by date;
You state for performance, however without knowing the full picture it's quite hard to understand how to optimise the query.
While I've been working on this, I noticed Gordon Linoff's answer, however I'll continue to write my version up as well, we both following the same path, but get to the answer a little different.
WITH DateData (date, datetoapply)
AS
(
SELECT
[date],
CASE DATEPART(w, [date])
WHEN 5 THEN DATEADD(d, 2, [date])
WHEN 6 THEN DATEADD(d, 1, [date])
ELSE date
END as 'datetoapply'
FROM inflow
WHERE [date] >= dateadd(year, -2, getdate())
)
SELECT datetoapply, count(*)
FROM DateData
GROUP BY datetoapply
ORDER BY datetoapply
While I could not get Gordon's query working as expected, I can confirm that "DATEPART(w, [date])" performs much better than "DATENAME(weekday, [date])", which if replaced in the query above increases the server processing time from 87ms to 181ms based on a table populated with 10k rows in Azure.

SQL Server 2008 Count(Distinct CASE?

SELECT
ScheduleDays = COUNT(DISTINCT(CAST(datediff(d, 0, a.ApptStart) AS datetime)))
FROM
Appointments a
WHERE
ApptKind = 1 AND
--filter on current month
a.ApptStart >= ISNULL(DATEADD(month, DATEDIFF(month, 0, GETDATE()), 0),'1/1/1900') AND
a.ApptStart < ISNULL(DATEADD(month, DATEDIFF(month, 0, GETDATE())+1, 0),'1/1/3000')AND
--filter all days that aren't Friday, and then give you all Fridays that have an hour > 12.
DATENAME(weekday, a.ApptStart) <> 'Friday' and DATEPART(hour, a.ApptStart) > 12 AND
--Filter on doctor
a.ResourceID in (201)
This query will look through appointment start times and not count Fridays as our Docs only work a half day on Fridays. I was told that we do want to count them, but only as half days (first time around I was told to exclude them lol).
Could someone please help me with a Case statement that will count Fridays that do not have an appointment after 12noon, as half a day? I believe it will have to go in the ScheduleDays=COUNT(DISTINCT(CAST(datediff(d,0,a.ApptStart) as datetime))). Perhaps we can put the Friday and after 12 filters in there instead of in the where clause if we are going to use case anyways. ScheduleDays=COUNT(DISTINCT CASE WHEN etc. I really appreciate the help.
You can't really count half things using count, so that is not the way to go. But, you can do it with arithmetic. I think something like this:
select (count(distinct (case when DATENAME(weekday, a.ApptStart) <> 'Friday'
then cast(a.apptstart as date)
end)
) +
0.5 * count(distinct (case when DATENAME(weekday, a.ApptStart) = 'Friday'
then cast(a.apptstart as date)
end)
)
) as ScheduleDays
If the docs only work on Fridays for half a day, I don't think you need to check for the time of the appointment. Of course, you can if you like by adding it into the second count.
Note that to count days, I used the simpler syntax of casting the datetime to a date.
EDIT:
With the hour check:
select (count(distinct (case when DATENAME(weekday, a.ApptStart) <> 'Friday'
then cast(a.apptstart as date)
end)
) +
0.5 * count(distinct (case when DATENAME(weekday, a.ApptStart) = 'Friday' and DATEPART(hour, a.ApptStart) <= 12
then cast(a.apptstart as date)
end)
)
) as ScheduleDays