What is a better alternative to a "helper" table in an Oracle database? - sql

Let's say I have an 'employees' table with employee start and end dates, like so:
employees
employee_id start_date end_date
53 '19901117' '99991231'
54 '19910208' '20010512'
55 '19910415' '20120130'
. . .
. . .
. . .
And let's say I want to get the monthly count of employees who were employed at the end of the month. So the resulting data set I'm after would look like:
month count of employees
'20150131' 120
'20150228' 118
'20150331' 122
. .
. .
. .
The best way I currently know how to do this is to create a "helper" table to join onto, such as:
helper_tbl
month
'20150131'
'20150228'
'20150331'
.
.
.
And then do a query like so:
SELECT t0b.month,
count(t0a.employee_id)
FROM employees t0a
JOIN helper_tbl t0b
ON t0b.month BETWEEN t0a.start_date AND t0a.end_date
GROUP BY t0b.month
However, this is somewhat annoying solution to me, because it means I'm having to create these little helper tables all the time and they clutter up my schema. I feel like other people must run into the same need for "helper" tables, but I'm guessing people have figured out a better way to go about this that isn't so manual. Or do you all really just keep creating "helper" tables like I do to get around these situations?
I understand this question is a bit open-ended up for stack overflow, so let me offer a more closed-ended version of the question which is, "Given just the 'employees' table, what would YOU do to get the resulting data set that I showed above?"

You can use a CTE to generate all the month values, either form a fixed starting point or based on the earliest date in your table:
with months (month) as (
select add_months(first_month, level - 1)
from (
select trunc(min(start_date), 'MM') as first_month from employees
)
connect by level <= ceil(months_between(sysdate, first_month))
)
select * from months;
With data that was an earliest start date of 1990-11-17 as in your example, that generates 333 rows:
MONTH
-------------------
1990-11-01 00:00:00
1990-12-01 00:00:00
1991-01-01 00:00:00
1991-02-01 00:00:00
1991-03-01 00:00:00
...
2018-06-01 00:00:00
2018-07-01 00:00:00
You can then use that in a query that joins to your table, something like:
with months (month) as (
select add_months(first_month, level - 1)
from (
select trunc(min(start_date), 'MM') as first_month from employees
)
connect by level <= ceil(months_between(sysdate, first_month))
)
select m.month, count(*) as employees
from months m
left join employees e
on e.start_date <= add_months(m.month, 1)
and (e.end_date is null or e.end_date >= add_months(m.month, 1))
group by m.month
order by m.month;
Presumably you wan to include people who are still employed, so you need to allow for the end date being null (unless you're using a magic end-date value for people who are still employed...)
With dates stored as string it's a bit more complicated but you can generate the month information in a similar way:
with months (month, start_date, end_date) as (
select add_months(first_month, level - 1),
to_char(add_months(first_month, level - 1), 'YYYYMMDD'),
to_char(last_day(add_months(first_month, level - 1)), 'YYYYMMDD')
from (
select trunc(min(to_date(start_date, 'YYYYMMDD')), 'MM') as first_month from employees
)
connect by level <= ceil(months_between(sysdate, first_month))
)
select m.month, m.start_date, m.end_date, count(*) as employees
from months m
left join employees e
on e.start_date <= m.end_date
and (e.end_date is null or e.end_date > m.end_date)
group by m.month, m.start_date, m.end_date
order by m.month;
Very lightly tested with a small amount of made-up data and both seem to work.

If you want to get the employees who were employed at the end of the month, then you can use the LAST_DAY function in the WHERE clause of the your query. Also, you can use that function in the GROUP BY clause of your query. So your query would be like below,
SELECT LAST_DAY(start_date), COUNT(1)
FROM employees
WHERE start_date = LAST_DAY(start_date)
GROUP BY LAST_DAY(start_date)
or if you just want to count employees employed per month then use below query,
SELECT LAST_DAY(start_date), COUNT(1)
FROM employees
GROUP BY LAST_DAY(start_date)

Related

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

SQL: Comparing two tables for missing records and then on the date fields

I have two tables as below
work_assignments
emp_id | start_date | End Date
------------------------------------------
1 | May-10-2017 | May-30-2017
1 | Jun-05-2017 | null
2 | May-08-2017 | null
hourly_pay
emp_id | start_date | End Date | Rate
-----------------------------------------------
1 | May-20-2017 | Jun-30-2017 | 75
1 | Jul-01-2017 | null | 80
These 2 tables share the emp_id (employee id) foreign key and joining these two I should be able to:
find employee records missing in the hourly_pay table.
Given the data here, the query should return emp_id 2 from work_assignments table
find the records where the hourly_pay start_date that are later than the work assignments start_date. Again, given the data here, the query should return emp_id 1 (because work_assignments.start_date has May-10-2017, while the earliest hourly_pay.start_date is on May-20-2017)
I am able to achieve the first part of result using the join query below
select distinct emp_id from work_contracts
left join hourly_pay hr USING(emp_id)
where hr.emp_id is null
I am stuck on the second part where probably I need a correlated subquery to tell the hourly pay table records that did not start before the work_assignments start_date? or is there any other way?
Do the date comparison in an inner query then wrap it to filter it to the ones that satisfy the late pay criteria.
select * from (
select distinct c.emp_id,
case when c.start_date < hr.start_date then 1 else 0 end as latePay
from work_contracts c
left join hourly_pay hr USING(emp_id)
) result
where latePay = 1
You can achieve second part using query
select distinct wc.emp_id
from (select emp_id, min(start_date) start_date from work_contracts group by emp_id) wc
join (select emp_id, min(start_date) start_date from hourly_pay group by emp_id) hr
on wc.emp_id = hr.emp_id
where wc.start_date < hr.start_date
This hints at a between condition, with some twists, but I've had extremely bad luck using betweens in joins. They appear to perform some form of cross-join on the back and end then filter out the actual join where-clause style. I know that's not very technical, but I've never done a non-equality condition in a join that's turned out well.
So, this may seem counter-intuitive, but I think exploding all date possibilities might actually be your best bet. Without knowing how big your date ranges actually are it's hard to say.
Also, I think this will actually satisfy both conditions in your question at once -- by telling you all work assignments that do not have corresponding pay rates.
Try this against your actual data and see how it works (and how long it takes).
with pay_dates as (
select
emp_id, rate,
generate_series (start_date, coalesce (end_date, current_date), interval '1 day') as pd
from hourly_pay
),
assignment_dates as (
select
emp_id, start_date,
generate_series (start_date, coalesce (end_date, current_date), interval '1 day') as wd
from work_assignments
)
select
emp_id, min (wd)::date as from_date,
max (wd)::date as thru_date
from
assignment_dates a
where
not exists (
select null
from pay_dates p
where p.emp_id = a.emp_id
and a.wd = p.pd
)
group by
emp_id, start_date
The results should be all work assignment ranges with no rates:
emp from thru
1 '2017-05-10' '2017-05-19'
2 '2017-05-08' '2017-11-14'
The cool thing is it would also remove any overlaps, where a work assignment was partially covered.
-- Edit 3/20/2018 --
Per your request, here is a break-down of what the logic does.
with pay_dates as(
select
emp_id, rate,
generate_series (start_date, coalesce (end_date, current_date), interval '1 day') as pd
from hourly_pay
)
This takes the hourly_pay data and breaks it into a record for each employee, for each day:
emp_id rate pay date
1 75 5/20/17
1 75 5/21/17
1 75 5/22/17
...
1 75 6/30/17
1 80 6/01/17
1 80 6/02/17
...
1 80 today
Next,
[implied "with"]
assignment_dates as (
select
emp_id, start_date,
generate_series (start_date, coalesce (end_date, current_date), interval '1 day') as wd
from work_assignments
)
Effectively does the same thing for the work assignments table, only preserving the "start date column" in each row.
Then the main query is this:
select
emp_id, min (wd)::date as from_date,
max (wd)::date as thru_date
from
assignment_dates a
where
not exists (
select null
from pay_dates p
where p.emp_id = a.emp_id
and a.wd = p.pd
)
group by
emp_id, start_date
Which draws from the two queries above. The important part is the anti-join:
not exists (
select null
from pay_dates p
where p.emp_id = a.emp_id
and a.wd = p.pd
)
That identifies every work assignment where there is no corresponding record for that employee, for that day.
So in essence, the query takes the data ranges from both tables, comes up with every possible date combination and then does an anti-join to see where they don't match.
While it seems counterintuitive, to take a single record and blow it up into multiple records, two things to consider:
Dates are very bounded creatures -- even in 10 years worth of data that only constitutes 4,000 or so records, which isn't much to a database, even when multiplied by an employee database. Your time frame looks much less than that.
I've had very, VERY bad luck using joins other than =, for example between or >. It seems in the background it does cartesians and then filters the results. By comparison, exploding the ranges at least gives you some control over how much data explosion occurs.
For grins, I did it with your sample data above and came up with this, which actually looks accurate:
1 '2017-05-10' '2017-05-19'
2 '2017-05-08' '2018-03-20'
Let me know if any of that is unclear.
You can solve this with using the daterange type (because, what you basically want is the missing ranges in hourly_pay table.).
I used the following operators in it:
+ range union
- range subtraction
&& test for range intersection
#> test for range containment
With these and a simple left join, you can write a query to find out which ranges are missing in the hourly_pay table.
select wa.emp_id, lower(dr) start_date, upper(dr) - 1 end_date
from work_assignments wa
left join hourly_pay hp on wa.emp_id = hp.emp_id
and daterange(wa.start_date, wa.end_date, '[]') && daterange(hp.start_date, hp.end_date, '[]')
cross join lateral (select case
when hp is null then daterange(wa.start_date, wa.end_date, '[]')
else daterange(wa.start_date, wa.end_date, '[]')
+ daterange(hp.start_date, hp.end_date, '[]')
- daterange(hp.start_date, hp.end_date, '[]')
end dr) dr
where not exists (select 1
from hourly_pay p
where p.emp_id = wa.emp_id
and daterange(p.start_date, p.end_date, '[]') #> dr)
-- emp_id | start_date | end_date
----------+------------+-------------
-- 1 | 2017-05-01 | 2017-05-19
-- 2 | 2017-05-08 | (null)
http://sqlfiddle.com/#!17/4bac0/14
Maybe I am a little caught up by the wording, but would this not suffice? This would return any emp_id where there is a record for which the hourly start date is after a work assignment start date
select distinct wc.emp_id from work_contracts wc
left join hourly_pay hr USING(emp_id)
where hr.start_date > wc.start_date
select distinct p.emp_id <br>
from hourly_pay p <br>
join work_assignments w on p.emp_id = w.emp_id <br>
where p.start_date < w.start_date <br>
Based on the stated requirement in the original question: find the records where the hourly_pay start_date that are later than the work assignments start_date. Again, given the data here, the query should return emp_id 1 (because work_assignments.start_date has May-10-2017, while the earliest hourly_pay.start_date is on May-20-2017)
This means to me that they only want the employee id number.
I would use not exists/exists:
select wa.empid
from work_assignments wa
where not exists (select 1 from hourly_pay hp where wa.emp_id = hp.emp_id);
and for the second:
select wa.*
from work_assignments wa
where not exists (select 1
from hourly_pay hp
where wa.emp_id = hp.emp_id and ep.start_date <= wp.start_date
);
The question is very particular on (2). However, I would expect that you would want hourly pay for the entire period of the assignment, not just the start date. If that is the case, then the OP should ask a new qustion.
Second query is very simple,
Try below query
select distinct h.emp_id
from work_assignments w inner join hourly_pay h
on
w.emp_id = h.emp_id
and h.start_date > w.start_date;
Looking at your data, I can make following assumptions:
1) There can be max one record for an employee that has end_date as null this condition applied to both tables.
2) Multiple records dates for same employee don't overlap When employee has multiple records (like Emp 1) , he/she can't have dates like [jan 1 - feb 1] and next record as [jan 15-feb 20] or [jan 15 - null] (they must be for non overlapping periods).
With these in mind, below query should work for you.
SELECT hourly_pay.*
FROM work_assignments
INNER JOIN hourly_pay USING(emp_id)
WHERE hourly_pay.start_date > work_assignments.start_date
AND ( hourly_pay.start_date < work_assignments.end_date
OR (work_assignments.end_date is null
AND hourly_pay.end_date is null) );
Explanation: The query joins both tables on emp_id then filters records that
1) Have start_date in hourly_pay > start_date in work_assignments
-AND-
2) Have start_date in hourly_pay < end_date in work_assignments (This is needed, so we can avoid comparing un-related time period records from both tables
-OR-
End dates of both table records are null, using assumption 1 (stated above)
there can be max one record for an employee that has end_date as null.
Based on your data, this query should return both records of EMP 1 in hourly_pay as start_date there is > start_date in work_assignments.
If you just need list of EMP IDs you can just select that column SELECT DISTINCT hourly_pay.emp_id ...(rest of the query)
http://sqlfiddle.com/#!17/f4595/1
Records missing in hourly_pay table;
Instead of using left join and then filtering null valued records, I suggest you to use not exists, It will work way faster.
SELECT w.emp_id, 'missing in the hourly_pay table' FROM work_assignments w
WHERE NOT exists (SELECT 1 FROM hourly_pay h WHERE h.emp_id = w.emp_id)
Records hourly_pay start_date is later than the work assignment start_date;
SELECT w.emp_id FROM work_assignments w
WHERE
NOT exist (
SELECT 1 FROM hourly_pay hp
WHERE
hp.start_date < w.start_date AND w.emp_id = hp.emp_id )
Second query actually includes the results from first query, so you can merge them like below:
SELECT
w.emp_id,
(CASE WHEN ( EXISTS
(SELECT 1 FROM hourly_pay h
WHERE
h.emp_id = w.emp_id ) )
THEN
'hourly_pay start_date is later'
ELSE
'missing in the hourly_pay table'
END)
FROM
work_assignments w
WHERE
NOT EXISTS (
SELECT
1
FROM
hourly_pay hp
WHERE
hp.start_date < w.start_date
AND w.emp_id = hp.emp_id
)
this will do the job nicely.
SELECT DISTINCT emp_id
FROM work_assingment
JOIN hourly_pay hr USING(emp_id)
WHERE hr.start_date < work_assingment.start_date;

SQL Query to return data for the first of every month

I want to find the number of staff in a department at the start of each month, for the last 12 months.
I can get the desired output using 12 separate queries and UNION ALL similar to below:
SELECT
o.DEP_ID
,COUNT(o.STAFF_ID) STAFF_COUNT
,TRUNC(SYSDATE,'MON') EFFECTIVE_DATE
FROM
OCCUPANCIES o
WHERE
o.START_DATE <= TRUNC(SYSDATE,'MON')
AND o.END_DATE >= TRUNC(SYSDATE,'MON')
GROUP BY
o.DEP_ID
,TRUNC(SYSDATE,'MON')
UNION ALL
SELECT
o.DEP_ID
,COUNT(o.STAFF_ID) STAFF_COUNT
,ADD_MONTHS(TRUNC(SYSDATE,'MON'),-1) EFFECTIVE_DATE
FROM
OCCUPANCIES o
WHERE
o.START_DATE <= ADD_MONTHS(TRUNC(SYSDATE,'MON'),-1)
AND o.END_DATE >= ADD_MONTHS(TRUNC(SYSDATE,'MON'),-1)
GROUP BY
o.DEP_ID
,ADD_MONTHS(TRUNC(SYSDATE,'MON'),-1)
This gives me output similar to the following:
Unfortunately my real query is very long, and editing it is becoming unwieldy to say the least because I am making the same changes in 12 places each time.
Is there a way of doing this in a single SELECT statement?
EDIT: I have uploaded an example to SQLFiddle
You can generate a list of effective dates and use it in your query
SELECT
o.DEP_ID
,COUNT(o.STAFF_ID) STAFF_COUNT
,dt.EFFECTIVE_DATE
FROM
OCCUPANCIES o,
(SELECT ADD_MONTHS(TRUNC(SYSDATE,'MON'), 1-LEVEL) EFFECTIVE_DATE
FROM dual
CONNECT BY LEVEL <=12) dt
WHERE
dt.EFFECTIVE_DATE BETWEEN o.START_DATE AND o.END_DATE
GROUP BY
o.DEP_ID
,dt.EFFECTIVE_DATE

Find employee tenure for a company

I have written the following query to get the employees tenure yearwise.
Ie. grouped by "less than 1 year", "1-2 years", "2-3 years" and "greater than 3 years".
To get this, I compare with employee staffed end_date.
But I am not able to get the correct result when comparing with staffed end_date.
I have pasted the complete code below, but the count I am getting is not correct.
Some employee who worked for more than 2 years is falling under <1 year column.
DECLARE #Project_Id Varchar(10)='ITS-004275';
With Cte_Dates(Period,End_date,Start_date,Project_Id)
As
(
SELECT '<1 Year' AS Period, GETDATE() AS End_Date,DATEADD(YY,-1,GETDATE()) AS Start_date,#Project_Id AS Project_Id
UNION
SELECT '1-2 Years', DATEADD(YY,-1,GETDATE()),DATEADD(YY,-2,GETDATE()),#Project_Id
UNION
SELECT '2-3 Years', DATEADD(YY,-2,GETDATE()),DATEADD(YY,-3,GETDATE()),#Project_Id
UNION
SELECT '>3 Years', DATEADD(YY,-3,GETDATE()),'',#Project_Id
),
--select * from Cte_Dates
--ORDER BY Start_date DESC
Cte_Staffing(PROJECT_ID,EMP_ID,END_DATE) AS
(
SELECT FK_Project_ID,EMP_ID,MAX(End_Date)AS END_DATE FROM DP_Project_Staffing
WHERE FK_Project_ID=#Project_Id
GROUP BY FK_Project_ID,Emp_ID
)
SELECT D.PROJECT_ID,D.Start_date,D.End_date,COUNT(S.EMP_ID) AS Count,D.Period
FROM Cte_Staffing S
RIGHT JOIN Cte_Dates D
ON D.Project_Id=S.PROJECT_ID
AND S.END_DATE<D.End_date AND S.END_DATE>D.Start_date
GROUP BY D.PROJECT_ID,D.Start_date,D.End_date,D.Period
i think this will solve the problem
as you can see, you should use is like this:
DATEADD(year, -1, GETDATE())
you should also get the GETDATE() to a parameter
I find your query logic a little bit messy. Why don't you just compute the total period for every employee and use CASE clause? I can help you with code if you'll give me DP_Project_Staffing table structure. Do you have begin_date field in it?
You are taking the MAX(End_date) of the CTE staffing table. In that case, when an employee has several entries, only the most recent will apply. You want to use MIN instead.
Like this:
Cte_Staffing(PROJECT_ID,EMP_ID,END_DATE) AS
(
SELECT FK_Project_ID, EMP_ID, MIN(End_Date)AS END_DATE
FROM DP_Project_Staffing
...
Re-reading your question, you probably don't want the staffing end_date for tenure calculation; you'd want to use the start_date. (Or whatever the column is called in DP_Project_Staffing)
I would also change the WHERE/JOIN clause to be inclusive on one of the sides, so you have either
AND S.END_DATE <= D.End_date AND S.END_DATE > D.Start_date
or
AND S.END_DATE < D.End_date AND S.END_DATE >= D.Start_date
Since you are using miliseconds in the date-comparison it won't make any difference in this case. However, should you change the granularity to be only the date, which would make more sense, you would lose all records where the employee started exactly 1 year, 2 years, etc. ago.
SELECT FK_Project_ID,E.Emp_ID,MIN(Start_Date) AS Emp_Start_Date ,MAX(End_Date) AS Emp_End_Date,
E.Competency,E.First_Name+' '+E.Last_Name+' ('+E.Emp_Id+')' as Name,'Period'=
CASE
WHEN DATEDIFF(MONTH,MIN(Start_Date),MAX(End_Date))<=12 THEN '<1 Year'
WHEN DATEDIFF(MONTH,MIN(Start_Date),MAX(End_Date))>12 AND DATEDIFF(MONTH,MIN(Start_Date),MAX(End_Date))<=24 THEN '1-2 Years'
WHEN DATEDIFF(MONTH,MIN(Start_Date),MAX(End_Date))>24 AND DATEDIFF(MONTH,MIN(Start_Date),MAX(End_Date))<=36 THEN '2-3 Years'
WHEN DATEDIFF(MONTH,MIN(Start_Date),MAX(End_Date))>36 THEN '>3 Years'
ELSE 'NA'
END
FROM DP_Project_Staffing PS
LEFT OUTER JOIN DP_Ext_Emp_Master E
ON E.Emp_Id=PS.Emp_ID
WHERE FK_Project_ID=#PROJ_ID
GROUP BY FK_Project_ID,E.Emp_ID,E.Competency,First_Name,Last_Name

How to generate list of all dates between sysdate-30 and sysdate+30?

Purpose & What I've Got So Far
I am attempting to create a view which checks for missing labor transactions. The view will be fed to a Crystal report.
In this case, the view should take all dates between sysdate+30 and sysdate -30, and then should left outer join all labor records by active employees for each of those dates. It then gives a count of the number of labor transactions for each employee for each date.
This gets passed to the Crystal Report, which will filter based on a specific date range (within the +/- 30 range by the view). From there, the count of all days will summed up per employee in Crystal, and employees will show up which have zero transactions.
The Problem
Without spitting out a list of every date, initially, I'm using labor transaction for each date, but some have no counts for any date. These folks show null transaction dates with zero hours. This indicates they have no charges for the entire period, which makes sense.
However, when Crystal does a filter on that data and selects a range, I believe it leaves out these null values, thus not allowing me to show the full range of folks who don't have time submitted.
The Question
Is there a way to do the equivalent of "select every date between (sysdate+30) and (sysdate-30)" in a view, so that I can use it to compare all the time against?
The SQL (for reference)
SELECT QUERY.LABORRECLABORCODE
, QUERY.LABORRECEMPLOYEENUM
, QUERY.PERSONRECDISPLAYNAME
, QUERY.TRANSSTARTDATE
, COUNT(TRANSROWSTAMP) AS ROWCOUNT
FROM (SELECT *
FROM (SELECT LABOR.LABORCODE AS LABORRECLABORCODE
, LABOR.LA20 AS LABORRECEMPLOYEENUM
, PERSON.DISPLAYNAME AS PERSONRECDISPLAYNAME
FROM LABOR
LEFT OUTER JOIN PERSON
ON ( LABOR.LABORCODE = PERSON.PERSONID )
WHERE LABOR.STATUS = 'ACTIVE'
AND LABOR.LA20 IS NOT NULL
AND PERSON.DISPLAYNAME IS NOT NULL
AND LOWER(PERSON.DISPLAYNAME) NOT LIKE '%kimball%'
AND LOWER(PERSON.DISPLAYNAME) NOT LIKE '%electrico%'
AND LOWER(PERSON.DISPLAYNAME) NOT LIKE '%misc labor cost adj%'
AND LOWER(PERSON.DISPLAYNAME) NOT LIKE '%brossoit%'
AND LOWER(PERSON.DISPLAYNAME) NOT LIKE '%brossiot%')PERSONINFO
LEFT OUTER JOIN (SELECT STARTDATE AS TRANSSTARTDATE
, LABORCODE AS TRANSLABORCODE
, ROWSTAMP AS TRANSROWSTAMP
FROM LABTRANS
WHERE STARTDATE BETWEEN ( SYSDATE - 30 ) AND ( SYSDATE + 30 ))LABTRANSLIMITED
ON ( PERSONINFO.LABORRECLABORCODE = LABTRANSLIMITED.TRANSLABORCODE ))QUERY
GROUP BY LABORRECLABORCODE
, TRANSSTARTDATE
, LABORRECEMPLOYEENUM
, PERSONRECDISPLAYNAME
ORDER BY LABORRECLABORCODE
, TRANSSTARTDATE
;
select trunc(sysdate)+31-level from dual connect by level <=61
This is a good method for generating any arbitrary list of values.
Or another method: pick a table with a lot of rows
select sysdate+30 - rownum from user_objects where rownum<61
In order to meet my requirements of being sysdate -30 and sysdate + 30 in a range, this seems be the most elegant way of doing things for now:
SELECT *
FROM (SELECT TRUNC(SYSDATE - ROWNUM) DT
FROM DUAL
CONNECT BY ROWNUM < 31
UNION
SELECT TRUNC(SYSDATE + ROWNUM) DT
FROM DUAL
CONNECT BY ROWNUM < 31)DATERANGE;
I used this answer from this SO Question and expanded upon that thinking, using a union to join the queries that went in separate directions.