Oracle - Procedure to merge with insert, update and delete - sql

I need to create a procedure to treat this case in the most performative way possible (is an extremely large amount of data).
I have a table called ORDER_A that every day receives a full load (its truncated, and all records are inserted again).
I have a table called ORDER_B which is a copy of ORDER_A, containing the same data and some additional control dates.
I also have a table MANAGER to save start and finish date, and if the procedure is running.
After all inserts are done in ORDER_A, i want to execute a procedure that, for each record on ORDER_A, must looks for a record with the same identifier (primary key: order_id) in table B.
If a record exists with the same order_id, and any of the other columns have changed, an update must be performed on table B
If a record exists with the same order_id, and no values ​​in the other columns have been modified, nothing should be performed, the record must remain the same in table B.
If there is no record with the same order_id, it must be inserted in table B.
If there is a record on ORDER_B that no longer exists on ORDER_A (it was deleted), the column "flag_deleted" must be updated to "1".
My tables are like this
CREATE TABLE ORDER_A
(
ORDER_ID NUMBER NOT NULL,
ORDER_CODE VARCHAR2(50),
ORDER_STATUS VARCHAR2(20),
ORDER_USER_ID NUMBER,
ORDER_DATE TIMESTAMP(6),
CHECKSUM_CODE VARCHAR2(40),
PRIMARY KEY (ORDER_ID)
);
CREATE TABLE ORDER_B
(
ORDER_ID NUMBER NOT NULL,
ORDER_CODE VARCHAR2(50),
ORDER_STATUS VARCHAR2(20),
ORDER_USER_ID NUMBER,
ORDER_DATE TIMESTAMP(6)
INSERT_AT TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP,
UPDATED_AT TIMESTAMP(6),
CHECKSUM_CODE VARCHAR2(40),
FLAG_DELETED NUMBER(1),
PRIMARY KEY (ORDER_ID)
);
-- index on checksum column for both tables
CREATE INDEX idx_cksum on ORDER_A (CHECKSUM_CODE ASC);
CREATE INDEX idx_cksum on ORDER_B (CHECKSUM_CODE ASC);
-- Manager table
CREATE TABLE MANAGER
(
TABLE_NAME VARCHAR2(40),
PROCEDURE_NAME VARCHAR2(50),
START_TS TIMESTAMP(6),
FINISH_TS TIMESTAMP(6),
IS_RUNNING NUMBER(1)
);
I'm thinking in something like this procedure below, but I'm not sure if it's the best way and how to deal with the delete case
create or replace procedure MERGE_DATA_ORDER
DECLARE
is_running number;
ex_running EXCEPTION;
BEGIN
SELECT IS_RUNNING INTO is_running FROM MANAGER WHERE PROCEDURE_NAME = 'MERGE_DATA_ORDER';
IF is_running = 1
then RAISE ex_running
ELSE
-- Update the flag on manager table
UPDATE MANAGER SET IS_RUNNING = 1, START_TS = SYSTIMESTAMP WHERE PROCEDURE_NAME = 'MERGE_DATA_ORDER';
COMMIT;
-- update all records with a checksum using STANDARD_HASH with MD5
UPDATE ORDER_A
SET CHECKSUM_CODE =
STANDARD_HASH
(
ORDER_ID ||
ORDER_CODE ||
ORDER_STATUS ||
ORDER_USER_ID ||
ORDER_DATE,
'MD5'
);
COMMIT;
-- then, I do a MERGE operation, using the checksum as a comparator
merge into ORDER_B b
using (select a.* from ORDER_A a) m
on (m.ORDER_ID = b.ORDER_ID)
when matched then
update
set
b.ORDER_ID = m.ORDER_ID,
b.ORDER_CODE = m.ORDER_CODE,
b.ORDER_STATUS = m.ORDER_STATUS,
b.ORDER_USER_ID = m.ORDER_USER_ID,
b.ORDER_DATE = m.ORDER_DATE,
b.COD_CHECKSUM = m.COD_CHECKSUM,
b.DAT_UPDATE = SYSTIMESTAMP
where b.CHECKSUM_CODE <> m.CHECKSUM_CODE
when not matched then
insert (
b.ORDER_ID,
b.ORDER_CODE,
b.ORDER_STATUS,
b.ORDER_USER_ID,
b.ORDER_DATE,
b.COD_CHECKSUM
)
values (
m.ORDER_ID,
m.ORDER_CODE,
m.ORDER_STATUS,
m.ORDER_USER_ID,
m.ORDER_DATE,
m.COD_CHECKSUM
);
END IF;
-- set the flag to 0
UPDATE MANAGER SET IS_RUNNING = 0, FINISH_TS = SYSTIMESTAMP WHERE PROCEDURE_NAME = 'MERGE_DATA_ORDER';
COMMIT;
END;
/
I need some help to complete this code, performance tips and deal with the delete issue;

I think you can do this as a single statement as part of the data load. Let's assume that ORDER_A has been loaded (but I will comment on that later). Then you can define the result of the insert/update by doing a full outer join between ORDER_A and ORDER_B, and the use a CASE statement to project the "correct" value from ORDER_A or ORDER_B. Similarly you can project the FLAG_DELTED. It would look something like this. In this example, I am skipping the MD5, but this could be added if really needed - more on that later too
select
case
when ( b.order_id is null ) then a.order_id
else case when (
b.ORDER_ID != m.ORDER_ID or
b.ORDER_CODE != m.ORDER_CODE or
b.ORDER_STATUS != m.ORDER_STATUS or
b.ORDER_USER_ID != m.ORDER_USER_ID or
b.ORDER_DATE != m.ORDER_DATE or
b.DAT_UPDATE != SYSTIMESTAMP ) then b.order_id else a.order_id end
end as newOrder_id
, case when ( b.order_id is null ) then a.order_code
else case when (
b.ORDER_ID != m.ORDER_ID or
b.ORDER_CODE != m.ORDER_CODE or
b.ORDER_STATUS != m.ORDER_STATUS or
b.ORDER_USER_ID != m.ORDER_USER_ID or
b.ORDER_DATE != m.ORDER_DATE or
b.DAT_UPDATE != SYSTIMESTAMP ) then b.order_code else a.order_code end
end as newOrder_code
, case when ( b.order_id is null ) then a.order_status
else case when (
b.ORDER_ID != m.ORDER_ID or
b.ORDER_CODE != m.ORDER_CODE or
b.ORDER_STATUS != m.ORDER_STATUS or
b.ORDER_USER_ID != m.ORDER_USER_ID or
b.ORDER_DATE != m.ORDER_DATE or
b.DAT_UPDATE != SYSTIMESTAMP ) then b.order_status else a.order_status end
end as newOrder_status
/* etc... ( Repeat for all projected columns )
Then for the flag_deleted column */
, case when ( a.order_id is null ) then 1
when ( b.order_id is null ) then 0
else b.flag_deleted
end as newFlag_deleted
from Order_b b
full outer join Order_a a
on b.order_id = a.order_id
It may be possible that ORDER_A could be an external table, so then you would just need to prepend this with a
CREATE TABLE NEW_ORDER_A as select....
And then you have the results you need.
Where you a hemorrhaging performance in your example, is the update of ORDER_A. You are generating redo, undo and losing any compression benefits. You are also maintaining indexes, but indexes are not needed.
Assuming you have resources, you cab now use DIRECT PATH and parallelism, and this would scale pretty well.
Lastly, if you really do need the MD5, you need to add a special character between each column, otherwise is will be ambiguous. For example, the following woukd have the same MD5
COL1 COL2
AA BBB
AAB BB

Related

unique constraint violated in stored procedure oracle

I have below stored procedure:
create or replace PROCEDURE CALCULATE_RECOVERY_HISTORY(p_month IN VARCHAR2) AS
l_id NUMBER;
BEGIN
ADD_LOG_INFO('CALCULATE_RECOVERY_HISTORY', 'Procedure Started');
l_id := SQ_AP_RECOVERY_HISTORY.NEXTVAL;
INSERT INTO t_ap_recovery_history (ID, RECOVERY_TARGET_MONTH, TARGET_INSTANCE, RECOVERY_PROGRESS, RECOVERY_TARGET, FAILED_TO_RECOVERY, FOCUS_AREA, IDENTIFIER_CLASS, CREATED_ON)
SELECT l_id,
a_recovery_target_month,
a_target_instance,
COUNT(CASE WHEN A_IS_RECOVERED = 'Y' THEN 1 END),
COUNT(CASE WHEN A_IS_RECOVERED IN ('Y', 'N') THEN 1 END),
COUNT(CASE WHEN A_IS_RECOVERED = 'N' THEN 1 END),
f.focus_area,
r.identifier_class,
SYSDATE
from t_ap_recovery_target t, t_ap_recovery_focusarea f, range r
where t.a_focus_area_id = f.id and t.a_range_id = r.id
and t.a_recovery_target_month = p_month
group by a_target_instance, a_recovery_target_month, f.focus_area, r.identifier_class;
COMMIT;
END CALCULATE_RECOVERY_HISTORY;
When I run the procedure, I get the error
ORA-00001: unique constraint violated.
I've also tried another way which is
SELECT SQ_AP_RECOVERY_HISTORY.NEXTVAL, a_recovery_target_month ...
But this also return another error which is
Sequence number not allowed here
What should I change in the code to solve this constraint issue?
Below is the table definition for T_AP_RECOVERY_HISTORY
CREATE TABLE "DIMSPST"."T_AP_RECOVERY_HISTORY"
( "ID" NUMBER(38,0),
"RECOVERY_TARGET_MONTH" VARCHAR2(6 BYTE) DEFAULT TO_CHAR(SYSTIMESTAMP, 'YYYYMM'),
"TARGET_INSTANCE" VARCHAR2(20 BYTE),
"RECOVERY_PROGRESS" NUMBER(38,0),
"RECOVERY_TARGET" NUMBER(38,0),
"FAILED_TO_RECOVERY" NUMBER(38,0),
"FOCUS_AREA" VARCHAR2(20 BYTE),
"IDENTIFIER_CLASS" VARCHAR2(42 BYTE),
"CREATED_ON" TIMESTAMP (6),
PRIMARY KEY ("ID")
Perform the aggregation in a sub-query and then apply the sequence value in an outer-query:
CREATE PROCEDURE CALCULATE_RECOVERY_HISTORY(
p_month IN VARCHAR2
)
AS
BEGIN
ADD_LOG_INFO('CALCULATE_RECOVERY_HISTORY', 'Procedure Started');
INSERT INTO t_ap_recovery_history (
ID,
RECOVERY_TARGET_MONTH,
TARGET_INSTANCE,
RECOVERY_PROGRESS,
RECOVERY_TARGET,
FAILED_TO_RECOVERY,
FOCUS_AREA,
IDENTIFIER_CLASS,
CREATED_ON
)
SELECT SQ_AP_RECOVERY_HISTORY.NEXTVAL,
a_recovery_target_month,
a_target_instance,
RECOVERY_PROGRESS,
RECOVERY_TARGET,
FAILED_TO_RECOVERY,
focus_area,
identifier_class,
SYSDATE
FROM (
SELECT a_recovery_target_month,
a_target_instance,
COUNT(CASE WHEN A_IS_RECOVERED = 'Y' THEN 1 END) AS RECOVERY_PROGRESS,
COUNT(CASE WHEN A_IS_RECOVERED IN ('Y', 'N') THEN 1 END) AS RECOVERY_TARGET,
COUNT(CASE WHEN A_IS_RECOVERED = 'N' THEN 1 END) AS FAILED_TO_RECOVERY,
f.focus_area,
r.identifier_class
FROM t_ap_recovery_target t
INNER JOIN t_ap_recovery_focusarea f
ON (t.a_focus_area_id = f.id)
INNER JOIN range r
ON (t.a_range_id = r.id)
WHERE t.a_recovery_target_month = p_month
GROUP BY
a_target_instance,
a_recovery_target_month,
f.focus_area,
r.identifier_class
);
END CALCULATE_RECOVERY_HISTORY;
/
Note: If you COMMIT in stored procedures then you cannot chain multiple procedures together and if one fails then ROLLBACK then all. Instead, you should COMMIT in the block that you call the procedures from.
fiddle
One option is to let Oracle create ID. You didn't specify database version you use, so trigger certainly is what would work:
create or replace trigger trg_bi_rec_hist
before insert on t_ap_recovery_history
for each row
begin
:new.id := SQ_AP_RECOVERY_HISTORY.NEXTVAL;
end;
/
Procedure then wouldn't contain insert into the ID column, i.e.
INSERT INTO t_ap_recovery_history (RECOVERY_TARGET_MONTH, ...)
SELECT a_recovery_target_month, ...
Another option (if your database version supports it) is to create ID as identity column instead of a trigger, e.g.
SQL> create table test
2 (id number generated always as identity);
Table created.
Or, if you would not create the trigger like in the previous answer, the procedure should look like below:
create or replace PROCEDURE CALCULATE_RECOVERY_HISTORY(p_month IN VARCHAR2) AS
l_id NUMBER;
BEGIN
ADD_LOG_INFO('CALCULATE_RECOVERY_HISTORY', 'Procedure Started');
INSERT INTO t_ap_recovery_history (ID, RECOVERY_TARGET_MONTH,
TARGET_INSTANCE, RECOVERY_PROGRESS, RECOVERY_TARGET,
FAILED_TO_RECOVERY, FOCUS_AREA, IDENTIFIER_CLASS, CREATED_ON)
with tb as (
SELECT a_recovery_target_month,
a_target_instance,
COUNT(CASE WHEN A_IS_RECOVERED = 'Y' THEN 1 END) c1,
COUNT(CASE WHEN A_IS_RECOVERED IN ('Y', 'N') THEN 1 END) c2,
COUNT(CASE WHEN A_IS_RECOVERED = 'N' THEN 1 END) c3,
f.focus_area,
r.identifier_class
from t_ap_recovery_target t, t_ap_recovery_focusarea f, range r
where t.a_focus_area_id = f.id and t.a_range_id = r.id
and t.a_recovery_target_month = p_month
group by a_target_instance, a_recovery_target_month,
f.focus_area, r.identifier_class
)
select SQ_AP_RECOVERY_HISTORY.NEXTVAL,
a_recovery_target_month,
a_target_instance,
c1,
c2,
c3,
focus_area,
identifier_class,
sysdate
from tb;
COMMIT;
END CALCULATE_RECOVERY_HISTORY;
In general triggers are detrimental to performance in case you have insert select inserting large numbers of rows in one go, or massive updates or massive deletes or merge.
If you have only DML affecting a small number of rows, triggers may save complexity, although I'd rather do more in stored procedures and less in triggers.

Business Rule in ORACLE SQL

I need to respect this Business Rule: Set EXPIRATION_DATE NULL if the TENANT is an OWNER of the APARTMENT. EXPIRATION_DATE indicates the expiry date of a rental contract
TENANT (COD_T, EXPIRATION_DATE, COD_APARTMENT) --where the tenant is living
OWNER (COD_O, COD_APARTMENT) --apartments that belong to the owner
APARTMENT (COD_APARTMENT)
I'd like to know: if I use this script and the CHECK became true, it will set automatically EXPIRATION_DATE to NULL?
If not, how can I do it?
CREATE TABLE TENANT(
COD_T CHAR(5) PRIMARY KEY,
COD_APARTMENT INT REFERENCES APARTMENT(COD_APARTMENT),
EXPIRATION_DATE DATE
(CHECK (COD_T IN ( SELECT COD_O -- same type COD_T
FROM OWNER O
WHERE COD_APARTMENT = O.CODAPARTMENT
)
)
AND EXPIRATION_DATE IS NULL
);
You can use a view do get the data in your required check constraint format
CREATE OR replace VIEW tenant_vw
AS
SELECT cod_t,
cod_apartment,
CASE
WHEN cod_t IN (SELECT cod_o -- same type COD_T
FROM owner O
WHERE cod_apartment = O.codapartment) THEN NULL
ELSE expiration_date
END AS EXPIRATION_DATE
FROM tenant;
You can use the view for such requirement but if you want your data to be updated like this then you need to create the trigger as follows:
CREATE TRIGGER TRG_TENANT
BEFORE INSERT OR UPDATE ON TENANT
FOR EACH ROW
DECLARE
CNT NUMBER;
BEGIN
SELECT COUNT(1)
INTO CNT
FROM OWNER O
WHERE O.CODAPARTMENT = :NEW.COD_APARTMENT
AND O.COD_O = :NEW.COD_T;
IF CNT > 0 THEN
:NEW.EXPIRATION_DATE := NULL;
END IF;
END;
/

ORACLE 12c Unique constraint failing in MERGE command which is matching all columns from the index

I have a table of aggregated data which were collected incorrectly and I am trying to merge results of select into it and I am hitting constraint violation and probably missing something obvious.
Table is simple:
CREATE TABLE "DVRA_STATS_AGG_HOURLY"
( "ID" NUMBER(*,0),
"SHIPMENTS" NUMBER,
"EVENT_DATETIME" DATE,
"COUNTRY" VARCHAR2(2 CHAR),
"DATA_TYPE" VARCHAR2(3 CHAR),
"EVENT_TYPE" CHAR(15)
)
CREATE UNIQUE INDEX "DVRA_STATS_AGG_HOURLY_PK" ON "DVRA_STATS_AGG_HOURLY" ("ID")
CREATE UNIQUE INDEX "DVRA_STATS_AGG_HOURLY_UK1" ON "DVRA_STATS_AGG_HOURLY" ("EVENT_DATETIME", "COUNTRY", "DATA_TYPE", "EVENT_TYPE")
CREATE INDEX "DVRA_STATS_AGG_HOURLY_INDEX1" ON "DVRA_STATS_AGG_HOURLY" ("EVENT_DATETIME" DESC)
With trigger sequence and trigger
CREATE OR REPLACE EDITIONABLE TRIGGER "DVRA_STATS_AGG_HOURLY_AINC"
BEFORE INSERT ON dvra_stats_agg_hourly
FOR EACH ROW
BEGIN
SELECT stats_seq.NEXTVAL
INTO :new.id
FROM dual;
END;
Merge is not complicated either:
MERGE INTO dvra_stats_agg_hourly stats
USING(
SELECT COUNT(*) as SHIPMENTS, TRUNC(event_datetime,'HH24') as event_datetime,COUNTRY,data_type,event_type
FROM AUDIT
WHERE event_type in (<List of events>)
and TRUNC(event_datetime,'HH24') < trunc(sysdate,'HH24')
and audittable.event_datetime is not null
and audittable.COUNTRY is not null
and audittable.data_type is not null
and audittable.event_type is not null
GROUP BY TRUNC(event_datetime,'HH24'),COUNTRY,data_type,event_type
) audittable
ON (
audittable.event_datetime = stats.event_datetime
and audittable.COUNTRY = stats.COUNTRY
and audittable.data_type = stats.data_type
and audittable.event_type = stats.event_type
)
WHEN MATCHED THEN
UPDATE SET stats.SHIPMENTS = audittable.SHIPMENTS WHERE stats.SHIPMENTS <> audittable.SHIPMENTS
WHEN NOT MATCHED THEN
INSERT (SHIPMENTS,event_datetime,STATS.COUNTRY,data_type,event_type)
VALUES (audittable.SHIPMENTS,audittable.event_datetime,audittable.COUNTRY ,audittable.data_type ,audittable.event_type)
;
And I am getting error:
ORA-00001: unique constraint (DVRA_MONITORING.DVRA_STATS_AGG_HOURLY_UK1) violated
I am matching on same columns as I am using in unique constraint. I am inserting only not matching rows and I am grouping by same columns so there should be only one row for unique combination of those columns.
I am only one updating table when I am doing this operation, so no other session manipulating data during run of the query.
What am I missing here?
Edit: Data in DVRA_STATS_AGG_HOURLY were inserted with insert into select. With same select which is used in this merge. Just some data were not properly loaded yet, so I am making correction.
Edit2: added not null
Edit3: Changes in merge query after discussion with Alex
MERGE INTO dvra_stats_agg_hourly stats
USING(
SELECT COUNT(*) as SHIPMENTS, TRUNC(event_datetime,'HH24') as event_datetime,COUNTRY,data_type,event_type
FROM BCTCUSTOM.V_DVRA_AUDIT
WHERE event_type in (<List of types>)
and TRUNC(event_datetime,'HH24') < trunc(sysdate,'HH24')
and TRUNC(event_datetime,'HH24') is not null
and event_datetime is not null
and COUNTRY is not null
and data_type is not null
and event_type is not null
GROUP BY TRUNC(event_datetime,'HH24'),COUNTRY,data_type,event_type
) audittable
ON (
audittable.event_datetime = stats.event_datetime
and ((audittable.COUNTRY is null and stats.COUNTRY is null) or audittable.COUNTRY = stats.COUNTRY)
and ((audittable.data_type is null and stats.data_type is null) or audittable.data_type = stats.data_type)
and ((audittable.event_type is null and stats.event_type is null) or audittable.event_type = stats.event_type)
)
WHEN MATCHED THEN
UPDATE SET stats.SHIPMENTS = audittable.SHIPMENTS WHERE stats.SHIPMENTS <> audittable.SHIPMENTS
WHEN NOT MATCHED THEN
INSERT (SHIPMENTS,event_datetime,STATS.COUNTRY,data_type,event_type)
VALUES (audittable.SHIPMENTS,audittable.event_datetime,audittable.COUNTRY ,audittable.data_type ,audittable.event_type)
;
If any of the four columns you are comparing in your ON clause are null then they won't match with your current conditions, since null = null is unknown.
You can add explicit null checks; instead of:
ON (
audittable.event_datetime = stats.event_datetime
and audittable.COUNTRY = stats.COUNTRY
and audittable.data_type = stats.data_type
and audittable.event_type = stats.event_type
)
do something like:
ON (
audittable.event_datetime = stats.event_datetime
and ((audittable.COUNTRY is null and stats.COUNTRY is null)
or audittable.COUNTRY = stats.COUNTRY)
and ((audittable.data_type is null and stats.data_type is null)
or audittable.data_type = stats.data_type)
and ((audittable.event_type is null and stats.event_type is null)
or audittable.event_type = stats.event_type)
)
db<>fiddle without nulls - works OK.
db<>fiddle with nulls - initially fails, but works with explicit null checks added.
Another possibility is a data type mismatch, as you're using CHAR(15) in the table you showed. If the audit table has that column defined as VARCHAR2(15) instead then the comparison will also fail (because "Oracle uses nonpadded comparison semantics whenever one or both values in the comparison have the data type VARCHAR2 or NVARCHAR2"), and the implicit conversion during the insert will cause the constraint violation. In that case, you can trim the CHAR value:
ON (
audittable.event_datetime = stats.event_datetime
and audittable.COUNTRY = stats.COUNTRY
and audittable.data_type = stats.data_type
and audittable.event_type = trim(stats.event_type)
)
db<>fiddle

How to bring both old and new vale in single row in oracle sql?

Create table query
CREATE TABLE ID_TAB (
ID VARCHAR2(20),
ID_VALUE VARCHAR2(20), FLAG VARCHAR2(20)
);
CREATE TABLE FACT_TABLE (
ID VARCHAR2(20),
VALUE VARCHAR2(20),
NAME VARCHAR2(100)
);
Insert Query
INSERT INTO ID_TAB VALUES('100','ABC','N');
INSERT INTO ID_TAB VALUES('120','ABC','Y');
INSERT INTO FACT_TABLE VALUES('100','MAX','ORANGE');
My objective is to update the fact table 'ID' column to 120 because it has the FLAG value as 'Y'
My original table has 50 million records.
How can we write the query in merge or using Update?
You could try the following (I didn't try it though):
UPDATE fact_table
SET fact_table.id = (SELECT yes_tab.id
FROM id_tab no_tab
JOIN id_tab yes_tab
ON no_tab.id_value = yes_tab.id_value
AND yes_tab.flag = 'Y'
WHERE no_tab.id = fact_table.id)
WHERE EXISTS (SELECT *
FROM id_tab
WHERE id_tab.id = fact_table.id
AND id_tab.flag = 'N');
The WHERE EXISTS makes sure you update only the elements with the flag in the corresponding ID_TAB set to 'N'.
The query for the SET searches the ID of the corresponding element having the flag set to 'Y'.
To get a performant solution (i.e. avoid the join completely) you may switch to a dynamic SQL.
I extended you example a bit to have more updated keys as follows.
Note that there is maximal one new value per ID_VALUE, but more old value with N flag are allowed, which all must be updated.
INSERT INTO ID_TAB VALUES('100','ABC','N'); -- old key
INSERT INTO ID_TAB VALUES('110','ABC','N'); -- old key
INSERT INTO ID_TAB VALUES('120','ABC','Y'); -- new key
INSERT INTO ID_TAB VALUES('200','EFG','N'); -- old key
INSERT INTO ID_TAB VALUES('210','EFG','N'); -- old key
INSERT INTO ID_TAB VALUES('220','EFG','Y'); -- new key
INSERT INTO FACT_TABLE VALUES('100','MAX','ORANGE');
INSERT INTO FACT_TABLE VALUES('110','MIN','ORANGE');
INSERT INTO FACT_TABLE VALUES('200','MAX','APPLE');
INSERT INTO FACT_TABLE VALUES('210','MIN','APPLE');
INSERT INTO FACT_TABLE VALUES('220','NEW','APPLE');
commit;
The UPDATE statements reflects all th eupdate options, e.g. the keys 100 and 110 should be chnaged to 200.
This leads to the following update
update FACT_TABLE
set ID = case
when ID in ('100','110') then '200'
when ID in ('200','210') then '220'
end
where ID in ('100','110','200','210');
Note that the where condition consist of all the keys with flag N and the case statement is produced per ID_VALUE mapping all keys with the flag N to the (only one) key with flag Y.
This makes the generation of the UPDATE statement an easy little task of a query on the table ID_TAB. See the query below. For the list creation the function LISTAGG is used. The main parts are commented in the query.
with old_keys as (
select /* get list of all old values */
listagg(''''||ID||'''',',') within group (order by ID) old_keys
from ID_TAB where flag = 'N'),
case_stmt as (
select ID_VALUE,
listagg(case when flag = 'N' then ''''||ID||'''' end,',') within group (order by ID) old_keys,
max(''''||case when flag = 'Y' then ID end||'''') new_key
from ID_TAB
group by ID_VALUE),
case_stmt2 as (
select
'when ID in ('||OLD_KEYS ||') then ' ||NEW_KEY ||' /* update for '|| ID_VALUE || ' */' case_when, ID_VALUE
from case_stmt),
case_stmt3 as ( /* concatenate CASE WHEN */
select listagg(CASE_WHEN,chr(13)) within group (order by ID_VALUE) case_when from case_stmt2)
select
'update FACT_TABLE
set ID = case
'||
case_when ||
'
end
where ID in ('||
(select old_keys from old_keys)||')' as update_stmt
from case_stmt3
On the sample data this SQL string is returned
update FACT_TABLE
set ID = case
when ID in ('100','110') then '120' /* update for ABC */
when ID in ('200','210') then '220' /* update for EFG */
end
where ID in ('100','110','200','210')

SQL - Efficient versioning of DNS records

So far I have come up with this solution that needs further refinement (big thanks to #postgresql on freenode).
The problem I am trying to overcome is an efficient way of storing DNS records whilst maintaining some sort of history. The issue I am currently having is with the wCTE which is inserting new records and deleting old records correctly. It isn't, however, readding records. The wCTE is:
WITH deltas AS (
SELECT o, n FROM (
SELECT id, name, domain_id, class_id, addr FROM record WHERE tld_id = $1
) AS o FULL OUTER JOIN record_temp n
ON (
o.name = n.name AND
o.domain_id = n.domain_id AND
o.class_id = n.class_id AND
o.addr = n.addr
)
WHERE (o.name, o.domain_id, o.class_id, o.addr)
IS DISTINCT FROM (n.name, n.domain_id, n.class_id, n.addr)
), mark_dead AS (
UPDATE record SET alive = FALSE
WHERE id IN (
SELECT (o).id FROM deltas WHERE (o).id IS NOT NULL
) RETURNING *
)
INSERT INTO record (name, domain_id, tld_id, class_id, addr)
SELECT (n).name, (n).domain_id, (n).tld_id, (n).class_id, (n).addr
FROM deltas WHERE
(n).name IS NOT NULL AND
(n).domain_id IS NOT NULL AND
(n).tld_id IS NOT NULL AND
(n).class_id IS NOT NULL AND
(n).addr IS NOT NULL
;
The o result has all the old records that do not exist in record_temp, n has all the records that are new and need to be inserted. I expect I need to add another join which pulls in (an inner join?) results that exist on both tables (which if marked as dead, need to be marked as alive).
The rest of the schema for reference is:
CREATE TABLE record (
id SERIAL,
name VARCHAR(255),
domain_id INT,
tld_id INT,
class_id INT,
addr INET,
alive BOOLEAN DEFAULT TRUE,
PRIMARY KEY (id),
CONSTRAINT fk1 FOREIGN KEY (domain_id) REFERENCES domain (id) MATCH SIMPLE,
CONSTRAINT fk2 FOREIGN KEY (tld_id) REFERENCES tld (id) MATCH SIMPLE,
UNIQUE(name, domain_id, class_id, addr)
);
CREATE TABLE record_history (
id SERIAL,
record_id INT,
history_type record_history_type,
stamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk1 FOREIGN KEY (record_id) REFERENCES record (id) MATCH SIMPLE,
PRIMARY KEY(id)
);
CREATE TEMP TABLE record_temp (
name VARCHAR(255),
domain_id INT,
tld_id INT,
class_id INT,
addr INET,
UNIQUE(name, domain_id, class_id, addr)
)
ON COMMIT DROP;
record_history is populated using functions and triggers and is populating how I expect it to, below are these triggers:
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO record_history (record_id, history_type) VALUES (NEW.id, 'added');
RETURN NEW;
END;
$$ language 'plpgsql';
RETURNS TRIGGER AS $$
BEGIN
IF NEW.alive = OLD.alive THEN
RETURN NEW;
END IF;
IF NEW.alive THEN
INSERT INTO record_history (record_id, history_type) VALUES (NEW.id, 'added');
END IF;
IF NOT NEW.alive THEN
INSERT INTO record_history (record_id, history_type) VALUES (NEW.id, 'deleted');
END IF;
RETURN NEW;
END;
$$ language 'plpgsql';
ON record FOR EACH ROW EXECUTE PROCEDURE
add_insert_record_history();
ON record FOR EACH ROW EXECUTE PROCEDURE
add_update_record_history();
I seem to have it working how I want with the following query, which I feel is incredibly unoptimized:
WITH deltas AS (
SELECT o, n FROM (
SELECT id, name, domain_id, class_id, addr FROM record WHERE tld_id = $1
) AS o FULL OUTER JOIN record_temp n
ON (
o.name = n.name AND
o.domain_id = n.domain_id AND
o.class_id = n.class_id AND
o.addr = n.addr
)
WHERE (o.name, o.domain_id, o.class_id, o.addr)
IS DISTINCT FROM (n.name, n.domain_id, n.class_id, n.addr)
), mark_dead AS (
UPDATE record SET alive = FALSE
WHERE id IN (
SELECT (o).id FROM deltas WHERE (o).id IS NOT NULL
) RETURNING *
), mark_alive AS (
UPDATE record SET alive = TRUE
WHERE alive = FALSE AND id IN (
SELECT id FROM (
SELECT id, name, domain_id, class_id, addr FROM record WHERE tld_id = $1
) AS o INNER JOIN record_temp n
ON (
o.name = n.name AND
o.domain_id = n.domain_id AND
o.class_id = n.class_id AND
o.addr = n.addr
)
) RETURNING *
)
INSERT INTO record (name, domain_id, tld_id, class_id, addr)
SELECT (n).name, (n).domain_id, (n).tld_id, (n).class_id, (n).addr
FROM deltas WHERE
(n).name IS NOT NULL AND
(n).domain_id IS NOT NULL AND
(n).tld_id IS NOT NULL AND
(n).class_id IS NOT NULL AND
(n).addr IS NOT NULL
;