Is it safe to drop and then create the foreign key constraints inside a transaction? - sql

I have a table A that references a table B. Table B needs to be populated with updated data from an external source and for efficiency I use TRUNCATE followed by a COPY. This is done even when the application is live.
To further improve efficiency, as suggested in the documentation, I want to drop and then recreate the foreign keys.
However I have some doubts.
If I drop the FKs, COPY and then recreate FKs inside the same transaction, can I be sure that the constraint is preserved even on data inserted in table A during the transaction? I ask this because in theory a transaction is atomic, but in the docs, about the temporary removal of FKs say:
there is a trade-off between data load speed and loss of error checking while the constraint is missing.
If there's a chance that a wrong reference is inserted in the meantime, what happens when you try to recreate the FK constraints?

TRUNCATE is not allowed on any table referenced by a foreign key, unless you use TRUNCATE CASCADE, which will also truncate the referencing tables. The DEFERRABLE status of the constraint does not affect this. I don't think there is any way around this; you will need to drop the constraint.
However, there is no risk of an integrity violation in doing so. ALTER TABLE ... ADD CONSTRAINT locks the table in question (as does TRUNCATE), so your import process is guaranteed to have exclusive access to the table for the duration of its transaction. Any attempts at concurrent inserts will simply hang until the import has committed, and by the time they are allowed to proceeed, the constraint will be back in place.

You can make the foreign key constraint deferrable (initially deferred). That way it will be checked just once at the end of the transaction.
ALTER TABLE
xxx
ADD CONSTRAINT
xxx_yyy_id_fk FOREIGN KEY (yyy_id)
REFERENCES
yyy
DEFERRABLE INITIALLY DEFERRED;
In all the cases, transactions are fully atomic in PostgreSQL (not only in theory), including DDL statements (such as CREATE/DROP constraint), so even if you drop a foreign key, then insert data, then create the foreign key and do everything in one transaction, then you are safe - if the recreation of the foreign key constraint fails, then the inserted data will also be dismissed.
Still, it is better to switch to deferred foreign keys, rather than dropping and then creating them.

Analytic answer: measure the number of new/same/updated/deleted records.
There are four cases:
The key in the B table is not present in the b_import: delete
The key in the b_import is not present on the old B: insert
The key is present in both old B and new B, but the contents are the same: ignore
The keys are the same, but the attribete values differ: Update
-- some test data for `A`, `B` and `B_import`:
CREATE TABLE b
( id INTEGER NOT NULL PRIMARY KEY
, payload varchar
);
INSERT INTO b(id,payload) SELECT gs, 'bb_' || gs::varchar
FROM generate_series(1,20) gs;
CREATE TABLE b_import
( id INTEGER NOT NULL PRIMARY KEY
, payload varchar
);
INSERT INTO b_import(id,payload) SELECT gs, 'bb_' || gs::varchar
FROM generate_series(10,15) gs;
-- In real life this table will be filled by a `COPY b_import FROM ...`
INSERT INTO b_import(id,payload) SELECT gs, 'b2_' || gs::varchar
FROM generate_series(16,25) gs;
CREATE TABLE a
( id SERIAL NOT NULL PRIMARY KEY
, b_id INTEGER references b(id) ON DELETE SET NULL
, aaaaa varchar
);
INSERT INTO a(b_id,aaaaa)
SELECT gs,'aaaaa_' || gs::text FROM generate_series(1,20) gs;
CREATE INDEX ON a(b_id); -- index supporting the FK
-- show it
SELECT a.id, a.aaaaa
,b.id, b.payload AS oldpayload
FROM a
FULL JOIN b ON a.b_id=b.id
ORDER BY a.id;
-- Do the actual I/U/D and report the numbers of affected rows
-- EXPLAIN
WITH ins AS ( -- INSERTS
INSERT INTO b(id, payload)
SELECT b_import.id, b_import.payload
FROM b_import
WHERE NOT EXISTS (
SELECT 1 FROM b
WHERE b.id = b_import.id
)
RETURNING b.id
)
, del AS ( -- DELETES
DELETE FROM b
WHERE NOT EXISTS (
SELECT 2 FROM b_import
WHERE b_import.id = b.id
)
RETURNING b.id
)
, upd AS ( -- UPDATES
UPDATE b
SET payload=b_import.payload
FROM b_import
WHERE b_import.id = b.id
AND b_import.payload IS DISTINCT FROM b.payload -- exclude idempotent updates
-- AND NOT EXISTS ( -- exclude deleted records
-- SELECT 3 FROM del
-- WHERE del.id = b_import.id
-- )
-- AND NOT EXISTS ( -- avoid touching freshly inserted rows
-- SELECT 4 FROM ins
-- WHERE ins.id = b_import.id
-- )
RETURNING b.id
)
SELECT COUNT(*) AS orgb
, (SELECT COUNT(*) FROM b_import) AS newb
, (SELECT COUNT(*) FROM ins) AS ninserted
, (SELECT COUNT(*) FROM del) AS ndeleted
, (SELECT COUNT(*) FROM upd) AS nupdated
FROM b
;
Dropping a constraint and rebuilding it after the import is expensive: all the records in both A and B are involved.
temporally ignoring the constraint is dangerous: the new B table could miss some rows that are still referenced by A's FK.
ergo: You could end up with a crippled model, and you'd have to rebuild As references (which is basically impossible, without additional information (which would be redundant, BTW))

Related

Need to generate CREATE TABLE scripts and INSERT scripts in order

CREATE TABLE a (
a_id NUMBER(10),
city VARCHAR2(255),
CONSTRAINT pk_a PRIMARY KEY ( a_id )
);
CREATE TABLE b (
b_id NUMBER(10),
a_id NUMBER(10),
city VARCHAR2(255),
CONSTRAINT pk_b PRIMARY KEY ( b_id ),
CONSTRAINT fk_b_a FOREIGN KEY ( a_id )
REFERENCES a ( a_id )
);
INSERT INTO a VALUES(1,'Mumbai');
INSERT INTO a VALUES(2,'Pune');
INSERT INTO b VALUES(1,1,'Mumbai');
INSERT INTO b VALUES(2,2,'Pune');
COMMIT;
I need to generate a create table ddl scripts in order of parent-child. Suppose, I have two tables A and B then script should give me ddl for table A and then for B.
From the below query I can get the parent child tables but how to generate ddl out of it.
SELECT p.table_name PARENT_TABLE, c.table_name CHILD_TABLE
FROM dba_constraints p, dba_constraints c
WHERE (p.constraint_type = 'P' OR p.constraint_type = 'U')
AND c.constraint_type = 'R'
AND p.constraint_name = c.r_constraint_name
AND p.table_name = UPPER('A')
and p.owner='TAM';
And same for INSERT DML scripts. I need to have the DML scripts in the order of parent-child.
I am stuck in generating the scripts.
I wouldn't bother.
I presume your intention is to port current schema to another schema/database.
if you already have such a script - that creates tables in appropriate order, as well as inserts rows into them - reuse it
you could help yourself if you first created tables (without foreign key constraints), then inserted rows, then created foreign key constraints (using alter table add constraint ...)
however, I'd suggest you to use Data Pump instead (or maybe even the original EXP and IMP utilities) - export will export the whole schema (if that's what you're doing) into a .DMP file; import would then import it into another schema, taking care about foreign key constraints (basically, it would do as I wrote before - apply constraints at the end of import)
it means that you don't have to take care about what comes first - Oracle will do everything for you

Create constraint for control insert in table

There are two tables - orders and a list of services. In the first there is a bool field that the order is approved, if it is true then you can’t insert / delete values in the second table. With the UPDATE of the first table and the DELETE of the second, it is clear.
INSERT make as
INSERT INTO b (a_id, b_value)
SELECT *
FROM (VALUES (1, 'AA1-BB1'),(1, 'AA1-BB2'),(1, 'AA1-BB3')) va
WHERE (SELECT NOT confirm FROM a WHERE a_id = 2);
https://dbfiddle.uk/?rdbms=postgres_12&fiddle=7b0086967c1c38b0c80ca5624ebe92e9
How to forbid to insert without triggers and stored procedures? Is it possible to compose somehow complex constraint or a foreign key for checking conditions at the DBMS level?
The most recent version of Postgres supports generated columns. So, you can do:
alter table b add confirm boolean generated always as (false) stored;
Then create a unique key in a:
alter table a add constraint unq_a_confirm_id unique (confirm, id);
And finally the foreign key relationship:
alter table b add constraint fk_b_a_id_confirm
foreign key (confirm, a_id) references a(confirm, id);
Now, only confirmed = false ids can be used. Note that this will prevent updates to a that would invalidate the foreign key constraint.

Conditional Unique Constraint on Memory Optimized Tables

I am trying to keep integrity in a MEMORY OPTIMIZED table I have. In that table is a foreign key (uniqueidentifier) that points to another table and an Active flag (bit) denoting whether the record is active or not.
I want to stop inserts from happening if the incoming record has the same foreign key as an existing record, only if the existing record is active (Active = 1).
Because this is a memory optimized table, I am limited in how I can go about this. I have tried creating a unique index and discovered they are not allowed in memory optimized tables.
UPDATE:
I ended up using a stored procedure to solve my problem. The stored procedure will do the check for me prior to the insert or update of a record.
Most folks get around the limitations of In-Memory table constraints using triggers. There are a number of examples listed here:
https://www.mssqltips.com/sqlservertip/3080/workaround-for-lack-of-support-for-constraints-on-sql-server-memoryoptimized-tables/
Specifically for your case this will mimic a unique constraint for insert statements but the poster has examples for update and delete triggers as well in the link above.
-- note the use of checksum to make a single unique value from the combination of two columns
CREATE TRIGGER InMemory.TR_Customers_Insert ON InMemory.Customers
WITH EXECUTE AS 'InMemoryUsr'
INSTEAD OF INSERT
AS
SET NOCOUNT ON
--CONSTRAINT U_OnDisk_Customersg_1 UNIQUE NONCLUSTERED (CustomerName, CustomerAddress)
IF EXISTS (
-- Check if rows to be inserted are consistent with CHECK constraint by themselves
SELECT 0
FROM INSERTED I
GROUP BY CHECKSUM(I.CustomerName, I.CustomerAddress)
HAVING COUNT(0) > 1
UNION ALL
-- Check if rows to be inserted are consistent with UNIQUE constraint with existing data
SELECT 0
FROM INSERTED I
INNER JOIN InMemory.tblCustomers C WITH (SNAPSHOT)
ON C.ChkSum = CHECKSUM(I.CustomerName, I.CustomerAddress)
)
BEGIN
;THROW 50001, 'Violation of UNIQUE Constraint! (CustomerName, CustomerAddress)', 1
END
INSERT INTO InMemory.tblCustomers WITH (SNAPSHOT)
( CustomerID ,
CustomerName ,
CustomerAddress,
chksum
)
SELECT NEXT VALUE FOR InMemory.SO_Customers_CustomerID ,
CustomerName ,
CustomerAddress,
CHECKSUM(CustomerName, CustomerAddress)
FROM INSERTED
GO

Column value must exist in non primary column of another table

Suppose I have two tables:
TABLE_1
ID AGE_T1 NAME
1 5 A
2 23 B
3 5 C
4 9 D
TABLE_2
AGE_T2 FREQUENCY
5 2
9 1
23 1
How can I ensure that a AGE_T2 value must be one of the values of AGE_T1 (non-unique column of TABLE_1)? Need to mention that both TABLE_1 and TABLE_2 are physical tables (not any logical structure as VIEW).
Note: If AGE_T1 were a primary key then a foreign key constraint would be enough for AGE_T2. I also have a plan to use INSERT TRIGGER if there is no better solution.
Create a materialized view to contain only the distinct ages in TABLE_1:
CREATE MATERIALIZED VIEW LOG ON TABLE_1
WITH SEQUENCE, ROWID(AGE_T1)
INCLUDING NEW VALUES;
CREATE MATERIALIZED VIEW TABLE_1_MV
BUILD IMMEDIATE
REFRESH FAST ON COMMIT
AS SELECT DISTINCT AGE_T1
FROM TABLE_1;
ALTER TABLE TABLE_1_MV ADD CONSTRAINT t1_mv__age_t1__pk PRIMARY KEY ( AGE_T1 );
Then you can add a FOREIGN KEY on TABLE_2 referencing this as the primary key:
ALTER TABLE TABLE_2 ADD CONSTRAINT t2__age__fk FOREIGN KEY ( AGE_T2 )
REFERENCES TABLE_1_MV ( AGE_T1 );
I think this should work. Not tested.
It's also based on a materialized view; the MV must refresh on commit as in MT0's solution. Let the MV select only rows that violate your lookup condition - and let the MV have a check condition that always evaluates to FALSE, so that it will always reject the insert/update/delete if your original lookup condition is violated. You can make the "always false" constraint deferrable if needed.
This way you will only need one MV, and you won't need to maintain anything.
create materialized view TABLE_2_MV
build immediate
refresh fast on commit
as select age_t2
from table_2
where age_t2 not in (select age_t1 from table_1);
alter table TABLE_2_MV add constraint t2_mv_chk (0 != 0);
Good luck!

Self-referencing constraint in MS SQL

Is it true that MS SQL restrict self-referencing constraints with ON DELETE CASCADE option?
I have a table with parent-child relation, PARENT_ID column is foreign key for ID. Creating it with ON DELETE CASCADE option causes error
"Introducing FOREIGN KEY constraint
may cause cycles or multiple cascade
paths. Specify ON DELETE NO ACTION or
ON UPDATE NO ACTION, or modify other
FOREIGN KEY constraints."
I can't believe that I have to delete this hierarchy in recursive mode. Is there any issue except triggers?
It is the case that you cannot set up ON DELETE CASCADE on a table with self-referencing constraints. There is a potential of cyclical logic problems, hence it won't allow it.
There's a good article here - though it's for version 8 rather than 9 of SQL - though the same rules apply.
I just answered another question where this question was bound as duplicate. I think it's worth to place my answer here too:
This is not possible. You can solve this with an INSTEAD OF TRIGGER
create table locations
(
id int identity(1, 1),
name varchar(255) not null,
parent_id int,
constraint pk__locations
primary key clustered (id)
)
GO
INSERT INTO locations(name,parent_id) VALUES
('world',null)
,('Europe',1)
,('Asia',1)
,('France',2)
,('Paris',4)
,('Lyon',4);
GO
--This trigger will use a recursive CTE to get all IDs following all ids you are deleting. These IDs are deleted.
CREATE TRIGGER dbo.DeleteCascadeLocations ON locations
INSTEAD OF DELETE
AS
BEGIN
WITH recCTE AS
(
SELECT id,parent_id
FROM deleted
UNION ALL
SELECT nxt.id,nxt.parent_id
FROM recCTE AS prv
INNER JOIN locations AS nxt ON nxt.parent_id=prv.id
)
DELETE FROM locations WHERE id IN(SELECT id FROM recCTE);
END
GO
--Test it here, try with different IDs. You can try WHERE id IN(4,3) also...
SELECT * FROM locations;
DELETE FROM locations WHERE id=4;
SELECT * FROM locations
GO
--Clean-Up (Carefull with real data!)
if exists(select 1 from INFORMATION_SCHEMA.TABLES where TABLE_NAME='locations')
---DROP TABLE locations;
CREATE TRIGGER MyTable_OnDelete ON MyTable
INSTEAD OF DELETE
AS
BEGIN
SET NOCOUNT ON;
DELETE FROM mt
FROM deleted AS D
JOIN MyTable AS mt
ON d.Id = mt.ParentId
DELETE FROM mt
FROM deleted AS D
JOIN MyTable AS mt
ON d.Id = mt.Id
END