Hierarchical query using oracle SQL - sql

We have following table to query data from,
EMPLOYEE_ABSENCE_TAB
emp_id
absence_id
from_date
to_date
absence_continuation
100
1
01/01/2022
03/01/2022
100
2
01/02/2022
05/02/2022
1
200
3
01/01/2022
07/01/2022
200
4
10/01/2022
14/01/2022
3
200
5
16/01/2022
20/01/2022
4
300
6
01/01/2022
14/01/2022
We need to connect data hierarchically based on the following logic.
Output should be emp_id, absence_days, from_date and to_date.
absence_days : number of absence days = sum(to_date - from_date)
from_date : first from_date where absence_continuation is NULL
to_date : last to_date connect by prior absence_id = absence_continuation
Expected Outcome
emp_id
absence_days
from_date
to_date
100
8
01/01/2022
05/02/2022
200
17
01/01/2022
20/01/2022
300
14
01/01/2022
14/01/2022
I tried hierarchical but could able to come up with working solution. How can this be achieved using hierarchical query in Oracle SQL?

You don't need to consider the hierarchy in the data (assuming that there are no overlaps); you just need to aggregate:
SELECT emp_id,
SUM(to_date - from_date + 1) AS absence_days,
MIN(from_date) AS from_date,
MAX(to_date) AS to_date
FROM EMPLOYEE_ABSENCE_TAB
GROUP BY emp_id
Which, for the sample data:
CREATE TABLE EMPLOYEE_ABSENCE_TAB
(emp_id, absence_id, from_date, to_date, absence_continuation) AS
SELECT 100, 1, DATE '2022-01-01', DATE '2022-01-03', NULL FROM DUAL UNION ALL
SELECT 100, 2, DATE '2022-02-01', DATE '2022-02-05', 1 FROM DUAL UNION ALL
SELECT 200, 3, DATE '2022-01-01', DATE '2022-01-07', NULL FROM DUAL UNION ALL
SELECT 200, 4, DATE '2022-01-10', DATE '2022-01-14', 3 FROM DUAL UNION ALL
SELECT 200, 5, DATE '2022-01-16', DATE '2022-01-20', 4 FROM DUAL UNION ALL
SELECT 300, 6, DATE '2022-01-01', DATE '2022-01-14', NULL FROM DUAL;
Outputs:
EMP_ID
ABSENCE_DAYS
FROM_DATE
TO_DATE
100
8
2022-01-01 00:00:00
2022-02-05 00:00:00
200
17
2022-01-01 00:00:00
2022-01-20 00:00:00
300
14
2022-01-01 00:00:00
2022-01-14 00:00:00
If you did want to consider the hierarchy and get the duration of each absence (and not the total absences per employee) then you can use:
SELECT emp_id,
absence_id,
SUM(to_date - from_date + 1) AS absence_days,
MIN(from_date) AS from_date,
MAX(to_date) AS to_date
FROM (
SELECT emp_id,
CONNECT_BY_ROOT absence_id AS absence_id,
to_date - from_date + 1 AS absence_days,
from_date,
to_date
FROM EMPLOYEE_ABSENCE_TAB
START WITH absence_continuation IS NULL
CONNECT BY PRIOR absence_id = absence_continuation
)
GROUP BY
emp_id,
absence_id
Which outputs the same (with an additional absence_id column for the id of the first part of the absence) for your sample data as you only have a single absence for each employee; if you had more absences then it would aggregate each absence separately.
db<>fiddle here

Related

Analytic function/logic to get min and max record date in Oracle

I have a requirement to fetch value based on eff_dt and end date. given below sample data.
Database : Oracle 11g
Example data:
id
val
eff_date
end_date
10
100
01-Jan-21
04-Jan-21
10
105
05-Jan-21
07-Jan-21
10
100
08-Jan-21
10-Jan-21
10
100
11-Jan-21
17-Jan-21
10
100
18-Jan-21
21-Jan-21
10
110
22-Jan-21
null
output:
id
val
eff_date
end_date
10
100
01-Jan-21
04-Jan-21
10
105
05-Jan-21
07-Jan-21
10
100
08-Jan-21
21-Jan-21
10
110
22-Jan-21
null
You can use the ROW_NUMBER analytic function and then aggregate:
SELECT id,
val,
MIN(eff_date) AS eff_date,
MAX(end_date) AS end_date
FROM (
SELECT t.*,
ROW_NUMBER() OVER (PARTITION BY id ORDER BY eff_date)
- ROW_NUMBER() OVER (PARTITION BY id, val ORDER BY eff_date) AS grp
FROM table_name t
)
GROUP BY id, val, grp
ORDER BY id, eff_date;
Which, for the sample data:
CREATE TABLE table_name (id, val, eff_date, end_date) AS
SELECT 10, 100, DATE '2021-01-01', DATE '2021-01-04' FROM DUAL UNION ALL
SELECT 10, 105, DATE '2021-01-05', DATE '2021-01-07' FROM DUAL UNION ALL
SELECT 10, 100, DATE '2021-01-08', DATE '2021-01-10' FROM DUAL UNION ALL
SELECT 10, 100, DATE '2021-01-11', DATE '2021-01-17' FROM DUAL UNION ALL
SELECT 10, 100, DATE '2021-01-18', DATE '2021-01-21' FROM DUAL UNION ALL
SELECT 10, 110, DATE '2021-01-22', null FROM DUAL;
Outputs:
ID
VAL
EFF_DATE
END_DATE
10
100
2021-01-01 00:00:00
2021-01-04 00:00:00
10
105
2021-01-05 00:00:00
2021-01-07 00:00:00
10
100
2021-01-08 00:00:00
2021-01-21 00:00:00
10
110
2021-01-22 00:00:00
null
From Oracle 12, you can use MATCH_RECOGNIZE to perform row-by-row processing:
SELECT *
FROM table_name t
MATCH_RECOGNIZE(
PARTITION BY id
ORDER BY eff_date
MEASURES
FIRST(val) AS val,
FIRST(eff_date) AS eff_date,
LAST(end_date) AS end_date
PATTERN (same_val+)
DEFINE same_val AS FIRST(val) = val
)
Which has the same output and is likely to be more efficient.
fiddle

I want to get the count of roll number for each month

ID
STUDENT_ID
STATUS_DATE
1002
434120010026
25-FEB-22
1000
434120010026
03-MAY-03
1001
434120010026
25-FEB-22
1020
434120020023
18-MAR-22
1021
434120020025
18-MAR-22
1022
434120020025
16-MAR-22
Tried this
select count(*),
trunc(status_date, 'mm')
from test_studentattendance
group by trunc(status_date, 'mm');
got count of roll number in each month not the roll numbers.
COUNT(*)
TRUNC(STATUS_DATE,'MM')
1
01-MAY-03
2
01-FEB-22
3
01-MAR-22
COUNT(*) is counting the number of rows in each group and is counting students that appear in the same month multiple times.
To get the number on roll each month, you need to COUNT the DISTINCT identifier for each student (which, I assume would be student_id):
SELECT COUNT(DISTINCT student_id) AS number_of_students,
TRUNC(status_date, 'mm') AS month
FROM test_studentattendance
GROUP BY TRUNC(status_date, 'mm');
Which, for your sample data:
CREATE TABLE test_studentattendance (ID, STUDENT_ID, STATUS_DATE) AS
SELECT 1002, 434120010026, DATE '2022-02-25' FROM DUAL UNION ALL
SELECT 1000, 434120010026, DATE '2003-05-03' FROM DUAL UNION ALL
SELECT 1001, 434120010026, DATE '2022-02-25' FROM DUAL UNION ALL
SELECT 1020, 434120020023, DATE '2022-03-18' FROM DUAL UNION ALL
SELECT 1021, 434120020025, DATE '2022-03-18' FROM DUAL UNION ALL
SELECT 1022, 434120020025, DATE '2022-03-16' FROM DUAL;
Outputs:
NUMBER_OF_STUDENTS
MONTH
1
2022-02-01 00:00:00
1
2003-05-01 00:00:00
2
2022-03-01 00:00:00
db<>fiddle here

Fetch record with max number in one column except if date in that column is > than today

I have a problem with fetching few exceptions from DB.
Example, table b:
sn
v_num
start_date
end_date
1
001
01-01-2019
31-12-2099
1
002
01-01-2021
31-01-2022
1
003
01-02-2022
31-12-2099
2
001
01-01-2022
31-12-2099
2
002
01-07-2022
31-07-2022
2
003
01-08-2022
31-12-2099
Expected output:
sn
v_num
start_date
end_date
1
003
01-02-2022
31-12-2099
2
001
01-01-2022
31-12-2099
Currently I'm here:
SELECT * FROM table a, table b
WHERE a.sn = b.sn
AND b.v_num = (SELECT max (v_num) FROM b WHERE a.sn = b.sn)
but obviously that is not good because of a few cases like this with sn = 2.
Conclusion, I need to get unique sn record where v_num is max (95% of them in DB) except in case if start_date of max v_num record is > today.
Filter using start_date <= TRUNC(SYSDATE) then use the ROW_NUMBER analytic function:
SELECT *
FROM (
SELECT a.*,
ROW_NUMBER() OVER (PARTITION BY sn ORDER BY v_num DESC) AS rn
FROM "TABLE" a
WHERE start_date <= TRUNC(SYSDATE)
)
WHERE rn = 1;
If the start_date has a time component then you can use start_date < TRUNC(SYSDATE) + INTERVAL '1' DAY to get all the values for today from 00:00:00 to 23:59:59.
If you can have ties for the maximum and want to return all the ties then you can use the RANK analytic function instead of ROW_NUMBER.
Which, for the sample data:
CREATE TABLE "TABLE" (sn, v_num, start_date, end_date) AS
SELECT 1, '001', DATE '2022-01-01', DATE '2099-12-31' FROM DUAL UNION ALL
SELECT 1, '002', DATE '2022-01-01', DATE '2022-01-31' FROM DUAL UNION ALL
SELECT 1, '003', DATE '2022-02-01', DATE '2099-12-31' FROM DUAL UNION ALL
SELECT 2, '001', DATE '2022-01-01', DATE '2099-12-31' FROM DUAL UNION ALL
SELECT 2, '002', DATE '2022-07-01', DATE '2022-07-31' FROM DUAL UNION ALL
SELECT 2, '003', DATE '2022-08-01', DATE '2099-12-31' FROM DUAL;
Outputs:
SN
V_NUM
START_DATE
END_DATE
RN
1
003
2022-02-01 00:00:00
2099-12-31 00:00:00
1
2
001
2022-01-01 00:00:00
2099-12-31 00:00:00
1
db<>fiddle here

How do I subtract multiple dates?

An image of the Discharge date and admission date in which i am supposed to subtract as (discharge date – admitted
date) +1 .
As you said - subtract and sum. Sample data in lines #1 - 5, query begins at line #6.
SQL> with test (who, admission_date, discharge_date) as
2 (select 'Leslie', date '2010-09-13', date '2010-09-25' from dual union all
3 select 'Leslie', date '2014-09-03', date '2014-09-21' from dual union all
4 select 'Leslie', date '2015-12-03', date '2015-12-14' from dual
5 )
6 select who, sum(discharge_date - admission_date + 1) total_days
7 from test
8 group by who;
WHO TOTAL_DAYS
------ ----------
Leslie 44
SQL>

Select dates older than time frame SQL

I am trying to find all records in a database with an admission date which is older than a certain time frame (in this case, all admission dates older than 4 days old).
I have:
select memberid, admitdate
from membertable
where admitdate < (sysdate-4)
As a result, I'm getting a lot of admission dates which match this, but I'm ALSO getting dates which are from only 2 days ago, so that doesn't match my code. What am I doing wrong?
If it helps, the admit dates have a format of mm/dd/yyyy.
Dates, including sysdate, have a time component. Even if all your admitdate values are at midnight that is still a time, and sysdate is only going to be at midnight if you run your query then.
select sysdate, sysdate-4, trunc(sysdate), trunc(sysdate)-4 from dual;
SYSDATE SYSDATE-4 TRUNC(SYSDATE) TRUNC(SYSDATE)-4
------------------- ------------------- ------------------- -------------------
2018-06-21 16:44:53 2018-06-17 16:44:53 2018-06-21 00:00:00 2018-06-17 00:00:00
If you filter your records on sysdate-4 then that will include any admitdate values up to, in this example, 2018-06-17 16:44:53; so presumably all the records for the 17th if they are actually all midnight.
with membertable (memberid, admitdate) as (
select 1, date '2018-06-15' from dual
union all select 2, date '2018-06-16' from dual
union all select 3, date '2018-06-17' from dual
union all select 4, date '2018-06-18' from dual
union all select 5, date '2018-06-19' from dual
union all select 6, date '2018-06-20' from dual
union all select 7, date '2018-06-21' from dual
)
select memberid, admitdate
from membertable
where admitdate < (sysdate-4);
MEMBERID ADMITDATE
---------- -------------------
1 2018-06-15 00:00:00
2 2018-06-16 00:00:00
3 2018-06-17 00:00:00
If you truncate the value you're comparing against then its time portion will also be treated as midnight, so you'll only match record up to - but not including - that point in time, 2018-06-17 00:00:00:
with membertable (memberid, admitdate) as (
select 1, date '2018-06-15' from dual
union all select 2, date '2018-06-16' from dual
union all select 3, date '2018-06-17' from dual
union all select 4, date '2018-06-18' from dual
union all select 5, date '2018-06-19' from dual
union all select 6, date '2018-06-20' from dual
union all select 7, date '2018-06-21' from dual
)
select memberid, admitdate
from membertable
where admitdate < trunc(sysdate)-4;
MEMBERID ADMITDATE
---------- -------------------
1 2018-06-15 00:00:00
2 2018-06-16 00:00:00
admitdate should be a date. You seem to be suggesting it is a string. You can try:
where to_date(admitdate, 'MM/DD/YYYY') < trunc(sysdate) - 4;
You can then fix the data in the table, so it is stored as a date.