PL/SQL code to find leave detail of employee - sql

I Have three tables :-
per_Absences_table have start_date, end_date.
per_absence_type have Privelege, Casual etc
per_people_table have Employee number, Employee name
I Want to find out number of leaves applied by an employee between two dates.
Eg :- If i pass '1-JAN-2013' as start date and '20-JUN-2013' as end date then all the leaves applied by the employee
will come as output
I Have written code for it which is working when i pas any parameter. But suppose there is an employee who applies a leave
from 10-May-2013 to 20-May-2013 and i pass the starting date as 11-May-2013 i.e. the employees who have taken leave after 11-may-2013 should come.
i.e. the leave applied from 10-May-2013 to 20-May-2013 should appear. Also if i want to find out the employees who have applied leave till 19-May-2103 even
then this leave should appear. All the other cases have been taken care of.
per_people_table :-
EMP_NUM EMP_NAME
P101 XYZ
PER_ABSENCE_TABLE
EMP_NUM START_DATE END_DATE TYPE_ID
101 10-May-2013 15-May-2013 1
per_absence_type
type_id leave_type
1 casual
Now, if i pass the parameter as 11-May-2013 (p_start_date) even then this record should appear.
and if i pass the parameter as 19-May-2013 (before the actual end date) even then this record should appear.
i.e.
declare
l_r varchar2(10) :=NULL;
L_E VARCHAR2(10) := NULL;
p_person_id VARCHAR2(10) :='P101',
p_start_date VARCHAR2(10);
p_end_date VARCHAR2(10);
leave_type VARCHAR2(10);
BEGIN
Leave_detail_packa.Leave_detail_packA(NULL,NULL,'P101','11-MAY-2013',NULL,NULL);
END;
Leave_detail_packa body
procedure Leave_details( errbuff out varchar2,
retcode out varchar2,
p_person_id VARCHAR2,
p_start_date varchar2,
p_end_date varchar2,
leave_type varchar2
)
as
l_st_date :=to_date(trunc(fnd_conc_date.string_to_date(p_start_date)));
l_end_date :=to_date(trunc(fnd_conc_date.string_to_date(p_end_date)));
/****************Cursor for start date ****************************/
Cursor c_var_st_date
is select
to_char(paa.date_start,'DD-MON-RRRR') Start_date
from per_Absences_table paa,
per_absence_type paat
where ( nvl(l_st_date,paa.date_start) between paa.date_start and paa.date_end
or paa.date_start >= nvl(l_st_date,paa.date_start))
paa.type_id =paat.type_id
and paat.name = nvl(leave_type,paat.name);
/****************Cursor for end date ****************************/
Cursor c_var_nd_date
is select
to_char(paa.date_end,'DD-MON-RRRR') End_date
from per_Absences_table paa,
per_absence_type paat
where ( nvl(l_st_date,paa.date_end) between paa.date_start and paa.date_end
or paa.date_end <= nvl(l_st_date,paa.date_end))
paa.type_id =paat.type_id
and paat.name = nvl(leave_type,paat.name);
/********** Cursor to give all the leave details ***************/
Cursor c_var(l_start_date varchar2,
l_end_date1 varchar2)
is
select
emp_number employee_number,
emp_name employee_name,
leave_type Leave_type,
to_char(paa.date_end,'DD-MON-RRRR') End_date,
to_char(paa.date_start,'DD-MON-RRRR') Start_date,
sum(No_of_days) Leave_days
from
per_Absences_table paa,
per_absence_type paat,
per_people_table
where
paa.date_start>= ( nvl(l_start_date),to_char(paa.date_start,'DD-MON-RRRR'))
and paa.date_end <= ( nvl(l_end_date1),to_char(paa.date_end,'DD-MON-RRRR'))
AND EMP_NUM=P_PERSON_ID;
/**** Begin looping******************************************************/
for ( c_var_number in c_var_st_date)
loop
for ( c_var_number1 in c_var_nd_date)
loop
for ( c_var_number3 in c_var(c_var_st_date.start_end,c_var_nd_date.end_date)
loop
dbms.output_put.line( c_var_number3.end_date,c_var_number3.start_date);
end loop;
end loop;
end loop;

Firstly, there's no need to use PL/SQL to do this at all. It can be done in a single SQL statement.
Secondly, you haven't provided the DDL for your tables so I'm going to assume they're set us as follows (also in a SQL Fiddle with additional cases):
create table per_people (
emp_num number not null
, emp_name varchar2(100) not null
, constraint pk_per_people primary key (emp_num)
);
create table per_absence_type (
type_id number not null
, leave_type varchar2(100) not null
, constraint pk_absence_type primary key (type_id)
);
create table per_absence (
emp_num number not null
, start_date date not null
, end_date date not null
, type_id number not null
, constraint pk_per_abs primary key (emp_num, start_date)
, constraint fk_per_abs_emp foreign key (emp_num)
references per_people(emp_num)
, constraint fk_per_abs_typ foreign key (type_id)
references per_absence_type (type_id)
, constraint chk_per_abs_dates check (start_date <= end_date )
);
These are the minimal constraints I'd have on these tables. If you're missing one of them I'd recommend adding it.
This query will give you all the information from all tables for all absences:
select pp.emp_num
, pp.emp_name
, pa.start_date
, pa.end_date
, pat.leave_type
from per_people pp
join per_absence pa
on pp.emp_num = pa.emp_num
join per_absence_type pat
on pa.type_id = pat.type_id
You want to find all employees who will be on leave between two dates. For this you need to add a WHERE clause to the above query. Let's assume that employees are taking the following absences:
insert all
into per_absence values( 101, date '2013-05-10', date '2013-05-15', 1)
into per_absence values( 101, date '2013-09-10', date '2013-09-10', 1)
into per_absence values( 102, date '2013-05-15', date '2013-05-20', 1)
into per_absence values( 103, date '2013-05-05', date '2013-05-20', 1)
select * from dual;
If you pass a start date of 2013-05-15 and an end date of 2013-05-20, you would expect three rows to be returned. If you pass a start date of 2013-05-16 you would only expect two.
You have to look at how you want to construct your WHERE clause. Looking at the date you want where your start date is between an absence start and end date or your end date is between an absence start and end date. Don't forget that an absence can only be one day so you want to include the start and end dates in the comparison.
This changes the query to:
select pp.emp_num
, pp.emp_name
, pa.start_date
, pa.end_date
, pat.leave_type
from per_people pp
join per_absence pa
on pp.emp_num = pa.emp_num
join per_absence_type pat
on pa.type_id = pat.type_id
where ( :start_date between pa.start_date and pa.end_date
or :end_date between pa.start_date and pa.end_date
)
Now, you want to find the number of leaves for a specific employee between the given dates. As your tables are properly normalised you can just GROUP BY the EMP_NUM to get it for every employee.
Assuming you want this for EMP_NUM 1 (single employee so no need for the GROUP BY) between 2013-05-15 and 2013-05-20 the query would look like this:
select count(*) as no_leaves
from per_people pp
join per_absence pa
on pp.emp_num = pa.emp_num
join per_absence_type pat
on pa.type_id = pat.type_id
where emp_num = 101
and ( date '2013-05-15' between pa.start_date and pa.end_date
or date '2013-05-20' between pa.start_date and pa.end_date
)

If I were faced with this problem, I would create a calendar table, JOIN using BETWEEN paa.date_start AND paa.date_end; that way regardless of when their leave started or ended I know whether they were on leave for any given day. If from there you are interested in finding out how many leaves they took, defining consecutive days as an instance of a leave, then you'd just have to find gaps between leave days.

Related

Generate Rows Between Two Dates, Copying Down The Values in the Remaining Columns

I'm trying to write a script that will look at the issue date and termination date for each policy in a table. I want to be able to take those two dates, create a row for each year in between those two dates, and then fill in the values in the remaining columns.
I've been working with a recursive CTE approach in Redshift and I've got to the point where I can create the annual records. The part I'm stuck on is how to include the other columns in the table and fill each of the created rows with the same information as the row above.
For example, if I start with a record that looks something like
policy_number
issue_date
termination_date
issue_state
product
plan_code
001
1985-05-26
2005-03-02
CT
ROP
123456
I want to build a table that would look like this
policy_number
issue_date
termination_date
issue_state
product
plan_code
start_date
001
1985-05-26
2005-03-02
CT
ROP
123456
1985-05-26
001
1985-05-26
2005-03-02
CT
ROP
123456
1986-05-26
001
1985-05-26
2005-03-02
CT
ROP
123456
1987-05-26
...
...
...
...
...
...
...
001
1985-05-26
2005-03-02
CT
ROP
123456
2004-05-26
001
1985-05-26
2005-03-02
CT
ROP
123456
2005-03-02
Here's the code I've got so far:
WITH RECURSIVE start_dt AS
(
SELECT MIN(issue_date) AS s_dt -- step 1: grab start date
FROM myTable
WHERE policy_number = '001'
GROUP BY policy_number
),
end_dt AS
(
SELECT MAX(effective_date) AS e_dt -- step 2: grab the termination date
FROM myTable
WHERE policy_number = '001'
GROUP BY policy_number
),
dates (dt) AS
(
-- start at the start date
SELECT s_dt dt -- selectin start date from step 1
FROM start_dt
UNION ALL
-- recursive lines
SELECT dateadd(YEAR,1,dt)::DATE dt -- converted to date to avoid type mismatch -- adding annual records until the termination date
FROM dates
WHERE dt <= (SELECT e_dt FROM end_dt)
-- stop at the end date
)
SELECT *
FROM dates
which yields
dt
1985-05-26
1986-05-26
1987-05-26
...
How can I include the rest the columns in my table? I'm also open to using a cross join if that would be a better approach. I'm expecting this to generate around 10,000,000 rows, so any optimization would be much appreciated.
If I understand correctly you have a table with begin/end dates and you have a process for generating all the needed dates to span the min / max of these. You want to apply this list of dates to the starting table to get all rows replicated between begin and end.
You have a good start - the list of dates. The usual process is to join the dates with the table using inequality conditions. (ON dt >= begin and dt <= end)
You will need to deal with some edge condition around the unique dates for each input row. If you need to maintain these unique dates you will need to fudge the join condition. All doable.
==============================================================
Back from biz trip and can give more concrete guidance.
There's 2 ways to do this. The first is the CTE approach you are driving down but this will pass all the data through each loop of the CTE. This could be slow. This would look like (including data setup):
create table mytable (
policy_number varchar(8),
issue_date timestamp,
termination_date timestamp,
issue_state varchar(4),
product varchar(16),
plan_code int);
insert into mytable values
('001', '1985-05-26', '2005-03-02', 'CT', 'ROP', 123456),
('002', '1988-07-25', '2005-08-07', 'CT', 'ROP', 654321)
;
with recursive pdata(policy_number, issue_date, termination_date,
issue_state, product, plan_code, start_date,
yr) as (
select policy_number, issue_date, termination_date, issue_state,
product, plan_code, issue_date as start_date, 0 as yr
from mytable
union all
select policy_number, issue_date, termination_date, issue_state,
product, plan_code,
issue_date + yr * (interval '1 years') as start_date,
yr + 1 as yr
from pdata
where start_date < termination_date
)
select policy_number, issue_date, termination_date,
issue_state, product, plan_code,
case when start_date > termination_date
then termination_date
else start_date
end as start_date
from pdata
order by start_date, policy_number;
The other way to do this is to generate the length of years in the recursive CTE but apply the data expansion in a loop join. This has the benefit of not carrying all the data through the recursive calls but has the expense of the loop join. It should be faster with large amounts of data but you can decide which is right for you.
Since each input row has its own date I left things in year intervals as this is cleaner. This looks like:
create table mytable (
policy_number varchar(8),
issue_date timestamp,
termination_date timestamp,
issue_state varchar(4),
product varchar(16),
plan_code int);
insert into mytable values
('001', '1985-05-26', '2005-03-02', 'CT', 'ROP', 123456),
('002', '1988-07-25', '2005-08-07', 'CT', 'ROP', 654321)
;
with recursive nums(yr, maxnum) as (
select 0::int as yr,
date_part('year', max(termination_date)) -
date_part('year', min(issue_date)) as maxnum
from mytable
union all
select yr + 1 as yr, maxnum
from nums
where yr <= maxnum
)
select policy_number, issue_date, termination_date,
issue_state, product, plan_code,
case when issue_date + yr * interval '1 year' > termination_date
then termination_date
else issue_date + yr * interval '1 year'
end as start_date
from mytable p
left join nums n
on termination_date + interval '1 year'
> issue_date + yr * interval '1 year'
order by start_date, policy_number;

Interpret this SQL in to English [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 4 years ago.
Improve this question
I've been given the task of rewriting certain sections of this SQL but am having trouble interpreting it completely. In plain English could an SQL master explain what is happening from "appointments_2015 AS" to the end.
CREATE TABLE appointment (
emp_id integer NOT NULL,
jobtitle varchar(128) NOT NULL,
salary decimal(10,2) NOT NULL,
start_date date NOT NULL,
end_date date NULL
);
ALTER TABLE appointment ADD CONSTRAINT pkey_appointment PRIMARY KEY
(emp_id, jobtitle, start_date);
ALTER TABLE appointment ADD CONSTRAINT chk_appointment_period CHECK
(start_date <= end_date);
WITH current_employees AS (
SELECT DISTINCT emp_id
FROM appointment
WHERE end_date IS NULL
),
appointments_2015 AS (
SELECT a.emp_id, salary,
CASE WHEN start_date < ’2015-01-01’ THEN ’2015-01-01’ ELSE start_date END
AS start_date,
CASE WHEN end_date < ’2016-01-01’ THEN end_date ELSE ’2015-12-31’ END AS
end_date
FROM appointment a
JOIN current_employees ce ON a.emp_id = ce.emp_id
WHERE start_date < ’2016-01-01’ AND (end_date >= ’2015-01-01’ OR end_date
IS NULL)
)
SELECT
emp_id,
SUM( salary * (end_date - start_date + 1) / 365 ) AS total
FROM appointments_2015
GROUP BY emp_id
What that code is doing is essentially creating a temporary table with the alias appointments_2015. That temp table will have the results from the query inside the AS () section.
The SELECT statement that follows is pulling from that appointments_2015 temp table.
Docs can be found here: WITH queries Docs
The CASE WHEN is like an IF statement. CASE WHEN x THEN y ELSE z END is equivalent to IF x THEN y ELSE z
appointments_2015 AS ( more sql ) You're creating a "virtual table" from the queries inside of the parenthesis.
Inside that your selecting start_date and end_date with a condition: "if it's older than 2015-01-01 set it to 2015-01-01 else use the original value. Then your joining in another table called "current_employees" where start_date is before 2016-01-01 and end_date is greater than or equal to 2015-01-01, or doesn't have a value.
When the "virtual table" has been created you query it and select emp_id and a SUM of a salary
Two temporary tables are created: current_employees and appointments_2015. Syntax:
WITH current_employees AS ( .... ),
appointments_2015 AS ( .... )
The last SELECT statement generates the result dataset: the total amount of salary in year 2015 per employee who started on or prior to Dec 31, 2015 and is still working (current employee).

JOIN tables ON DATE = NUMBER?

I am trying to join two tables in Oracle SQL. One table has a DATE data type which represents a date(go figure) the other has an NUMBER data type which represents a month. I need to join the tables on the DATE's month and the NUMBER. I tried TO_CHAR() but it didn't work. Any suggestions?
Oracle's EXTRACT() function may do the trick ( https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions050.htm ). Suppose we have 2 tables, populated with test data, like so:
create table numbers_ (num_ number);
create table dates_ (date_ date);
begin
for i in 1 .. 12
loop
insert into numbers_ values (i);
end loop;
insert into dates_ values ('15-JUL-2017');
insert into dates_ values ('16-AUG-2017');
insert into dates_ values ('17-SEP-2017');
end;
/
We can use EXTRACT to get the "months" from the dates_ table:
select extract (month from date_) from dates_;
EXTRACT(MONTHFROMDATE_)
7
8
9
Use the "extracted" months for joining the tables:
select *
from
numbers_ N,
( select extract( month from date_ ) month from dates_ ) D
where N.num_ = D.month;
-- output
NUM_ MONTH
7 7
8 8
9 9
If you need more columns from the dates_ table, add them into the subquery (and to the main SELECT clause). Example:
select
N.num_
, D.date_
, D.month
from
numbers_ N,
( select
extract( month from date_ ) month
, date_
from dates_ ) D
where N.num_ = D.month;
(See also: dbfiddle)
Or - better (as #Wernfried Domscheit suggested):
select
N.num_
, D.date_
from
numbers_ N join dates_ D
on extract(month from D.date_) = N.num_ ;

SQL View to return Listing of Effective Products

Please assist with creating an SQL view.
On DB2 For i V7R2
Situation:
Departments at my company are allowed to sell a listing of products,
up until they are replace with a new product. On the day that the new product becomes effective the Department is allowed to sell both Products.
At the COB, the old product is no longer allowed to be sold, and needs to be returned.
Required:
SQL query to return the listing of "allowed" products for a specific date.
The query needs to return:
"Green-Ladder" and "Red-Ladder" `WHERE EFFDAT = CURRENT_DATE
Example Data Set:
drop table QTEMP/Product_EffectiveDate_TestTable;
create table QTEMP/Product_EffectiveDate_TestTable (
Dept varchar(50) not null,
EffDat date not null,
PrdCde varchar(50) not null);
insert into QTEMP/Product_EffectiveDate_TestTable
( Dept, EffDat, PrdCde)
values
('Department A', CURRENT_DATE + 10 DAY , 'Blue-Ladder'),
('Department A', CURRENT_DATE , 'Green-Ladder'),
('Department A', CURRENT_DATE - 10 DAY , 'Red-Ladder'),
('Department A', CURRENT_DATE - 20 DAY , 'Yellow-Ladder') ;
My answer for a single product per department is:
select *
from qtemp.Product_EffectiveDate_TestTable a
where effdat = (select max(effdat)
from qtemp.Product_EffectiveDate_TestTable
where effdat < current_date
and dept = a.dept)
or effdat = current_date
You can convert this to a view if you are only interested in products for the current date. However if you want to be able to query it for any given date, you will have to create a table function.
The view would look something like this:
create view Products_By_Department as
select *
from qtemp.Product_EffectiveDate_TestTable a
where effdat = (select max(effdat)
from qtemp.Product_EffectiveDate_TestTable
where effdat < current_date
and dept = a.dept)
or effdat = current_date;
The UTF could look like this:
create or replace function xxxxxx.UTF_ProductsByDepartment
(
p_date Date
)
returns table
(
Dept Varchar(50),
EffDat Date,
PrdCde Varchar(50),
)
language sql
reads sql data
no external action
not deterministic
disallow parallel
return
select dept, effdat, prdcde
from qtemp.Product_EffectiveDate_TestTable a
where effdat = (select max(effdat)
from qtemp.Product_EffectiveDate_TestTable
where effdat < p_date
and dept = a.dept)
or effdat = p_date;
You would use the UTF like this:
select * from table(xxxxxx.utf_ProductsByDepartment(date('2017-06-13'))) a
Note that you cannot put a function in QTEMP, so you will have to replace xxxxxx with an appropriate library, or you can leave it unqualified, and set the default schema some other way.
I would solve this by changing your data design if possible. It would be preferable to have a start and end date on each row. Reasons:
It makes for a much simpler query.
It's a clearer, easier to understand design.
It is more flexible, allowing future changes to your business requirements. "Hey, actually we need to still sell this old version of the product" is the kind of pernicious requirement that has a way of popping up later, and ideally you would be able to handle this without rewriting application code.
In the event that you can't change the data design, I would use a subquery to create the end date:
with start_end_dates as (
select Dept,
EffDat as start_date,
lead (EffDat) over (partition by Dept order by EffDat) as end_date,
ProdCd
from table
)
select * from start_end_dates where
current date between start_date and coalesce(end_date,'9999-12-31');
This assumes that the effective date refers to rows within a particular department. Alter the partition clause as necessary if that's not true.

PL/SQL Code to fetch headcount for each month in one quarter [duplicate]

I have one table, per_all_peopl_f, with following columns:
name person_id emp_flag effective_start_date effective_end_date DOJ
--------------------------------------------------------------------------------
ABC 123 Y 30-MAR-2011 30-MAR-2013 10-FEB-2011
ABC 123 Y 24-FEB-2011 27-FEB-2011 10-FEB-2011
DEF 345 N 10-APR-2012 30-DEC-4712 15-SEP-2011
There are many entries (1000+) with repeated data and different effective start dates.
I have to calculate the Workforce headcount. That is, the number of employees that exits the company quarterly.
The following columns have to be fetched:
Headcount in 2012 (1st quarter)
Headcount in 2013 (1st quarter)
difference between the two headcounts
% difference
The query I used to find the headcount quarterly is:
function1:
CREATE OR REPLACE FUNCTION function_name
(l_end_date ,l_start_date )
RETURN number;
IS
l_emp
BEGIN
select count(distinct papf.person_id)
into l_emp
from per_all_people_f papf
where papf.emp_flag ='Y'
and effective_start_date >=l_end_date
and effective_end_date <=l_start_date ;
return l_emp;
END function_name;
Main package:
create xx_pack_name body
is
cursor cur_var
is
select function_name('01-MAR-2012','31-MAY-2012') EMP_2012,
function_name('01-MAR-2013','31-MAY-2013') EMP_2013,
function_name('01-MAR-2012','31-MAY-2012')-function_name('01-MAR-2013','31-MAY-2013') Diff
from dual;
end xx_pack_name ;
Is this cost efficient?
Seems to be a good variant in case if there are index on at least effective_start_date and effective_end_date fields of per_all_people_f table.
Ideal variant for this query is
create index x_per_all_people_search on per_all_people_f(
effective_start_date,
effective_end_date,
person_id,
emp_flag
)
but it may be too expensive to maintain (disk cost, insertion speed).
Also, cursor in package body must contain subquery and reuse function call results:
cursor cur_var
is
select
EMP_2012,
EMP_2013,
(EMP_2013 - EMP_2012) Diff
from (
select
function_name('01-MAR-2012','31-MAY-2012') EMP_2012,
function_name('01-MAR-2013','31-MAY-2013') EMP_2013
from dual
);
Of course best solution is to minimize context switches and get all values from single SQL query. Also, you can supply parameters directly to cursor:
cursor cur_var(
start_1 date, end_1 date,
start_2 date, end_2 date
)
is
select
EMP_2012,
EMP_2013,
(EMP_2013 - EMP_2012) Diff
from (
select
(
select
count(distinct papf.person_id)
from
per_all_people_f papf
where
papf.emp_flag = 'Y'
and
effective_start_date >= trunc(start_1)
and
effective_end_date <= trunc(end_1)
) EMP_2012,
(
select
count(distinct papf.person_id)
from
per_all_people_f papf
where
papf.emp_flag = 'Y'
and
effective_start_date >= trunc(start_2)
and
effective_end_date <= trunc(end_2)
) EMP_2013
from dual
);
From my point of view function/cursor parameters is too generic, may be better to create a wrapper which takes as input parameters quarter number and two years to compare.
And last, if results planned to be used in PL/SQL (I suppose that because of returning a single row) don't use cursor at all, just return calculated values through output parameters. From another point of view if you need to get quarter data for full year in one cursor it may be more efficient to count all quarters and compare it in single query.