SQL query for all the days of a month - sql

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')

Related

Oracle SQL - selecting only records that appear in each month of a year

I have a data set of monthly billing records, but I want to filter out newer accounts that don't have a 12 month history and I'm not really sure how to approach this.
I was thinking about something using row number = 12 but not sure how to do that at the level of detail for each account and not the entire dataset. Also saw a post here about having count(date) = 12, but I don't think I'm executing that correctly.
You use the windowing capability of aggregate functions, specifying a PARTITION BY.
SELECT *
FROM (SELECT x.*,
COUNT(*) OVER (PARTITION BY account_no) cnt
FROM monthly_billing_records x)
WHERE cnt >= 12
What Oracle will do is generate that query block results, then make a second internal pass over those results and do a GROUP BY whatever your PARTITION BY column list is, compute the COUNT(*), and join that back to the results on the PARTITION BY columns. That way you can summarize multiple rows and report the result on each individual row. It can do a lot more complicated stuff than that, so read up on PARTITION BY, ROWS BETWEEN , etc..
If you want the id for accounts with 12 months of data (the current month and the preceding 11 months) then you can GROUP BY the account's id and COUNT the DISTINCT dates within each month (WHERE the dates are within the expected time period) and then to only get those with a full 12 month history you can use a HAVING clause:
SELECT id,
COUNT(DISTINCT TRUNC(dt, 'MM')) AS num_months
FROM table_name
WHERE dt < ADD_MONTHS(TRUNC(SYSDATE, 'MM'), +1)
AND dt >= ADD_MONTHS(TRUNC(SYSDATE, 'MM'), -11)
GROUP BY id
HAVING COUNT(DISTINCT TRUNC(dt, 'MM')) = 12;
Which, for the sample data:
CREATE TABLE table_name (id, dt) AS
-- Every month for the current month and past 11 months
SELECT 1, ADD_MONTHS(TRUNC(SYSDATE, 'MM'), 1 - LEVEL) FROM DUAL CONNECT BY LEVEL <= 12
UNION ALL
-- 12 months of historical data but not for the current month
SELECT 2, ADD_MONTHS(TRUNC(SYSDATE, 'MM'), 0 - LEVEL) FROM DUAL CONNECT BY LEVEL <= 12
UNION ALL
-- Only 4 months of data
SELECT 3, ADD_MONTHS(TRUNC(SYSDATE, 'MM'), 1 - LEVEL) FROM DUAL CONNECT BY LEVEL <= 4;
Outputs:
ID
NUM_MONTHS
1
12
fiddle

How to get current employee counts as of every date in the last 5 years

--PL/SQL
I want two resulting columns. One column is every date in the last 5 years and the other column is the employee count as of each date.
I have a query below where you enter a date as a parameter and it tells you how many people are employed on that date but I don't know how to extrapolate it to achieve my goal above. Achieving my goal would make the parameter unnecessary so I am looking to get rid of it.
Please help, I'm stumped!
select count(person_id), :EFF_DATE
from
(
select paa.person_id
from apps.per_all_assignments_f paa --employee assignments
,apps.per_assignment_status_types past --assignment statuses
where paa.assignment_status_type_id = past.assignment_status_type_id
and past.user_status in ('Active Assignment','Transitional - Active','Transitional -
Inactive','Sabbatical','Sabbatical 50%')
and :EFF_DATE between paa.effective_start_date and paa.effective_end_date
group by paa.person_id
)
If I understood you correctly, you need to create a calendar which contains all dates in last 5 years, and then join it to tables you currently use in that query.
Something like this (untested, as I don't have your tables nor data):
WITH
calendar (datum)
AS
-- last 5 years
( SELECT TRUNC (SYSDATE) - LEVEL + 1
FROM DUAL
CONNECT BY LEVEL <=
TRUNC (SYSDATE) - ADD_MONTHS (TRUNC (SYSDATE), -12 * 5))
SELECT c.datum, COUNT (DISTINCT paa.person_id) cnt
FROM calendar c
JOIN apps.per_all_assignment_f paa
ON c.datum BETWEEN paa.effective_start_date
AND paa.effective_end_date
JOIN apps.per_assignment_status_types past
ON past.assignment_status_type_id = paa.assignment_status_type_id
WHERE past.user_status IN ('Active Assignment',
'Transitional - Active',
'Transitional - Inactive',
'Sabbatical',
'Sabbatical 50%')
GROUP BY c.datum
ORDER BY c.datum;

Rolling 12 month filter criteria in SQL

Having an issue in SQL script where I’m trying to achieve filter criteria of rolling 12 months in the day column which stored data as a text in server.
Goal is to count sizes for product at retail store location over the last 12 months from the current day. Currently, in my query I'm using the criteria of year 2019 which only counts the sizes for that year but not for rolling 12 months from current date.
CALENDARDAY column is in text field in the data set and data stores in yyyymmdd format.
When trying to run below script in Tableau with GETDATE and DATEADD function it is giving me a functional error. I am trying to access SAP HANA server with below query.
Any help would be appreciated
Select
SKU, STYLE_ID, Base_Style_ID, COLOR, SIZEKEY, STORE, Year,
count(SIZEKEY)over(partition by STYLE_ID,COLOR,STORE,Year) as SZ_CNT
from
(
select
a."RAW" As SKU,
a."STYLENUM" As STYLE_ID,
mat."BASENUM" AS Base_Style_ID,
a."COLORNUM" AS COLOR,
a."SIZE" AS SIZEKEY,
a."STORENUM" AS STORE,
substring(a."CALENDARDAY",1,4) As year
from PRTRPT_XRE as a
JOIN ZAT_SKU As mat On a."RAW" = mat."SKU"
where a."ORGANIZATION" = 'M20'
and a."COLORNUM" is not null
and substring(a."CALENDARDAY",1,4) = '2019'
Group BY
a."RAW",
a."STYLENUM",
mat."BASENUM",
a."ZCOLORCD",
a."SIZE",
a."STORENUM",
substring(a."CALENDARDAY",1,4)
)
I have never worked on that DB / Server, so I don't have a way to test this.
But hopefully this will work (expecting exact 12 months before today's date)
AND ADD_MONTHS (TO_DATE (a."CALENDARDAY", 'YYYY-MM-DD'), 12) > CURRENT_DATE
or
AND ADD_MONTHS (a."CALENDARDAY", 12) > CURRENT_DATE
Below condition from one of our CALENDAR table also worked same way as ADD_MONTHS mentioned in above response
select distinct CALENDARDAY
from
(
select FISCALWEEK, CALENDARDAY, CNST, row_number()over(partition by CNST order by FISCALWEEK desc) as rnum
from
(
select distinct FISCALWEEK, CALENDARDAY, 'A' as CNST
from CALENDARTABLE
where CALENDARDAY < current_date
order by 1,2
)
) where rnum < 366

How to fill value as zero when No data exists for particular week in oracle

I have a table with following structure.
Note_title varchar2(100)
Note_created_on date
Now in a report, I want to show all notes created week-wise, So I implemented the following solution for it.
SELECT to_char(Note_created_on - 7/24,'ww')||'/'||to_char(Note_created_on - 7/24,'yyyy') as Week ,
nvl(COUNT(Note_title),'0') as AMOUNT
FROM Notes
GROUP BY to_char(Note_created_on - 7/24,'ww') ,
to_char(Note_created_on -7/24,'yyyy')
ORDER BY to_char(Note_created_on - 7/24,'ww') DESC
And i am getting correct output from it, But suppose week 42,45 do not have any created Note then its just missing it.
Sample Output:
WEEK AMOUNT
46/2018 3
44/2018 22
43/2018 45
41/2018 1
40/2018 2
39/2018 27
38/2018 23
So How can I get zero values for week 42,45 instead of leaving them out?
First you would need to generate all the weeks between each year, after that would left join with the Notes tables on the weeks and group by the weeks generated. Eg:
with weeks
as ( select level as lvl /*Assume 52 weeks in a calendar year..*/
from dual
connect by level <=52
)
,weeks_year
as (select distinct
b.lvl||'/'||trunc(Note_created_on,'YYYY') as week_year_val /*From the start of year in Note_created_on*/
from Notes a
join weeks b
on 1=1
)
SELECT a.week_year_val as Week
,COUNT(Note_title) as AMOUNT
FROM weeks_year a
LEFT JOIN Notes b
ON a.week_year_val=to_char(b.Note_created_on - 7/24,'ww')||'/'||to_char(b.Note_created_on - 7/24,'yyyy')
GROUP BY a.week_year_val
ORDER BY a.week_year_val DESC
If you want to perform this for the current year, you may use the following SQL statement which uses such a RIGHT JOIN as below :
SELECT d.week as Week,
nvl(COUNT(Note_title), '0') as AMOUNT
FROM Notes
RIGHT JOIN
(SELECT lpad(level,2,'0')|| '/' ||to_char(sysdate,'yyyy') as week,
'0' as amount FROM dual CONNECT BY level <= 53) d
ON
( d.week =
to_char(Note_created_on - 7 / 24, 'ww') ||'/'||to_char(Note_created_on - 7 / 24, 'yyyy') )
GROUP BY d.week
ORDER BY d.week DESC;
P.S. There's a common belief that a year is composed of 52 weeks, true but truncated :). So, I used 53,
Notice that select to_char( date'2016-12-31' - 7 / 24, 'ww') from dual yields 53 as a sample.
Rextester Demo
As mentioned by jarlh:
Create a list of weeks:
SELECT TO_CHAR(LEVEL, 'FM00')||'/2018' wk
FROM dual
CONNECT BY LEVEL <= 53
This query generates 53 rows, and level is just a number.. 1.. 2.. upto 53. We format it to become 01/2018, 02/2018.. 53/2018
If you plan to use this query in other years, you'd be better off making the year dynamic:
SELECT TO_CHAR(LEVEL, 'FM00')||TO_CHAR(sysdate-7/24,'/YYYY') wk
FROM dual
CONNECT BY LEVEL <= 53
(Credits to Barbaros for pointing out that the last day of any year is reported by Oracle as being in week 53, or said another way 7*52 = 364)
We left join the notes data onto it. I wasn't really clear on why you subtracted 7 hours from the date (time zone?) but I left it. I removed the complexity of the count, as you seem to only want the count of records in a particular week. I also removed the double to_char, because you can do it all in a single operation. One doesn't need to TO_CHAR(date, 'WW')||'/'||TO_CHAR(date,'YYYY') etc.. you just tochar with WW/YYYY as a format. Our query now looks like:
SELECT lst.wk as week, COALESCE(amt, 0) as amount FROM
(
SELECT TO_CHAR(LEVEL, 'FM00')||TO_CHAR(sysdate-7/24,'/YYYY') wk
FROM dual
CONNECT BY LEVEL <= 52
) lst
LEFT OUTER JOIN
(
SELECT
to_char(Note_created_on - 7/24,'ww/yyyy') as wk,
COUNT(*) as amt
FROM Notes
GROUP BY to_char(Note_created_on - 7/24,'ww/yyyy')
) dat
ON lst.wk = dat.wk
ORDER BY lst.wk
For weeks where there are no note, the left join records a null against that week, so we coalesce it to make it 0.
You can, of course, do the query in other ways (many ways), here's a compare:
SELECT lst.wk as week, COUNT(dat.wk) as amount FROM
(
SELECT TO_CHAR(LEVEL, 'FM00')||TO_CHAR(sysdate-7/24,'/YYYY') wk
FROM dual
CONNECT BY LEVEL <= 52
) lst
LEFT OUTER JOIN
(
SELECT
to_char(Note_created_on - 7/24,'ww/yyyy') as wk
FROM Notes
) dat
ON lst.wk = dat.wk
GROUP BY lst.wk
ORDER BY lst.wk
In this form we do the groupby/count after the join. By counting the dat.wk, which for some lst.wk might be NULL, we can omit the coalesce, because count(null) is 0

Oracle - Split a record into multiple records

I have a schedule table for each month schedule. And this table also has days off within that month. I need a result set that will tell working days and off days for that month.
Eg.
CREATE TABLE SCHEDULE(sch_yyyymm varchar2(6), sch varchar2(20), sch_start_date date, sch_end_date date);
INSERT INTO SCHEDULE VALUES('201703','Working Days', to_date('03/01/2017','mm/dd/yyyy'), to_date('03/31/2017','mm/dd/yyyy'));
INSERT INTO SCHEDULE VALUES('201703','Off Day', to_date('03/05/2017','mm/dd/yyyy'), to_date('03/07/2017','mm/dd/yyyy'));
INSERT INTO SCHEDULE VALUES('201703','off Days', to_date('03/08/2017','mm/dd/yyyy'), to_date('03/10/2017','mm/dd/yyyy'));
INSERT INTO SCHEDULE VALUES('201703','off Days', to_date('03/15/2017','mm/dd/yyyy'), to_date('03/15/2017','mm/dd/yyyy'));
Using SQL or PL/SQL I need to split the record with Working Days and Off Days.
From above records I need result set as:
201703 Working Days 03/01/2017 - 03/04/2017
201703 Off Days 03/05/2017 - 03/10/2017
201703 Working Days 03/11/2017 - 03/14/2017
201703 Off Days 03/15/2017 - 03/15/2017
201703 Working Days 03/16/2017 - 03/31/2017
Thank You for your help.
Edit: I've had a bit more of a think, and this approach works fine for your insert records above - however, it misses records where there are not continuous "off day" periods. I need to have a bit more of a think and will then make some changes
I've put together a test using the lead and lag functions and a self join.
The upshot is you self-join the "Off Days" onto the existing tables to find the overlaps. Then calculate the start/end dates on either side of each record. A bit of logic then lets us work out which date to use as the final start/end dates.
SQL fiddle here - I used Postgres as the Oracle function wasn't working but it should translate ok.
select sch,
/* Work out which date to use as this record's Start date */
case when prev_end_date is null then sch_start_date
else off_end_date + 1
end as final_start_date,
/* Work out which date to use as this record's end date */
case when next_start_date is null then sch_end_date
when next_start_date is not null and prev_end_date is not null then next_start_date - 1
else off_start_date - 1
end as final_end_date
from (
select a.*,
b.*,
/* Get the start/end dates for the records on either side of each working day record */
lead( b.off_start_date ) over( partition by a.sch_start_date order by b.off_start_date ) as next_start_date,
lag( b.off_end_date ) over( partition by a.sch_start_date order by b.off_start_date ) as prev_end_date
from (
/* Get all schedule records */
select sch,
sch_start_date,
sch_end_date
from schedule
) as a
left join
(
/* Get all non-working day schedule records */
select sch as off_sch,
sch_start_date as off_start_date,
sch_end_date as off_end_date
from schedule
where sch <> 'Working Days'
) as b
/* Join on "Off Days" that overlap "Working Days" */
on a.sch_start_date <= b.off_end_date
and a.sch_end_date >= b.off_start_date
and a.sch <> b.off_sch
) as c
order by final_start_date
If you had a dates table this would have been easier.
You can construct a dates table using a recursive cte and join on to it. Then use the difference of row number approach to classify rows with same schedules on consecutive dates into one group and then get the min and max of each group which would be the start and end dates for a given sch. I assume there are only 2 sch values Working Days and Off Day.
with dates(dt) as (select date '2017-03-01' from dual
union all
select dt+1 from dates where dt < date '2017-03-31')
,groups as (select sch_yyyymm,dt,sch,
row_number() over(partition by sch_yyyymm order by dt)
- row_number() over(partition by sch_yyyymm,sch order by dt) as grp
from (select s.sch_yyyymm,d.dt,
/*This condition is to avoid a given date with 2 sch values, as 03-01-2017 - 03-31-2017 are working days
on one row and there is an Off Day status for some of these days.
In such cases Off Day would be picked up as sch*/
case when count(*) over(partition by d.dt) > 1 then min(s.sch) over(partition by d.dt) else s.sch end as sch
from dates d
join schedule s on d.dt >= s.sch_start_date and d.dt <= s.sch_end_date
) t
)
select sch_yyyymm,sch,min(dt) as start_date,max(dt) as end_date
from groups
group by sch_yyyymm,sch,grp
I couldn't get the recursive cte running in Oracle. Here is a demo using SQL Server.
Sample Demo in SQL Server