Update the top row of each group - Oracle - oracle

The need is to update only the top row of each group of a table from the data of other table.
I need to update table A with details from table B
Table A
---------
ID Name Date PCNO
1 abc 1/1/12 123
2 def 1/1/12 234
3 fgh 1/2/12 222
4 asd 1/2/12 234
TABLE B
-----------
ID Name Date PCNO
1 adsf 1/1/12 4343
2 sdf 1/2/12 9347
For each top record of table A grouped by "Date" and ordered by PCNO desc, I would like to update the values from table B.
Do i use rank for this purpose.?

You can use rank or dense-rank (or even row-number) to get identify the 'top' row, though you may need t consider what to do if ties are possible in your real data:
select a.id, a.name, a.date_col, a.pcno,
dense_rank() over (partition by date_col order by pcno desc) as rnk
from table_a a;
ID NAME DATE_COL PCNO RNK
---------- ---- ---------- ---------- ----------
2 def 2012-01-01 234 1
1 abc 2012-01-01 123 2
4 asd 2012-01-02 234 1
3 fgh 2012-01-02 222 2
And you can join to table B to get the new values for the top-ranked:
select a.id, a.name, a.date_col, a.pcno,
dense_rank() over (partition by a.date_col order by a.pcno desc) as rnk,
case when dense_rank() over (partition by a.date_col order by a.pcno desc) = 1
then b.name else a.name end as new_name,
case when dense_rank() over (partition by a.date_col order by a.pcno desc) = 1
then b.pcno else a.pcno end as new_pcno
from table_a a
join table_b b on b.date_col = a.date_col;
ID NAME DATE_COL PCNO RNK NEW_ NEW_PCNO
---------- ---- ---------- ---------- ---------- ---- ----------
2 def 2012-01-01 234 1 adsf 4343
1 abc 2012-01-01 123 2 abc 123
4 asd 2012-01-02 234 1 sdf 9347
3 fgh 2012-01-02 222 2 fgh 222
and you can then use that in a merge statement:
merge into table_a target
using (
select a.id, a.name, a.date_col, a.pcno,
dense_rank() over (partition by a.date_col order by a.pcno desc) as rnk,
case when dense_rank() over (partition by a.date_col order by a.pcno desc) = 1
then b.name else a.name end as new_name,
case when dense_rank() over (partition by a.date_col order by a.pcno desc) = 1
then b.pcno else a.pcno end as new_pcno
from table_a a
join table_b b on b.date_col = a.date_col
) source
on (source.id = target.id)
when matched then update
set target.name = source.new_name, target.pcno = source.new_pcno
where source.rnk = 1;
or maybe
merge into table_a target
using (
select a.id, a.name, a.date_col, a.pcno,
case when dense_rank() over (partition by a.date_col order by a.pcno desc) = 1
then b.name else a.name end as new_name,
case when dense_rank() over (partition by a.date_col order by a.pcno desc) = 1
then b.pcno else a.pcno end as new_pcno
from table_a a
join table_b b on b.date_col = a.date_col
) source
on (source.id = target.id)
when matched then update
set target.name = source.new_name, target.pcno = source.new_pcno
where target.name != source.new_name or target.pcno != source.new_pcno;
either of which reports 2 rows merged, and then:
select * from table_a;
ID NAME DATE_COL PCNO
---------- ---- ---------- ----------
1 abc 2012-01-01 123
2 adsf 2012-01-01 4343
3 fgh 2012-01-02 222
4 sdf 2012-01-02 9347
You may need to adjust it if there isn't always going to be a match for a date, though the inner join ought to take care of that.
db<>fiddle demo

Related

Converting rows into Column in Oracle without any relation

I have a query which will fetch two rows only and I want to bring second row data into columns with different column name.
Below is the original query result.
The expected result is like
Expected result.
Please help how shd I proceed, not able to figure out with PIVOT.
Here's one option; see comments within code.
SQL> with
2 your_query (column1, column2, column3) as
3 -- this is what your current query returns
4 (select 1, 'ABC', 123 from dual union all
5 select 2, 'XYZ', 456 from dual
6 ),
7 temp as
8 -- distinguish 1st from 2nd row
9 (select y.*,
10 row_number() over (order by column1) rn
11 from your_query y
12 )
13 -- finally, cross join two rows and conditionally display columns.
14 -- MAX is here to avoid empty "cells"
15 select max(case when a.rn = 1 then a.column1 end) as col1,
16 max(case when a.rn = 1 then a.column2 end) as col2,
17 max(case when a.rn = 1 then a.column3 end) as col3,
18 --
19 max(case when b.rn = 2 then b.column1 end) as col4,
20 max(case when b.rn = 2 then b.column2 end) as col5,
21 max(case when b.rn = 2 then b.column3 end) as col6
22 from temp a cross join temp b;
COL1 COL COL3 COL4 COL COL6
---------- --- ---------- ---------- --- ----------
1 ABC 123 2 XYZ 456
SQL>

Requirement in Oracle PL/SQL to update labels

I have 2 tables - tab1 , tab2 with following data
tab1 data:
OID Label
1 MX1
1 MX2
1 MX3
2 MX4
2 MX5
tab2 data:
OID ID Label
1 5678
1 2347
1 9687
2 4567
2 3455
The join condition between these two tables is oid column.I need to create a process which will update Label column from tab1 to Label column of tab2.It doesn't matter which label gets assigned to which record of tab2 for a particular oid. The only check that needs to happen is that both the tables should have same number of records for an oid.The final outcome should be the following
tab2 data:
OID ID Label
1 5678 MX1
1 2347 MX2
1 9687 MX3
2 4567 MX4
2 3455 MX5
Again, it doesn't matter which label gets assigned to tab2 for a particular oid,but the same label cannot be repeated for a particular oid.What would be the best way to write a code for this requirement?
Here is a sql solution:
merge into tab2
using
(
select t2."id" as ide,t1."label" labela from
(select rownum n,"label","oid" from tab1 order by "oid")t1,
(select rownum n, a2.* from tab2 a2 order by "oid")t2
where t1.n=t2.n and
t1."oid"=t2."oid"
) tb4
on (tab2."id" = tb4.ide)
when matched then
update set tab2."label" = tb4.labela;
Result:
oid| id | label
-----------------
1 5678 mx1
1 2347 mx2
1 9687 mx3
2 4567 mx4
2 3455 mx5
Sample tables:
SQL> select * from tab1 order by oid, label;
OID LAB
---------- ---
1 mx1
1 mx2
1 mx3
2 mx4
2 mx5
SQL> select * from tab2 order by oid, id;
OID ID LAB
---------- ---------- ---
1 2347
1 5678
1 9687
2 3455
2 4567
SQL>
This is query that returns desired result:
SQL> with
2 t1 as (select oid, label, rowid rwid,
3 row_number() over (partition by oid order by label) rn
4 from tab1
5 ),
6 t2 as (select oid, id, rowid rwid,
7 row_number() over (partition by oid order by id) rn
8 from tab2
9 )
10 select b.oid, b.id, a.label
11 from t1 a join t2 b on a.oid = b.oid and a.rn = b.rn;
OID ID LAB
---------- ---------- ---
1 2347 mx1
1 5678 mx2
1 9687 mx3
2 3455 mx4
2 4567 mx5
SQL>
A few options I tried: correlated update won't work because of
ORA-01779: cannot modify a column which maps to a non key-preserved table
SQL> update (
2 with
3 t1 as (select oid, label, rowid rwid,
4 row_number() over (partition by oid order by label) rn
5 from tab1
6 ),
7 t2 as (select oid, id, rowid rwid, label,
8 row_number() over (partition by oid order by id) rn
9 from tab2
10 )
11 select b.oid, b.id, b.label b_label, a.label a_label
12 from t1 a join t2 b on a.oid = b.oid and a.rn = b.rn
13 )
14 set b_label = a_label;
set b_label = a_label
*
ERROR at line 14:
ORA-01779: cannot modify a column which maps to a non key-preserved table
SQL>
MERGE won't work because of
ORA-01732: data manipulation operation not legal on this view
SQL> merge into
2 (select oid, id, label, row_Number() over (partition by oid order by id ) rn from tab2) b
3 using (select oid, label, row_number() over (partition by oid order by label) rn from tab1) a
4 on (a.oid = b.oid and
5 a.rn = b.rn)
6 when matched then update set
7 b.label = a.label;
(select oid, id, label, row_Number() over (partition by oid order by id ) rn from tab2) b
*
ERROR at line 2:
ORA-01732: data manipulation operation not legal on this view
SQL>
Merge would accept a view (created with create view ...), but a view has to be updateable; this one can't be because it contains analytic function.
What's left is a PL/SQL procedure:
SQL> begin
2 for cur_r in (with
3 t1 as (select oid, label, rowid rwid,
4 row_number() over (partition by oid order by label) rn
5 from tab1
6 ),
7 t2 as (select oid, id, rowid rwid,
8 row_number() over (partition by oid order by id) rn
9 from tab2
10 )
11 select b.rwid, a.label
12 from t1 a join t2 b on a.oid = b.oid and a.rn = b.rn
13 )
14 loop
15 update tab2 b set
16 b.label = cur_r.label
17 where b.rowid = cur_r.rwid;
18 end loop;
19 end;
20 /
PL/SQL procedure successfully completed.
SQL> select * from tab2 order by oid, id;
OID ID LAB
---------- ---------- ---
1 2347 mx1
1 5678 mx2
1 9687 mx3
2 3455 mx4
2 4567 mx5
SQL>
Maybe someone has another idea; I'd like to see it & learn something new.

I want to delete duplication with condition from a table in PLSQL

I want to delete the dup lines using PLSQL. The sample of the table is below
Policy #
Price
Dealno for Loan #
Price of Loan
PersonID
123
10
Loan123
1,000
abc
123
10
Loan123
3,000
abc
456
10
Loan456
500
xyz
456
10
Loan456
500
null
As you can see, in the case of Policy #123, I try to get the line with the highest amount of Price of Loan. Which mean the Price of Loan for 3,000.
For Policy #456, I want to get the one without null value.
Is there a way for me to achieve that in PLSQL.
Thank you
This query identifies if a row is OK (rn = 1) or if is is a duplicated copy (rn > 1) based on your definition
select POLICY#, PRICE, LOAN#, PRICE_LOAN, PERSON_ID,
row_number() over (partition by POLICY# order by PRICE_LOAN desc, PERSON_ID nulls last) as rn
from tab
;
POLICY# PRICE LOAN# PRICE_LOAN PER RN
---------- ---------- -------- ---------- --- ----------
123 10 loan123 3000 abc 1
123 10 loan123 1000 abc 2
456 10 loan4563 500 xyz 1
456 10 loan4563 500 2
Note that you use row_number where you partition by on the unique key and order by so that you get first the row that should be taken.
So to get the duplicates only you use this query
with rn as (
select POLICY#, PRICE, LOAN#, PRICE_LOAN, PERSON_ID,
row_number() over (partition by POLICY# order by PRICE_LOAN desc, PERSON_ID nulls last) as rn
from tab
)
select * from rn where rn > 1;
POLICY# PRICE LOAN# PRICE_LOAN PER RN
---------- ---------- -------- ---------- --- ----------
123 10 loan123 1000 abc 2
456 10 loan4563 500 2
Based on this you write the DELETE statement (enclose in BEGIN ... END if you insist in PL/SQL)
delete from tab where rowid in
(
with rn as (
select POLICY#, PRICE, LOAN#, PRICE_LOAN, PERSON_ID,
row_number() over (partition by POLICY# order by PRICE_LOAN desc, PERSON_ID nulls last) as rn
from tab
)
select rowid from rn where rn > 1
);
You may check if the delete worked fine ....
select * from tab;
POLICY# PRICE LOAN# PRICE_LOAN PER
---------- ---------- -------- ---------- ---
123 10 loan123 3000 abc
456 10 loan4563 500 xyz
... and commit

Oracle sort values into column

I have a table like this:
time length name
00:01:00 2 a
00:11:22 2 a
01:01:00 45 a
00:23:00 3 b
and I want to retrieve data from the table in the form:
a b
time length time length
00:01:00 2 00:23:00 3
00:11:22 2
01:01:00 2
so it is a simple task of rearranging data, atm I am doing this in a bash script, but I wonder if there is an easy way to do it in Oracle?
You can use analytical function ROW_NUMBER and full outer join as follows:
WITH CTE1 AS
(SELECT T.*, ROW_NUMBER() OVER (ORDER BY LENGTH, TIME) AS RN FROM YOUR_TABLE T WHERE NAME = 'a'),
CTE2 AS
(SELECT T.*, ROW_NUMBER() OVER (ORDER BY LENGTH, TIME) AS RN FROM YOUR_TABLE T WHERE NAME = 'b')
SELECT A.TIME, A.LENGTH, B.TIME, B.LENGTH
FROM CTE1 A FULL OUTER JOIN CTE2 B
ON A.RN = B.RN
Note: You need to use proper order by to order the records as per your requirement. I have used LENGTH, TIME
You can use a multi-column pivot, by adding an extra column that links the related A and B values; presumably by time order, something like:
select time_col, length_col, name_col,
dense_rank() over (partition by name_col order by time_col) as rnk
from your_table;
TIME_COL LENGTH_COL N RNK
-------- ---------- - ----------
00:01:00 2 a 1
00:11:22 2 a 2
01:01:00 45 a 3
00:23:00 3 b 1
and then pivot based on that:
select *
from (
select time_col, length_col, name_col,
dense_rank() over (partition by name_col order by time_col) as rnk
from your_table
)
pivot (
max(time_col) as time_col, max(length_col) as length_col
for name_col in ('a' as a, 'b' as b)
);
RNK A_TIME_C A_LENGTH_COL B_TIME_C B_LENGTH_COL
---------- -------- ------------ -------- ------------
1 00:01:00 2 00:23:00 3
2 00:11:22 2
3 01:01:00 45
I've left the rnk value in the output; if you don't want that you can list the columns in the select list:
select a_time_col, a_length_col, b_time_col, b_length_col
from ...
Or you could do the same thing with conditional aggregation (which is what pivot uses under the hood anyway):
select
max(case when name_col = 'a' then time_col end) as time_col_a,
max(case when name_col = 'a' then length_col end) as length_col_a,
max(case when name_col = 'b' then time_col end) as time_col_b,
max(case when name_col = 'b' then length_col end) as length_col_b
from (
select time_col, length_col, name_col,
dense_rank() over (partition by name_col order by time_col) as rnk
from your_table
)
group by rnk
order by rnk;
TIME_COL LENGTH_COL_A TIME_COL LENGTH_COL_B
-------- ------------ -------- ------------
00:01:00 2 00:23:00 3
00:11:22 2
01:01:00 45
db<>fiddle

Rownum for a column with lag

I have this query
SELECT CASE WHEN LAG(emp_id) OVER( ORDER BY NULL ) = emp_id THEN '-'
ELSE emp_id END "Employee ID",
row_number() over (partition by emp_id order by emp_id) as "S/N",
family_mem_id "MemID",
CASE WHEN LAG(emp_id) OVER(ORDER BY NULL ) = emp_id THEN 0
ELSE (SUM(amount_paid) OVER(PARTITION BY emp_id)) END "Total amount"
FROM Employee
ORDER BY emp_id;
And it shows me result like this: Resultset
I want to add row number (first column SN) for Employee ID for in between rows I want to set it as null, for e.g. For Employee ID -> S/N 2-> F904 (SN should be null). How can I do that?
You can use the outer query to create the SN column as follows:
SELECT CASE WHEN "S/N" = 1 THEN '<1>' END AS SN, T.* FROM -- added this
(SELECT
CASE WHEN LAG(emp_id) OVER( ORDER BY NULL ) = emp_id THEN '-'
ELSE emp_id END "Employee ID",
row_number() over (partition by emp_id order by emp_id) as "S/N",
family_mem_id "MemID",
CASE WHEN LAG(emp_id) OVER(ORDER BY NULL ) = emp_id THEN 0
ELSE (SUM(amount_paid) OVER(PARTITION BY emp_id)) END "Total amount"
FROM Employee ) T
ORDER BY "Employee ID", "S/N"; -- Added this
Note that I have also changed the ORDER BY clause (which is moved to the outer query).
Cheers!!
Use dense_rank() to enumerate rows:
select case lag(emp_id) over( order by emp_id ) when emp_id then null
else '<'||dense_rank() over (order by emp_id)||'>'
end sn,
nullif(emp_id, lag(emp_id) over( order by emp_id )) empid,
row_number() over (partition by emp_id order by emp_id) rn,
family_mem_id memid,
case lag(emp_id) over(order by emp_id) when emp_id then null
else (sum(amount_paid) over(partition by emp_id))
end total
FROM Employee order by emp_id;
dbfiddle and sample output:
SN EMPID RN MEMID TOTAL
---- ----- ------ ----- ----------
<1> 101 1 F901 200
2 F904
<2> 102 1 F901 135
2 F901
<3> 103 1 F901 185
2 F901
3 F901

Resources