Break down an oracle script into smaller scripts - sql

I am trying to run a script which changes 200,000 records. But I want to change each 10000 record separately, so the first 10000 then the second 10000 and so on.
FOR d IN ids -- ids is a cursor which is filled by 200,000 record
LOOP
UPDATE doctor
SET name =
(SELECT name
FROM source
WHERE id = d.id
AND type =
(SELECT MAX(type)
FROM Da
WHERE i = id
)
)
WHERE pat = d.pat;
COMMIT;
END LOOP;

What are you implying by saying change 10000 records separately. Do you mean you want to commit in blocks of 10000s?
You can do that using a counter
record_count := 0
FOR d IN ids -- ids is a cursor which is filled by 200,000 record
LOOP
UPDATE doctor
SET name =
(SELECT name
FROM source
WHERE id = d.id
AND type =
(SELECT MAX(type)
FROM Da
WHERE i = id
)
)
WHERE pat = d.pat;
record_count := record_count +1;
IF mod(record_count,10000) = 0
THEN
COMMIT;
END IF;
END LOOP;
COMMIT; -- COMMIT for the last time if you have residual records

Related

Check if table is empty in SQLite

I want to know if there is an better way to check if a table of a given database is empty.
My version:
import sqlite3
con = sqlite3.connect('emails.db')
cur = con.cursor()
cur.execute("SELECT count(*) FROM (select 1 from my_table limit 1);")
print(cur.fetchall()[0][0])
con.close()
Output:
0 # If table `my_table' is empty
1 # If table 'my_table' is not empty
Yo can use:
SELECT EXISTS (SELECT 1 FROM my_table);
this will return 1 row with 1 column which will be 0 if the table is empty or 1 if it contains any rows.
EXISTS will return as soon as it finds the 1st row in the table and it will not scan the whole table.

How to update column value to a table from another table in a stored procedure?

I created a procedure from querying other tables including transaction tbl to settle all transaction records with a reference number and date automatically stamped on it.
What I try to do is my settle_transaction procedure needs to generate my query from the selected statement and insert them into the settlement table. Meanwhile, I also need to update the ref_num and processed date as a "stamp" to the transaction table so that I don't have duplicated settlement when calling the procedure again. Otherwise, I don't know how to stop showing the same settlement data twice
Here is the procedure to output a settlement tbl and structure similar below:
BEGIN
for r_client in
(
select clientid,
client_name, sum(transaction) total_amount
from transaction_tbl tran join terminal_tbl term
on tran.terminalid = term.terminalid join client_tbl c on c.clientid = term.clientid
where refnr is null
)
loop
v_refnr := get_refnr;
insert into settlement_tbl
(
Ref_Num,
Total,
CLIENTID,
TITLE,
processeddate
)
values (v_refnr, total_amount, clientid,
name,sysdate);
update_refnr(v_refnr, sysdate)
end loop;
END
Output:
| reference_num | total amount | client id | client name | processed_date |
|---------------|--------------|-----------|-------------|----------------|
When I execute the above procedure, it populates all the result from the select query. However, if I execute again, it will duplicate the same result especially the total amount.
I'm seeking a solution to put another procedure/function inside this settlement procedure to prevents duplicate records from the selected query in this procedure.
I use the ref. no# and process_date to update the existing reference num and date to the transaction tbl show below.
| transaction_num | transaction amount | reference_num | processed_date |
|-----------------|--------------------|---------------|----------------|
Here is the attempted code I put inside the settlement procedure but still shows duplicated records and can not update to the transaction tbl.
procedure update_refnr(
p_refnr in number,
p_processeddate in date
)
is
begin
UPDATE TRANSACTION t
SET t.refnr = p_refnr
WHERE EXISTS (SELECT p_processeddate
FROM terminal_tbl
WHERE t.TERMINALID= term.TERMINALID
AND t.processeddate = p_processeddate
AND t.refnr IS NULL);
--exception handling below
end update_refnr;
I also tried other SQL reference but cannot compile.
Ideally, I don't have duplicated records in my settlement tbl when I retrieve each record from my stored procedure.
You want to insert new data into your table only when it doesn't already exist. As others have said, you can use MERGE to do that:
BEGIN
for r_client in (select clientid,
client_name,
sum(transaction) total_amount
from transaction_tbl tran
join terminal_tbl term
on tran.terminalid = term.terminalid
join client_tbl c
on c.clientid = term.clientid
where refnr is null)
loop
v_refnr := get_refnr;
MERGE INTO settlement_tbl s
USING (SELECT v_refnr AS REF_NUM,
total_amount AS TOTAL,
clientid AS CLIENTID,
name AS TITLE,
SYSDATE AS PROCESSEDDATE
FROM DUAL) d
ON (s.REF_NUM = d.REF_NUM)
WHEN NOT MATCHED THEN
INSERT (Ref_Num, Total, CLIENTID, TITLE, processeddate)
VALUES (d.REF_NUM, d.TOTAL, d.CLIENTID, d.TITLE, d.PROCESSEDDATE);
update_refnr(v_refnr, sysdate);
END LOOP;
END;
WHEN NOT MATCHED inserts new data when v_refnr does not already exist in your table.
Best of luck.

Issue with the performance of the query

I have two tables tab1 and tab2 each are having two columns acc_num and prod_code. I need to update prod_code in tab2 from tab1. Below is the sample data in both the tables:
TAB1
acnum Prod
-------------------
1 A
2 B
2 C
3 X
3 X
Tab2
acnum Prod
-------------------
1 null
2 null
2 null
3 null
3 null
And for the 2nd table after update all the distinct codes should be concatenated. Below is the sample output.
Tab2
acnum Prod
-------------------
1 A
2 B|C
2 B|C
3 X
3 X
I am able to achieve this through PL/SQL, but it's taking ages to complete. (Actual tables are having millions of records). Below is the code I am using.
DECLARE
l_acnum dbms_sql.varchar2a;
l_prod dbms_sql.varchar2a;
l_prod2 VARCHAR2(10):= NULL;
l_count NUMBER := 0;
CURSOR cr_acnum
IS
SELECT DISTINCT(acnum) FROM tab1;
CURSOR cr_prod(l_acnum_dum IN VARCHAR2)
IS
SELECT prod FROM tab1 WHERE acnum = l_acnum_dum;
BEGIN
OPEN cr_acnum;
FETCH cr_acnum bulk collect INTO l_acnum;
CLOSE cr_acnum;
FOR i IN l_acnum.first .. l_acnum.last
LOOP
OPEN cr_prod(l_acnum(i));
FETCH cr_prod bulk collect INTO l_prod;
CLOSE cr_prod;
FOR m IN l_prod.first .. l_prod.last
LOOP
IF m <> 1 THEN
IF l_prod(m) = l_prod(m-1) THEN
l_prod2 := l_prod(m);
ELSE
l_prod2 := l_prod2||'|'||l_prod(m);
END IF;
ELSE
l_prod2 := l_prod(m);
END IF;
END LOOP;
UPDATE tab2 SET prod = l_prod2 WHERE acnum = l_acnum(i);
END LOOP;
END;
This pl/sql block is taking ages to complete. Is there anyway I can achieve the same through query rather than PL/SQL or may be by efficient PL/SQL. I tried BULK COLLECT also but of no use. Data is in Oracle DB. Thanks a lot for your time.
This will concatenate those values as long as they don't exceed a certain total length. You may also want to do a subquery to dedupe them if there are any dupes.
Update: here is with LISTAGG
update table2 set Prod = (
SELECT LISTAGG(t1.Prod, ', ') WITHIN GROUP (ORDER BY t1.Prod) "Prod"
FROM Table1 t1
where t1.acnum = table2.acnum)
Thanks everyone for your input. I achived the same using your inputs. I used 2 step solution:
Step 1) Create a lookup table for product and account no.
create table lkup_tbl
as SELECT acnum, LISTAGG(Prod, '|') WITHIN GROUP (ORDER BY Prod) product
FROM (select distinct acnum, Prod from tab1) tab
GROUP BY acnum;
Step 2) Now update all the tables by joining this lookup table.
update tab2 t1
set (t1.Prod) = (select product from lkup_tbl t2
where t2.acnum = t1.acnum
);

PL SQL loop through list of ids

I have a list of names.
john, sam, peter, jack
I want to query the same sql with each of above as the filter. Each query will give me a unique employee id, which I want to use to delete some other records.
select emp_id from employee where emp_name like '%john%';
Let's say for the first query, I get the id as 1001. So the delete queries would be like following.
delete from account_details where emp_id = 1001;
delete from hr_details where emp_id = 1001;
delete from pay_role_details where emp_id = 1001;
I have to repeat this for a list of employees. Pseudocode would be like following.
var emp_list = ['john', 'jack', 'kate', 'peter', 'sam',...]
for each :employee_name in emp_list
select emp_id as :var_emp_id from employee where emp_name like '%:employee_name%';
delete from account_details where emp_id = :var_emp_id;
delete from hr_details where emp_id = :var_emp_id;
delete from pay_role_details where emp_id = :var_emp_id;
end loop
I want a PL-SQL query to do this. Please help. Thanks.
What I tried is something like the following.
set serveroutput on;
begin
loop x in ('john','jack', 'kate') loop as :name
select emp_id as var_emp_id from employee where emp_name like '%:name%';
// delete queries
end loop;
end;
P.S. Although accoring to the question, like query may result in multiple records, in actual scenario, it is guaranteed to be only one record. Why I use like is that in actual scenario, it is a list of reference numbers instead of names. The reference number has some other pre texts and post texts and my comma seperated list has only the numbers.
Perhaps the following will help:
BEGIN
FOR aName IN (SELECT 'john' AS EMP_NAME FROM DUAL
UNION ALL
SELECT 'sam' AS EMP_NAME FROM DUAL
UNION ALL
SELECT 'peter' AS EMP_NAME FROM DUAL
UNION ALL
SELECT 'jack' AS EMP_NAME FROM DUAL)
LOOP
FOR emp IN (SELECT * FROM EMPLOYEE WHERE EMP_NAME LIKE '%' || aName.EMP_NAME || '%')
LOOP
DELETE FROM ACCOUNT_DETAILS a WHERE a.EMP_ID = emp.EMP_ID;
DELETE FROM HR_DETAILS h WHERE h.EMP_ID = emp.EMP_ID;
DELETE FROM PAY_ROLE_DETAILS p WHERE p.EMP_ID = emp.EMP_ID;
DBMS_OUTPUT.PUT_LINE('Deleted data for employee with EMP_ID=' || emp.EMP_ID);
END LOOP; -- emp
END LOOP; -- aName
END;
Study this until you understand how and why it works.
Share and enjoy.
Do you really need a cursor to do so? Try to skip cursor if possible to avoid poor performance/memory usage on huge data.
delete from account_details inner join employee on account_details.emp_id = employee.emp_id where WHERE CONTAINS(employee.emp_name, '"John" OR "Sam" OR "Max"', 1) >0;
delete from hr_details inner join employee on hr_details.emp_id = employee.emp_id where WHERE CONTAINS(employee.emp_name, '"John" OR "Sam" OR "Max"', 1) >0;
delete from pay_role_details inner join employee on pay_role_details.emp_id = employee.emp_id where WHERE CONTAINS(employee.emp_name, '"John" OR "Sam" OR "Max"', 1) >0;
Use a PL/SQL cursor to select all the IDs you want to delete and then just loop it and issue the DELETE statements with every pass.
In-depth info on cursors can be found here: http://www.oracle.com/technetwork/issue-archive/2013/13-mar/o23plsql-1906474.html
For dynamic SQL see here: http://docs.oracle.com/cd/E11882_01/appdev.112/e25519/dynamic.htm#LNPLS627
Code example:
PROCEDURE delete_stuff
IS
id AS NUMBER;
CURSOR your_cursor IS
SELECT emp_id FROM employee WHERE CONTAINS(employee.emp_name, '"John" OR "Sam" OR "Max"', 1) > 0;
OPEN your_cursor;
LOOP
FETCH your_cursor INTO id;
EXIT WHEN your_cursor%NOTFOUND;
EXECUTE IMMEDIATE 'DELETE FROM account_details WHERE emp_id = :id' USING id;
EXECUTE IMMEDIATE 'DELETE FROM hr_details WHERE emp_id = :id' USING id;
EXECUTE IMMEDIATE 'DELETE FROM pay_role_details WHERE emp_id = :id' USING id;
CLOSE your_cursor;
END LOOP;
EXCEPTION
WHEN OTHERS THEN NULL;
END delete_stuff;

Assigning a unique id to the first instance of a string in PL/SQL

I have a set of data that has multiple rows with the same unique_string_identifier. I want to assign a new unique ID from a sequence to the first instance of a row with that unique_string_identifier then give any following rows with the same unique_string_identifier the ID times -1. I've tried it three different ways, but I always get
ORA-30483: window functions are not allowed here
Here are my attempts:
UPDATE my_table
set my_id =
CASE WHEN LAG(unique_string_identifier, 1, '-') OVER (order by unique_string_identifier) <> unique_string_identifier THEN my_id_seq.nextval
ELSE LAG(-1 * my_id, 1, '-') OVER (order by unique_string_identifier) END CASE
where import_run_id = a_run_id;
I've also tried this:
UPDATE my_table
set my_id = my_id_seq.nextval
where row_number() over (partition by unique_string_identifier order by line_id) = 1;
//another update statement to make the rows with null ID's equal to the negative id joined on unique_string_identifier
And this:
UPDATE my_Table
set my_id =
decode(unique_string_identifier, LAG(unique_string_identifier, 1, '-') OVER (order by unique_string_identifier), LAG( my_id, 1, '-') OVER (order by unique_string_identifier), my_id_seq.nextval)
where import_run_id = a_run_id;
How can I make this work?
EDIT: Also for my own enrichment, if anyone can explain why these 3 statements (which all seem pretty different to me) end up getting the exact same ORA error, I'd appreciate it.
I couldn't work out a simple MERGE or set of UPDATEs, but here is a potential solution that might work fine, tested on Oracle 11g, using PL/SQL:
Test scenario:
create table my_table (unique_string varchar2(100));
insert into my_table values ('aaa');
insert into my_table values ('aaa');
insert into my_table values ('aaa');
insert into my_table values ('bbb');
insert into my_table values ('bbb');
insert into my_table values ('ccc');
alter table my_table add (id number);
create sequence my_seq;
Here's the PL/SQL to do the update:
declare
cursor c is
select unique_string
,row_number()
over (partition by unique_string order by 1)
as rn
from my_table
order by unique_string
for update of id;
r c%rowtype;
begin
open c;
loop
fetch c into r;
exit when c%notfound;
if r.rn = 1 then
update my_table
set id = my_seq.nextval
where current of c;
else
update my_table
set id = my_seq.currval * -1
where current of c;
end if;
end loop;
close c;
end;
/
Results from my test (note that the sequence had advanced a little by this stage):
select * from my_table;
UNIQUE_STRING ID
============= ==
aaa 7
aaa -7
aaa -7
bbb 8
bbb -8
ccc 9
P.S. I've been a bit sneaky and taken advantage of Oracle's tendency to return ROW_NUMBER in the order that the rows are returned; to be more robust and correct, I'd put the query in a subquery and ORDER BY unique_string, rn.