Oracle PL/SQL Creating tables in cursor? - oracle

I'm relatively new to PL/SQL, so please bear with me.
I'm creating two tables that will be used in the FOR loop cursor. My intention was to create these two tables at the start (before the cursor loop), truncate them inside the cursor loop whenever I need it cleared and insert new values, then drop them at after the cursor loop completes.
(After looking a bit on SO, I think this can be done with REF CURSOR, but I'm not very clear on how to use that so I decided to go ahead with the first approach.)
I tried creating the tables in the DECLARE section, then in the BEGIN section before the loop starts, but it raises the error 'ORA-065500 and PLS-00103 Encountered the symbol CREATE when expecting begin function pragma ...'
Note that I am dropping the tables after the LOOP ends but before the END section of the cursor.
Should I create the tables outside the cursor , before the DECLARE section, and drop them after the cursor END section? I thought it should be possible to create permanent oracle tables inside cursors?
Update: Posting code here-
1)
DECLARE
CREATE TABLE T1
(
col1 VARCHAR2(128),
col2 VARCHAR2(128),
col3 NUMBER(3) NOT NULL,
col3 FLOAT(100)
);
CREATE TABLE T2 AS
SELECT * FROM other_table WHERE 1 = 0;
CURSOR CUR IS ...
BEGIN
FOR rec IN CUR
LOOP
--Do stuff here
END LOOP;
Drop table T1;
Drop table T2;
END;
/
2)
DECLARE
CURSOR CUR IS ...
BEGIN
CREATE TABLE T1
(
col1 VARCHAR2(128),
col2 VARCHAR2(128),
col3 NUMBER(3) NOT NULL,
col3 FLOAT(100)
);
CREATE TABLE T2 AS
SELECT * FROM other_table WHERE 1 = 0;
FOR rec IN CUR
LOOP
--Do stuff here
END LOOP;
Drop table T1;
Drop table T2;
END;
/
(1) and (2) both don't work.
Update- Do we need EXECUTE IMMEDIATE for this? How do I know when I need EXECUTE IMMEDIATE? Do we also need EXECUTE IMMEDIATE for truncating tables in cursors?

Usually you don't perform DDLs (create, alter, drop) inside your PL/SQL procedure. If you need a table to store some temporary data, you can create temporary tables for this purpose. In your case I would first create the tables
CREATE GLOBAL TEMPORARY TABLE T1
(
col1 VARCHAR2(128),
col2 VARCHAR2(128),
col3 NUMBER(3) NOT NULL,
col3 FLOAT(100)
);
CREATE GLOBAL TEMPORARY TABLE T2 AS
SELECT * FROM other_table WHERE 1 = 0;
And then the procedure would look like this
DECLARE
CURSOR CUR IS ...
BEGIN
FOR rec IN CUR
LOOP
--Do stuff here
END LOOP;
DELETE FROM T1;
DELETE FROM T2;
END;
/
Of course the tables would not be dropped after that, but I suppose you want to use your PL/SQL procedure regularly, not only once, right?
If you still want to peform DDL in your procedure, then you must use dynamic sql (execute immediate). However you have to be aware that DDL operations perform implicit commits, so your procedure would not be a single atomic transaction.

Related

Procedure to Create Backup Table For multiple table each having different Where condition

Create or replace procedure PROC AS
V_TABLE_NAME VARCHAR2(255);
V_LIST SYS_REFCURSOR;
DATE_VALUE_INS VARCHAR2(10);
BEGIN
DATE_VALUE_INS:=TO_CHAR(SYSDATE,'YYMMDD');
OPEN V_LIST FOR
SELECT NAME FROM DW.table_name_list ;
LOOP
FETCH V_LIST
INTO V_TABLE_NAME;
EXIT WHEN V_LIST%NOTFOUND;
EXECUTE IMMEDIATE 'CREATE TABLE Schema.'||V_TABLE_NAME||'_'||DATE_VALUE_INS||' AS SELECT * FROM DW.'||V_TABLE_NAME;
END LOOP;
CLOSE V_LIST;
end;
I have created this Proc which takes value from a table which has Table_name and create Backup using Execute Immediate.
Now the requirement has changed that i only need to create backup for partial records (i.e. where clause on each table )
I have 6 tables as such .
New Approach i am thinking is :
EXECUTE IMMEDIATE 'CREATE TABLE Schema.'||V_TABLE_NAME||'_'||DATE_VALUE_INS||' AS SELECT * FROM DW.'||V_TABLE_NAME where some condition;
But the problem becomes all 6 have different column to filter on.
My Ask is How should I change my design of proc to Adjust this new Requirement.
6 tables? Why bother? Create a procedure which - depending on table name passed as a parameter - in IF-THEN-ELSE runs 6 different CREATE TABLE statements.
On the other hand, another approach would be to create backup tables in advance (at SQL level), add BACKUP_DATE column to each of them, and - in procedure - just perform INSERT operation which doesn't require dynamic SQL at all.
For example:
create table emp_backup as select * from emp where 1 = 2;
alter table emp_backup add backup_date date;
create or replace procedure p_backup (par_table_name in varchar2) is
begin
if par_table_name = 'EMP' then
insert into emp_backup (empno, ename, job, sal, backup_date)
select empno, ename, job, sal, trunc(sysdate)
from emp
where deptno = 20; --> here's your WHERE condition
elsif par_table_name = 'DEPT' then
insert into dept_backup (...)
select ..., trunc(sysdate)
from dept
where loc = 'DALLAS';
elsif ...
...
end if;
end;
/
Doing so, you'd easier access backup data as you'd query only one table, filtered by BACKUP_DATE. That's also good if you have to search for some data that changed several days ago, but you don't know exact day. What would you rather do: query 10 tables (and still not find what you're looking for), or query just one table and find that info immediately?

How to write procedure to copy data from one table to another table

Hello I have this situation
I have a table with 400 millions of records I want to migrate some data to another table for get better performance.
I know that the process could be slow and heavy and I want to execute something like this
INSERT INTO TABLLE_2 SELECT NAME,TAX_ID,PROD_CODE FROM TABLE_1;
But I want this in a procedure that iterates the table from 50000 to 50000 records.
I saw this but it DELETE the rows and the target is SQLServer and not Oracle
WHILE 1 = 1
BEGIN
DELETE TOP (50000) FROM DBO.VENTAS
OUTPUT
Deleted.AccountNumber,
Deleted.BillToAddressID,
Deleted.CustomerID,
Deleted.OrderDate,
Deleted.SalesOrderNumber,
Deleted.TotalDue
INTO DATOS_HISTORY.dbo.VENTAS
WHERE OrderDate >= '20130101'
and OrderDate < '20140101'
IF ##ROWCOUNT = 0 BREAK;
END;
If it's just for a one-time migration, don't paginate/iterate at all. Just use a pdml append insert-select:
BEGIN
EXECUTE IMMEDIATE 'alter session enable parallel dml';
INSERT /*+ parallel(8) nologging append */ INTO table_2
SELECT name,tax_id,prod_code from table_1;
COMMIT;
EXECUTE IMMEDIATE 'alter session disable parallel dml';
END;
Or if the table doesn't yet exist, CTAS it to be even simpler:
CREATE OR REPLACE TABLE table_2 PARALLEL (DEGREE 8) NOLOGGING AS
SELECT name,tax_id,prod_code FROM table_1;
If you absolutely must iterate due to other business needs you didn't mention, you can use PL/SQL for that as well:
DECLARE
CURSOR cur_test
IS
SELECT name,tax_id,prod_code
FROM table_1;
TYPE test_tabtype IS TABLE OF cur_test%ROWTYPE;
tab_test test_tabtype;
var_limit integer := 50000;
BEGIN
OPEN cur_test;
FETCH cur_test BULK COLLECT INTO tab_test LIMIT var_limit;
LOOP
-- do whatever else you need to do
FORALL i IN tab_test.FIRST .. tab_test.LAST
INSERT INTO table_2
(name,tax_id,prod_code)
VALUES (tab_test(i).name, tab_test(i).tax_id, tab_test(i).prod_code);
COMMIT;
EXIT WHEN tab_test.COUNT < var_limit;
FETCH cur_test BULK COLLECT INTO tab_test LIMIT var_limit;
END LOOP;
CLOSE cur_test;
END;
That won't be nearly as fast, though. If you need even faster, you can create a SQL object and nested table type instead of the PL/SQL types you see here and then you can do a normal INSERT /*+ APPEND */ SELECT ... statement that will use direct path. But honestly, I doubt you really need iteration at all..

Declare Table Variable in Oracle Procedure

I'm having a heck of a time trying to find an example of this being done. I have a procedure, and as part of that procedure I want to store the results of a SELECT statement so that I can work against that set, and then use it as a reference to update the original records when it's all done.
The difficulty I'm having is in declaring the temporary table variable. Here's an example of what I'm trying to do:
PROCEDURE my_procedure
IS
output_text clob;
temp_table IS TABLE OF MY_TABLE%ROWTYPE; -- Error on this line
BEGIN
SELECT * BULK COLLECT INTO temp_table FROM MY_TABLE WHERE SOME_DATE IS NULL;
-- Correlate results into the clob for sending to email (working)
-- Set the SOME_DATE value of the original record set where record is in temp_table
I get an error on the second occurrence of IS, saying that it is an unexpected symbol. This suggests to me that my table variable declaration is either wrong, or in the wrong place. I've tried putting it into a DECLARE block after BEGIN, but I just get another error.
Where should this declaration go? Alternatively, if there is a better solution I'll take that too!
CREATE OR REPLACE PROCEDURE PROCEDURE1 AS
output_text clob;
type temp_table_type IS TABLE OF MY_TABLE%ROWTYPE;
temp_table temp_table_type;
BEGIN
SELECT * BULK COLLECT INTO temp_table FROM MY_TABLE;
END PROCEDURE1;
or
CREATE OR REPLACE PROCEDURE PROCEDURE1 ( output_text OUT clob ) IS
type temp_table_type IS TABLE OF MY_TABLE%ROWTYPE
INDEX BY BINARY_INTEGER;
temp_table temp_table_type;
BEGIN
SELECT * BULK COLLECT INTO temp_table FROM MY_TABLE;
FOR indx IN 1 .. temp_table.COUNT
LOOP
something := temp_table(indx).col_name;
END LOOP;
END PROCEDURE1;
I had a similiar problem and found this:
Selecting Values from Oracle Table Variable / Array?
The global temporary table can be used like a regular table, but its content is only temporary (deleted at end of session/transaction) and each session has its own table content.
If you don't need dynamic SQL this can be used as good solution:
CREATE GLOBAL TEMPORARY TABLE temp_table
(
column1 NUMBER,
column2 NUMBER
)
ON COMMIT DELETE ROWS;
PROCEDURE my_procedure
IS
output_text clob;
BEGIN
-- Clear temporary table for this session (to be sure)
DELETE FROM temp_table;
-- Insert data into temporary table (only for this session)
INSERT INTO temp_table SELECT * FROM MY_TABLE WHERE SOME_DATE IS NULL;
-- ...
END;
The only disadvantages are, in my opinion, that you got another table and that the temporary table is not dynamic.

FORALL+ EXECUTE IMMEDIATE + INSERT Into tbl SELECT

I have got stuck in below and getting syntax error - Please help.
Basically I am using a collection to store few department ids and then would like to use these department ids as a filter condition while inserting data into emp table in FORALL statement.
Below is sample code:
while compiling this code i am getting error, my requirement is to use INSERT INTO table select * from table and cannot avoid it so please suggest.
create or replace Procedure abc(dblink VARCHAR2)
CURSOR dept_id is select dept_ids from dept;
TYPE nt_dept_detail IS TABLE OF VARCHAR2(25);
l_dept_array nt_dept_detail;
Begin
OPEN dept_id;
FETCH dept_id BULK COLLECT INTO l_dept_array;
IF l_dept_array.COUNT() > 0 THEN
FORALL i IN 1..l_dept_array.COUNT SAVE EXCEPTIONS
EXECUTE IMMEDIATE 'INSERT INTO stg_emp SELECT
Dept,''DEPT_10'' FROM dept_emp'||dblink||' WHERE
dept_id = '||l_dept_array(i)||'';
COMMIT;
END IF;
CLOSE dept_id;
end abc;
Why are you bothering to use cursors, arrays etc in the first place? Why can't you just do a simple insert as select?
Problems with your procedure as listed above:
You don't declare procedures like Procedure abc () - for a standalone procedure, you would do create or replace procedure abc as, or in a package: procedure abc is
You reference a variable called "dblink" that isn't declared anywhere.
You didn't put end abc; at the end of your procedure (I hope that was just a mis-c&p?)
You're effectively doing a simple insert as select, but you're way over-complicating it, plus you're making your code less performant.
You've not listed the column names that you're trying to insert into; if stg_emp has more than two columns or ends up having columns added, your code is going to fail.
Assuming your dblink name isn't known until runtime, then here's something that would do what you're after:
create Procedure abc (dblink in varchar2)
is
begin
execute immediate 'insert into stg_emp select dept, ''DEPT_10'' from dept_emp#'||dblink||
' where dept_id in (select dept_ids from dept)';
commit;
end abc;
/
If, however, you do know the dblink name, then you'd just get rid of the execute immediate and do:
create Procedure abc (dblink in varchar2)
is
begin
insert into stg_emp -- best to list the column names you're inserting into here
select dept, 'DEPT_10'
from dept_emp#dblink
where dept_id in (select dept_ids from dept);
commit;
end abc;
/
There appears te be a lot wrong with this code.
1) why the execute immediate? Is there any explicit requirement for that? No, than don't use it
2) where is the dblink variable declared?
3) as Boneist already stated, why not a simple subselect in the insert statement?
INSERT INTO stg_emp SELECT
Dept,'DEPT_10' FROM dept_emp#dblink WHERE
dept_id in (select dept_ids from dept );
For one, it would make the code actually readable ;)

How do you select into a nested type in oracle pl/sql?

I want to be able to delete by rowid then immediately insert the data being deleted in an audit table.
There are far too many records to
INSERT INTO ... SELECT CRITERIA then DELETE ... CRITERIA.
I already know how to do everything just using rowid and INSERT INTO ... SELECT.
Inside package body:
TYPE some_type IS RECORD (
row_id ROWID,
full_row table_name%ROWTYPE
);
TYPE some_type_list IS TABLE OF some_type
INDEX BY BINARY_INTEGER;
PROCEDURE do_stuff
IS
lc_data SYS_REFCURSOR;
lt_recs some_type_list;
BEGIN
OPEN lc_date FOR
SELECT rowid, a.*
FROM table_name;
LOOP
FETCH lc_data
BULK COLLECT INTO lt_recs
LIMIT 50000;
EXIT WHEN lt_recs.COUNT = 0;
--
FORALL i IN lt_recs.FIRST..lt_recs.LAST
DELETE table_name
WHERE ROWID = lt_recs(i).row_id;
--
FORALL i IN lt_recs.FIRST..lt_recs.LAST
INSERT INTO table_name_audit VALUES lt_recs(i).full_row;
END LOOP;
END;
If I try that i get the following error:
Line: 117 Column: 25 Type: error Text: PLS-00597: expression 'LT_RECS' in the INTO list is of wrong type
Oracle versions prior to 11gR2 restrict us to use BULK COLLECT into a collection (nested table or varray) of records. Read more here on Oracle Docs.
If you want to see how it is done in 11gR2, scroll down to EDIT 2 section of this answer.
An alternative tho this can be the use of separate collections for every column- an approach that is most widely used. In this you can have:
/*
TYPE some_type IS RECORD (
row_id ROWID,
full_row table_name%ROWTYPE
);
TYPE some_type_list IS TABLE OF some_type
INDEX BY BINARY_INTEGER;
-- */
CREATE TYPE t_row_id IS TABLE OF ROWID;
CREATE TYPE t_col1 IS TABLE OF table_name.col1%TYPE;
CREATE TYPE t_col2 IS TABLE OF table_name.col2%TYPE;
CREATE TYPE t_col3 IS TABLE OF table_name.col3%TYPE;
...
...
CREATE TYPE t_colN IS TABLE OF table_name.colN%TYPE;
PROCEDURE do_stuff
IS
lc_data SYS_REFCURSOR;
-- lt_recs some_type_list;
row_id t_row_id;
col1 t_col1;
col2 t_col2;
col3 t_col3;
...
...
colN t_colN;
BEGIN
OPEN lc_date FOR
SELECT rowid, a.*
FROM table_name;
LOOP
FETCH lc_data
BULK COLLECT INTO row_id, col1, col2, col3, ..., colN
LIMIT 50000;
EXIT WHEN lt_recs.COUNT = 0;
--
FORALL i IN row_id.FIRST..row_id.LAST
DELETE table_name
WHERE ROWID = row_id(i);
--
FORALL i IN col1.FIRST..col1.LAST
INSERT INTO table_name_audit VALUES (col1(i), col2(i), col3(i), ..., colN(i));
END LOOP;
END;
I have not removed many of the rows in your program in order to let you understand the changes.
EDIT : Refer to the "Restrictions on BULK COLLECT" section of the Oracle Docs link I have given above and also here.
EDIT #2 :
You have to use CREATE TYPE ... IS OBJECT instead of RECORD. Also, You need to modify the SELECT statement the way I have done when I tried it. Please see the Oracle Docs here and a StackOverflow question here for further reference.
The code I tried on my machine (runs Oracle 11g R2) is as follows:
-- SELECT * FROM user_objects WHERE object_type = 'TYPE';
CLEAR SCREEN;
SET SERVEROUTPUT ON;
CREATE OR REPLACE TYPE temp_t_test AS OBJECT ( -- << OBJECT, not RECORD.
test_id INTEGER
, test_val VARCHAR2(50)
);
/
CREATE OR REPLACE TYPE temp_tbl_test AS TABLE OF TEMP_T_TEST;
/
DECLARE
v_test TEMP_TBL_TEST;
BEGIN
SELECT temp_t_test(t_id, t_val) -- << Notice the syntax
-- I'm selecting the columns as the defined OBJECT type.
BULK COLLECT INTO v_test
FROM (SELECT 1 AS t_id, 'ABCD' AS t_val FROM dual
UNION ALL
SELECT 2, 'WXYZ' FROM dual
UNION ALL
SELECT 3, 'PQRS' FROM dual);
dbms_output.put_line('Bulk Collect Successful!');
END;
/
** OUTPUT **:
TYPE temp_t_test compiled
TYPE temp_tbl_test compiled
anonymous block completed
Bulk Collect Successful!
I don't think that I'd take this approach at all, to be honest.
A faster method would be along the lines of performing a multitable insert:
insert the table columns into the audit table, possibly using direct path (APPEND hint) for efficiency
insert the rowid's into an on-commit-delete-rows global temporary table.
Then perform a delete against the original table using DELETE .. WHERE ROWID IN (SELECT ORIGINAL_ROWID FROM MY_GLOBAL_TEMP_TAB)
... and then commit.
Faster, and less code I think.
What you're trying to works in 11gR2 - what version are you on?.
The only wrong-looking thing in your post is this:
OPEN lc_date FOR
SELECT rowid, a.*
FROM table_name;
It ought to be this ...
OPEN lc_data FOR
SELECT a.rowid, a.*
FROM table_name a;
... but these may simply be typos you introduced when sanitizing your code to post here.

Resources