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).
Related
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 2 years ago.
Improve this question
How to get all the dates between start_date and end_date, including start_date and end_date?
But not those dates that do not fall in the range.
eg: 08-01-2020 should not be in the output because it is not in any of the work_id dates range
so 08-01-2020 is one of the non-working dates
So I want all the dates except the non-working dates.
Sample table :
Sample output :
either this
or this
Create a list of dates of target range, add working and non-working flags to the list, and filter by the flag.
You can get the expected output with the following query.
-- 1st, generate a list of dates
with dates as (
select min(start_date) as dates, max(end_date) as max_date from dates_range
union all
select dateadd(day, 1, dates), max_date from dates
where dates < max_date
),
-- 2nd, generate a list of dates with working/non working flag
all_dates as (
select distinct
d1.dates,
case when d2.work_id is not null then 'true' else 'false' end as working
from dates as d1 left outer join dates_range as d2
on d1.dates between d2.start_date and d2.end_date
)
-- 3rd, filter either that working is true or false
select dates as Non_working_dates from all_dates where working = 'false';
db<>fiddle
First you should create a table valued function that generates a list of dates for each entry in your table
CREATE FUNCTION DatesByRange(#start_date date, #end_date date)
RETURNS #Dates TABLE
(
Dates date
)
AS
BEGIN
WITH cteDateRanges AS
(
SELECT #start_date AS workday
UNION ALL
SELECT DATEADD(DAY, 1, workday) FROM cteDateRanges
WHERE workday < #end_date
)
INSERT INTO #Dates
SELECT workday FROM cteDateRanges
RETURN;
END;
Then you can use this function with a cross apply
SELECT DISTINCT x.* FROM YourTable
CROSS APPLY(SELECT * FROM dbo.DatesByRange(t.start_date, t.end_date)) x
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)
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.
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.
I have a table which looks like this:
I'm trying to write a query which will return this:
I'm trying to merge records based on the effect_date, but only if the end_dates are within the effect_date and end_date range.
select employee
, effect_date
, max(end_date) end_date
, max(clinical_fte)
, max(admin_fte)
, max(mgmt_fte)
, max(other_fte)
from table
group by employee
, effect_date
As stated before by Chris Farmer, the second requirement that end date has to be between effect_date and end_date is silly, because it will always be true.
I've chosen max for all records you want to merge, because you haven't stated how you want to merge them. Feel free to adjust to your needs ;)
Try this SQL Query:
select *
from TABLE
where end_date between effect_date and end_date