Oracle - select statement to pivot and change how data is displayed - oracle

I currently have data in an Oracle tabel that looks like this:
Person_ID Fieldname Fieldnumber
123 2016 Salary 50000
123 2015 Salary 45000
123 2014 Salary 40000
123 2016 Vacation Days 5
456 2016 Salary 50000
456 2016 Vacation Days 5
789 2016 Salary 90000
789 2016 Vacation Days 5
I would like write a select statement to Pivot and change the display of the data so it looks like this:
Person_ID Fieldname 2016 2015 2014
123 Salary 55000 45000 40000
123 Vacation Days 5
456 Salary 50000
456 Vacation Days 5
789 Salary 90000
789 Salary 5
How can I do this so that if new field names are added, my SQL will automatically add new columns when I run it? Is there a way to do this with pivot or should I use union or another function that I am not familiar with?
The pivot I usually run looks like this but I don't think the concept will work in this scenario:
select *
from cust_matrix
unpivot
(
state_counts
for state_code in ("New York","Conn","New Jersey","Florida","Missouri")
)
order by "Puchase Frequency", state_code
Thank you

Don't be confused by the fact that there are two columns that are not part of the "pivot", in your case person_id and fieldname. Pivoting works exactly the same way.
Notes - your input table is obviously missing a column name (for year) - which better not be Year, a reserved word in Oracle. I used yr in my sample inputs. You also have a typo in the output (where did 55000 come from, for year 2016 and person_id = 123? The inputs had 50000).
And, you can have 2016 as a column name, but it would have to be double-quoted and it may cause unexpected problems later. Best to use standard column names - which, in particular, can't begin with a digit. I used y2016 etc.
with
inputs ( person_id, yr, fieldname, fieldnumber ) as (
select 123, 2016, 'Salary' , 50000 from dual union all
select 123, 2015, 'Salary' , 45000 from dual union all
select 123, 2014, 'Salary' , 40000 from dual union all
select 123, 2016, 'Vacation Days', 5 from dual union all
select 456, 2016, 'Salary' , 50000 from dual union all
select 456, 2016, 'Vacation Days', 5 from dual union all
select 789, 2016, 'Salary' , 90000 from dual union all
select 789, 2016, 'Vacation Days', 5 from dual
)
-- end of test data; actual solution (SQL query) begins below this line
select person_id, fieldname, y2016, y2015, y2014
from inputs
pivot ( max(fieldnumber) for yr in (2016 as y2016, 2015 as y2015, 2014 as y2014) )
order by person_id, fieldname
;
PERSON_ID FIELDNAME Y2016 Y2015 Y2014
--------- ------------- ----- ----- -----
123 Salary 50000 45000 40000
123 Vacation Days 5
456 Salary 50000
456 Vacation Days 5
789 Salary 90000
789 Vacation Days 5

Related

Ranking a table of records by year in Oracle sql

I have a table of records in a Oracle DBA which has the two columns
Employee Id and year
12343. 2016
12343. 2017
12343. 2018
12343. 2019
I want to rank this with 2019 having rank 3 ,2018 rank 2 and 2107 as rank 1.i only need three years of data for the employee.(2019,2018,2017)..all others should be exuded
I achieved this by using dense rank.
Where I need help is on a requirement if the max year of the employee is 2018 then the rank should be 3 and for 2017 should be 2 .for e.g
Emoyee I'd. Year. Rank
2000. 2017. 2
2000. 2018. 3
Similarly if another employee max year is 2017 he should have rank 3
So basically all latest year for that employeee should start with 3.
Any help to achieve this uisng Oracle sql will be handy.
Thanks in advance
Something like this, perhaps?
with
test_data (employee, yr) as (
select 1001, 2015 from dual union all
select 1001, 2016 from dual union all
select 1001, 2017 from dual union all
select 1001, 2018 from dual union all
select 1001, 2019 from dual union all
select 1003, 2018 from dual union all
select 1003, 2019 from dual union all
select 1003, 2020 from dual union all
select 1008, 2015 from dual union all
select 1008, 2016 from dual union all
select 1008, 2017 from dual
)
select employee, yr,
4 - rank() over (partition by employee order by yr desc) as rnk
from test_data
where yr in (2017, 2018, 2019)
order by employee, yr desc -- or whatever is needed
;
EMPLOYEE YR RNK
---------- ---------- ----------
1001 2019 3
1001 2018 2
1001 2017 1
1003 2019 3
1003 2018 2
1008 2017 3
Note that the with clause is not part of the solution; I included it there to generate test data. Remove it, and use your actual table and column names.

Identify Most Recent Record on Yearly Snapshot by partition of Arrange Id

I have Scenario Like below and want set Indicator based on Arrange Id, Login Date.. If User login website multiple time in Calendar Year then Most recent record need to set Y else N. Also I need to set Indicator like Bottom two rows as well.. ( Means 1121221 Accessed on last year recent 12/13/2017 need to set 'Y' and if user accessed in next immediate year 1/12/2018 then 'Y' )
enter image description here
Here's one option. What does it do?
the TEST CTE are some sample rows. Note ARRANGE_ID = 999, which has dates from 2017 and 2019 (which means that there are no consecutive years, so the date in 2019 should get the indicator 'N'. You didn't say, though, what would happen if there's yet another date in 2019; would both of them get 'N', or would the max login date still get a 'Y'?
the INTER CTE uses the MAX analytic function to find the maximum login date for the year and the LAG analytic function which returns the previous login date (so that I could check whether those years are consecutive or not)
the final query uses CASE to find whether certain row satisfies conditions to make the indicator equal to 'Y'
Here you go:
SQL> with test (arrange_id, login_date) as
2 (select 234, date '2017-02-18' from dual union all
3 select 234, date '2017-04-13' from dual union all
4 select 234, date '2017-11-14' from dual union all
5 select 234, date '2018-01-14' from dual union all
6 select 234, date '2018-09-11' from dual union all
7 select 234, date '2019-04-02' from dual union all
8 select 234, date '2019-05-18' from dual union all
9 select 112, date '2017-02-23' from dual union all
10 select 112, date '2017-12-13' from dual union all
11 select 112, date '2018-01-12' from dual union all
12 select 999, date '2017-01-01' from dual union all
13 select 999, date '2017-05-25' from dual union all
14 select 999, date '2019-01-01' from dual
15 ),
16 inter as
17 (select arrange_id,
18 login_date,
19 max(login_date) over
20 (partition by arrange_id, extract (year from login_date)) maxdate,
21 lag(login_date) over (partition by arrange_id order by login_date) prev_date
22 from test
23 )
24 select arrange_id,
25 login_date,
26 case when login_date = maxdate and
27 extract(year from login_date) - extract(year from prev_date) <= 1 then 'Y'
28 else 'N'
29 end indicator
30 from inter
31 order by arrange_id, login_date;
ARRANGE_ID LOGIN_DATE I
---------- ---------- -
112 02/23/2017 N
112 12/13/2017 Y -- Y because it is MAX in 2017
112 01/12/2018 Y -- Y because it is MAX in 2018 and 2018 follows 2017
234 02/18/2017 N
234 04/13/2017 N
234 11/14/2017 Y -- Y because it is MAX in 2017
234 01/14/2018 N
234 09/11/2018 Y -- Y because it is MAX in 2018 and 2018 follows 2017
234 04/02/2019 N
234 05/18/2019 Y -- Y because it is MAX in 2019 and 2019 follows 2018
999 01/01/2017 N
999 05/25/2017 Y -- Y because it is MAX in 2017
999 01/01/2019 N -- N because it is MAX in 2019, but 2019 doesn't follow 2017
13 rows selected.
SQL>

PL SQL Group By Issue

I have two tables T1 and T2.
T1 has an ID column that is generated as a sequence.
It also has two columns first name and Last name.
The table T2 is connected to table T1 via the ID column (referential).
T2 table has a salary column, that is revised every few years.
I want to get all the first name, last name, salary , and salary date if the salary has changed.
I am not able to get this information using the ID.
A second ID is generated for the same FN and LN pair if the employee comes up for review.
For Example :-
ID FN LN
1 John Doe
2 John Doe
ID SALARY DATE
1 $1 2015
2 $2 2018
I am trying something like this
SELECT T.FN ||' '|| T.LN AS NAME, COUNT(*) AS CT,
S.SALARY, S.DATE
SALARYTABLE S, EMP T
WHERE S.ID=T.ID
HAVING COUNT(*) > 1
GROUP BY (T.FN ||' '|| T.LN);
I have solved this by using a Java program. I have to store all the ID's and loop through all the records and check if the FN and LN matches and then extract the Date and Salary. This is inefficient and I want to do it within PL/SQL.
Please help. Thank you.
Well, your data model is kind of wrong; you shouldn't rely on distinguishing people on their names. What if yet another "John Doe" gets employed?
Anyway: would something like this do?
CTEs T1 and T2 simulate your tables. I added some more rows, just to make sure that the following query doesn't fail too obviously
INTER CTE joins those two tables and calculates employee's previous salary (using the LAG function)
the final query select rows (from INTER) whose current and previous salary are different
As you already have those tables, you'd use lines 16 onwards.
SQL> with
2 t1 (id, fn, ln) as
3 (select 1, 'John', 'Doe' from dual union all
4 select 2, 'John', 'Doe' from dual union all
5 select 3, 'John', 'Doe' from dual union all
6 select 5, 'Billy', 'Jean' from dual union all
7 select 6, 'Billy', 'Jean' from dual
8 ),
9 t2 (id, salary, c_date) as
10 (select 1, 1, 2015 from dual union all
11 select 2, 2, 2018 from dual union all
12 select 3, 2, 2019 from dual union all
13 select 5, 3, 2016 from dual union all
14 select 6, 3, 2017 from dual
15 ),
16 inter as
17 (select
18 t1.id, t1.fn, t1.ln,
19 t2.id, t2.salary, t2.c_date,
20 lag(t2.salary) over (partition by t1.fn, t1.ln
21 order by c_date) prev_salary
22 from t1 join t2 on t1.id = t2.id
23 )
24 select i.fn, i.ln, i.salary, i.c_date
25 from inter i
26 where i.salary <> nvl(i.prev_salary, i.salary)
27 order by i.ln, i.c_date;
FN LN SALARY C_DATE
----- ---- ---------- ----------
John Doe 2 2018
SQL>

Average Function with not null columns - Hive

I want to calculate an average for the first 3 years income which is not NULL for eg :
employee id 2016 2015 2014 2013 2012 2011 2010
1 100 NULL 200 50 10 50 50
average should be on 100 + 200 + 50 / 3
employee id 2016 2015 2014 2013 2012 2011 2010
2 NULL 100 NULL 50 NULL 25 100
average should be 100 + 50 + 25 / 3
Get one row per year with union all. Then rank the rows with row_number function so that non-null rows would be ranked first. Then get the average of first 3 rows.
select employee_id,avg(income)
from (select employee_id,yr,income
,row_number() over(partition by employee_id order by cast((income is not null) as int) desc,yr desc) as rnum
from (select employee_id,2016 as yr,`2016` as income from tbl
union all
select employee_id,2015 as yr,`2015` as income from tbl
union all
select employee_id,2014 as yr,`2014` as income from tbl
union all
select employee_id,2013 as yr,`2013` as income from tbl
union all
select employee_id,2012 as yr,`2012` as income from tbl
union all
select employee_id,2011 as yr,`2011` as income from tbl
union all
select employee_id,2010 as yr,`2010` as income from tbl
) t
) t
where rnum <= 3
group by employee_id
When 2 columns have values, the result would be (val1+val2)/2.
When only one column has a value, the result would be that column.
When all columns have a null value, null is returned.

Get Gap between time range

In WORK_TIME column in my database table (EMP_WORKS), i have records as below.
WORK_TIME
19:03:00
20:00:00
21:02:00
21:54:00
23:04:00
00:02:00
i want to create a database view using these data. for it i need to get Gap between these times as below.
WORK_TIME GAP
19:03:00 -
20:00:00 00:57:00 (Gap between 19:03:00 and 20:00:00)
21:02:00 01:02:00 (Gap between 20:00:00 and 21:02:00)
21:54:00 00:52:00 (Gap between 21:02:00 and 21:54:00)
23:04:00 01:10:00 (Gap between 21:54:00 and 23:04:00)
00:02:00 00:58:00 (Gap between 23:04:00 and 00:02:00)
How could i do this ?
This query will get you the differences in hours:
SELECT
work_time,
( work_time - LAG(work_time) OVER (ORDER BY work_time) ) * 24 AS gap
FROM emp_works
Example on SQL Fiddle returns this:
WORK_TIME GAP
November, 07 2012 19:03:00+0000 (null)
November, 07 2012 20:00:00+0000 0.95
November, 07 2012 21:02:00+0000 1.033333333333
November, 07 2012 21:54:00+0000 0.866666666667
November, 07 2012 23:04:00+0000 1.166666666667
November, 08 2012 00:02:00+0000 0.966666666667
First you will need to have a primary key in the table containing the DATE/TIME field.
I have set up this demo on SQL Fiddle .. Have a look
I have represented the gap as a factor of hours between the two times. You can manipulate the figure to represent minutes, or days, whatever.
SELECT
TO_CHAR(A.WORK_TIME,'HH24:MI:SS') WORK_FROM,
TO_CHAR(B.WORK_TIME,'HH24:MI:SS') WORK_TO,
ROUND(24*(B.WORK_TIME-A.WORK_TIME),2) GAP FROM
sample A,
SAMPLE B
WHERE A.ID+1 = B.ID(+)
If your primary key values have difference greater than 1 (gaps within the values of the primary key) then you will need to offset the value dynamically like this:
SELECT
TO_CHAR(A.WORK_TIME,'HH24:MI:SS') WORK_FROM,
TO_CHAR(B.WORK_TIME,'HH24:MI:SS') WORK_TO,
ROUND(24*(B.WORK_TIME-A.WORK_TIME),2) GAP FROM
sample A,
SAMPLE B
WHERE b.ID = (select min(C.ID) from sample c where c.id>A.ID)
According to your desired result, provided in the question, you want to see time interval. And also I suppose that the WORK_TIME column is of date datatype and there is a date part(otherwise there will be a negative result of subtraction (previous value of WORK_TIME from 00.02.00)).
SQL> create table Work_times(
2 work_time
3 ) as
4 (
5 select to_date('01.01.2012 19:03:00', 'dd.mm.yyyy hh24:mi:ss') from dual union all
6 select to_date('01.01.2012 20:00:00', 'dd.mm.yyyy hh24:mi:ss') from dual union all
7 select to_date('01.01.2012 21:02:00', 'dd.mm.yyyy hh24:mi:ss') from dual union all
8 select to_date('01.01.2012 21:54:00', 'dd.mm.yyyy hh24:mi:ss') from dual union all
9 select to_date('01.01.2012 23:04:00', 'dd.mm.yyyy hh24:mi:ss') from dual union all
10 select to_date('02.01.2012 00:02:00', 'dd.mm.yyyy hh24:mi:ss') from dual
11 )
12 /
Table created
SQL>
SQL> select to_char(t.work_time, 'hh24.mi.ss') work_time
2 , (t.work_time -
3 lag(t.work_time) over(order by WORK_TIME)) day(1) to second(0) Res
4 from work_times t
5 ;
WORK_TIME RES
--------- -------------------------------------------------------------------------------
19.03.00
20.00.00 +0 00:57:00
21.02.00 +0 01:02:00
21.54.00 +0 00:52:00
23.04.00 +0 01:10:00
00.02.00 +0 00:58:00
6 rows selected

Resources