I am trying to create a Calendar Table with following columns and requirements. I have this calendar table made previously and I am not able to update it to my current requirements.
Date
Week No
Week Name
Week Start Date
Week End Date
Month
Month Start Date
Month End Date
List item
Quarter
Quarter Start Date
Quarter End Date
Year
Requirements
--Week Starts from Sunday and ends at Saturday
--Week Name should be in format 31Aug--to--06Sep
--Month should be ending date of Week's Month
--Calendar should start from 2015-08-31
--Financial Year should be in format of FY16-17
--Year should start from 1-Jun Till 31 May
--Quarter should be of 3 months of above mentioned dates
--Week No should be incremental number should not get reset after year completion.
The Query
DECLARE #StartDate date = '20150831'
DECLARE #CutoffDate date = '20300101'
;WITH seq(n) AS
(
SELECT 0 UNION ALL SELECT n + 1 FROM seq
WHERE n < DATEDIFF(DAY, #StartDate, #CutoffDate)
),
d(d) AS
(
SELECT DATEADD(DAY, n, #StartDate) FROM seq
),
src AS /*SOURCE TABLE WITH OBJECT DEFINITION*/
(
SELECT
TheDate = CONVERT(date, d),
TheDay = DATEPART(DAY, d),
TheDayName = DATENAME(WEEKDAY, d),
TheWeek = DATEPART(WEEK, d),
TheDayOfWeek = DATEPART(WEEKDAY, d),
TheMonth = DATEPART(MONTH, d),
TheMonthName = DATENAME(MONTH, d),
TheQuarter = Concat('Q',DATEPART(Quarter, d)),
Financial_Year = DATEPART(YEAR, d),
Financial_Quarter=Datepart(QUARTER,d),
TheYear = DATEPART(YEAR, d),
TheFirstOfMonth = DATEFROMPARTS(YEAR(d), MONTH(d), 1),
TheFirstOfFYear = DATEFROMPARTS(YEAR(d), 4, 1),
TheFirstOfYear = DATEFROMPARTS(YEAR(d), 1, 1),
TheLastOfYear = DATEFROMPARTS(YEAR(d), 12, 31),
TheDayOfYear = DATEPART(DAYOFYEAR, d)
FROM d
),
Dimension AS
(
SELECT
TheDate,
TheDay,
TheDayName,
TheDayOfWeek,
-- TheDayOfWeekInMonth = CONVERT(tinyint, ROW_NUMBER() OVER
-- (PARTITION BY TheFirstOfMonth, TheDayOfWeek ORDER BY TheDate)),
TheDayOfYear,
TheWeek,
TheFirstOfWeek = DATEADD(DAY, 1 - TheDayOfWeek, TheDate),
TheLastOfWeek = DATEADD(DAY, 6, DATEADD(DAY, 1 - TheDayOfWeek, TheDate)),
TheMonth,
TheMonthName,
TheFirstOfMonth,
TheLastOfMonth = MAX(TheDate) OVER (PARTITION BY TheYear, TheMonth),
TheFirstOfNextMonth = DATEADD(MONTH, 1, TheFirstOfMonth),
TheLastOfNextMonth = DATEADD(DAY, -1, DATEADD(MONTH, 2, TheFirstOfMonth)),
TheQuarter,
TheFirstOfQuarter = MIN(TheDate) OVER (PARTITION BY TheYear, TheQuarter),
TheLastOfQuarter = MAX(TheDate) OVER (PARTITION BY TheYear, TheQuarter),
TheYear,
TheFirstOfYear = DATEFROMPARTS(TheYear, 1, 1),
TheFirstOfFYear = DATEFROMPARTS(TheYear, 4, 1),
TheLastOfYear,
MMYYYY = CONVERT(char(2), CONVERT(char(8), TheDate, 101))
+ CONVERT(char(4), TheYear),
Financial_Quarter = Datepart(Quarter,DATEADD(MONTH, -3, TheFirstOfMonth)), /*Starting Financial Quarter from April*/
Financial_Year =CASE
WHEN Financial_Quarter = 1 THEN DATEPART(Year,Dateadd(Year,-1,TheFirstofYear)) ELSE THEYEAR END
FROM src
)
SELECT * FROM Dimension
ORDER BY TheDate
OPTION (MAXRECURSION 0);
How to convert Months depending on the weeks for example a month starts on Sunday (week also starts from Sunday) date be 01-MM-YYYY and the month should always end on Saturday giving 4 weeks normally in a month. The month cannot start or end with dates of previous week or month it should always have only the whole weeks, starting from Sunday and ending on Saturday.
As I mention in the comments, seems you just need a windowed COUNT. This is a guess, based on a lack of expected results, but this should get you on the right path. I also use the same set based method I used for your colleague's question:
DECLARE #StartDate date = '20150831'
DECLARE #CutoffDate date = '20300101';
/*
; is a terminator, not a "beginingator". It goes at the end of ALL your statements,
not at the start of statements that require the PREVIOUS statement to be properly terminated.
*/
WITH N AS
(SELECT N
FROM (VALUES (NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL)) N(N)),
Tally AS
(SELECT 0 AS I
UNION ALL
SELECT TOP (DATEDIFF(DAY, #StartDate, #CutoffDate))
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1,N N2,N N3, N N4), --Up to 1000 rows. Add more cross joins for more rows
D AS
(SELECT DATEADD(DAY, T.I, #StartDate) AS d
FROM Tally T),
Src AS /*SOURCE TABLE WITH OBJECT DEFINITION*/
(SELECT CONVERT(date, d) AS TheDate,
DATEPART(DAY, d) AS TheDay,
DATENAME(WEEKDAY, d) AS TheDayName,
DATEPART(WEEK, d) AS TheWeek,
DATEPART(WEEKDAY, d) AS TheDayOfWeek,
DATEPART(MONTH, d) AS TheMonth,
DATENAME(MONTH, d) AS TheMonthName,
CONCAT('Q', DATEPART(QUARTER, d)) AS TheQuarter,
DATEPART(YEAR, d) AS Financial_Year,
DATEPART(QUARTER, d) AS Financial_Quarter,
DATEPART(YEAR, d) AS TheYear,
DATEFROMPARTS(YEAR(d), MONTH(d), 1) AS TheFirstOfMonth,
DATEFROMPARTS(YEAR(d), 4, 1) AS TheFirstOfFYear,
DATEFROMPARTS(YEAR(d), 1, 1) AS TheFirstOfYear,
DATEFROMPARTS(YEAR(d), 12, 31) AS TheLastOfYear,
DATEPART(DAYOFYEAR, d) AS TheDayOfYear
FROM d),
Dimension AS
(SELECT TheDate,
TheDay,
TheDayName,
TheDayOfWeek,
CONVERT(tinyint, ROW_NUMBER() OVER (PARTITION BY TheFirstOfMonth, TheDayOfWeek ORDER BY TheDate)) AS TheDayOfWeekInMonth,
TheDayOfYear,
TheWeek,
DATEADD(DAY, 1 - TheDayOfWeek, TheDate) AS TheFirstOfWeek,
DATEADD(DAY, 6, DATEADD(DAY, 1 - TheDayOfWeek, TheDate)) AS TheLastOfWeek,
CONVERT(tinyint, DENSE_RANK() OVER (PARTITION BY TheYear, TheMonth ORDER BY TheWeek)) AS TheWeekOfMonth,
TheMonth,
TheMonthName,
TheFirstOfMonth,
MAX(TheDate) OVER (PARTITION BY TheYear, TheMonth) AS TheLastOfMonth,
DATEADD(MONTH, 1, TheFirstOfMonth) AS TheFirstOfNextMonth,
DATEADD(DAY, -1, DATEADD(MONTH, 2, TheFirstOfMonth)) AS TheLastOfNextMonth,
TheQuarter,
MIN(TheDate) OVER (PARTITION BY TheYear, TheQuarter) AS TheFirstOfQuarter,
MAX(TheDate) OVER (PARTITION BY TheYear, TheQuarter) AS TheLastOfQuarter,
TheYear,
DATEFROMPARTS(TheYear, 1, 1) AS TheFirstOfYear,
DATEFROMPARTS(TheYear, 4, 1) AS TheFirstOfFYear,
TheLastOfYear,
CONVERT(char(2), CONVERT(char(8), TheDate, 101)) + CONVERT(char(4), TheYear) AS MMYYYY,
DATEPART(QUARTER, DATEADD(MONTH, -3, TheFirstOfMonth)) AS Financial_Quarter, /*Starting Financial Quarter from April*/
CASE
WHEN Financial_Quarter = 1 THEN DATEPART(YEAR, DATEADD(YEAR, -1, TheFirstOfYear))
ELSE TheYear
END AS Financial_Year,
COUNT(CASE WHEN DATENAME(WEEKDAY,TheDate) = 'Sunday' THEN 1 END) OVER (PARTITION BY YEAR(TheDate), MONTH(TheMonth) ORDER BY TheDate) AS TheWeekNo
FROM src)
SELECT *
FROM Dimension
ORDER BY TheDate;
Here is a single SELECT query that uses the WITH RECURSIVE statement. It does not require loops procedures etc.
A RECURSIVE SQL common table expression (CTE) is a query that continuously references a previous result until it returns an empty result. It's achieved using a CTE, which in SQL is known as a “WITH” statement.
In the query below, the second date_range with statement has a UNION ALL. The first query in that statement is the first record of the table, and the subsequent records are all from the UNION ALL second statement where it continuously references itself until it reaches the end_date, as expresses as "WHERE next_date <= end_date"
WITH RECURSIVE min_max_dates AS (
SELECT
MIN(start_at) start_date,
MAX(end_at) end_date
FROM table_or_hard_code ay
),
date_range AS (
select
start_date date,
DATE_TRUNC('month', start_date) month_date,
MONTH(start_date) as month_num,
YEAR(start_date) as month_year,
TO_CHAR((start_date),'MMMM') as month_display,
MONTHNAME (start_date) as month_short_display,
CONCAT(TO_CHAR((start_date),'MMMM'), ', ', YEAR(start_date)) as month_year_display,
DATEADD(DAY, 1, start_date) as next_date,
start_date,
end_date
FROM min_max_dates
UNION ALL
SELECT
dr.next_date date,
DATE_TRUNC('month', dr.next_date) month_date,
MONTH(dr.next_date) as month_num,
YEAR(dr.next_date) as month_year,
TO_CHAR((dr.next_date),'MMMM') as month_display,
MONTHNAME (dr.next_date) as month_short_display,
CONCAT(TO_CHAR((dr.next_date),'MMMM'), ', ', YEAR(dr.next_date)) as month_year_display,
DATEADD(DAY, 1, dr.next_date) as next_date,
start_date,
end_date
FROM date_range dr
WHERE next_date <= end_date
)
SELECT date, month_date, month_year, month_num, month_display, month_short_display, month_year_display
FROM date_range
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.
I have the following that selects from a log and groups down to minute (excluding seconds and milisec):
SELECT DATEPART(YEAR, [Date]) AS YEAR, DATEPART(MONTH, [Date]) AS MONTH,
DATEPART(DAY, [Date]) AS DAY, DATEPART(HOUR, [Date]) AS HOUR,
DATEPART(MINUTE, [Date]) AS MIN, COUNT(*) AS COUNT
FROM [database].[dbo].[errorlog]
GROUP BY DATEPART(YEAR, [Date]), DATEPART(MONTH, [Date]), DATEPART(DAY, [Date]),
DATEPART(HOUR, [Date]), DATEPART(MINUTE, [Date])
ORDER BY DATEPART(YEAR, [Date]) DESC, DATEPART(MONTH, [Date]) DESC,
DATEPART(DAY, [Date]) DESC, DATEPART(HOUR, [Date]) DESC,
DATEPART(MINUTE, [Date]) DESC;
But as you can see thats a lot of fuzz just for getting a count, so I wonder if there is a better way to group it so I get grouped down to minutes in respect to year, month, day and hour?
This should would work:
select CAST([Date] AS smalldatetime) as time_stamp, count(*) as count
FROM [database].[dbo].[errorlog]
group by CAST([Date] AS smalldatetime)
order by CAST([Date] AS smalldatetime) desc;
Update after comments on this answer:
select dateadd(second,-datepart(ss,[Date]),[Date]) as time_stamp, count(*) as count
FROM [database].[dbo].[errorlog]
group by dateadd(second,-datepart(ss,[Date]),[Date])
order by dateadd(second,-datepart(ss,[Date]),[Date]) desc ;
The first solution rounds up the timestamp to the nearest minute. I realised that this is not exactly what the OP wanted.
So, the second solution just substracts the seconds part from the timestamp and leaves the timestamp with seconds as zero(Assuming [Date] does not have fractional seconds)
DATEADD(minute,DATEDIFF(minute,'20010101',[Date]),'20010101')
Should round all Date column values down to the nearest minute. So:
SELECT DATEADD(minute,DATEDIFF(minute,'20010101',[Date]),'20010101'),
COUNT(*) AS COUNT
FROM [database].[dbo].[errorlog]
GROUP BY DATEADD(minute,DATEDIFF(minute,'20010101',[Date]),'20010101')
ORDER BY DATEADD(minute,DATEDIFF(minute,'20010101',[Date]),'20010101') DESC;
(You could move this expression into a subquery if you want to further reduce the repetition)
You could do something like this to get
declare #now datetime
set #now = GETDATE()
select dateadd(minute, mm, #now) as date, c from (
select DATEDIFF(minute, #now, [Date]) as mm, COUNT(1) as c
from [database].[dbo].[errorlog]
group by DATEDIFF(minute, #now, [Date])
) t