Number of days of a week inside a month in SQL - sql

Using SQL Server 2019, therefore also using T-SQL, I'm trying to get the number of days of a particular week, identified by the ISO 8601 week definition, that fall inside a particular month.
The table that contains the data has the following fields:
Year | Month | IsoWeek
-----------------------
2020 | 12 | 50
2020 | 12 | 51
2020 | 12 | 52
2020 | 12 | 53
2021 | 01 | 01
2021 | 01 | 02
2021 | 01 | 03
For example the last week of 2020, week 53, has started on Monday 28th Dec 2020 and has finished on Sunday 3rd Jan 2021.
So I want to get 4 for Dec2020 and 3 for Jan2021 in order to create a new table like the following:
Year | Month | IsoWeek | WeekDays
------------------------------------
2020 | 12 | 52 | 7
2020 | 12 | 53 | 4
2021 | 01 | 53 | 3
2021 | 01 | 01 | 7
2021 | 01 | 02 | 7
Is there a way to do it entirely in SQL?

This is sort of a SQL Server setting (or trick) which works because the 1st of January, 1900 was a Monday. Since that's where SQL Server starts counting from it makes it easier to locate the first Thursday of any month. Thanks to Jeff Moden btw. I got this from something he wrote. Maybe there's a better way to this now, idk
with iso_dts_cte(yr, mo, wk) as (
select * from (values ('2020', '12', '50'),
('2020', '12', '51'),
('2020', '12', '52'),
('2020', '12', '53'),
('2021', '01', '01'),
('2021', '01', '02'),
('2021', '01', '03')) v(yr, mo, wk))
select iso.*, v.*
from iso_dts_cte iso
cross apply (values (cast(dateadd(wk,datediff(wk,0,'01/04/'+iso.yr),0)+((iso.wk-1)*7) as date),
cast(dateadd(wk,datediff(wk,0,'01/04/'+iso.yr),0)+((iso.wk)*7)-1 as date))) v(start_dt, end_dt);
yr mo wk start_dt end_dt
2020 12 50 2020-12-07 2020-12-13
2020 12 51 2020-12-14 2020-12-20
2020 12 52 2020-12-21 2020-12-27
2020 12 53 2020-12-28 2021-01-03
2021 01 01 2021-01-04 2021-01-10
2021 01 02 2021-01-11 2021-01-17
2021 01 03 2021-01-18 2021-01-24
To expand the week ranges into days and then count by calendar year and calendar month you could try something like this.
[Edit] It's my understanding the date hierarchy you're looking for is 1) calendar year, 2) calendar month, 3) iso week. The output seems to match the example now. However, there's not a way to ORDER BY to display like the example.
with
iso_dts_cte(yr, mo, wk) as (
select * from (values ('2020', '12', '50'),
('2020', '12', '51'),
('2020', '12', '52'),
('2020', '12', '53'),
('2021', '01', '01'),
('2021', '01', '02'),
('2021', '01', '03')) v(yr, mo, wk)),
days_cte(n) as (
select * from (values (1),(2),(3),(4),(5),(6),(7)) v(n))
select year(dt.calc_dt) cal_yr, month(dt.calc_dt) cal_mo, iso.wk, count(*) day_count
from iso_dts_cte iso
cross apply (values (cast(dateadd(wk,datediff(wk,0,'01/04/'+iso.yr),0)+((iso.wk-1)*7) as date),
cast(dateadd(wk,datediff(wk,0,'01/04/'+iso.yr),0)+((iso.wk)*7)-1 as date))) v(start_dt, end_dt)
cross join days_cte d
cross apply (values (dateadd(day, d.n-1, v.start_dt))) dt(calc_dt)
group by year(dt.calc_dt), month(dt.calc_dt), iso.wk;
cal_yr cal_mo wk day_count
2020 12 50 7
2020 12 51 7
2020 12 52 7
2020 12 53 4
2021 1 01 7
2021 1 02 7
2021 1 03 7
2021 1 53 3

I would assume you could use:
Select DatePart(weekday, '2021/01/01')
and loop for each months, the value here is 5 as the day of the week is Friday, thus there is 3 days for that first week and 4 days for the previous month last week since there is 7 days per week.
If you have the week number 1 to 53 and the year, if it's the first or last week of the month, you need to calculate using the weekday logic written above. Otherwise it's 7.
You could create a scalar function with a similar logic and create your table using the function in it. I believe you are expecting something simpler which nothing come to mind right now.
You can also use "SET DATEFIRST 1;" to change what is considered the first day of the week (Monday / Sunday).
Ended up creating a proof of concept:
CREATE FUNCTION calculateWeekDays(#y int,#m int,#w int)
RETURNS INT
AS BEGIN
DECLARE #numberOfDays INT = 7;
DECLARE #firstDayOfMonth datetime = DATEFROMPARTS (#y, #m, 1);
DECLARE #nextMonth datetime = (SELECT DATEADD(MONTH, 1, #firstDayOfMonth));
DECLARE #lastDayOfMonth datetime = (SELECT DATEADD(DAY, -1, #nextMonth));
DECLARE #weekOfYearStartOfMonth INT = (select DatePart(week, #firstDayOfMonth));
DECLARE #weekOfYearEndOfMonth INT = (select DatePart(week, #lastDayOfMonth));
DECLARE #firstWeekOfMonth INT = (select datediff(week, dateadd(week, datediff(day,0,dateadd(month, datediff(month,0,#firstDayOfMonth),0))/7, 0),#firstDayOfMonth-1) + 1);
DECLARE #lastWeekOfMonth INT = (select datediff(week, dateadd(week, datediff(day,0,dateadd(month, datediff(month,0,#lastDayOfMonth),0))/7, 0),#lastDayOfMonth-1) + 1);
IF #w - #weekOfYearStartOfMonth % 52 = 0
SET #numberOfDays = (select 8-DatePart(weekday, #firstDayOfMonth));
ELSE IF #w = #weekOfYearEndOfMonth
SET #numberOfDays = (select DatePart(weekday, #nextMonth)-1);
RETURN #numberOfDays;
END
GO
from that scalar function you can get the result using:
select yr, mo, wk, dbo.calculateWeekDays(yr,mo,wk) as wd from (
values (2020, 12, 50),
(2020, 12, 51),
(2020, 12, 52),
(2020, 12, 53),
(2021, 01, 01),
(2021, 01, 02),
(2021, 01, 03),
(2021, 03, 13),
(2021, 03, 14),
(2021, 04, 14),
(2021, 04, 15)) v(yr, mo, wk)
it give the following result:
year
mo
wk
wd
2020
12
50
7
2020
12
51
7
2020
12
52
7
2020
12
53
4
2021
1
1
3
2021
1
2
7
2021
1
3
7
----
--
--
--
2021
3
13
7
2021
3
14
3
2021
4
14
4
2021
4
15
7

Related

Calculate running sum of previous 3 months from monthly aggregated data

I have a dataset that I have aggregated at monthly level. The next part needs me to take, for every block of 3 months, the sum of the data at monthly level.
So essentially my input data (after aggregated to monthly level) looks like:
month
year
status
count_id
08
2021
stat_1
1
09
2021
stat_1
3
10
2021
stat_1
5
11
2021
stat_1
10
12
2021
stat_1
10
01
2022
stat_1
5
02
2022
stat_1
20
and then my output data to look like:
month
year
status
count_id
3m_sum
08
2021
stat_1
1
1
09
2021
stat_1
3
4
10
2021
stat_1
5
8
11
2021
stat_1
10
18
12
2021
stat_1
10
25
01
2022
stat_1
5
25
02
2022
stat_1
20
35
i.e 3m_sum for Feb = Feb + Jan + Dec. I tried to do this using a self join and wrote a query along the lines of
WITH CTE AS(
SELECT date_part('month',date_col) as month
,date_part('year',date_col) as year
,status
,count(distinct id) as count_id
FROM (date_col, status, transaction_id) as a
)
SELECT a.month, a.year, a.status, sum(b.count_id) as 3m_sum
from cte as a
left join cte as b on a.status = b.status
and b.month >= a.month - 2 and b.month <= a.month
group by 1,2,3
This query NEARLY works. Where it falls apart is in Jan and Feb. My data is from August 2021 to Apr 2022. The means, the value for Jan should be Nov + Dec + Jan. Similarly for Feb it should be Dec + Jan + Feb.
As I am doing a join on the MONTH, all the months of Aug - Nov are treated as being values > month of jan/feb and so the query isn't doing the correct sum.
How can I adjust this bit to give the correct sum?
I did think of using a LAG function, but (even though I'm 99% sure a month won't ever be missed), I can't guarantee we will never have a month with 0 values, and therefore my LAG function will be summing the wrong rows.
I also tried doing the same join, but at individual date level (and not aggregating in my nested query) but this gave vastly different numbers, as I want the sum of the aggregation and I think the sum from the individual row was duplicated a lot of stuff I do a COUNT DISTINCT on to remove.
You can use a SUM with a window frame of 2 PRECEDING. To ensure you don't miss rows, use a calendar table and left-join all the results to it.
SELECT *,
SUM(a.count_id) OVER (ORDER BY c.year, c.month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
FROM Calendar c
LEFT JOIN a ON a.year = c.year AND a.month = c.month
WHERE c.year >= 2021 AND c.year <= 2022;
db<>fiddle
You could also use LAG but you would need it twice.
It should be #Charlieface's answer - only that I get one different result than you put in your expected result table:
WITH
-- your input - and I avoid keywords like "MONTH" or "YEAR"
-- and also identifiers starting with digits are forbidden -
indata(mm,yy,status,count_id,sum_3m) AS (
SELECT 08,2021,'stat_1',1,1
UNION ALL SELECT 09,2021,'stat_1',3,4
UNION ALL SELECT 10,2021,'stat_1',5,8
UNION ALL SELECT 11,2021,'stat_1',10,18
UNION ALL SELECT 12,2021,'stat_1',10,25
UNION ALL SELECT 01,2022,'stat_1',5,25
UNION ALL SELECT 02,2022,'stat_1',20,35
)
SELECT
*
, SUM(count_id) OVER(
ORDER BY yy,mm
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS sum_3m_calc
FROM indata;
-- out mm | yy | status | count_id | sum_3m | sum_3m_calc
-- out ----+------+--------+----------+--------+-------------
-- out 8 | 2021 | stat_1 | 1 | 1 | 1
-- out 9 | 2021 | stat_1 | 3 | 4 | 4
-- out 10 | 2021 | stat_1 | 5 | 8 | 9
-- out 11 | 2021 | stat_1 | 10 | 18 | 18
-- out 12 | 2021 | stat_1 | 10 | 25 | 25
-- out 1 | 2022 | stat_1 | 5 | 25 | 25
-- out 2 | 2022 | stat_1 | 20 | 35 | 35

getting first day and last day of a quarter and 2 quarters back for a date

how to get first day and last day of a quarter for a date?
and also first day and last day of 2 quarters back for a date in Hive or sql
for example for Feb 03 2014 first day and last day of the quarter will be
Jan 01 2014 and Mar 31 2014
and for the same date first and last day of 2 quarters back will be Jul 01 2013 and Sep 31 2013
You can accomplish this in the following way (not too fancy, but there is no direct way). To make it simpler, I just concatenated both output dates
-- before Hive 1.3
select
case
when ceil(month(mydate)/ 3.0) = 1 then concat("Jan 01 ",year(mydate),"|","Mar 31 ",year(mydate))
when ceil(month(mydate)/ 3.0) = 2 then then concat("Apr 01 ",year(mydate),"|","Jun 30 ",year(mydate))
when ceil(month(mydate)/ 3.0) = 3 then then concat("Jul 01 ",year(mydate),"|","Sep 30 ",year(mydate))
when ceil(month(mydate)/ 3.0) = 4 then then concat("Oct 01 ",year(mydate),"|","Dec 31 ",year(mydate))
else
null
end,
ceil(month(mydate)) as quarter
from (
select
from_unixtime(unix_timestamp('Feb 03 2014' , 'MMM dd yyyy')) as mydate
) t;
--Hive 1.3 or higher
select
case
when quarter(mydate) = 1 then concat("Jan 01 ",year(mydate),"|","Mar 31 ",year(mydate))
when quarter(mydate) = 2 then then concat("Apr 01 ",year(mydate),"|","Jun 30 ",year(mydate))
when quarter(mydate) = 3 then then concat("Jul 01 ",year(mydate),"|","Sep 30 ",year(mydate))
when quarter(mydate) = 4 then then concat("Oct 01 ",year(mydate),"|","Dec 31 ",year(mydate))
else
null
end,
ceil(month(mydate)) as quarter
from (
select
from_unixtime(unix_timestamp('Feb 03 2014' , 'MMM dd yyyy')) as mydate
) t;
just replace the hardcoded date for your column in the select in the inner query

SQL: Sum Only Certain Rows Depending on Start and End Date

I have two tables: The 1st table contains a unique identifier (UI). Each unique identifier has a column containing a start date (yyyy-mm-dd), and a column containing an end date (yyyy-mm-dd). The 2nd table contains the temperature for each day, with separate columns for the month, day, year and temperature. I would like to join those tables and get the compiled temperature for each unique identifier; however I would the compiled temperature to only include the days from the second table that fall between start and end dates from the 1st table.
For example, if one record has a start_date of 12/10/15 and an end date of 12/31/15, I would like to have a column containing compiled temperatures for the 10th-31s. If the next record has a start date 12/3/15-12/17/15, I'd like the column next to it to show the compiled temperature for the 3rd-17th. I'll include the query I have so far, but it is not too helpful because I have not really gotten very far:
; with Temps as (
select MONTH, DAY, YEAR, Temp
from Temperatures
where MONTH = 12
and YEAR = 2016
)
Select UI, start_date, end_date, location, SUM(temp)
from Table1 t1
Inner join Temps
on temps.month = month(t1.start_date)
I appreciate any help you might be able to give. Let me know I need to elaborate on anything.
Table 1
UI Start_Date End_Date
2080 12/5/2015 12/31/2015
1266 12/1/2015 12/31/2015
1787 12/17/2015 12/28/2015
1621 12/3/2015 12/20/2015
1974 12/10/2015 12/12/2015
1731 12/25/2015 12/31/2015
Table 2
Month Day Year Temp
12 1 2016 34
12 2 2016 32
12 3 2016 35
12 4 2016 37
12 5 2016 32
12 6 2016 30
12 7 2016 31
12 8 2016 36
12 9 2016 48
12 10 2016 42
12 11 2016 33
12 12 2016 41
12 13 2016 31
12 14 2016 29
12 15 2016 46
12 16 2016 48
12 17 2016 38
12 18 2016 29
12 19 2016 45
12 20 2016 37
12 21 2016 48
12 22 2016 46
12 23 2016 44
12 24 2016 45
12 25 2016 35
12 26 2016 44
12 27 2016 29
12 28 2016 38
12 29 2016 29
12 30 2016 35
12 31 2016 40
Table 3 (Expected Result)
UI Start_Date End_Date Compiled Temp
2080 12/5/2015 12/31/2015 1101
1266 12/1/2015 12/31/2015 1167
1787 12/17/2015 12/28/2015 478
1621 12/3/2015 12/20/2015 668
1974 12/10/2015 12/12/2015 126
1731 12/25/2015 12/31/2015 250
You could do something like this:
; WITH temps AS (
SELECT CONVERT(DATE, CONVERT(CHAR(4), [YEAR]) + '-' + CONVERT(CHAR(2), [MONTH]) + '-' + CONVERT(VARCHAR(2), [DAY])) [TDate], [Temp]
FROM Temperatures
WHERE [MONTH] = 12
AND [YEAR] = 2015
)
SELECT [UI], [start_date], [end_date]
, (SELECT SUM([temp])
FROM temps
WHERE [TDate] BETWEEN T1.[start_date] AND T1.[end_date]) [Compiled Temp]
FROM Table1 T1
No need for a join.
You can do a simple join of the two tables as well. You don't need to use a CTE.
--TEST DATA
if object_id('Table1','U') is not null
drop table Table1
create table Table1 (UI int, Start_Date date, End_Date date)
insert Table1
values
(2080,'12/05/2015','12/31/2015'),
(1266,'12/01/2015','12/31/2015'),
(1787,'12/17/2015','12/28/2015'),
(1621,'12/03/2015','12/20/2015'),
(1974,'12/10/2015','12/12/2015'),
(1731,'12/25/2015','12/31/2015')
if object_id('Table2','U') is not null
drop table Table2
create table Table2 (Month int, Day int, Year int, Temp int)
insert Table2
values
(12,1, 2015,34),
(12,2, 2015,32),
(12,3, 2015,35),
(12,4, 2015,37),
(12,5, 2015,32),
(12,6, 2015,30),
(12,7, 2015,31),
(12,8, 2015,36),
(12,9, 2015,48),
(12,10,2015,42),
(12,11,2015,33),
(12,12,2015,41),
(12,13,2015,31),
(12,14,2015,29),
(12,15,2015,46),
(12,16,2015,48),
(12,17,2015,38),
(12,18,2015,29),
(12,19,2015,45),
(12,20,2015,37),
(12,21,2015,48),
(12,22,2015,46),
(12,23,2015,44),
(12,24,2015,45),
(12,25,2015,35),
(12,26,2015,44)
--AGGREGATE TEMPS
select t1.Start_Date, t1.End_Date, avg(t2.temp) AvgTemp, sum(t2.temp) CompiledTemps
from table1 t1
join table2 t2 ON t2.Year between datepart(year, t1.Start_Date) and datepart(year, t1.End_Date)
and t2.Month between datepart(month,t1.Start_Date) and datepart(month,t1.End_Date)
and t2.Day between datepart(day, t1.Start_Date) and datepart(day, t1.End_Date)
group by t1.Start_Date, t1.End_Date

How many weeks are in each month of the year 2015 and 2016 based on ISO standard?

This year 2014 has:
Jan-4
Feb-4
Mar-5
Apr-4
May-4
Jun-5
Jul-4
Aug-4
Sep-5
Oct-4
Nov-4
Dec-5
How to calculate this for any given year?
There are multiple ways to define "weeks in a month" exactly. Assuming your count is defined (as your numbers indicate):
How many Mondays lie in each month of the year?
You can generate it like that:
Simple:
SELECT EXTRACT(month FROM d) AS mon, COUNT(*) AS weeks
FROM generate_series('2014-01-01'::date
, '2014-12-31'::date
, interval '1 day') d
WHERE EXTRACT(isodow FROM d) = 1 -- only Mondays
GROUP BY 1
ORDER BY 1;
Fast:
SELECT EXTRACT(month FROM d) AS mon, COUNT(*) AS weeks
FROM generate_series ('2014-01-01'::date -- offset to first Monday
+ (8 - EXTRACT(isodow FROM '2014-01-01'::date)::int)%7
, '2014-12-31'::date
, interval '7 days') d
GROUP BY 1
ORDER BY 1;
Either way you get:
mon weeks
1 4
2 4
3 5
4 4
5 4
6 5
7 4
8 4
9 5
10 4
11 4
12 5
Just replace 2014 with the year of interest in each query.
Applying the ISO 8601 to a month as suggested here
select
to_char(d, 'YYYY Mon') as "Month",
case when
extract(dow from d) in (2,3,4)
and
extract(day from (d + interval '1 month')::date - 1) + extract(dow from d) >= 33
then 5
else 4
end as weeks
from generate_series(
'2014-01-01'::date, '2014-12-31', '1 month'
) g (d)
;
Month | weeks
----------+-------
2014 Jan | 5
2014 Feb | 4
2014 Mar | 4
2014 Apr | 4
2014 May | 5
2014 Jun | 4
2014 Jul | 5
2014 Aug | 4
2014 Sep | 4
2014 Oct | 5
2014 Nov | 4
2014 Dec | 4

Oracle 11g - Unpivot

I have a table like this
Date Year Month Day Turn_1 Turn_2 Turn_3
28/08/2014 2014 08 28 Foo Bar Xab
And i would like to "rotate" it in something like this:
Date Year Month Day Turn Source
28/08/2014 2014 08 28 Foo Turn_1
28/08/2014 2014 08 28 Bar Turn_2
28/08/2014 2014 08 28 Xab Turn_3
I need the "Source" column because i need to join this results to another table that say:
Source Interval
Turn_1 08 - 18
Turn_2 11 - 20
Turn_3 18 - 24
For now i have use unpivot to rotate the table, but i dont know how to display the "Source" column (and if it is possible):
select dt_date, df_year, df_month, df_turn
from my_rotatation_table
unpivot( df_turn
for x in(turn_1,
turn_2,
turn_3
))
SOLVED:
select dt_date, df_year, df_month, df_turn, df_source
from my_rotatation_table
unpivot( df_turn
for df_source in(turn_1 as 'Turn_1',
turn_2 as 'Turn_2',
turn_3 as 'Turn_3'
))
Use this query:
with t (Dat, Year, Month, Day, Turn_1, Turn_2, Turn_3) as (
select sysdate, 2014, 08, 28, 'Foo', 'Bar', 'Xab' from dual
)
select dat, year, month, day, turn, source from t
unpivot (
source for turn in (Turn_1, Turn_2, Turn_3)
)
DAT YEAR MONTH DAY TURN SOURCE
----------------------------------------------
08/01/2014 2014 8 28 TURN_1 Foo
08/01/2014 2014 8 28 TURN_2 Bar
08/01/2014 2014 8 28 TURN_3 Xab