update changed column in a table - sql

I have two tables in Oracle
like
table 1:
id
c1
c2
c3
One
Two
Three
Four
table 2:
id
c1
c2
c3
One
Two
Three
Four
they have the same rows and data.
I want to write a procedure that
when I run, each row that change in table 1 update in table 2 and the main point is:
only columns that change update. because different columns may be change in a row each time.
I do not want to update whole columns in a row.

Assuming this is a non-bogus requirement :) the only way to track a change to a column is with a trigger.
create or replace trigger t1_upd before update on t1 for each row is
begin
if :old.c1 != :new.c1 then
update t2
set t2.c1 = :new.c1
where t2.id = :new.id;
end if;
if :old.c2 != :new.c2 then
update t2
set t2.c2 = :new.c2
where t2.id = :new.id;
end if;
-- and so on
end;
/
Untested code, may contain errors.
Notes
If you have lots of columns this could be an expensive process: you're turning a single update into multiple updates
It presumes the pre-existence of matching rows in the second table. You will need to handle that if you don't already (say with an insert trigger).

Related

Multiple Columns and different Join Conditions for Oracle update

I have 2 tables , 1 is location and the Other one is Look up table. I have to look into the look up table for the location values and if they are present mark them as 'Y' and 'N' along with their corresponding values
I have written individual update Statements as below:
**Location1,L1value**
Update Location
set (Location1,L1value) =
(select UPPER(VAlue),'Y' from Location_lookup where trim(Location1)=Location
where exists (select 1 from Location_lookup where trim(Location1)=Location);
commit;
**Location2,value**
Update Location
set (Location2,L2value) =
(select UPPER(VAlue),'Y' from Location_lookup where trim(Location2)=Location
where exists (select 1 from Location_lookup where trim(Location2)=Location);
commit;
Similarly for 3rd flag and value.
Is there a way to write single update for all the three conditions? Reason why I am looking for single update is that I have 10+ million records and I do not want to scan the records three different times. The lookup table has > 32 million records.
Here is a solution which uses Oracle's bulk FORALL ... UPDATE capability. This is not quite as performative as a pure SQL solution but it is simpler to code and the efficiency difference probably won't matter much for 10 million rows on a modern enterprise server, especially if this is a one-off exercise.
Points to note:
You don't say whether LOCATION has a primary key. For this answer I have assumed it has an ID column. The solution won't work if there isn't a primary key, but if your table doesn't have a primary key you've likely got bigger problems.
Your question mentions setting the FLAG columns "as 'Y' and 'N'" but the required output only shows 'Y' setting. I have included processing for 'N' but see the coda underneath.
declare
cursor get_locations is
with lkup as (
select *
from location_lookup
)
select locn.id
,locn.location1
,upper(lup1.value) as l1value
,nvl2(lup1.value, 'Y', 'N') as l1flag
,locn.location2
,upper(lup2.value) as l2value
,nvl2(lup2.value, 'Y', 'N') as l2flag
,locn.location3
,upper(lup3.value) as l3value
,nvl2(lup3.value, 'Y', 'N') as l3flag
from location locn
left outer join lkup lup1 on trim(locn.location1) = lup1.location
left outer join lkup lup2 on trim(locn.location2) = lup2.location
left outer join lkup lup3 on trim(locn.location3) = lup3.location
where lup1.location is not null
or lup2.location is not null
or lup3.location is not null;
type t_locations_type is table of get_locations%rowtype index by binary_integer;
t_locations t_locations_type;
begin
open get_locations;
loop
fetch get_locations bulk collect into t_locations limit 10000;
exit when t_locations.count() = 0;
forall idx in t_locations.first() .. t_locations.last()
update location
set l1value = t_locations(idx).l1value
,l1flag = t_locations(idx).l1flag
,l2value = t_locations(idx).l2value
,l2flag = t_locations(idx).l2flag
,l3value = t_locations(idx).l3value
,l3flag = t_locations(idx).l3flag
where id = t_locations(idx).id;
end loop;
close get_locations;
end;
/
There is a working demo on db<>fiddle here. The demo output doesn't exactly match the sample output posted in the query, because that doesn't the given input data.
Setting flags to 'Y' or 'N'?
The code above uses left outer joins on the lookup table. If a row is found the NVL2() function will return 'Y' otherwise it returns 'N'. This means the flag columns are always populated, regardless of whether the value columns are. The exception is for rows which have no matches in LOCATION_LOOKUP for any location (ID=4000 in my demo). In this case the flag columns will be null. This inconsistency follows from the inconsistencies in the question.
To resolve it:
if you want all flag columns to be populated with 'N' remove the WHERE clause from the get_locations cursor query.
if you don't want to set flags to 'N' change the NVL2() function calls accordingly: nvl2(lup1.value, 'Y', null) as l1flag

Should I clear collections every fetch loop to be used in forall? PLSQL Oracle

Lets say I have a huge data(100k-1m rows) in table1 that I need to do some checking then update their status on table2 based on the filtered results.
As you can see with my simplified code. I bulk collect 1000 rows per batch and filter them into 3 different collections (dsa1, dsa2, dsa3) then later forall update this 3 collections into table2.
My problem here is that lets say the first fetch gets 100 rows into dsa1, then the second fetch only gets 70 rows into dsa1. When the forall update runs it also updates the old 30 rows in dsa1 from the first fetch.
2 Solutions I think of is first, is delete all elements in collects every fetch loop. and second is to put the forall outside the fetch loop which will make the 3 collections very large but forall is only called once.
The second solution will take up a lot of memory right? please advice what is the best solutions
declare
cursor c1 is
select t1.id, t1.status, t2.con from table1 t1, table2 t2
where t1.id = t2.id;
type ty_c1 is table of c1%rowtype;
asd1 ty_c1 := ty_c1();
type ty_id is table of c1.id%type index by pls_integer;
dsa1 ty_id;
dsa2 ty_id;
dsa3 ty_id;
begin
open c1;
loop
fetch c1 bulk collect into asd1 limit 1000;
exit when asd1.count = 0;
for i in 1 .. asd1.count
loop
if (asd1(i).status = 'ACT') then
dsa1(i).id := asd1(i).id;
elsif (asd1(i).status = 'NOT ACT') then
dsa2(i).id := asd1(i).id;
else
dsa3(i).id := asd1(i).id;
end if;
end loop;
forall idx in indices of dsa1
update table2 set con = 'ACTIVE'
where id = dsa1(idx).id;
forall idx in indices of dsa2
update table2 set con = 'NOT ACTIVE'
where id = dsa2(idx).id;
forall idx in indices of dsa3
update table2 set con = 'DEAD'
where id = dsa3(idx).id;
end loop;
close c1;
end;
From a performance perspective, building the three collections and hitting the database only once will be more performant at the expense of using more memory.
So answer depends on the typical volume you will be processing and available memory.
You can also have a fourth collection which remains empty and assign the empty collection to dsa1,dsa2 and dsa3 within the loop rather than deleting the entries.
But looking at your code, since only the status is changing, why not create a collection based on a record of item and status or have a second collection to hold status and then regardless of status, you would be updating 1000 records at a time with a single collection.
And a step further, since 1M records is nothing for an oracle database, (billions of records are huge, a million records is trivial) just use the database to do a single update select statement
update table2 t2
set conn=(select decode(t1.status, 'ACT', 'ACTIVE', 'NOT ACT','NON ACTIVE','DEAD')
from table1 t1)
where t2.id=t1.id
NB rows in t2 that dont exist in t1 will have conn set to null but you can restrict where clauses to limit impact if this is not desired.
you could also add a parallel hint on both the update and the select to use more CPUs and if you are able to modify the data model, using a numeric code instead of a varchar to represent the status would also be more efficient.

SQL Server trigger to update another table's column

I have two SQL Server tables called Table A and Table B. I have an application which inserts one row into Table A and three rows into Table B at the same time. As you can see in the screenshot below, we can link these inserted records based on their ID column in Table A and TransID column in Table B.
During the data insert on table B, if any rows out of 3 inserted rows contain a value called Printed in the Printed column, I want to update my Table A's relevant record's PrintStatus column to Printed as well.
How do I write a SQL Server trigger for this?
Well the best solution is to do this in your code(app) but if there is no way,
you can write a Trigger After Insert for Table B like the trigger example below:
CREATE TRIGGER [dbo].[YourTrigger] ON [dbo].[TableB]
AFTER INSERT
AS
DECLARE #id INT
BEGIN
SET NOCOUNT ON;
SET #id = (SELECT DISTINCT ID FROM Inserted)
IF EXISTS (SELECT * FROM Inserted WHERE Printed='Printed')
UPDATE TableA
SET PrintStatus='Printed'
WHERE ID = #id
END
May this help you
It could be correct for your problem : (not sure at 100%)
CREATE TRIGGER TriggerTableB
ON TableB
AFTER INSERT
AS
UPDATE TableA AS A
SET PrintStatus = 'Printed'
WHERE A.TranID = inserted.ID
AND 'Printed' = (SELECT MAX(I.Printed)
FROM inserted AS I)
I would recommend querying for the information:
select a.*,
(case when exists (select 1
from b
where b.id = a.tranid and b.printed = 'Printed'
)
then 'Printed'
end) as printstatus
from a;
This is simpler than writing a query and you can wrap this in a view.
From a performance perspective, an index on b(id, printed) should make this pretty fast -- and not slow down inserts.
A trigger can be quite complicated, if you want to take inserts, updates, and deletes into account. I prefer to avoid such complication, if possible.

Update all rows for one column in a table with data in another table

I appreciate any advice on this..
I have two tables where I have to update a column in my primary table with data that resides in another secondary table. I cannot rely on views, etc as this data has to be able to be edited by the user in APEX in the future. I am basically pre-populating the data for the users to reduce their manual entry.
Primary Table = Table 1
Secondary Table = Table 2
Columns to be updated in Table 1 = FTE_ID, ACCOUNT_TYPE
Columns where the data will come from Table 2 = R_ID, ACCOUNT_TYPE
Common column in both tables = TABLE1.FID AND TABLE2.FID
Here is what I have tried, but I get "single-row subquery returns more than one row" because there are multiple table1.fid rows in table1. I basically want to perform this update for ALL rows where TABLE1.FID = TABLE2.FID.
Here is my attempt:
UPDATE TABLE1
SET TABLE1.FTE_ID =
(SELECT TABLE2.R_ID FROM TABLE2 WHERE TABLE1.FID = TABLE2.FID);
Error:
single-row subquery returns more than one row
Thanks for your help,
You can fix the proximate problem by using aggregation or row number:
UPDATE TABLE1
SET TABLE1.FTE_ID = (SELECT MAX(TABLE2.R_ID)
FROM TABLE2
WHERE TABLE1.FID = TABLE2.FID
);
The subquery can only return one row; it is an "arbitrary" value from the possible matching values.
If the field is a character field and you want all matching values, then perhaps listagg is more appropriate:
UPDATE TABLE1
SET TABLE1.FTE_ID = (SELECT LISTAGG(t2.R_ID, ',') WITHIN GROUP (ORDER BY t2.R_ID)
FROM TABLE2 t2
WHERE TABLE1.FID = t2.FID
);

Oracle SQL, trying to get one value from a select/join to use to update one column in one table?

I have one table with the following columns:
T_RESOLVED_DATE
I_HOUSEHOLD_NUMBER
I_RESOLVED_SET_NUMBER
I_STATION_CODE
I_RESOLVED_START_MIN
I_DURATION
I_PERSON_NUMBER
I_COVIEW_DEMO_ID
Initially, I_COVIEW_DEMO_ID is set to null.
Then I have another table with the following columns:
T_RESOLVED_DATE
I_HOUSEHOLD_NUMBER
I_PERSON_NUMBER
I_AGE
T_GENDER
I_COVIEW_DEMO_ID
I am trying to update I_COVIEW_DEMO_ID in the first table by using the value of I_COVIEW_DEMO_ID in the second table where the T_RESOLVED_DATE, I_HOUSEHOLD_NUMBER, and I_PERSON_NUMBER are equal in both tables. The first table may contain multiple rows with the same DATE, HOUSEHOLD_NUMBER, and PERSON_NUMBER, because the rows can vary by the rest of the columns.
I have tried to do a select and a group by which seems to get me part way there, but I am getting a "single-row subquery returns more than one row" error when I try to update the columns in the first table. This is what I've tried, along with variations of it:
UPDATE
Table1
SET
I_COVIEW_DEMO_ID =
(SELECT
b.I_COVIEW_DEMO_ID
FROM Table1 a,
Table2 b
WHERE a.I_HOUSEHOLD_NUMBER = b.I_HOUSEHOLD_NUMBER AND
a.I_PERSON_NUMBER = b.I_PERSON_NUMBER AND
a.T_RESOLVED_DATE = b.T_RESOLVED_DATE
GROUP BY b.I_COVIEW_DEMO_ID);
Any suggestions?
I was able to get it to work using this statement:
MERGE INTO table1 a
USING
(
SELECT DISTINCT
T_RESOLVED_DATE,
I_HOUSEHOLD_NUMBER,
I_PERSON_NUMBER,
I_COVIEW_DEMO_ID
FROM
table2
) b
ON
(
a.T_RESOLVED_DATE = b.T_RESOLVED_DATE
AND a.I_HOUSEHOLD_NUMBER = b.I_HOUSEHOLD_NUMBER
AND a.I_PERSON_NUMBER = b.I_PERSON_NUMBER
) WHEN MATCHED THEN
UPDATE SET
a.I_COVIEW_DEMO_ID = b.I_COVIEW_DEMO_ID;
As per our discussion on the comments this would be a simple PLSQL block to do what you need. I'm doing direct from my head without test, so you may need to fix some sintaxe mistake.
BEGIN
FOR rs IN ( SELECT I_HOUSEHOLD_NUMBER,
I_PERSON_NUMBER,
I_COVIEW_DEMO_ID,
T_RESOLVED_DATE
FROM Table2 ) LOOP
UPDATE Table1
SET I_COVIEW_DEMO_ID = rs.I_COVIEW_DEMO_ID
WHERE I_PERSON_NUMBER = rs.I_PERSON_NUMBER
AND I_HOUSEHOLD_NUMBER = rs.I_HOUSEHOLD_NUMBER
AND T_RESOLVED_DATE = rs.T_RESOLVED_DATE;
END LOOP;
--commit after all updates, if there is many rows you should consider in
--making commits by blocks. Define a count and increment it whithin the for
--after some number of updates you commit and restart the counter
COMMIT;
END;