Dynamically calculate how many months have passed - SQL Server - sql

I am trying to calculate how many months ago the date field was
I have a table
CREATE TABLE Date(
Date Date
);
INSERT INTO Date (Date)
VALUES ('05-01-18'),
('04-01-18'),
('03-01-18'),
('02-01-18'),
('01-01-18'),
('12-01-17'),
('11-01-17');
And a query
SELECT Date ,
MONTH(Date),
CASE WHEN MONTH(Date) = MONTH(GETDATE()) Then 'Current Month'
WHEN MONTH(Date) = MONTH(GETDATE()) -1 Then '1 Month Ago'
WHEN MONTH(Date) = MONTH(GETDATE()) -2 Then '2 Month Ago'
ELSE 'n/a' END AS [Months Ago]
FROM Date
Which gives me the correct result:
| Date | | Months Ago |
|------------|----|---------------|
| 2018-05-01 | 5 | Current Month |
| 2018-04-01 | 4 | 1 Month Ago |
| 2018-03-01 | 3 | 2 Month Ago |
| 2018-02-01 | 2 | n/a |
| 2018-01-01 | 1 | n/a |
| 2017-12-01 | 12 | n/a |
| 2017-11-01 | 11 | n/a |
But is there anyway to create this dynamically instead of keep having to write case expressions. So if anyone add's more dates in the future this will just work without having to add more cases?

You exactly want datediff():
select datediff(month, date, getdate()) as num_months_ago
datediff() counts the number of month boundaries between two dates. So, Dec 31 is "one month before" Jan 1. This appears to be the behavior that you want.
I don't see an advantage to putting this in a string format.

In case you do want this to have a string format:
SELECT D.[Date],
DATEPART(MONTH,D.[Date]) AS [Month],
CASE WHEN V.DD = 0 THEN 'Current Month'
WHEN V.DD = 1 THEN '1 Month Ago'
ELSE CONVERT(varchar(4), V.DD) + ' Months ago' END AS MonthsAgo
FROM [Date] D
CROSS APPLY (VALUES(DATEDIFF(MONTH, D.[Date], GETDATE()))) V(DD);
I, however, agree with Gordon, SQL Server isn't really the palce to do that type of formatting. :)

Related

How can I aggregate values based on an arbitrary monthly cycle date range in SQL?

Given a table as such:
# SELECT * FROM payments ORDER BY payment_date DESC;
id | payment_type_id | payment_date | amount
----+-----------------+--------------+---------
4 | 1 | 2019-11-18 | 300.00
3 | 1 | 2019-11-17 | 1000.00
2 | 1 | 2019-11-16 | 250.00
1 | 1 | 2019-11-15 | 300.00
14 | 1 | 2019-10-18 | 130.00
13 | 1 | 2019-10-18 | 100.00
15 | 1 | 2019-09-18 | 1300.00
16 | 1 | 2019-09-17 | 1300.00
17 | 1 | 2019-09-01 | 400.00
18 | 1 | 2019-08-25 | 400.00
(10 rows)
How can I SUM the amount column based on an arbitrary date range, not simply a date truncation?
Taking the example of a date range beginning on the 15th of a month, and ending on the 14th of the following month, the output I would expect to see is:
payment_type_id | payment_date | amount
-----------------+--------------+---------
1 | 2019-11-15 | 1850.00
1 | 2019-10-15 | 230.00
1 | 2019-09-15 | 2600.00
1 | 2019-08-15 | 800.00
Can this be done in SQL, or is this something that's better handled in code? I would traditionally do this in code, but looking to extend my knowledge of SQL (which at this stage, isnt much!)
Click demo:db<>fiddle
You can use a combination of the CASE clause and the date_trunc() function:
SELECT
payment_type_id,
CASE
WHEN date_part('day', payment_date) < 15 THEN
date_trunc('month', payment_date) + interval '-1month 14 days'
ELSE date_trunc('month', payment_date) + interval '14 days'
END AS payment_date,
SUM(amount) AS amount
FROM
payments
GROUP BY 1,2
date_part('day', ...) gives out the current day of month
The CASE clause is for dividing the dates before the 15th of month and after.
The date_trunc('month', ...) converts all dates in a month to the first of this month
So, if date is before the 15th of the current month, it should be grouped to the 15th of the previous month (this is what +interval '-1month 14 days' calculates: +14, because the date_trunc() truncates to the 1st of month: 1 + 14 = 15). Otherwise it is group to the 15th of the current month.
After calculating these payment_days, you can use them for simple grouping.
I would simply subtract 14 days, truncate the month, and add 14 days back:
select payment_type_id,
date_trunc('month', payment_date - interval '14 day') + interval '14 day' as month_15,
sum(amount)
from payments
group by payment_type_id, month_15
order by payment_type_id, month_15;
No conditional logic is actually needed for this.
Here is a db<>fiddle.
You can use the generate_series() function and make a inner join comparing month and year, like this:
SELECT specific_date_on_month, SUM(amount)
FROM (SELECT generate_series('2015-01-15'::date, '2015-12-15'::date, '1 month'::interval) AS specific_date_on_month)
INNER JOIN payments
ON (TO_CHAR(payment_date, 'yyyymm')=TO_CHAR(specific_date_on_month, 'yyyymm'))
GROUP BY specific_date_on_month;
The generate_series(<begin>, <end>, <interval>) function generate a serie based on begin and end with an specific interval.

SQL null values not being shown in results

I'm having trouble getting the null values on a SQL Query. This is the description of the problem:
Gross income by week. Money is collected from guests when they leave.
For each Thursday in November and December 2016, show the total amount
of money collected from the previous Friday to that day, inclusive.
Here's the code that I've written that should return the weekly income from Thursday to previous Friday, the answer i get is partially correct as the weeks that have income are correctly displayed while the weeks that don't have any income are not displayed. I've tried adding a IFNULL clause but that's still not fixing the problem.
SELECT DATE_ADD(MAKEDATE(2016, 7), INTERVAL WEEK(DATE_ADD(calendar.i, INTERVAL booking.nights - 5 DAY), 0) WEEK) AS Thursday, IFNULL(SUM(booking.nights * rate.amount) + SUM(e.amount),0) AS weekly_ncome
FROM booking
RIGHT OUTER
JOIN calendar ON booking.booking_date = calendar.i
JOIN rate ON (booking.occupants = rate.occupancy AND booking.room_type_requested = rate.room_type)
LEFT JOIN (
SELECT booking_id, IFNULL(SUM(amount),0) AS amount
FROM extra
GROUP BY booking_id
) AS e ON (e.booking_id = booking.booking_id)
GROUP BY Thursday;
For reference, this is a question found on SQLzoo Guesthouse section, question 15. This is the expected result:
+------------+---------------+
| Thursday | weekly_income |
+------------+---------------+
| 2016-11-03 | 0.00 |
| 2016-11-10 | 12608.94 |
| 2016-11-17 | 13552.56 |
| 2016-11-24 | 12929.69 |
| 2016-12-01 | 11685.14 |
| 2016-12-08 | 13093.79 |
| 2016-12-15 | 8975.87 |
| 2016-12-22 | 1395.77 |
| 2016-12-29 | 0.00 |
| 2017-01-05 | 0.00 |
+------------+---------------+
I get the same as above but the ones with weekly income of 0 don't show up.
Here is one way to get the Thursdays in Nov 2016 and Dec 2016:
SELECT i AS thursday, i - INTERVAL 6 DAY AS friday
FROM calendar
WHERE i >= '2016-11-01' AND i - INTERVAL 6 DAY <= '2016-12-31' AND DAYOFWEEK(i) = 5
Just left join your data with this, make sure that you join with the checkout date (booking_date + nights days):
SELECT
thursday, SUM(
COALESCE(booking.nights * rate.amount, 0) +
COALESCE(extras.total, 0)
) AS weekly_income
FROM (
SELECT i AS thursday, i - INTERVAL 6 DAY AS friday
FROM calendar
WHERE i >= '2016-11-01' AND i - INTERVAL 6 DAY <= '2016-12-31' AND DAYOFWEEK(i) = 5
) AS thursdays
LEFT JOIN (
booking
INNER JOIN rate ON booking.occupants = rate.occupancy AND booking.room_type_requested = rate.room_type
LEFT JOIN (
SELECT booking_id, SUM(amount) AS total
FROM extra
GROUP BY booking_id
) AS extras ON booking.booking_id = extras.booking_id
) ON booking.booking_date + INTERVAL booking.nights DAY BETWEEN friday AND thursday
GROUP BY thursday

How to write a SQL statement to sum data using group by the same day of every two neighboring months

I have a data table like this:
datetime data
-----------------------
...
2017/8/24 6.0
2017/8/25 5.0
...
2017/9/24 6.0
2017/9/25 6.2
...
2017/10/24 8.1
2017/10/25 8.2
I want to write a SQL statement to sum the data using group by the 24th of every two neighboring months in certain range of time such as : from 2017/7/20 to 2017/10/25 as above.
How to write this SQL statement? I'm using SQL Server 2008 R2.
The expected results table is like this:
datetime_range data_sum
------------------------------------
...
2017/8/24~2017/9/24 100.9
2017/9/24~2017/10/24 120.2
...
One conceptual way to proceed here is to redefine a "month" as ending on the 24th of each normal month. Using the SQL Server month function, we will assign any date occurring after the 24th as belonging to the next month. Then we can aggregate by the year along with this shifted month to obtain the sum of data.
WITH cte AS (
SELECT
data,
YEAR(datetime) AS year,
CASE WHEN DAY(datetime) > 24
THEN MONTH(datetime) + 1 ELSE MONTH(datetime) END AS month
FROM yourTable
)
SELECT
CONVERT(varchar(4), year) + '/' + CONVERT(varchar(2), month) +
'/25~' +
CONVERT(varchar(4), year) + '/' + CONVERT(varchar(2), (month + 1)) +
'/24' AS datetime_range,
SUM(data) AS data_sum
FROM cte
GROUP BY
year, month;
Note that your suggested ranges seem to include the 24th on both ends, which does not make sense from an accounting point of view. I assume that the month includes and ends on the 24th (i.e. the 25th is the first day of the next accounting period.
Demo
I would suggest dynamically building some date range rows so that you can then join you data to those for aggregation, like this example:
+----+---------------------+---------------------+----------------+
| | period_start_dt | period_end_dt | your_data_here |
+----+---------------------+---------------------+----------------+
| 1 | 24.04.2017 00:00:00 | 24.05.2017 00:00:00 | 1 |
| 2 | 24.05.2017 00:00:00 | 24.06.2017 00:00:00 | 1 |
| 3 | 24.06.2017 00:00:00 | 24.07.2017 00:00:00 | 1 |
| 4 | 24.07.2017 00:00:00 | 24.08.2017 00:00:00 | 1 |
| 5 | 24.08.2017 00:00:00 | 24.09.2017 00:00:00 | 1 |
| 6 | 24.09.2017 00:00:00 | 24.10.2017 00:00:00 | 1 |
| 7 | 24.10.2017 00:00:00 | 24.11.2017 00:00:00 | 1 |
| 8 | 24.11.2017 00:00:00 | 24.12.2017 00:00:00 | 1 |
| 9 | 24.12.2017 00:00:00 | 24.01.2018 00:00:00 | 1 |
| 10 | 24.01.2018 00:00:00 | 24.02.2018 00:00:00 | 1 |
| 11 | 24.02.2018 00:00:00 | 24.03.2018 00:00:00 | 1 |
| 12 | 24.03.2018 00:00:00 | 24.04.2018 00:00:00 | 1 |
+----+---------------------+---------------------+----------------+
DEMO
declare #start_dt date;
set #start_dt = '20170424';
select
period_start_dt, period_end_dt, sum(1) as your_data_here
from (
select
dateadd(month,m.n,start_dt) period_start_dt
, dateadd(month,m.n+1,start_dt) period_end_dt
from (
select #start_dt start_dt ) seed
cross join (
select 0 n union all
select 1 union all
select 2 union all
select 3 union all
select 4 union all
select 5 union all
select 6 union all
select 7 union all
select 8 union all
select 9 union all
select 10 union all
select 11
) m
) r
-- LEFT JOIN YOUR DATA
-- ON yourdata.date >= r.period_start_dt and data.date < r.period_end_dt
group by
period_start_dt, period_end_dt
Please don't be tempted to use "between" when it comes to joining to your data. Follow the note above and use yourdata.date >= r.period_start_dt and data.date < r.period_end_dt otherwise you could double count information as between is inclusive of both lower and upper boundaries.
I think the simplest way is to subtract 25 days and aggregate by the month:
select year(dateadd(day, -25, datetime)) as yr,
month(dateadd(day, -25, datetime)) as mon,
sum(data)
from t
group by dateadd(day, -25, datetime);
You can format yr and mon to get the dates for the specific ranges, but this does the aggregation (and the yr/mon columns might be sufficient).
Step 0: Build a calendar table. Every database needs a calendar table eventually to simplify this sort of calculation.
In this table you may have columns such as:
Date (primary key)
Day
Month
Year
Quarter
Half-year (e.g. 1 or 2)
Day of year (1 to 366)
Day of week (numeric or text)
Is weekend (seems redundant now, but is a huge time saver later on)
Fiscal quarter/year (if your company's fiscal year doesn't start on Jan. 1)
Is Holiday
etc.
If your company starts its month on the 24th, then you can add a "Fiscal Month" column that represents that.
Step 1: Join on the calendar table
Step 2: Group by the columns in the calendar table.
Calendar tables sound weird at first, but once you realize that they are in fact tiny even if they span a couple hundred years they quickly become a major asset.
Don't try to cheap out on disk space by using computed columns. You want real columns because they are much faster and can be indexed if necessary. (Though honestly, usually just the PK index is enough for even wide calendar tables.)

Turning week number into week commencing in SSRS/SQL Server

I have a report that is grouped on week number but for presentation reasons want it to be week commencing.
Select
datepart(wk,[rhStartTime]) as [week number]
...
group by datepart(wk,[rhStartTime]),[rhOperatorName])
where
[week number] >= #StartWeek
and [week number] <= #EndWeek
My report parameters use week number to filter the data with #StartWeek and #EndWeek being integers that plug into the SQL. My question is one of presentation. It is tough for users to understand what Week 15 means in context so I would like to alter my output to show Week Commencing rather than week number but for the backend to still use weeknumber. I also don't want users to be able to pick any date because they will invariably pick dates that span multiple weeks without a full weeks data.
I look at similar questions and one here
SO question
recommended SQL of the format
DATEADD(dd, -(DATEPART(dw, WeddingDate)-1), WeddingDate) [WeekStart]
But plugging my columns into that format gave me a bit of a mess. It didn't group how I was expecting.
SELECT
DATEADD(dd, -(datepart(wk,[rhStartTime]))-1), [rhStartTime])) as [week commencing]
,datepart(wk,[rhStartTime])) as [week number]
...
group by datepart(wk,[rhStartTime])),DATEADD(dd, -(datepart(wk,[rhStartTime]))-1), [rhStartTime])),[rhoperatorname]
I got this output
where I was looking for all those week 15s to be grouped together with just one week commencing date.
Try This will work.This retrieves the dates eliminating time part of it
SELECT
Dateadd(dd,-(datepart(wk,convert( varchar(10),[rhStart Time],120))-1), convert( varchar(10),[rhStart Time],120))
,datepart(wk,[rhStart Time])) as [week number]
...
from Table X
group by Dateadd(dd,-(datepart(wk,convert( varchar(10),[rhStart Time],120))-1), convert( varchar(10),[rhStart Time],120))
,datepart(wk,[rhStart Time]))
,[Agent Name]
I think your problem is in how you are using the examples you have seen elsewhere and not with the examples themselves, as I have just tested the logic and it seems to be working for me without issue, as you can see in the script below.
I think your main problem is that you are not removing the time portion of your StartTime values, which you will need to do if you want to group all values that occur on the same day. The easiest way to do this is to simply cast or convert the values to date data types:
select cast(StartTime as date) as CastToDate
,convert(date, StartTime, 103) as ConvertToDate -- You may need to use 101 depending on your server setting for dd/mm/yyyy or mm/dd/yyyy
Script:
declare #StartDate date = '20170325'
,#EndDate date = '20170403';
-- Tally table to create dates to use in functions:
with n(n) as(select n from (values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n(n))
,d(d) as(select top(datediff(d,#StartDate,#EndDate)+1) dateadd(d,row_number() over (order by (select null))-1,#StartDate) from n n1,n n2,n n3,n n4,n n5,n n6)
select d
,datepart(week,d) as WeekNumber
,DATEADD(dd, -(DATEPART(dw, d)-1), d) as WeekCommencing
from d
order by d;
Output:
+------------+------------+----------------+
| d | WeekNumber | WeekCommencing |
+------------+------------+----------------+
| 2017-03-25 | 12 | 2017-03-19 |
| 2017-03-26 | 13 | 2017-03-26 |
| 2017-03-27 | 13 | 2017-03-26 |
| 2017-03-28 | 13 | 2017-03-26 |
| 2017-03-29 | 13 | 2017-03-26 |
| 2017-03-30 | 13 | 2017-03-26 |
| 2017-03-31 | 13 | 2017-03-26 |
| 2017-04-01 | 13 | 2017-03-26 |
| 2017-04-02 | 14 | 2017-04-02 |
| 2017-04-03 | 14 | 2017-04-02 |
+------------+------------+----------------+
Replace the field value in your SQL code with the expression below to remove time
DATEADD(dd, -(DATEPART(dw,[rhStartTime]) -1), DATEDIFF(dd, 0, [rhStartTime]) )
You can also achieve the same result by using the expression below in SSRS (change it to match your date field)
= DATEADD("d", - DATEPART(DateInterval.Weekday,Fields!rhStartTime.Value) +1,Fields!rhStartTime.Value)
Thanks for the answers. I'm sure they probably would have worked if I were more competent. In the end I created a simple table on my server with year,weeknumber,commencedate as the column headings and manually created them in excel. Then I linked my results as a cte to that table where year = 2017 and cte.weeknumber = commencedate.weeknumber It seems to have worked.
Now in my SSRS report parameter I am using weeknumber as the value and commence date as the label. So I don't have to change any of the other configuration.

Query grouped by calendar week / Issue: Date expands over more than one year

I have an SQL Query which calculates a quote of two sums. This quote shall be calculated on a weekly basis. Therefore is used Datepart('ww', Date, 2, 2) to get the calendar week.
This query worked fine during 2014, but now I am facing a problem.
The users choses with date pickers in a form from and to date which is then used in the where clause to only select relevant records.
If they chose a from date from past year (e.g. 2014) and a to date from this year my query shows irrelvant data, because it does not cosinder the year just the calendar week.
So, how group by calendar week AND only chose records from the correct year.
SELECT DatePart('ww',Date,2,2) AS WEEK,
Year(Workload_Date) AS [YEAR],
(SUM(Value1)+SUM(Value2))AS [Total1],
(SUM(Value3)+SUM(Value4)) AS [Total2],
((SUM(Value1)+SUM(Value2))/(SUM(Value3)+SUM(Value4))) AS [Quote]
FROM tbl
WHERE DatePart('ww',Date,2,2) Between DatePart('ww',FromDatePickerField,2,2) And DatePart('ww',ToDatePickerField,2,2)
GROUP BY DatePart('ww',Date,2,2), Year(Date)
ORDER BY Year(Date), DatePart('ww',Date,2,2);
The table contains one record per day.
Date | Value1 | Value2 | Value3 | Value4 |
01.01.2014 | 4 | 3 | 2 | 3 |
02.01.2014 | 4 | 3 | 9 | 3 |
03.01.2014 | 4 | 3 | 4 | 1 |
04.01.2014 | 4 | 3 | 1 | 3 |
...
01.01.2015 | 4 | 3 | 6 | 3 |
02.01.2015 | 4 | 3 | 3 | 7 |
You could base your logic on Week_Ending_date instead of the week number and year, that way you could aggregate all you data on a weekly basis and let SQL handle the week/year detection logic.
Incase, you have a date range that spans 2 years, even then the calculations will be based on the week_ending_date and should work out correctly.
Something like...
SELECT DATEADD(dd, 7-(DATEPART(dw, DATE)), DATE) AS WEEK_ENDING_DATE
,Year(DATEADD(dd, 7-(DATEPART(dw, DATE)), DATE)) AS [YEAR]
,(SUM(Value1) + SUM(Value2)) AS [Total1]
,(SUM(Value3) + SUM(Value4)) AS [Total2]
,((SUM(Value1) + SUM(Value2)) / (SUM(Value3) + SUM(Value4))) AS [Quote]
FROM tbl
WHERE DATEADD(dd, 7-(DATEPART(dw, DATE)), DATE) BETWEEN DATEADD(dd, 7-(DATEPART(dw, FromDatePickerField)), FromDatePickerField)
AND DATEADD(dd, 7-(DATEPART(dw, ToDatePickerField)), ToDatePickerField)
and date >= FromDatePickerField
and date <= ToDatePickerField
GROUP BY DATEADD(dd, 7-(DATEPART(dw, DATE)), DATE)
ORDER BY DATEADD(dd, 7-(DATEPART(dw, DATE)), DATE)
I see 2 possible solutions.
1) You stop the user while using the datepicker on the form whenever the years of the two datepickers are different. Maybe a MsgBox or something,
2) You change the to-DatePicker to the from-Year.
DECLARE #MinDate DATE = '01.11.2014'
DECLARE #tmpDate DATE = '12.02.2015'
DECLARE #MaxDate DATE = (CASE WHEN Year(#MinDate) != Year(#tmpDate) THEN Convert(DATE, '31.12.' + Convert(VARCHAR(4), Year(#MinDate))) ELSE #tmpDate END)
SELECT #MinDate, #tmpDate, #MaxDate
Result :
minDate tmpDate maxDate
01.11.2014 12.02.2015 31.12.2014