How can I dynamically recover columnName_N from a cursor, N being a number incremented in a LOOP? - sql

thanks for taking time to read and maybe answer my question!
Note that I am a beginner and should not be considered a pro but i did search for the answer without finding it , maybe due to my uncommon problem and/or lack of knowledge about it.
I have the following problem at work, i know it is not really supposed to happen, but here it is, on my desk... :
I have a table (conv_temp1) with the following columns:
ID No Sigle Info COLUMN_1 COLUMN_2 COLUMN_3 COLUMN_4 COLUMN_5 .. COLUMN_50
I have this cursor:
CURSOR c_minis IS
SELECT *
FROM conv_temp1;
I am trying to do something as the following:
FOR v_rsrec IN c_minis LOOP
l_column_i := 1;
dbms_output.put_line('----- Beginning - v_rsrec.id ----');
FOR boucle IN REVERSE 1.. 50 LOOP
--this is my problem, i am trying to acces a cursor column "dynamically"
EXECUTE IMMEDIATE 'v_declared_varchar2 := COLUMN_'|| l_column_i ||';';
IF v_declared_varchar2 IS NOT NULL THEN
dbms_output.put_line('I am doing something with the information!');
--(Dont worry, YES I am re-structuring it in a new table...)
END IF;
l_column_i := l_column_i + 1;
END LOOP;
dbms_output.put_line('-----c end - v_rsrec.id ----');
END LOOP;
Is there a way to perform such a thing as accessing a different column (only the number changes in the name of those) depending on where i am in my iterations?
ex, if I have already done 10 iterations, i will recover information from COLUMN_11 in my cursor.

A better solution would be to normalize the table. Break it into two tables as:
CREATE TABLE CONV_TEMP_HEADER
(ID_NO NUMBER
CONSTRAINT PK_CONV_TEMP_HEADER
PRIMARY KEY
USING INDEX,
SIGLE_INFO VARCHAR2(100)); -- or whatever
CREATE TABLE CONV_TEMP_DETAIL
(ID_DETAIL NUMBER,
ID_NO NUMBER
CONSTRAINT CONV_TEMP_DETAIL_FK1
REFERENCES CONV_TEMP_HEADER(ID_NO)
ON DELETE CASCADE,
IDX NUMBER,
COLUMN_VALUE VARCHAR2(100)
CONSTRAINT CONV_TEMP_DETAIL_UQ1
UNIQUE(ID_NO, IDX));
This way, instead of having to generate column names dynamically and figure out how to use DBMS_SQL, you can get your data using a simple join:
SELECT h.*, d.*
FROM CONV_TEMP_HEADER h
LEFT OUTER JOIN CONV_TEMP_DETAIL d
ON d.ID_NO = h.ID_NO;
Share and enjoy.

For other people with basic knowledge and the same needs, here is my solution:
CREATE OR REPLACE VIEW temp_view_name AS
SELECT ROWNUM AS ind, t.* FROM (
SELECT DISTINCT m.*
FROM conv_temp1 m ) t ;
This should be accessible from a EXECUTE IMMEDIATE query while a cursor isnt.

Related

Oracle View loop on specific tables only

I got many tables of which some end with digits (date).
Due to technical reasons I had to create a view on every table (just a workaround)
The current state is something like:
begin
for i in (select table_name
from user_tables
where table_name like 'XXX_%'
)
loop
execute immediate 'CREATE VIEW '||replace(i.TABLE_NAME,'XXX_','YYY_')||' AS SELECT * FROM '||i.TABLE_NAME;
end loop;
end;
/
All it does is creating views for every table which begins with 'XXX_'. Its almost what I need.
But now, sometimes tablenames look like "XXX_tablename_20210302", which is a manual backup.
Its not always 8 digits (date), sometimes shorter, soemtimes longer numbers. I would like to avoid all tables, which do have numbers in the end of the tablename (from right to left till first "_" checkup, its a number maybe?)
Does anyone know how to solve it?
Im kind of stuck here.
You can use regular expressions with regexp_like:
create table xxx_t ( c1 int );
create table xxx_t_20210401 ( c1 int );
select table_name from user_tables
where regexp_like ( table_name, '^XXX_.*[^0-9]+$' );
TABLE_NAME
XXX_T
This finds all the tables that
Start with XXX_ XXX_
Followed by any characters .*
That do not end with a digit [^0-9]+$

Insert row into SQL table based on existing row

I need to add a new row to a table in Oracle. The problem is that the table has 50 columns and I really don't want to write them all out for an INSERT statement. I tried to do a SELECT INTO statement to duplicate the row and then change the fields I care about individually, but this results in a UNIQUE violation on the primary key. So what I really want to do is declare a variable that holds one row without naming all the columns, change the primary key field, and then insert that variable. How do?
You can use %ROWTYPE in an anonymous PL/SQL block to declare a record representing a row from a table and then select a row into that record and change the primary key and insert the updated record. You can even re-use it for multiple inserts:
DECLARE
rec SOME_TABLE%ROWTYPE;
BEGIN
SELECT *
INTO rec
FROM SOME_TABLE
WHERE A = 1; -- Primary Key
rec.A := 2; -- Change the primary key value.
INSERT INTO SOME_TABLE VALUES rec;
rec.A := 3; -- Change the primary key again.
INSERT INTO SOME_TABLE VALUES rec;
FOR i IN 4 .. 9 LOOP
rec.A := i; -- Change it repeatedly...
INSERT INTO SOME_TABLE VALUES rec;
END LOOP;
FOR i IN 1 .. 3 LOOP
rec.A := SOME_SEQUENCE.NEXTVAL; -- Or you can manage the primary key's value using a sequence.
INSERT INTO SOME_TABLE VALUES rec;
END LOOP;
END;
/
SQLFIDDLE
I have often wanted to do something similar to this, but it's just not possible in any SQL variant I know of. You cannot ask for only some of the columns in a table without explicitly naming them (or perhaps defining a view on them in advance).
The only shortcut I can suggest is to dump the list of column names into a convenient location and then just copy it into an insert statement, changing only the value you need:
insert into foo (select 'newC1' as c1, c2, c3, c4, ..., c50 from foo where bar='baz');
::edit:: In fact, I do this so often that I wrote a Python script to help me. I tell it what table I'm editing, some where clause that matches exactly 1 row, the list of column(s) I want to change, and the list of new value(s) I want in those columns. Then it does the rest.

History table referencing other values in the table / accessing package table variables

I have a system for tracking usage of computers in a lab. Slightly simplified, it works out to:
Machines are associated with a lab.
Machines have a binary logged_in state, which gets updated automatically when users log in and out.
There is a view keyed on the lab which gathers the total number of seats associated with the lab, and the current number in use for that lab.
What I would like to do is add a history or audit table, which would track changes to lab population over time. I had a trigger on the machine table to store the time and the total lab population in my lab history table every time the machine table changed. The problem is that, in order to get the new total for the lab, I have to examine the other values in the machine table. This results in a table mutating error.
Some things I found on here and elsewhere suggested that I should create a package to track the labs being changed. Use a before trigger to clear the list, a row trigger to store each labid being changed, and an after trigger to update the history table with new values for only those labs whose ids are in the list. I've tried that, but can't figure out how to access the values I've stored in the package table (or even if it is storing them properly in the first place.) As will no doubt be obvious, I'm unfamiliar with PL/SQL packages and table variables - the whole syntax of referring to table entries like arrays struck me as vaguely heretical though incredibly useful if it works. So most of the below is just copied and adapted from other solutions I've found, but they didn't stretch as far as how to actually use my table of changed lablocids, assuming its being created properly in the first place. The following simply tells me that pg_machine_in_use_pkg.changedlablocids does not exist when I try to compile the final trigger.
create or replace package labstats_adm.pg_machine_in_use_pkg
as
type arr is table of number index by binary_integer;
changedlablocids arr;
empty arr;
end;
/
create or replace trigger labstats_adm.pg_machine_in_use_init
before insert or update
on labstats_adm.pg_machine
begin
-- begin each update with a blank list of changed lablocids
pg_machine_in_use_pkg.changedlablocids := pg_machine_in_use_pkg.empty;
end;
/
--
create or replace trigger labstats_adm.pg_machine_in_use_update
after insert or update of in_use,lablocid
on labstats_adm.pg_machine
for each row
begin
-- record lablocids - old and new - of changed machines
if :new.lablocid is not null then
pg_machine_in_use_pkg.changedlablocids( pg_machine_in_use_pkg.changedlablocids.count+1 ) := :new.lablocid;
end if;
if :old.lablocid is not null and :old.lablocid != :new.lablocid then
pg_machine_in_use_pkg.changedlablocids( pg_machine_in_use_pkg.changedlablocids.count+1 ) := :old.lablocid;
end if;
end;
create or replace trigger labstats_adm.pg_machine_lab_history
after insert or update of in_use,lablocid
on labstats_adm.pg_machine
begin
-- for each lablocation we just logged a change to, update that labs history
insert into labstats_adm.pg_lab_history (labid, time, total_seats, used_seats)
select labid, systimestamp, total_seats, used_seats
from labstats_adm.lab_usage
where labid in (
select distinct labid from pg_machine_in_use_pkg.changedlablocids
);
end;
/
On the other hand, if there is a better overall approach than the package, I'm all ears.
After some reflection I've got to go with #tbone on this one. In my experience a history table should be a copy of the data in the "real" table with fields added to show when particular 'version' of the data shown by a row in the history table was in effect. So if the "real" table is something like
CREATE TABLE REAL_TABLE
(ID_REAL_TABLE NUMBER PRIMARY KEY,
COL2 NUMBER,
COL3 VARCHAR2(50));
then I'd create the history table as
CREATE TABLE HIST_TABLE
(ID_HIST_TABLE NUMBER PRIMARY KEY
ID_REAL_TABLE NUMBER,
COL2 NUMBER,
COL3 VARCHAR2(50),
EFFECTIVE_START_DT TIMESTAMP(9) NOT NULL,
EFFECTIVE_END_DT TIMESTAMP(9));
and I'd have the following triggers to get everything populated:
CREATE TRIGGER REAL_TABLE_BI
BEFORE INSERT ON REAL_TABLE
REFERENCING OLD AS OLD
NEW AS NEW
FOR EACH ROW
BEGIN
IF :NEW.ID_REAL_TABLE IS NULL THEN
:NEW.ID_REAL_TABLE := REAL_TABLE_SEQUENCE.NEXTVAL;
END IF;
END REAL_TABLE_BI;
CREATE TRIGGER HIST_TABLE_BI
BEFORE INSERT ON HIST_TABLE
FOR EACH ROW
BEGIN
IF :NEW.ID_HIST_TABLE IS NULL THEN
:NEW.ID_HIST_TABLE := HIST_TABLE_SEQUENCE.NEXTVAL;
END IF;
END HIST_TABLE_BI;
CREATE TRIGGER REAL_TABLE_AIUD
AFTER INSERT OR UPDATE OR DELETE ON REAL_TABLE
FOR EACH ROW
DECLARE
tsEffective_start_date TIMESTAMP(9) := SYSTIMESTAMP;
tsEffective_end_date TIMESTAMP(9) := dtEffective_start_date - INTERVAL '0.000000001' SECOND;
BEGIN
IF UPDATING OR DELETING THEN
UPDATE HIST_TABLE
SET EFFECTIVE_END_DATE := tsEffective_end_date
WHERE ID_REAL_TABLE = :NEW.ID_REAL_TABLE AND
EFFECTIVE_END_DATE IS NULL;
END IF;
IF INSERTING OR UPDATING THEN
INSERT INTO HIST_TABLE (ID_REAL_TABLE, COL2, COL3, EFFECTIVE_START_DATE)
VALUES (:NEW.ID_REAL_TABLE, :NEW.COL2, :NEW.COL3, tsEffective_start_date);
END IF;
END REAL_TABLE_AIUD;
Using this method the "history" table has all historical versions of the data in the "real" table PLUS a complete copy of the "current" data from the "real" table; this is done to simplify queries which need to report on all versions of the data in the table up to and including present values.
The advantage of using triggers to do all this is that the maintenance of the primary keys and the history table becomes automatic and can't be easily circumvented or forgotten.
Share and enjoy.
Sorry so slow to get back; its taken me a bit of fiddling, and I haven't had a lot of time to work on it.
Thanks to Bob Jarvis for pointing me at the compound triggers, which cleaned up the overall structure significantly. After that, I just had to sanitise the way I'm getting values back out of my table variable. On the odd chance that someone else stumbles over this looking for the answer to the same problem, I'll post my final solution here:
create or replace
trigger pg_machine_in_use_update
for insert or update or delete of in_use,lablocid
on labstats_adm.pg_machine
compound trigger
type arr is table of number index by binary_integer;
changedlabids arr;
idx binary_integer;
after each row is
newlabid labstats_adm.pg_labs.labid%TYPE;
oldlabid labstats_adm.pg_labs.labid%TYPE;
begin
-- store the labids of any changed locations
-- PL/SQL does not like us testing for the existence of something that isn't there, so just set it twice if necessary
if ( :new.lablocid is not null ) then
select labid into newlabid from labstats_adm.pg_lablocation where lablocid = :new.lablocid;
changedlabids( newlabid ) := 1;
end if;
if ( :old.lablocid is not null ) then
select labid into oldlabid from labstats_adm.pg_lablocation where lablocid = :old.lablocid;
changedlabids( oldlabid ) := 1;
end if;
end after each row;
after statement is
begin
idx := changedlabids.FIRST;
while idx is not null loop
insert into labstats_adm.pg_lab_history (labid, time, total_seats, used_seats)
select labid, systimestamp, total_seats, used_seats
from labstats_adm.lab_usage
where labid = idx;
idx := changedlabids.NEXT(idx);
end loop;
end after statement;
end pg_machine_in_use_update;

PL/SQL loop through cursor

My problem isn't overly complicated, but I am a newbie to PL/SQL.
I need to make a selection from a COMPANIES table based on certain conditions. I then need to loop through these and convert some of the fields into a different format (I have created functions for this), and finally use this converted version to join to a reference table to get the score variable I need. So basically:
select id, total_empts, bank from COMPANIES where turnover > 100000
loop through this selection
insert into MY_TABLE (select score from REF where conversion_func(MY_CURSOR.total_emps) = REF.total_emps)
This is basically what I am looking to do. It's slightly more complicated but I'm just looking for the basics and how to approach it to get me started!
Here's the basic syntax for cursor loops in PL/SQL:
BEGIN
FOR r_company IN (
SELECT
ID,
total_emps,
bank
FROM
companies
WHERE
turnover > 100000
) LOOP
INSERT INTO
my_table
SELECT
score
FROM
ref_table
WHERE
ref.total_emps = conversion_func( r_company.total_emps )
;
END LOOP;
END;
/
You don't need to use PL/SQL to do this:
insert into my_table
select score
from ref r
join companies c
on r.total_emps on conversion_func(c.total_emps)
where c.turnover > 100000
If you have to do this in a PL/SQL loop as asked, then I'd ensure that you do as little work as possible. I would, however, recommend bulk collect instead of the loop.
begin
for xx in ( select conversion_func(total_emps) as tot_emp
from companies
where turnover > 100000 ) loop
insert into my_table
select score
from ref
where total_emps = xx.tot_emp
;
end loop;
end;
/
For either method you need one index on ref.total_emps and preferably one on companies.turnover

How to merge rows + retrieve new and existing keys

In an Oracle table (e.g. MYTABLE, with a numeric sequenced field as primary key), I have to insert several thousand of rows, but some of them are supposed to already exist in the table.
Naturally, I should try to use MERGE but I need, as well, to retrieve all created (when inserting) and existing (when updating) primary keys.
As well, it should be as fast as possible.
Is the following attempt (pseudo code) the only way to go? Thanks.
keys_list = empty array
for each row to merge
do query 'SELECT PK_MYTABLE FROM MYTABLE WHERE PK_MYTABLE = '+row.pk_mytable
==> retrieve key
if found then:
add key to keys_list
else:
do query 'INSERT INTO MYTABLE (PK_MYTABLE, ...) VALUES (SEQ_MYTABLE.NEXTVAL, ...)'
do query 'SELECT SEQ_MYTABLE.CURRVAL FROM DUAL' ==> retrieve key
add key to keys_list
Add a MODIFICATION_DATE column to the table
Grab and save the sysdate.
When you merge update/insert the value of the sysdate as well.
When the merge is complete, select the rows where the MODIFICATION_DATE = SYSDATE and you
have the set you are interested in.
Why can't you use a MERGE statement for this? This is exactly what a MERGE is for. Here is a rough idea of how it would look...
merge into mytable mt
using
(
select key_field, value_field from sourcetable
) st
on
( mt.key_field = st.key_field )
when matched then update
set mt.value_field = st.value_field
when not matched then insert
( key_field, value_field )
values
( st.key_field, st.value_field )
;
Using a MERGE statement is fast because it is a single statement and the Oracle optimizer can utilize indexes and choose a better explain path than iterating through a cursor using PL/SQL.
If the keys are being generated from a sequence, then the normal way to get the key generated by that insert is to use the returning clause:
declare
v_insert_seq integer;
begin
insert into t1 (pk, c1)
values (myseq.nextval, 'value') returning pk into v_insert_seq;
end;
/
However, as best as I can tell, the merge statement doesn't support that returning feature.
Depending on the source of your new rows, there are different ways you could do this. If you are inserting one row at a time, then the approach above will work pretty well.
To detect the duplicate records, just catch the exceptions when you are inserting (when dup_val_on_index) and then handle them with updates.
If your source of rows is another table, you probably want to look at bulk inserts, and allowing Oracle to return you an array of new PK values. I tried this, but couldn't get it working, so perhaps it's not supported (or I'm missing something today - it gives a syntax error):
declare
type t_type is table of t1.pk%type;
v_insert_seqs t_type;
begin
insert into t1 (pk, c1)
select level newpk, 'value' c1value
from dual
connect by level <= 10 returning pk bulk collect into v_insert_seqs;
exception
when dup_val_on_index then
raise;
end;
/
The next best thing is to select the rows into arrays and then use bulk binds with the returning clause to capture the new PK IDs and also use Save Exceptions to catch all the rows that failed to inserted. Then you can process any of the failed inserted afterwards:
set serveroutput on
declare
type t_pk is table of t1.pk%type;
type t_c1 is table of t1.c1%type;
v_pks t_pk;
v_c1s t_c1;
v_new_pks t_pk;
ex_dml_errors EXCEPTION;
PRAGMA EXCEPTION_INIT(ex_dml_errors, -24381);
begin
-- get the batch of rows you want to insert
select level newpk, 'value' c1
bulk collect into v_pks, v_c1s
from dual connect by level <= 10;
-- bulk bind insert, saving exceptions and capturing the newly inserted
-- records
forall i in v_pks.first .. v_pks.last save exceptions
insert into t1 (pk, c1)
values (v_pks(i), v_c1s(i)) returning pk bulk collect into v_new_pks;
exception
-- Process the exceptions
when ex_dml_errors then
for i in 1..SQL%BULK_EXCEPTIONS.count loop
DBMS_OUTPUT.put_line('Error: ' || i ||
' Array Index: ' || SQL%BULK_EXCEPTIONS(i).error_index ||
' Message: ' || SQLERRM(-SQL%BULK_EXCEPTIONS(i).ERROR_CODE));
end loop;
end;
/
If you are running Oracle 10 or better, you might be able to do much the same thing, for nearly free by issuing a commit before the merge to update the SCN, then after the merge,
use the ORA_ROWSCN to detect which rows have changed.