how do I create a calculated field that returns days remaining till end of FISCAL_QUARTER? - sql

Current output with no DAYS_LEFT_IN_QUARTERI am new to using Snowflake and was tasked to create a Calendar Dimension table that would aid in reporting weekly / monthly /quarterly reports. I am confused on how to return days remaining in the FISCAL_QUARTER. Q1 spans from Feb - Apr.
Attached below is the code I have been writing to generate the dates projecting 14 years in the future.
--Set the start date and number of years to produce
SET START_DATE = '2012-01-01';
SET NUMBER_DAYS = (SELECT TRUNC(14 * 365));
--Set parameters to force ISO
ALTER SESSION SET WEEK_START = 1, WEEK_OF_YEAR_POLICY = 1;
WITH CTE_MY_DATE AS (
SELECT DATEADD(DAY, SEQ4(), $START_DATE) AS MY_DATE
FROM TABLE(GENERATOR(ROWCOUNT=>$NUMBER_DAYS)) -- Number of days after reference date in previous line
)
SELECT
MY_DATE::date
,YEAR(MY_DATE) AS YEAR
,MONTH(MY_DATE) AS MONTH
,MONTHNAME(MY_DATE) AS MONTH_ABBREVIATION
,DAY(MY_DATE)
,DAYOFWEEK(MY_DATE)
,WEEKOFYEAR(MY_DATE)
,DAYOFYEAR(MY_DATE)
,YEAR(ADD_MONTHS(DATE_TRUNC('month', MY_DATE),11)) AS FISCAL_YEAR
,CONCAT('Q', QUARTER(ADD_MONTHS(DATE_TRUNC('month', MY_DATE),11))) AS FISCAL_QUARTER
,MONTH(ADD_MONTHS(DATE_TRUNC('month', MY_DATE),11)) AS FISCAL_MONTH
FROM CTE_MY_DATE
;

firstly your generator will get gaps, as SEQx() function are allowed to have gaps, so you need to use SEQx() as the OVER BY of a ROW_NUMBER like so:
WITH cte_my_date AS (
SELECT DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY SEQ4()), $START_DATE) AS my_date
FROM TABLE(GENERATOR(ROWCOUNT=>$NUMBER_DAYS)) -- Number of days after reference date in previous line
)
and days left in quarter, is the day truncated to quarter, +1 quarter, date-diff in days to day:
,DATEDIFF('days', my_date, DATEADD('quarter', 1, DATE_TRUNC('quarter', my_date))) AS days_left_in_quarter

How's this? You can copy/paste the code straight into snowflake to test.
Using last_day() tends to make it look a little tidier :-)
WITH CTE_MY_DATE AS (
SELECT DATEADD(DAY, SEQ4(), current_date()) AS MY_DATE
FROM TABLE(GENERATOR(ROWCOUNT=>300)))
SELECT
MY_DATE::date
,YEAR(last_day(my_date,year)) AS FISCAL_YEAR
,concat('Q',quarter(my_date)) AS FISCAL_QUARTER
,datediff(d, my_date, last_day(my_date,quarter)) AS
DAYS_LEFT_IN_QUARTER
FROM CTE_MY_DATE

Related

Snowflake sql query to assign weeks to a month

I know about Snowflake date function to find out day, week, month, year, etc.
I want to have weeks start from Saturday each week to next Saturday.
following gives an idea how to extract, but need something to address my specific case.
How to get week number of month for any given date on snowflake SQL
If four days or more in week period belong to a certain month, I would assign the week to that month; otherwise, to the next month
example:
Week of April 29, 2023 to May 5, 2023 has less then four days in April so want to consider it as May
Week of May 23, 2023 to June 2nd, 2023 has more than four days in May so I would like to consider it as May
I want to assign weeks to a month with more days of one month (four or more days)
Snowflake will allow you to set the first day of the week with a parameter.
https://docs.snowflake.com/en/sql-reference/parameters.html#label-week-start
This will allow you to set the first day of the week at Saturday.
Doing so will result in the WEEK() function counting weeks in a year using saturday as a delimiter between weeks.
Now we just need to find which actual month has the most days for any given week and assign that week to the proper month.
I have an example script below that serves as an example on how to make a custom date dimension table. You can generate the table once and join against it to retrieve your custom date attributes.
/***************************************************************************
A WEEK_START session variable of 0 is the default Snowflake behavior
and has weeks start on Monday and end of Sunday (ISO standard).
https://docs.snowflake.com/en/sql-reference/parameters.html#label-week-start
-- 6 = Saturday is day 1 of of the week
*********************************************************************************************/
alter session set week_start = 6;
/*********************************************************************************************
The parameters below define the temporal boundaries of the calendar table. The values must be
DATE type and can be hardcoded, the result of a query, or a combination of both.
For example, you could set date_start and date_end based on the MIN and MAX date of the table
with the finest date granularity in your data.
*********************************************************************************************/
SET date_start = TO_DATE('2022-12-18');
SET date_end = current_date(); --TIP: for the current date use current_date();
--This sets the num_days parameter to the number of days between start and end
--this value is used for the generator
set num_days = (select datediff(day, $date_start, $date_end+1));
--CTE to hold generated date range
create or replace transient table calendar as
with gen_cte as (
select
dateadd(day,'-' || row_number() over (order by null),
dateadd(day, '+1', $date_end)
) as date_key
from table (generator(rowcount => ($num_days)))
order by 1)
-- calendar table expressions
, step_1 as (
select
date_key,
, dayofmonth(date_key) as day_of_month
, week(date_key) as week_num --*see comments
--, dayofweekiso(date_key) as day_of_week_iso,
, dayofweek(date_key) as day_of_week
, dayname(date_key) as day_name
, month(date_key) as month_num
--, weekiso(date_key) as week_iso_num, --*see comments
, year(date_key) as year_
, year_ || '-' ||week_num::string as year_week_key
, count(date_key) over (partition by year_week_key, month_num) as days_of_week_in_month
--ceil(dayofmonth(date_key) / 7) as day_instance_in_month --used to identify 'floating' events such as "fourth thursday of november"
FROM gen_cte)
-- calculate the max number of days in each month for any week in year
, step_2 as (
select
year_week_key
, month_num
, max(step_1.days_of_week_in_month) as max_days_of_week_in_month
from step_1
group by year_week_key, month_num)
-- for any week with 2 actual month values, assign the month with the most number of days
, step_3 as (
select
year_week_key
, month_num
, row_number() over (partition by year_week_key order by max_days_of_week_in_month desc ) as month_rank
from step_2
qualify month_rank = 1
)
select
s1.date_key
, s1.day_of_month
, s1.week_num
, s1.day_of_week
, s1.day_name
, s3.month_num as assigned_month_num
, s1.month_num as actual_month_num
, s1.year_
from step_1 s1
left join step_3 s3
on s1.year_week_key = s3.year_week_key
;
-- select from your new date dimension table
select * from calendar;

SQL query for all the days of a month

i have the following table RENTAL(book_date, copy_id, member_id, title_id, act_ret_date, exp_ret_date). Where book_date shows the day the book was booked. I need to write a query that for every day of the month(so from 1-30 or from 1-29 or from 1-31 depending on month) it shows me the number of books booked.
i currently know how to show the number of books rented in the days that are in the table
select count(book_date), to_char(book_date,'DD')
from rental
group by to_char(book_date,'DD');
my questions are:
How do i show the rest of the days(if let's say for some reason in my database i have no books rented on 20th or 19th or multiple days) and put the number 0 there?
How do i show the number of days only of the current month so(28,29,30,31 all these 4 are possible depending on month or year)... i am lost . This must be done using only SQL query no pl/SQL or other stuff.
The following query would give you all days in the current month, in your case you can replace SYSDATE with your date column and join with this query to know how many for a given month
SELECT DT
FROM(
SELECT TRUNC (last_day(SYSDATE) - ROWNUM) dt
FROM DUAL CONNECT BY ROWNUM < 32
)
where DT >= trunc(sysdate,'mm')
The answer is to create a table like this:
table yearsmonthsdays (year varchar(4), month varchar(2), day varchar(2));
use any language you wish, e.g. iterate in java with Calendar.getInstance().getActualMaximum(Calendar.DAY_OF_MONTH) to get the last day of the month for as many years and months as you like, and fill that table with the year, month and days from 1 to last day of month of your result.
you'd get something like:
insert into yearsmonthsdays ('1995','02','01');
insert into yearsmonthsdays ('1995','02','02');
...
insert into yearsmonthsdays ('1995','02','28'); /* non-leap year */
...
insert into yearsmonthsdays ('1996','02','01');
insert into yearsmonthsdays ('1996','02','02');
...
insert into yearsmonthsdays ('1996','02','28');
insert into yearsmonthsdays ('1996','02','29'); /* leap year */
...
and so on.
Once you have this table done, your work is almost finished. Make an outer left join between your table and this table, joining year, month and day together, and when no lines appear, the count will be zero as you wish. Without using programming, this is your best bet.
In oracle, you can query from dual and use the conncect by level syntax to generate a series of rows - in your case, dates. From there on, it's just a matter of deciding what dates you want to display (in my example I used all the dates from 2014) and joining on your table:
SELECT all_date, COALESCE (cnt, 0)
FROM (SELECT to_date('01/01/2014', 'dd/mm/yyyy') + rownum - 1 AS all_date
FROM dual
CONNECT BY LEVEL <= 365) d
LEFT JOIN (SELECT TRUNC(book_date), COUNT(book_date) AS cnt
FROM rental
GROUP BY book_date) r ON d.all_date = TRUNC(r.book_date)
There's no need to get ROWNUM involved ... you can just use LEVEL in the CONNECT BY:
WITH d1 AS (
SELECT TRUNC(SYSDATE, 'MONTH') - 1 + LEVEL AS book_date
FROM dual
CONNECT BY TRUNC(SYSDATE, 'MONTH') - 1 + LEVEL <= LAST_DAY(SYSDATE)
)
SELECT TRUNC(d1.book_date), COUNT(r.book_date)
FROM d1 LEFT JOIN rental r
ON TRUNC(d1.book_date) = TRUNC(r.book_date)
GROUP BY TRUNC(d1.book_date);
Simply replace SYSDATE with a date in the month you're targeting for results.
All days of the month based on current date
select trunc(sysdate) - (to_number(to_char(sysdate,'DD')) - 1)+level-1 x from dual connect by level <= TO_CHAR(LAST_DAY(sysdate),'DD')
It did works to me:
SELECT DT
FROM (SELECT TRUNC(LAST_DAY(SYSDATE) - (CASE WHEN ROWNUM=1 THEN 0 ELSE ROWNUM-1 END)) DT
FROM DUAL
CONNECT BY ROWNUM <= 32)
WHERE DT >= TRUNC(SYSDATE, 'MM')
In Oracle SQL the query must look like this to not miss the last day of month:
SELECT DT
FROM(
SELECT trunc(add_months(sysdate, 1),'MM')- ROWNUM dt
FROM DUAL CONNECT BY ROWNUM < 32
)
where DT >= trunc(sysdate,'mm')

Find previous equivalent dates over the past two calender years

If today is say 15th August 2012 then the query should return the following
15/01/2011,
15/02/2011,
...
...
15/07/2012
15/08/2012
If today is 31st August 2012 then the query would return
31/01/2011,
28/02/2011, <<<<this is the nearest date
...
...
31/07/2012
31/08/2012
We have a vw_DimDate in our Warehouse which should help
edit
It contains the following fields
Currently I'm using the following but it seems rather convoluted! ...
DECLARE #Dt DATETIME = '31 JUL 2012'--GETDATE()
;WITH DateSet_cte(DayMarker)
AS
(
SELECT DayMarker
FROM WHData.dbo.vw_DimDate
WHERE
DayMarker >= CONVERT(DATETIME,CONVERT(CHAR(4),DATEADD(YEAR,-1,#Dt),112) + '0101') AND
DayMarker <=#Dt
)
, MaxDate_cte(MaxDate)
AS
(
SELECT [MaxDate] = MAX(DayMarker)
FROM DateSet_cte
)
SELECT
[Mth] = CONVERT(DATETIME,CONVERT(CHAR(6),a.DayMarker,112) + '01')
, MAX(a.DayMarker) [EquivDate]
FROM DateSet_cte a
WHERE DAY(a.DayMarker) <= (SELECT DAY([MaxDate]) FROM MaxDate_cte)
GROUP BY CONVERT(DATETIME,CONVERT(CHAR(6),a.DayMarker,112) + '01')
;with Numbers as (
select distinct number from master..spt_values where number between 0 and 23
), Today as (
select CONVERT(date,CURRENT_TIMESTAMP) as d
)
select
DATEADD(month,-number,d)
from
Numbers,Today
where DATEPART(year,DATEADD(month,-number,d)) >= DATEPART(year,d) - 1
Seems odd to want a variable number of returned values based on how far through the year we are, but that's what I've implemented.
When you use DATEADD to add months to a value, then it automatically adjusts the day number if it would have produced an out of range date (e.g. 31st February), such that it's the last day of the month. Or, as the documentation puts it:
If datepart is month and the date month has more days than the return month and the date day does not exist in the return month, the last day of the return month is returned.
Of course, if you already have a numbers table in your database, you can eliminate the first CTE. You mentioned that you "have a vw_DimDate in our Warehouse which should help", but since I have no idea on what that (presumably, a) view contains, it wasn't any help.

Return just the last day of each month with SQL

I have a table that contains multiple records for each day of the month, over a number of years. Can someone help me out in writing a query that will only return the last day of each month.
SQL Server (other DBMS will work the same or very similarly):
SELECT
*
FROM
YourTable
WHERE
DateField IN (
SELECT MAX(DateField)
FROM YourTable
GROUP BY MONTH(DateField), YEAR(DateField)
)
An index on DateField is helpful here.
PS: If your DateField contains time values, the above will give you the very last record of every month, not the last day's worth of records. In this case use a method to reduce a datetime to its date value before doing the comparison, for example this one.
The easiest way I could find to identify if a date field in the table is the end of the month, is simply adding one day and checking if that day is 1.
where DAY(DATEADD(day, 1, AsOfDate)) = 1
If you use that as your condition (assuming AsOfDate is the date field you are looking for), then it will only returns records where AsOfDate is the last day of the month.
Use the EOMONTH() function if it's available to you (E.g. SQL Server). It returns the last date in a month given a date.
select distinct
Date
from DateTable
Where Date = EOMONTH(Date)
Or, you can use some date math.
select distinct
Date
from DateTable
where Date = DATEADD(MONTH, DATEDIFF(MONTH, -1, Date)-1, -1)
In SQL Server, this is how I usually get to the last day of the month relative to an arbitrary point in time:
select dateadd(day,-day(dateadd(month,1,current_timestamp)) , dateadd(month,1,current_timestamp) )
In a nutshell:
From your reference point-in-time,
Add 1 month,
Then, from the resulting value, subtract its day-of-the-month in days.
Voila! You've the the last day of the month containing your reference point in time.
Getting the 1st day of the month is simpler:
select dateadd(day,-(day(current_timestamp)-1),current_timestamp)
From your reference point-in-time,
subtract (in days), 1 less than the current day-of-the-month component.
Stripping off/normalizing the extraneous time component is left as an exercise for the reader.
A simple way to get the last day of month is to get the first day of the next month and subtract 1.
This should work on Oracle DB
select distinct last_day(trunc(sysdate - rownum)) dt
from dual
connect by rownum < 430
order by 1
I did the following and it worked out great. I also wanted the Maximum Date for the Current Month. Here is what I my output is. Notice the last date for July which is 24th. I pulled it on 7/24/2017, hence the result
Year Month KPI_Date
2017 4 2017-04-28
2017 5 2017-05-31
2017 6 2017-06-30
2017 7 2017-07-24
SELECT B.Year ,
B.Month ,
MAX(DateField) KPI_Date
FROM Table A
INNER JOIN ( SELECT DISTINCT
YEAR(EOMONTH(DateField)) year ,
MONTH(EOMONTH(DateField)) month
FROM Table
) B ON YEAR(A.DateField) = B.year
AND MONTH(A.DateField) = B.Month
GROUP BY B.Year ,
B.Month
SELECT * FROM YourTableName WHERE anyfilter
AND "DATE" IN (SELECT MAX(NameofDATE_Column) FROM YourTableName WHERE
anyfilter GROUP BY
TO_CHAR(NameofDATE_Column,'MONTH'),TO_CHAR(NameofDATE_Column,'YYYY'));
Note: this answer does apply for Oracle DB
Here's how I just solved this. day_date is the date field, calendar is the table that holds the dates.
SELECT cast(datepart(year, day_date) AS VARCHAR)
+ '-'
+ cast(datepart(month, day_date) AS VARCHAR)
+ '-'
+ cast(max(DATEPART(day, day_date)) AS VARCHAR) 'DATE'
FROM calendar
GROUP BY datepart(year, day_date)
,datepart(month, day_date)
ORDER BY 1

Calculating Open incidents per month

We have Incidents in our system with Start Time and Finish Time and project name (and other info) .
We would like to have report: How many Incidents has 'open' status per month per project.
Open status mean: Not finished.
If incident is created in December 2009 and closed in March 2010, then it should be included in December 2009, January and February of 2010.
Needed structure should be like this:
Project Year Month Count
------- ------ ------- -------
Test 2009 December 2
Test 2010 January 10
Test 2010 February 12
....
In SQL Server:
SELECT
Project,
Year = YEAR(TimeWhenStillOpen),
Month = DATENAME(month, MONTH(TimeWhenStillOpen)),
Count = COUNT(*)
FROM (
SELECT
i.Project,
i.Incident,
TimeWhenStillOpen = DATEADD(month, v.number, i.StartTime)
FROM (
SELECT
Project,
Incident,
StartTime,
FinishTime = ISNULL(FinishTime, GETDATE()),
MonthDiff = DATEDIFF(month, StartTime, ISNULL(FinishTime, GETDATE()))
FROM Incidents
) i
INNER JOIN master..spt_values v ON v.type = 'P'
AND v.number BETWEEN 0 AND MonthDiff - 1
) s
GROUP BY Project, YEAR(TimeWhenStillOpen), MONTH(TimeWhenStillOpen)
ORDER BY Project, YEAR(TimeWhenStillOpen), MONTH(TimeWhenStillOpen)
Briefly, how it works:
The most inner subselect, that works directly on the Incidents table, simply kind of 'normalises' the table (replaces NULL finish times with the current time) and adds a month difference column, MonthDiff. If there can be no NULLs in your case, just remove the ISNULL expression accordingly.
The outer subselect uses MonthDiff to break up the time range into a series of timestamps corresponding to the months where the incident was still open, i.e. the FinishTime month is not included. A system table called master..spt_values is also employed there as a ready-made numbers table.
Lastly, the main select is only left with the task of grouping the data.
A useful technique here is to create either a table of "all" dates (clearly that would be infinite so I mean a sufficiently large range for your purposes) OR create two tables: one of all the months (12 rows) and another of "all" years.
Let's assume you go for the 1st of these:
create table all_dates (d date)
and populate as appropriate. I'm going to define your incident table as follows
create table incident
(
incident_id int not null,
project_id int not null,
start_date date not null,
end_date date null
)
I'm not sure what RDBMS you are using and date functions vary a lot between them so the next bit may need adjusting for your needs.
select
project_id,
datepart(yy, all_dates.d) as "year",
datepart(mm, all_dates.d) as "month",
count(*) as "count"
from
incident,
all_dates
where
incident.start_date <= all_dates.d and
(incident.end_date >= all_dates.d or incident.end_date is null)
group by
project_id,
datepart(yy, all_dates.d) year,
datepart(mm, all_dates.d) month
That is not going to quite work as we want as the counts will be for every day that the incident was open in each month. To fix this we either need to use a subquery or a temporary table and that really depends on the RDBMS...
Another problem with it is that, for open incidents it will show them against all future months in your all_dates table. adding a all_dates.d <= today solves that. Again, different RDBMSs have different methods of giving back now/today/systemtime...
Another approach is to have an all_months rather than all_dates table that just has the date of first of the month in it:
create table all_months (first_of_month date)
select
project_id,
datepart(yy, all_months.first_of_month) as "year",
datepart(mm, all_months.first_of_month) as "month",
count(*) as "count"
from
incident,
all_months
where
incident.start_date <= dateadd(day, -1, dateadd(month, 1, first_of_month)
(incident.end_date >= first_of_month or incident.end_date is null)
group by
project_id,
datepart(yy, all_months.first_of_month),
datepart(mm, all_months.first_of_month)