Related
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 am learning SQL at the moment and needed a query that would return the previous 12 weeks (excluding the current week) and the below worked just fine - that was until we moved in to 2019!
My table has 4 columns, BuildWeek, BuildYear, Info1, Info2, all are int.
select *
from Dashboard
where BuildWeek in (datepart(week, getdate()) - 1,
datepart(week, getdate()) - 2,
datepart(week, getdate()) - 3,
datepart(week, getdate()) - 4,
datepart(week, getdate()) - 5,
datepart(week, getdate()) - 6,
datepart(week, getdate()) - 7,
datepart(week, getdate()) - 8,
datepart(week, getdate()) - 9,
datepart(week, getdate()) - 10,
datepart(week, getdate()) - 11,
datepart(week, getdate()) - 12,
datepart(week, getdate()) - 13)
and BuildYear = datepart(year, getdate())
order by
BuildWeek desc
I know this is not the cleanest query so I'm quite open to being educated, I have tried a few things (using dateadd to no avail) but cannot seem to get it to function how I wish. I'm guessing the above stems from the query perhaps looking into a minus (current date -1 would be 0, -2 would be -1 so no results would be found?) but I'm not sure how to make it look backwards to return those extra weeks.
Another solution would be to generate a date from BuildWeek and BuildYear (say, the first day of the week), that can be used in the WHERE clause.
The advantage with this approach is that is guaranteed to return the records of the last 12 weeks, even if there is not exactly 12 of them (for example if you have gaps in the weeks series), or if future records exist.
SELECT d.*
FROM Dashboard d
WHERE
DATEADD( wk, DATEDIFF( wk, 7, CAST( d.BuildYear AS NVARCHAR(100) ) ) + (d.BuildWeek-1) , 7 )
BETWEEN DATEADD( week, -12, GETDATE() ) AND GETDATE()
ORDER BY
d.BuildYear DESC,
d.BuildWeek DESC
Tested in this db fiddle.
Assuming you have one row per week:
select top (12) d.*
from Dashboard d
order by d.year desc, d.BuildWeek desc;
To avoid future weeks:
select top (12) d.*
from Dashboard d
where year < year(getdate()) or
(year = year(getdate()) and buildweek <= datepart(week, getdate())
order by d.year desc, d.BuildWeek desc;
Or, if you want to use a where and years have 52 weeks:
select d.*
from dashboard d
where (y.year * 52 + buildweek) >= year(getdate()) * 52 + datepart(week, getdate());
This method becomes a bit trickier if years can have 53 weeks.
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
Background
I have a table set up in a SQL Server environment that contains a log of various activity that I'm tracking. Particular log items use unique codes to categorize what activity is taking place and a datetime field tracks when that activity occurred.
Problem
I would like to, using either a single query or a stored procedure, get an average of hourly counts of activity, grouped by day of the week. Example:
Day | Hour | Average Count
-------------------------------
Monday | 8 | 5
Monday | 9 | 5
Monday | 10 | 9
...
Tuesday | 8 | 4
Tuesday | 9 | 3
...etc
Right now I've got a query setup that spits out the counts per hour per day, but my problem is taking it a step further and getting average by day of week. Here's my current query:
SELECT CAST([time] AS date) AS ForDate,
DATEPART(hour, [time]) AS OnHour,
COUNT(*) AS Totals
FROM [log] WHERE [code] = 'tib_imp.8'
GROUP BY CAST(time AS date),
DATEPART(hour,[time])
ORDER BY ForDate Asc, OnHour Asc
Any suggestions as to how I might accomplish this?
Thanks in advance!
Guessing here:
SELECT [Day], [Hour], [DayN], AVG(Totals) AS [Avg]
FROM
(
SELECT
[Day] = DATENAME(WEEKDAY, [time]),
[DayN] = DATEPART(WEEKDAY, [time]),
[Hour] = DATEPART(HOUR, [time]),
Totals = COUNT(*)
FROM dbo.[log]
WHERE [code] = 'tib_imp.8'
GROUP BY
DATENAME(WEEKDAY, [time]),
DATEPART(WEEKDAY, [time]),
DATEPART(HOUR, [time])
) AS q
GROUP BY [Day], [Hour], [DayN]
ORDER BY DayN;
Again, without data, I might once again be throwing a handful of mud at the wall and hoping it sticks, but perhaps what you need is:
SELECT [Day], [Hour], [DayN], AVG(Totals) AS [Avg]
FROM
(
SELECT
w = DATEDIFF(WEEK, 0, [time]),
[Day] = DATENAME(WEEKDAY, [time]),
[DayN] = DATEPART(WEEKDAY, [time]),
[Hour] = DATEPART(HOUR, [time]),
Totals = COUNT(*)
FROM dbo.[log]
WHERE [code] = 'tib_imp.8'
GROUP BY
DATEDIFF(WEEK, 0, [time]),
DATENAME(WEEKDAY, [time]),
DATEPART(WEEKDAY, [time]),
DATEPART(HOUR, [time])
) AS q
GROUP BY [Day], [Hour], [DayN]
ORDER BY DayN;
This is also going to produce integer-based averages, so you may want to cast the Totals alias on the inner query to DECIMAL(something,something).
; WITH a AS (
SELECT CAST([time] AS date) AS ForDate
, DATEPART(hour, [time]) AS OnHour
, txtW=DATENAME(WEEKDAY,[time])
, intW=DATEPART(WEEKDAY,[time])
, Totals=COUNT(*)
FROM [log] WHERE [code] = 'tib_imp.8'
GROUP BY CAST(time AS date)
, DATENAME(WEEKDAY,[time])
, DATEPART(WEEKDAY,[time])
, DATEPART(hour,[time])
)
SELECT [Day]=txtW
, [Hour]=OnHour
, [Average Count]=AVG(Totals)
FROM a
GROUP BY txtW, intW, OnHour
ORDER BY intW, OnHour
I want to write a select that aggregates over data (which has a DATETIME column as ID) with ANY interval theoretically possible (like 1hr, 1hr and 22seconds, 1year and 3minutes, etc. ).
This select should be able to aggregate by 1hr, 12min, 14seconds and should return 3 rows
SELECT DATEPART(YEAR,id) as year,
DATEPART(MONTH,id) as month,
DATEPART(DAY,id) as day,
DATEPART(HOUR,id) as hour,
DATEPART(MINUTE,id) as minute,
AVG([Open]),
AVG([Close]),
AVG([Min]),
AVG([Max])
FROM QuoteHistory
where id between '2000-02-06 17:00:00.000' and '2000-02-06 20:36:42.000'
GROUP BY
DATEPART(YEAR,id),
DATEPART(MONTH,id),
DATEPART(DAY,id),
DATEPART(HOUR,id),
DATEPART(MINUTE,id)
ORDER BY 1,2,3,4,5;
I am kind of stuck here and can't get my head around this problem.. For "simple intervals" like "30 minutes" i could just add a modulo
DATEPART(MINUTE,id)%2
but when the interval "touches" more than 1 part of the date, I'm stuck.
Any help appreciated, thx!
Assuming some parameters here:
;WITH Date_Ranges AS (
SELECT
#min_datetime AS start_datetime,
DATEADD(SECOND, #seconds,
DATEADD(MINUTE, #minutes,
DATEADD(HOUR, #hours,
DATEADD(DAY, #days,
DATEADD(WEEK, #weeks,
DATEADD(MONTH, #months,
DATEADD(YEAR, #years, #min_datetime))))))) AS end_datetime
UNION ALL
SELECT
DATEADD(SECOND, 1, end_datetime),
DATEADD(SECOND, #seconds,
DATEADD(MINUTE, #minutes,
DATEADD(HOUR, #hours,
DATEADD(DAY, #days,
DATEADD(WEEK, #weeks,
DATEADD(MONTH, #months,
DATEADD(YEAR, #years, end_datetime)))))))
FROM
Date_Ranges
WHERE
DATEADD(SECOND, 1, end_datetime) < #max_datetime
)
SELECT
DR.min_datetime,
DR.max_datetime,
AVG([Open]),
AVG([Close]),
AVG([Min]),
AVG([Max])
FROM
Date_Ranges DR
LEFT OUTER JOIN Quote_History QH ON
QH.id BETWEEN DR.min_datetime AND DR.max_datetime
GROUP BY
DR.min_datetime,
DR.max_datetime
ORDER BY
DR.min_datetime,
DR.max_datetime
You might need to fiddle with how to handle the edge cases (that 1 second range between date ranges could be a problem depending on your data). This should hopefully point you in the right direction though.