PL SQL map result set to multiple records - sql

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;

Related

Nested cursor performance tuning

I have 2 cursors , one to fetch records from a table of 50 columns and 10,000 + data and another to check if a particular column exists in another big table (2 million data). I should write to a file all the records from cursor 1 for a year , if that column exists in cursor 2 then i should print an error message as exists and not delete them . If it does not exist then i should delete the row and write it to the same file and message as record deleted.
I used a nested cursor , the performance is too bad as it is processing each row from cursor 1 against cursor 2 , every time .
CURSOR cursor1
IS
select a.* ,a.rowid
FROM table1 a
WHERE a.table1.year = p_year;
CURSOR check_c2(lv_cd )
IS
Select DISTINCT 'Y'
from table2
where table2 ='R'
AND table2.year= p_year
and table2_code= lv_cd ;
BEGIN :
FOR r in cursor1 LOOP
EXIT WHEN cursor1%NOTFOUND;
OPEN check_c2(r.cd);
FETCH check_c2 INTO lv_check;
IF check_c2%NOTFOUND THEN
lv_check :='N';
END IF;
CLOSE check_c2;
IF lv_check ='Y' THEN
lv_msg =(r.col1,r.col2....r.col50, R code exists do not delete)
utl_file.put_line(lv_log_file, lv_msg, autoflush=>TRUE);
ELSE
DELETE from table1 where rowid= r.rowid
lv_msg =(r.col1,r.col2....r.col50, delete row)
utl_file.put_line(lv_log_file, lv_msg, autoflush=>TRUE);
END IF;
END LOOP;
Don't have enough reputation to write comments som will write as an answer.
Didn't you try to add some time marks to understand which parts are the most time spending?
Does table2 have index by year and code? What's the explain plan of cursor2 query? If yes - how many rows are there average for year+code combination?
If the amount of data selected overall from table 2 is huge - then it probably can be faster to do a single query with full scan/index range scan by year on table2, grouping and hash left outer join from table1 to table2 like
select a.*, a.rowid, nvl2(c.code, 'Y', 'N') check_col
from table1 a,
(
select distinct code
from table2 b
where b.year = p_year
) c
where a.year = p_year
and c.code(+) = a.cd
How about this? A 3-steps-operation:
Step 1: "save" rows you'll later delete
create table log_table as
select *
from table1 a
where exists (select null
from table2 b
where b.year = a.year
and b.code = a.code
);
Step 2: delete rows:
delete from table1 a
where exists (select null
from table2 b
where b.year = a.year
and b.code = a.code
);
Step 3: if you must, store rows saved in the LOG_TABLE into that file of yours. If not, leave them in LOG_TABLE.
utl_file.put_line in the loop will be an overhead.Try appending to lv_msg till length of the string is 32767 bytes and write just once.
This will definitely reduce the I/O and performance should be improved.

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;

SQL query to use different tables conditionally

I have a stored procedure which uses different tables for a join based on an input parameter. Currently I have to write the SQL query twice (with only the table name difference). Is it possible to combine them so I do not have to repeat SQL query logic twice?
Current code:
CREATE PROCEDURE SampleProc
#Condition BIT
AS
IF #Condition = 0
BEGIN
SELECT *
FROM TableA1 A /* Use TableA1 instead of TableA2 */
INNER JOIN TableB B ON A.Id = B.Id /* The rest of the query remains the same */
/* Inner Join some more complex join logic */
END
ELSE
BEGIN
SELECT *
FROM TableA2 A /* Use TableA2 instead of TableA1 */
INNER JOIN TableB B ON A.Id = B.Id /* The rest of the query remains the same */
/* Inner Join some more complex join logic */
END
END
One of the possible ways is to store TableA1 / TableA2 data to a temp table first and use the temp table to join inside a complex query. Is there any better way?
If the two tables have the same structure (as implied by the temp table comment), you can do:
select . . .
from ((select a.* from tablea1 a where #condition = 0
) union all
(select a.* from tablea2 a where #condition <> 0
)
) a inner join
b
Another alternative is dynamic SQL, but that can be tricky to maintain -- because the code looks like a string.
Sometimes, you can do this with a left join as well:
select b.*, coalesce(a1.col, a2.col) as col
from b left join
tablea1 a1
on a1.id = b.id and #condition = 0 left join
tablea2 a2
on a2.id = b.id and #condition <> 0
where a1.id is not null or a2.id is not null
. . .
Although this should have good performance, it has the downside that all references to a columns need to use coalesce().
if TableA1 and TableA2 have same columns, try this
SELECT
*
From
( select
*
from
TableA1
where
#Condition = 0
union all
select
*
from
TableA2
where
#Condition != 0) as A
INNER JOIN
TableB B
On
A.Id =B.Id

Inner join 2 tables but return all if 1 table empty

I have 2 tables say A and B, and I want to do a join on them.
Table A will always have records in it.
When table B has rows in it, I want the query to turn all the rows in which table A and table B matches. (i.e. behave like inner join)
However, if table B is empty, I'd like to return everything from table A.
Is this possible to do in 1 query?
Thanks.
Yes, for results like this, use LEFT JOIN.
Basically what INNER JOIN does is it only returns row where it has atleast one match on the other table. The LEFT JOIN, on the other hand, returns all records on the left hand side table whether it has not match on the other table.
To further gain more knowledge about joins, kindly visit the link below:
Visual Representation of SQL Joins
I came across the same question and, as it was never answered, I post a solution given to this problem somewhere else in case it helps someone in the future.
See the source.
select *
from TableA as a
left join TableB as b
on b.A_Id = a.A_Id
where
b.A_Id is not null or
not exists (select top 1 A_Id from TableB)
Here is another one, but you need to add one "null" row to table B if it's empty
-- In case B is empty
Insert into TableB (col1,col2) values (null,null)
select *
from TableA as a inner join TableB as b
on
b.A_Id = a.A_Id
or b.A_Id is null
I would use an if-else block to solve it like below:
if (select count(*) from tableB) > 0
begin
Select * from TableA a Inner Join TableB b on a.ID = b.A_ID
end
else
begin
Select * from TableA
end
Try This
SELECT t1.* FROM table1 AS t1 INNER JOIN table2 AS t2 ON t1.something = t2.someotherthing UNION SELECT * FROM table1 WHERE something = somethingelse;
This is solution:
CREATE TABLE MyData(Id INT, Something VARCHAR(10), OwnerId INT);
CREATE TABLE OwnerFilter(OwnerId INT);
SELECT *
FROM
(SELECT NULL AS Gr) AS Dummy
LEFT JOIN OwnerFilter F ON (1 = 1)
JOIN MyData D ON (F.OwnerId IS NULL OR D.OwnerId = F.OwnerId);
Link to sqlfiddle: http://sqlfiddle.com/#!6/0f9d9/7
I did the following:
DECLARE #TableB TABLE (id INT)
-- INSERT INTO #TableB
-- VALUES (some ids to filter by)
SELECT TOP 10 *
FROM [TableA] A
LEFT JOIN #TableB B
ON A.ID = B.id
WHERE B.id IS NOT NULL
OR iif(exists(SELECT *
FROM TableB), 1, 0) = 0
Now:
If TableB is empty (leave the commented lines commented) you'll get the top 10.
If TableB has some ids in it, you'll only join by those.
I do not know how efficient this is. Comments are welcome.
Maybe use a CTE
;WITH ctetable(
Select * from TableA
)
IF(EXISTS(SELECT 1 FROM TableB))
BEGIN
Select * from ctetable
Inner join TableB
END
ELSE
BEGIN
Select * from ctetable
END
or dynamic SQL
DECLARE #Query NVARCHAR(max);
SET #QUERY = 'Select * FROM TableA';
IF(EXISTS(SELECT 1 FROM TableB))
BEGIN
SET #QUERY = CONCAT(#QUERY,' INNER JOIN TableB');
END
EXEC sp_executesql #Query

Deleting rows from multiple related tables

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.