Oracle SQL - max() with NULL values - sql

I have a table that has a series of time based events, each bound with a start and end date. For the most recent (current) event, the end date is NULL. Im trying to collapse the duplicative rows and only show the earliest start date and the latest end date. With the NULL being in the date field, that row is ignored. I can dummy up an end date value with NVL(), but that will cause the front end logic to search for and replace that value.
Is there anyway to get max() function to sort NULL as high?
CREATE TABLE CONG_MEMBER_TERM
(
CONG_MEMBER_TERM_ID NUMBER(10) NOT NULL,
CHAMBER_CD VARCHAR2(30 BYTE) NOT NULL,
CONG_MEMBER_ID NUMBER(10) NOT NULL,
STATE_CD CHAR(2 BYTE) NOT NULL,
DISTRICT NUMBER(10),
START_DT TIMESTAMP(6) WITH TIME ZONE,
END_DT TIMESTAMP(6) WITH TIME ZONE
)
This query works, but drops the row where end date is NULL.
select CONG_MEMBER_ID,
district,
min(start_dt),
max(end_dt)
from CONG_MEMBER_TERM
where CONG_MEMBER_ID = 1716
group by CONG_MEMBER_ID, district;
This query fixes that, but now I have a "dummy" end date value(9/9/9999). Something I would rather not have to code around.
select CONG_MEMBER_ID,
district,
min(start_dt),
max(nvl(end_dt, to_date('9/9/9999', 'mm/dd/yyyy')))
from CONG_MEMBER_TERM
where CONG_MEMBER_ID = 1716
group by CONG_MEMBER_ID, district;
Thanks.

max(end_dt) keep (dense_rank first order by end_dt desc nulls first)
upd:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE t
(val int, s date, e date)
;
INSERT ALL
INTO t (val, s, e)
VALUES (1, sysdate-3, sysdate-2)
INTO t (val, s, e)
VALUES (1, sysdate-2, sysdate-1)
INTO t (val, s, e)
VALUES (1, sysdate-1, null)
INTO t (val, s, e)
VALUES (2, sysdate-1, sysdate-.5)
INTO t (val, s, e)
VALUES (2, sysdate-.5, sysdate-.25)
SELECT * FROM dual
;
Query 1:
select val, min(s), max(e) keep (dense_rank first order by e desc nulls first)
from t group by val
Results:
| VAL | MIN(S) | MAX(E)KEEP(DENSE_RANKFIRSTORDERBYEDESCNULLSFIRST) |
---------------------------------------------------------------------------------------------
| 1 | November, 13 2012 14:15:46+0000 | (null) |
| 2 | November, 15 2012 14:15:46+0000 | November, 16 2012 08:15:46+0000 |

select CONG_MEMBER_ID
, district
, min(start_dt)
, NULLIF(MAX(NVL(end_dt
,TO_DATE('9999-09-09','YYYY-MM-DD')
)
)
,TO_DATE('9999-09-09','YYYY-MM-DD')
)
from CONG_MEMBER_TERM
where CONG_MEMBER_ID = 1716
group by CONG_MEMBER_ID
, district
;

Related

SQLite query to find datetime difference between multiple rows

Here are my two tables' structures in SQLite
CREATE TABLE user
(
id integer PRIMARY KEY,
name TEXT
);
CREATE TABLE attendanceTable
(
id Integer,
mydate datetime,
startJob boolean
);
if startJob is 1 it implies that the employee is starting the job and if startJob is 0 it means employee is stopping the job.
attendanceTable is sorted by mydate column
I want output as worked hour by individual employees.
Input of query can be two different dates e.g. 2021-08-20 and 2021-08-22
From which I want to know "How much each person has worked?"
Output should be:
[id, name, userWorkedTime]
[1, Alice, 09:00]
[2, Bob, 07:00]
12:00 to 16:00 + 22:00 to 03:00 = 9 hours
13:00 to 17:00 + 12:00 to 15:00 = 7 hours
Input of query 2021-08-20 and 2021-08-21 - output should be:
[id, name, userWorkedTime]
[1, Alice, 09:00]
[2, Bob, 04:00]
12:00 to 16:00 + 22:00 to 03:00 = 9 hours
13:00 to 17:00 = 4 hours
It may possible that Alice starts her job at 11 PM and end her job at 3 AM on next day[So working hour would be 4 hours]
I believe that the following will accomplish the results you desire:-
WITH
/* The date selection parameters - change as necessary */
cte_selection(selection_start,selection_end) AS (SELECT '2020-08-20','2020-08-22'),
/* Extract data per shift - aka combine start and end
note that extract is 1 day befor and 1 day after actual selection criteria
as previous/subsequent days may be relevant
*/
cte_part1(userid,name,periodstart,periodend,duration) AS
(
SELECT
user.id,
name,
strftime('%s',mydate),
strftime('%s',
(
SELECT mydate
FROM attendancetable
WHERE id = at.id
AND NOT startjob
AND mydate > at.mydate
ORDER BY mydate ASC
LIMIT 1
)
) AS endjob,
(strftime('%s',
(
SELECT mydate
FROM attendancetable
WHERE id = at.id
AND NOT startjob
AND mydate > at.mydate
ORDER BY mydate ASC
LIMIT 1
)
) - strftime('%s',at.mydate)) AS duration
FROM attendancetable AS at
JOIN user ON at.id = user.id
WHERE startjob
AND mydate
BETWEEN date
(
(SELECT selection_start FROM cte_selection)
,'-1 day'
)
AND date
(
(SELECT selection_end FROM cte_selection)
,'+1 day'
)
),
/* split times if period crosses a day*/
cte_part2(userid,name,periodstart,startdate,periodend,enddate,duration,startday_duration,nextday_duration) AS
(
SELECT
userid,
name,
periodstart,
date(periodstart,'unixepoch') AS startdate,
periodend,
date(periodend,'unixepoch') AS enddate,
duration,
CASE
WHEN date(periodstart,'unixepoch') = date(periodend,'unixepoch') THEN duration
ELSE strftime('%s',date(periodstart,'unixepoch')||'24:00:00') - periodstart
END AS startday_duration,
CASE
WHEN date(periodstart,'unixepoch') = date(periodend,'unixepoch') THEN 0
ELSE periodend - strftime('%s',date(periodend,'unixepoch')||'00:00:00')
END AS nextday_duration
FROM cte_part1
),
/* generate new rows for following days */
cte_part3(userid,name,periodstart,startdate,periodend,enddate,duration,startday_duration,nextday_duration) AS
(
SELECT
userid,
name,
strftime('%s',date(periodend,'unixepoch')||'00:00:00'),
date(periodend,'unixepoch'),
periodend,
enddate,
nextday_duration,
nextday_duration,
0
FROM cte_part2
WHERE nextday_duration
),
/* combine both sets */
cte_part4 AS (SELECT * FROM cte_part2 UNION ALL SELECT * FROM cte_part3)
/* Group the final data */
SELECT *,time(sum(startday_duration),'unixepoch') AS time_worked
FROM cte_part4
WHERE startdate BETWEEN (SELECT selection_start FROM cte_selection) AND (SELECT selection_end FROM cte_selection) GROUP BY userid
;
e.g. :-
and :-
Note All results with the exception of the time_worked are arbitrary values from the underlying data. However, userid and name will be correct as they would be the same for each group. The other values will be a value from the group.
you can easily apply changes to the final query to include or exclude columns.
The full testing SQL being :-
DROP TABLE IF EXISTS user;
CREATE TABLE IF NOT EXISTS user (id integer PRIMARY KEY,name TEXT);
DROP TABLE IF EXISTS attendanceTable ;
CREATE TABLE attendanceTable(id Integer,mydate datetime,startJob boolean);
INSERT INTO user VALUES (1,'Alice'),(2,'Bob');
INSERT INTO attendanceTable VALUES
(1,'2020-08-20 12:00:00',1),
(2,'2020-08-20 13:00:00',1),
(1,'2020-08-20 16:00:00',0),
(2,'2020-08-20 17:00:00',0),
(1,'2020-08-20 22:00:00',1),
(1,'2020-08-21 03:00:00',0),
(2,'2020-08-22 12:00:00',1),
(2,'2020-08-22 15:00:00',0)
;
WITH
/* The date selection parameters - change as necessary */
cte_selection(selection_start,selection_end) AS (SELECT '2020-08-20','2020-08-22'),
/* Extract data per shift - aka combine start and end
note that extract is 1 day befor and 1 day after actual selection criteria
as previous/subsequent days may be relevant
*/
cte_part1(userid,name,periodstart,periodend,duration) AS
(
SELECT
user.id,
name,
strftime('%s',mydate),
strftime('%s',
(
SELECT mydate
FROM attendancetable
WHERE id = at.id
AND NOT startjob
AND mydate > at.mydate
ORDER BY mydate ASC
LIMIT 1
)
) AS endjob,
(strftime('%s',
(
SELECT mydate
FROM attendancetable
WHERE id = at.id
AND NOT startjob
AND mydate > at.mydate
ORDER BY mydate ASC
LIMIT 1
)
) - strftime('%s',at.mydate)) AS duration
FROM attendancetable AS at
JOIN user ON at.id = user.id
WHERE startjob
AND mydate
BETWEEN date
(
(SELECT selection_start FROM cte_selection)
,'-1 day'
)
AND date
(
(SELECT selection_end FROM cte_selection)
,'+1 day'
)
),
/* split times if period crosses a day*/
cte_part2(userid,name,periodstart,startdate,periodend,enddate,duration,startday_duration,nextday_duration) AS
(
SELECT
userid,
name,
periodstart,
date(periodstart,'unixepoch') AS startdate,
periodend,
date(periodend,'unixepoch') AS enddate,
duration,
CASE
WHEN date(periodstart,'unixepoch') = date(periodend,'unixepoch') THEN duration
ELSE strftime('%s',date(periodstart,'unixepoch')||'24:00:00') - periodstart
END AS startday_duration,
CASE
WHEN date(periodstart,'unixepoch') = date(periodend,'unixepoch') THEN 0
ELSE periodend - strftime('%s',date(periodend,'unixepoch')||'00:00:00')
END AS nextday_duration
FROM cte_part1
),
/* generate new rows for following days */
cte_part3(userid,name,periodstart,startdate,periodend,enddate,duration,startday_duration,nextday_duration) AS
(
SELECT
userid,
name,
strftime('%s',date(periodend,'unixepoch')||'00:00:00'),
date(periodend,'unixepoch'),
periodend,
enddate,
nextday_duration,
nextday_duration,
0
FROM cte_part2
WHERE nextday_duration
),
/* combine both sets */
cte_part4 AS (SELECT * FROM cte_part2 UNION ALL SELECT * FROM cte_part3)
/* Group the final data */
SELECT *,time(sum(startday_duration),'unixepoch') AS time_worked
FROM cte_part4
WHERE startdate BETWEEN (SELECT selection_start FROM cte_selection) AND (SELECT selection_end FROM cte_selection) GROUP BY userid
;
DROP TABLE IF EXISTS user;
DROP TABLE IF EXISTS attendanceTable ;

change row data to column with out using Pivot function in SQL Server 2012

I am trying to Pivot table data
Create table strategy(date date,[event] varchar(100),eType varchar(100))
insert into dbo.strategy (DATE, [event], eType) values
('1 Jan 2018' , 'ABZPD', 'dev'),
('1 Jan 2018', 'BFSPD', 'stage'),
('1 Jan 2018', 'BFSPD', 'pre-dep');
select * from strategy
Expected output:
DATE
ABZPD
BFSPD
BFSZH
BHXPD
BHXZH
BRSZH
BRUPQ
2018-01-01
dev
stage
NULL
NULL
NULL
NULL
NULL
2018-01-01
NULL
pre-dep
NULL
NULL
NULL
NULL
NULL
Below Code I tried
SELECT *
FROM
(
SELECT DATE
,[event]
,eType
FROM [strategy]
where datename(year, DATE) = 2018 or datename(year,DATE)=2019
) AS SourceTable
PIVOT
(
MAX(eType)
FOR event IN ([ABZPD],[BFSPD]
,[BFSZH]
,[BHXPD]
,[BHXZH]
,[BRSZH]
,[BRUPQ] )
) AS PivotTable;
drop table strategy
Below is the output I am getting :
DATE
ABZPD
BFSPD
BFSZH
BHXPD
BHXZH
BRSZH
BRUPQ
2018-01-01
dev
stage
NULL
NULL
NULL
NULL
NULL
You can achieve this by adding ROW_NUMBER to your query, but anyways - you pivot for MAX(etype), so the result you showed was correct... what exactly do you want to achieve with the query? The expected result looks a bit odd... however, following an example with Rownumber:
DECLARE #strategy TABLE(date date,[event] varchar(100),eType varchar(100))
insert into #strategy (DATE, [event], eType) values
('1 Jan 2018' , 'ABZPD', 'dev'),
('1 Jan 2018', 'BFSPD', 'stage'),
('1 Jan 2018', 'BFSPD', 'pre-dep');
WITH cteStrategy AS(
SELECT *, ROW_NUMBER() OVER (PARTITION BY date, event ORDER BY eType) rn
FROM #strategy
)
SELECT *
FROM
(
SELECT DATE
,[event]
,eType
,rn
FROM cteStrategy
where datename(year, DATE) = 2018 or datename(year,DATE)=2019
) AS SourceTable
PIVOT
(
MAX(eType)
FOR event IN ([ABZPD],[BFSPD]
,[BFSZH]
,[BHXPD]
,[BHXZH]
,[BRSZH]
,[BRUPQ] )
) AS PivotTable;
I would suggest writing the query as:
select date,
max(case when event = 'ABZPD' then eType end) as ABZPD,
max(case when event = 'BFSPD' then eType end) as BFSPD,
max(case when event = 'BFSZH' then eType end) as BFSZH,
max(case when event = 'BHXPD' then eType end) as BHXPD,
max(case when event = 'BHXZH' then eType end) as BHXZH,
max(case when event = 'BRUPQ' then eType end) as BRUPQ
from (select s.*,
row_number() over (partition by date order by date) as seqnum
from strategy s
where date >= '2018-01-01' and date < '2020-01-01'
) s
group by date, seqnum;
Note in particular the change to how the dates are filtered. datename() returns a string and you are comparing that string to a number. Don't mix data types! Date ranges are usually better for string comparisons anyway.
I have a preference for conditional aggregation over bespoke pivot functionality, because aggregation is much more flexible.

Get last date of modification in database by value

How it is possible to get - when was the last change (by date) - in
this table:
id
date
value
1
01.01.2021
0.0
1
02.01.2021
10.0
1
03.01.2021
15.0
1
04.01.2021
25.0
1
05.01.2021
25.0
1
06.01.2021
25.0
Of course I could use clause where and it will works, but i have a lot of rows and for some i don't now exactly day when this happend.
The resault should be:
id
date
value
1
04.01.2021
25.0
Try this one:
with mytable as (
select 1 as id, date '2021-01-01' as date, 0 as value union all
select 1, date '2021-01-02', 10 union all
select 1, date '2021-01-03', 15 union all
select 1, date '2021-01-04', 25 union all
select 1, date '2021-01-05', 25 union all
select 1, date '2021-01-06', 25
)
select id, array_agg(struct(date, value) order by last_change_date desc limit 1)[offset(0)].*
from (
select *, if(value != lag(value) over (partition by id order by date), date, null) as last_change_date
from mytable
)
group by id
in this scenario I would be using two field in my database "created_at and updated_at" with the type as "timestamp". You may simply fetch your records using OrderBY "updated_at" field.
see what this gives you:
SELECT MAX(date) OVER (PARTITION BY(value)) AS lastChange
FROM Table
WHERE id = 1
The following query and reproducible example on db-fiddle works. I've also included some additional test records.
CREATE TABLE my_data (
`id` INTEGER,
`date` date,
`value` INTEGER
);
INSERT INTO my_data
(`id`, `date`, `value`)
VALUES
('1', '01.01.2021', '0.0'),
('1', '02.01.2021', '10.0'),
('1', '03.01.2021', '15.0'),
('1', '04.01.2021', '25.0'),
('1', '05.01.2021', '25.0'),
('1', '06.01.2021', '25.0'),
('2', '05.01.2021', '25.0'),
('2', '06.01.2021', '23.0'),
('3', '03.01.2021', '15.0'),
('3', '04.01.2021', '25.0'),
('3', '05.01.2021', '17.0'),
('3', '06.01.2021', '17.0');
Query #1
SELECT
id,
date,
value
FROM (
SELECT
*,
row_number() over (partition by id order by date desc) as id_rank
FROM (
SELECT
id,
m1.date,
m1.value,
rank() over (partition by id,m1.value order by date asc) as id_value_rank,
CASE
WHEN (m1.date = (max(m1.date) over (partition by id,m1.value ))) THEN 1
ELSE 0
END AS is_max_date_for_group,
CASE
WHEN (m1.date = (max(m1.date) over (partition by id ))) THEN 1
ELSE 0
END AS is_max_date_for_id
from
my_data m1
) m2
WHERE (m2.is_max_date_for_group = m2.is_max_date_for_id and is_max_date_for_group <> 0 and id_value_rank=1) or (id_value_rank=1 and is_max_date_for_id=0)
) t
where t.id_rank=1
order by id, date, value;
id
date
value
1
04.01.2021
25
2
06.01.2021
23
3
05.01.2021
17
View on DB Fiddle
I actually find that the simplest method is to enumerate the rows by id/date and by id/date/value in descending order. These are the same for the last group . . . and the rest is aggregation:
select id, min(date), value
from (select t.*,
row_number() over (partition by id order by date desc) as seqnum,
row_number() over (partition by id, value order by date desc) as seqnum_2
from t
) t
where seqnum = seqnum_2
group by id;
If you use lag(), I would recommend using qualify for performance:
select t.*
from (select t.*
from t
qualify lag(value) over (partition by id order by date) <> value or
lag(value) over (partition by id order by date) is null
) t
qualify row_number() over (partition by id order by date desc) = 1;
Note: Both of these work if the value is the same for all rows. Other methods may not work in that situation.

Find the true start end dates for customers that have multiple accounts in SQL Server 2014

I have a checking account table that contains columns Cust_id (customer id), Open_Date (start date), and Closed_Date (end date). There is one row for each account. A customer can open multiple accounts at any given point. I would like to know how long the person has been a customer.
eg 1:
CREATE TABLE [Cust]
(
[Cust_id] [varchar](10) NULL,
[Open_Date] [date] NULL,
[Closed_Date] [date] NULL
)
insert into [Cust] values ('a123', '10/01/2019', '10/15/2019')
insert into [Cust] values ('a123', '10/12/2019', '11/01/2019')
Ideally I would like to insert this into a table with just one row, that says this person has been a customer from 10/01/2019 to 11/01/2019. (as he opened his second account before he closed his previous one.
Similarly eg 2:
insert into [Cust] values ('b245', '07/01/2019', '09/15/2019')
insert into [Cust] values ('b245', '10/12/2019', '12/01/2019')
I would like to see 2 rows in this case- one that shows he was a customer from 07/01 to 09/15 and then again from 10/12 to 12/01.
Can you point me to the best way to get this?
I would approach this as a gaps and islands problem. You want to group together groups of adjacents rows whose periods overlap.
Here is one way to solve it using lag() and a cumulative sum(). Everytime the open date is greater than the closed date of the previous record, a new group starts.
select
cust_id,
min(open_date) open_date,
max(closed_date) closed_date
from (
select
t.*,
sum(case when not open_date <= lag_closed_date then 1 else 0 end)
over(partition by cust_id order by open_date) grp
from (
select
t.*,
lag(closed_date) over (partition by cust_id order by open_date) lag_closed_date
from cust t
) t
) t
group by cust_id, grp
In this db fiddle with your sample data, the query produces:
cust_id | open_date | closed_date
:------ | :--------- | :----------
a123 | 2019-10-01 | 2019-11-01
b245 | 2019-07-01 | 2019-09-15
b245 | 2019-10-12 | 2019-12-01
I would solve this with recursion. While this is certainly very heavy, it should accommodate even the most complex account timings (assuming your data has such). However, if the sample data provided is as complex as you need to solve for, I highly recommend sticking with the solution provided above. It is much more concise and clear.
WITH x (cust_id, open_date, closed_date, lvl, grp) AS (
SELECT cust_id, open_date, closed_date, 1, 1
FROM (
SELECT cust_id
, open_date
, closed_date
, row_number()
OVER (PARTITION BY cust_id ORDER BY closed_date DESC, open_date) AS rn
FROM cust
) AS t
WHERE rn = 1
UNION ALL
SELECT cust_id, open_date, closed_date, lvl, grp
FROM (
SELECT c.cust_id
, c.open_date
, c.closed_date
, x.lvl + 1 AS lvl
, x.grp + CASE WHEN c.closed_date < x.open_date THEN 1 ELSE 0 END AS grp
, row_number() OVER (PARTITION BY c.cust_id ORDER BY c.closed_date DESC) AS rn
FROM cust c
JOIN x
ON x.cust_id = c.cust_id
AND c.open_date < x.open_date
) AS t
WHERE t.rn = 1
)
SELECT cust_id, min(open_date) AS first_open_date, max(closed_date) AS last_closed_date
FROM x
GROUP BY cust_id, grp
ORDER BY cust_id, grp
I would also add the caveat that I don't run on SQL Server, so there could be syntax differences that I didn't account for. Hopefully they are minor, if present.
you can try something like that:
select distinct
cust_id,
(select min(Open_Date)
from Cust as b
where b.cust_id = a.cust_id and
a.Open_Date <= b.Closed_Date and
a.Closed_Date >= b.Open_Date
),
(select max(Closed_Date)
from Cust as b
where b.cust_id = a.cust_id and
a.Open_Date <= b.Closed_Date and
a.Closed_Date >= b.Open_Date
)
from Cust as a
so, for every row - you're selecting minimal and maximal dates from all overlapping ranges, later distinct filters out duplicates

SQL Pulling results by year

Problem: I have dated salary information stored in a table. I need to display one result per year. For each year, I want to display the max dated record from the year prior. The problem is that some years don’t have data (salary didn’t change). I need these rows to contain the max record prior to that year (it may be from 2 years before or even 3).
My query right now would work if each row has data… but it doesn’t account for years where there is no data. How can I update this sql to pull the desired results below:
Example of data:
sch_sal_svc.beg_date -------sch_sal_svc.beg_date.per_plan_data
1/1/2007---100
6/1/2007---200
1/1/2008---300
1/1/2011---400
8/1/2011---500
9/1/2012---600
Current Results
1/1/2008---200
1/1/2011---300
1/1/2012---500
Desired Results
1/1/2008---200
1/1/2009---300
1/1/2010---300
1/1/2011---300
1/1/2012---500
SQL:
SELECT
years.control_id,
years.ssn,
ebe.plan_id,
to_number(to_char(years.sal_date,'yyyy')),
null as per_plan_salary,
null as per_vest_hours,
null as per_credsvc_hours,
LEAST(s.rate_1,cl.comp_genl),
null as salary_1,
null as per_comm,
null as per_overtime,
null as per_ncr,
null as salary_2
FROM
sch_sal_svc s
, (select distinct ssn, control_id, TRUNC(beg_date,'YEAR') as sal_date from sch_sal_svc where beg_date > to_date('12/31/1900', 'mm/dd/yyyy')) years
, employee_benefit_elig ebe, compliance_limits cl
WHERE
years.ssn = ebe.ssn
and years.control_id = ebe.control_id
and to_number(to_char(years.sal_date,'yyyy')) = cl.limit_year
and to_number(to_char(years.sal_date,'yyyy')) <= to_number(to_char(sysdate,'yyyy'))
and s.beg_date = (
select max(s2.beg_date) from sch_sal_svc s2
where s2.ssn = years.ssn and s2.control_id = years.control_id
and s2.beg_date <= years.sal_date
)
and s.ssn = years.ssn
and s.control_id = years.control_id
and ebe.benefit_id = 'DB'
and ebe.control_id = 'CLIENT'
and ebe.plan_id in ('100', '200')
CREATE TABLE sch_sal_svc
(
beg_date DATE
, per_plan_data NUMBER
);
INSERT INTO sch_sal_svc VALUES(TO_DATE('01/01/2007', 'DD/MM/YYYY'), 100);
INSERT INTO sch_sal_svc VALUES(TO_DATE('06/01/2007', 'DD/MM/YYYY'), 200);
INSERT INTO sch_sal_svc VALUES(TO_DATE('01/01/2008', 'DD/MM/YYYY'), 300);
INSERT INTO sch_sal_svc VALUES(TO_DATE('01/01/2011', 'DD/MM/YYYY'), 400);
INSERT INTO sch_sal_svc VALUES(TO_DATE('08/01/2011', 'DD/MM/YYYY'), 500);
INSERT INTO sch_sal_svc VALUES(TO_DATE('09/01/2012', 'DD/MM/YYYY'), 600);
SELECT MIN(beg_date) FROM sch_sal_svc;
-- 2007-01-01 00:00:00
SELECT d.r_level + NUMTOYMINTERVAL(1, 'YEAR') AS d_date
, NVL -- the salary must be updated at least once in three years
(
NVL
(
NVL
(
s.per_plan_data
, LAG(s.per_plan_data, 1) OVER (PARTITION BY 1 ORDER BY d.r_level)
)
, LAG(s.per_plan_data, 2) OVER (PARTITION BY 1 ORDER BY d.r_level)
)
, LAG(s.per_plan_data, 3) OVER (PARTITION BY 1 ORDER BY d.r_level)
) AS lag_per_plan_data
FROM
(
SELECT DATE'2006-01-01' + NUMTOYMINTERVAL(LEVEL, 'YEAR') AS r_level -- min beg_date minus 1
FROM DUAL
CONNECT BY
LEVEL < (SELECT TO_CHAR(MAX(beg_date), 'YYYY') - TO_CHAR(MIN(beg_date), 'YYYY') + 2 FROM sch_sal_svc)
) d
LEFT JOIN
(
SELECT beg_date
, per_plan_data
FROM sch_sal_svc
WHERE (beg_date) IN
(
SELECT MAX(beg_date)
FROM sch_sal_svc
GROUP BY
TRUNC(beg_date, 'YYYY')
)
) s
ON d.r_level = TRUNC(s.beg_date, 'YYYY')
;
/*
2008-01-01 00:00:00 200
2009-01-01 00:00:00 300
2010-01-01 00:00:00 300
2011-01-01 00:00:00 300
2012-01-01 00:00:00 500
2013-01-01 00:00:00 600
*/