Deleting rows from multiple related tables - sql

There are three tables related one-to-one on the identifier. I need to delete all the records from the three tables that match the criteria A.ID = B.ID = C.ID
Now I do it in the following way:
DECLARE
CURSOR CUR IS
SELECT C.ID FROM A
INNER JOIN B ON A."ID" = B."ID"
INNER JOIN C ON B."ID" = C."ID"
WHERE A.STATUS = 'ERROR';
IDX NUMBER;
BEGIN
FOR REC IN CUR LOOP
IDX := REC.ID;
DELETE FROM C WHERE C."ID" = IDX;
DELETE FROM B WHERE B."ID" = IDX;
DELETE FROM A WHERE BP."ID" = IDX;
END LOOP;
COMMIT;
END;
A lot of data and this way for very long runs. Is there any way to delete faster?

You could create a PL/SQL type to store the IDs.
CREATE TYPE t_ids AS TABLE OF NUMBER;
Delete all records from table a that match the criterias, and return the IDs into a variable of that type. Then delete all records from b and c with these IDs.
DECLARE
ids_to_delete t_ids;
BEGIN
DELETE FROM a
WHERE a.status = 'ERROR'
AND EXISTS ( SELECT 1 FROM b WHERE b.id = a.id )
AND EXISTS ( SELECT 1 FROM c WHERE c.id = a.id )
RETURNING a.id
BULK COLLECT INTO ids_to_delete;
DELETE FROM b
WHERE id IN ( SELECT COLUMN_VALUE FROM TABLE( ids_to_delete ) );
DELETE FROM c
WHERE id IN ( SELECT COLUMN_VALUE FROM TABLE( ids_to_delete ) );
END;
This should perform a lot better, since it requires no loop and does everything in three SQL statements, instead of three statements per ID.

Related

SQL conditional joins

I have a table-valued function with joins where I want to choose which join I use depending on a local variable like:
DECLARE #type int;
Then do some logic with #type and set it to 1.
SELECT ...
FROM table t
inner join ... a on a.id = t.id and #type = 1 -- Only trigger this join if #type is 1
inner join ... b on b.id = t.id and #type = 2 -- Only trigger this join if #type is 2
So my question is: how can I choose which join to trigger depending on the value of #type (if even possible).
The reason I want to do this is that the SELECT statement is massive, and I don't want repetitive code in the script.
Use left join instead:
SELECT ...
FROM table t LEFT JOIN
a
ON a.id = t.id AND #type = 1 LEFT JOIN
b
ON b.id = t.id AND #type = 2 ;
You might need WHERE #type IN (1, 2) if you want an empty result set for other values.
You will need COALESCE() in the SELECT to combine the columns:
COALESCE(a.col1, b.col1) as col1
This should be quite efficient. However, you might want to simply use UNION ALL:
SELECT ...
FROM table t JOIN
a
ON a.id = t.id
WHERE #type = 1
UNION ALL
SELECT ...
FROM table t JOIN
b
ON b.id = t.id
WHERE #type = 2 ;
You could union your two tables within a subquery. For any similar columns (i.e. would be in the same column in the outer select) you can place them above each other, for columns unique to each source you'd need to pad the other side of the union with NULL, e.g.
SELECT t.id,
a.SimilarCol,
a.UniqueToA,
a.UniqueToB
FROM Table AS t
INNER JOIN
( SELECT a.id,
a.SimilarCol, -- Column you would want to consider the same in each table
a.UniqueToA, -- Column Unique to this table
UniqueToB = NULL -- Column Unique to the other table
FROM SomeTable AS a
WHERE #Type = 1
UNION ALL
SELECT b.id,
b.SimilarCol,
UniqueToA = NULL,
b.UniqueToB
FROM SomeOtherTable AS b
WHERE #type = 2
) AS a
ON a.id = t.id;
Example on db<>Fiddle

Loop through list of values in Oracle query results

In Oracle, is it possible to loop through values that are returned from a query and use those in a nested query?
For example:
For each A
In Table1
Where B = C
(Select D
From Table2
Where D = A)
Loop
End;
Just use SQL:
SELECT D
FROM Table2
WHERE D IN ( SELECT A FROM Table1 WHERE B = C )
Or you can use collections:
CREATE TYPE int_list IS TABLE OF NUMBER(10,0);
then:
DECLARE
a_array int_list;
d_array int_list;
BEGIN
SELECT a
BULK COLLECT INTO a_array
FROM table1
WHERE b = c;
SELECT d
BULK COLLECT INTO d_array
FROM Table2
WHERE d MEMBER OF a_array;
FOR i IN 1 .. d_array.COUNT LOOP
NULL;
END LOOP;
END;
/
You can use a for loop using PL/SQL to accomplish your goal. The general syntax follows. Replace NULL; with whatever logic you wish in the inner for loop.
BEGIN
FOR r IN (
SELECT a
FROM table1
WHERE b = c)
LOOP
FOR s IN (
SELECT d
FROM table2
WHERE d = a)
LOOP
NULL;
END LOOP;
END LOOP;
END;

Procedure deleting related rows

I want to implement procedure that will delete rows from 3 related table.
I have table, A, B, and C.
Table A
ID
Table B
ID
tableAId
Table C
ID
tablceBId
relation table A - B is one to many and relation B - C is one to many.
Now I want to implement a procedure that would delete all rows that are related with Table A. I do not want to make these delete on the cascade. Can I achieve this in one query?
I pass ID of row in Table A.
CREATE PROCEDURE delTableA
#tableA_ID int
AS
BEGIN
DELETE C
FROM C
INNER JOIN B ON C.ID = B.tableCId
INNER JOIN A ON B.ID = A.tableBId
WHERE A.ID = #tableA_ID
DELETE B
FROM B
INNER JOIN A ON B.ID = A.tableBId
WHERE A.ID = #tableA_ID
DELETE FROM A
WHERE A.ID = #tableA_ID
END
GO
As in,
CREATE TABLE C (ID int)
CREATE TABLE B (ID int, tableCId int)
CREATE TABLE A (ID int, tableBId int)
GO
INSERT INTO c VALUES (1)
INSERT INTO b VALUES (1, 1)
INSERT INTO a VALUES (1, 1)
GO
CREATE PROCEDURE delTableA
#tableA_ID int
AS
BEGIN
DELETE C
FROM C
INNER JOIN B ON C.ID = B.tableCId
INNER JOIN A ON B.ID = A.tableBId
WHERE A.ID = #tableA_ID
DELETE B
FROM B
INNER JOIN A ON B.ID = A.tableBId
WHERE A.ID = #tableA_ID
DELETE FROM A
WHERE A.ID = #tableA_ID
END
GO
EXEC delTableA 1
GO
If you run this query, all are empty:
select * from A
select * from B
select * from C

PL SQL map result set to multiple records

If we have 3 tables for example A, B and C and a cursor like
FOR i IN (
SELECT *
FROM A
JOIN B ON(...)
JOIN C ON(...)
) LOOP
--is there an easy way to map every row to 3 records(A%rowtype, B%rowtype and C%rowtype)?
END LOOP;
Notice that fetching records by ROWID is faster than by index. But you will gain a tiny loss of performance (comparing to simple FOR LOOP) with the following trick, because nothing is free.
DECLARE
l_rec_a table_A%ROWTYPE;
l_rec_b table_B%ROWTYPE;
BEGIN
FOR i IN (SELECT a.ROWID first
, b.ROWID second
FROM table_A AS a
JOIN table_B AS b on a.id = b.id)
LOOP
SELECT * INTO l_rec_a FROM table_A WHERE ROWID = i.first;
SELECT * INTO l_rec_b FROM table_B WHERE ROWID = i.second;
/* do something */
END LOOP;
END;

Is a scalar database function used in a join called once per distinct set of inputs or once per row?

If I have a sql statement like this:
select *
from tableA a
inner join tableB b on dbo.fn_something(a.ColX) = b.ColY
if you assume there are 5 rows in tableA with the same value for ColX will dbo.fn_something() be called with that value 5 times or just one time?
Clearly this is a trivial example, but I'm interested for the purposes of thinking about performance in a more complex scenario.
UPDATE
Thanks #DStanley, following from your answer I investigated further. Using SQL Profiler with the SP:StmtStarting event on the SQL below illustrates what happens. i.e. as you said: the function will be called once for each row in the join.
This has an extra join from the original question.
create table tableA
( id int )
create table tableB
( id_a int not null
, id_c int not null
)
create table tableC
( id int )
go
create function dbo.fn_something( #id int )
returns int
as
begin
return #id
end
go
-- add test data
-- 5 rows:
insert into tableA (id) values (1), (2), (3), (4), (5)
-- 5 rows:
insert into tableC (id) values (101), (102), (103), (104), (105)
-- 25 rows:
insert into tableB (id_a, id_c) select a.id, c.id from tableA a, tableC c
go
-- here dbo.fn_something() is called 25 times:
select *
from tableA a
inner join tableB b on a.id = b.id_a
inner join tableC c on c.id = dbo.fn_something(b.id_c)
-- here dbo.fn_something() is called just 5 times,
-- as the 'b.id_c < 102' happens to be applied first.
-- That's likely to depend on whether SQL thinks it's
-- faster to evaluate the '<' or the function.
select *
from tableA a
inner join tableB b on a.id = b.id_a
inner join tableC c on c.id = dbo.fn_something(b.id_c) and b.id_c < 102
go
drop table tableA ;
drop table tableB;
drop table tableC;
drop function dbo.fn_something;
go
It will be called for each row in a. I do not know of any optimization that would call the function just for unique inputs. If performance is an issue you could create a temp table with distinct input values and use thoce results in your join, but I would only do that it it was an issue - don't assume it's a problem and clutter your query unnecessarily.
If you declare your function as schema bound, it can be run one for each unique case. This requires that the function be deterministic and always has the same output for a given input.
CREATE FUNCTION dbo.fn_something (#id INT)
RETURNS INT
WITH SCHEMABINDING
AS
BEGIN
RETURN #id
END
GO