I have a table TestTable with columns of col_test1, col_test2, col_test3 ...
and I want to create a loop that accesses each of these columns individually and find the max value and place it in the variable made in the declare block and simply dbms.out.put it.
Declare
my_array sys.dbms_debug_vc2coll := sys.dbms_debug_vc2coll('col_test1','col_test2','col_test2');
v_test number(8,0);
Begin
for r in my_array.first..my_array.last
loop
select max(my_array(r)) into v_test from TestTable;
dbms_output.put_line(v_test);
end loop;
End;
/
The output I get is just the string 'col_test1'which should be 50.
This is done through oracle SQL. Is there any way to achieve this?
You could use dynamic SQL for this
Declare
my_array sys.dbms_debug_vc2coll := sys.dbms_debug_vc2coll('col_test1','col_test2','col_test2');
v_test number(8,0);
Begin
for r in my_array.first..my_array.last
loop
execute immediate 'select max(' || my_array(r) || ') from TestTable'
into v_test;
dbms_output.put_line(v_test);
end loop;
End;
If you're going to resort to dynamic SQL, however, it would generally make more sense to build a single SQL statement that took that max of all three columns in one pass rather than potentially doing three separate table scans on the same table.
Related
I want to save a list of column values into the variable input_cols and then loop over the values
CREATE OR REPLACE PROCEDURE map_values (mapping_table VARCHAR2(64)) AS
TYPE col_names IS TABLE OF VARCHAR2(64);
input_cols col_names;
BEGIN
EXECUTE IMMEDIATE
'SELECT COLUMN_NAME FROM SYS.DBA_TAB_COLUMNS
WHERE TABLE_NAME = '' ' || mapping_table || ' '' '
BULK COLLECT INTO (input_cols);
FOR in_col IN input_cols
LOOP
dbms_output.put_line ('test');
END LOOP;
END;
I am getting the error
PLS-00103: Encountered the symbol "LOOP" when expecting one of the following: * & - + / at mod remainder rem .. <an exponent (**)> ||
Although you can construct dynamic queries by concatenating values, it's generally better to use bind variables where possible, for example:
execute immediate
'select column_name from user_tab_columns where table_name = :b1'
bulk collect into input_cols
using p_table;
I recommend getting into the habit of anchoring types in your code to the corresponding database object, when there is one. For example, this:
mapping_table dba_tab_columns.table_name%type
instructs the compiler to look up the type of dba_tab_columns.table_name and use that. However, I would generally avoid the dba_ views in procedures like this and stick to user_ views, e.g. user_tab_columns, to limit them to objects you own. If you must use dba_ views, you should also include the table owner, as there may be more than one table with the same name.
I also prefer to name my parameters in a way that separates them from column names etc. There are various conventions (camelCase, prefixing with i_ for in or p_ for parameter, prefixing with the procedure name e.g. map_values.mapping_table), so pick one you like.
Putting that together, you get something like this:
create or replace procedure map_values
( p_table user_tab_columns.table_name%type )
as
type col_names is table of user_tab_columns.column_name%type;
input_cols col_names;
begin
execute immediate
'select column_name from user_tab_columns where table_name = :b1 order by column_id'
bulk collect into input_cols
using p_table;
for i in 1..input_cols.count loop
dbms_output.put_line(input_cols(i));
end loop;
end map_values;
Or, if you don't specifically need a collection and just want to loop through a result set:
create or replace procedure map_values
( p_table user_tab_columns.column_name%type )
as
columns_cur sys_refcursor;
colname user_tab_columns.column_name%type;
begin
open columns_cur for
'select column_name from user_tab_columns where table_name = :b1 order by column_id'
using p_table;
loop
fetch columns_cur into colname;
exit when columns_cur%notfound;
dbms_output.put_line(colname);
end loop;
close columns_cur;
end;
As Koen pointed out in the comments, though, there is no need for dynamic SQL in this example, so a much simpler version could be just:
create or replace procedure map_values
( p_table user_tab_columns.column_name%type )
as
begin
for r in (
select column_name from user_tab_columns
where table_name = p_table
order by column_id
)
loop
dbms_output.put_line(r.column_name);
end loop;
end map_values;
Word of advice: use a tool like SQL Developer to create your procedures. They show the compilation errors in a much clearer way. If you're new to PL/SQL, start with the very basics (empty procedure), compile, fix error if any and add code. There are 3 blocking issues in your code - debugging that is pretty hard.
I added a comment for each of the errors
create or replace PROCEDURE map_values
(mapping_table VARCHAR /* just define the datatype, not the precision */
)
AS
TYPE col_names IS TABLE OF VARCHAR2(64) INDEX BY BINARY_INTEGER;
input_cols col_names;
BEGIN
EXECUTE IMMEDIATE
'SELECT COLUMN_NAME FROM SYS.DBA_TAB_COLUMNS
WHERE TABLE_NAME = ''' ||mapping_table|| ''' '
BULK COLLECT INTO input_cols; /* no brackets needed */
dbms_output.put_line ('test:');
FOR in_col IN 1 .. input_cols.COUNT /* this is not a implicit cursor but a collection - you need to iterate over it.*/
LOOP
dbms_output.put_line ('test:'||input_cols(in_col));
END LOOP;
END;
/
Please do not use dynamic SQL for this use case, static SQL is sufficient and will be more efficient.
For looping, you have to use indexes and get the element from collection by index. There is no functionality that will enable you to directly assign collection element to a variable (like you are trying).
create or replace procedure map_values(mapping_table sys.dba_tab_columns%table_name)
as
type col_names is table of sys.dba_tab_columns.column_name%type;
input_cols col_names;
begin
select column_name
bulk collect into input_cols
from sys.dba_tab_columns
where table_name = mapping_table;
for i in 1 .. input_cols.count
loop
dbms_output.put_line(input_cols(i));
end loop;
end;
I am building a procedure, where I`m first creating a select statement and store it in an VARCAHR variable.
I now want to execute that query and store the whole result set in an variable to loop through it or use directly in a for loop.
I only find examples where the Select is hard written in the for loop definition.
How do i exchange the Select statement with my variable that holds my select statement?
for r IN (SELECT ... FROM ...)
loop
--do sth;
end loop;
how i want to use it :
statement := 'SELECT .... FROM ...';
for r IN (statement) -- HOW TO DO THIS
loop
--do sth;
end loop;
For a dynamic ref cursor, you need to define everything explicitly:
declare
sqlstring long := 'select 123 as id, ''demo'' as somevalue from dual where dummy = :b1';
resultset sys_refcursor;
type demo_rectype is record
( id integer
, somevalue varchar2(30) );
demorec demo_rectype;
begin
open resultset for sqlstring using 'X';
loop
fetch resultset into demorec;
exit when resultset%notfound;
dbms_output.put_line('id=' || demorec.id || ' somevalue=' || demorec.somevalue);
end loop;
close resultset;
end;
You can parse the cursor and figure out the column names and datatypes with DBMS_SQL. Example here: www.williamrobertson.net/documents/refcursor-to-csv.shtml
There is a table contains this kind of data: select to_char(sysdate,'day') from dual in a column. I want to get results of the every query that the table keeps.
My result set should be the result of select to_char(sysdate,'day') from dual query. So in this case it is a tuesday.
SO_SQL_BODY is Varchar2.
I wrote this code but it returns only table data.
CREATE or replace PROCEDURE a_proc
AS
CURSOR var_cur IS
select SO_SQL_BODY FROM SO_SUB_VARS group by SO_SQL_BODY;
var_t var_cur%ROWTYPE;
TYPE var_ntt IS TABLE OF var_t%TYPE;
var_names var_ntt;
BEGIN
OPEN var_cur;
FETCH var_cur BULK COLLECT INTO var_names;
CLOSE var_cur;
FOR indx IN 1..var_names.COUNT LOOP
DBMS_OUTPUT.PUT_LINE(var_names(indx).SO_SQL_BODY);
END LOOP;
END a_proc;
DECLARE
res varchar2(4000);
sql_str varchar2(1000);
BEGIN
FOR r IN
(select SO_SQL_BODY FROM SO_SUB_VARS WHERE SO_SQL_BODY IS NOT NULL
)
LOOP
sql_str := r.SO_SQL_BODY;
EXECUTE immediate sql_str INTO res;
dbms_output.put_line(sql_str);
dbms_output.put_line('***********************');
dbms_output.put_line(res);
dbms_output.put_line('***********************');
END LOOP;
END;
/
Try this - iterate to not null records - execute them and print the result.This script works supposing the fact that SO_SQL_BODY contains a query which projects only one column.Also if the projection is with more than two columns then try to use a refcursor and dbms_sql package
İf var_names(indx).SO_SQL_BODY output is a runnable sql text;
CREATE or replace PROCEDURE a_proc
AS
CURSOR var_cur IS
select SO_SQL_BODY FROM SO_SUB_VARS group by SO_SQL_BODY;
var_t var_cur%ROWTYPE;
TYPE var_ntt IS TABLE OF var_t%TYPE;
var_names var_ntt;
BEGIN
OPEN var_cur;
FETCH var_cur BULK COLLECT INTO var_names;
CLOSE var_cur;
FOR indx IN 1..var_names.COUNT LOOP
DBMS_OUTPUT.PUT_LINE(var_names(indx).SO_SQL_BODY);
EXECUTE IMMEDIATE var_names(indx).SO_SQL_BODY;
END LOOP;
END a_proc;
You don't need a full cursor for this example. An implicit one would make it a lot shorter.
create or replace procedure a_proc is
lReturnValue varchar2(250);
begin
for q in (select so_sql_body from so_sub_vars group by so_sql_body)
loop
execute immediate q.so_sql_body into lReturnValue;
dbms_output.put_line(lReturnValue);
end loop;
end a_proc;
You should add an exception handler that will care for cases where there is a bad SQL query in your table. Also note that executing querys saved in a database table is your entry point to SQL injection.
I was given a stored proc which generates an open cursor which is passed as output to a reporting tool. I re-wrote this stored proc to improve performance. What I'd like to do is to show that the two result sets are the same for a given set of input parameters.
Something that is the equivalent of:
select * from CURSOR_NEW
minus
select * from CURSOR_OLD
union all
select * from CURSOR_OLD
minus
select * from CURSOR_NEW
Each cursor returns several dozen columns from a large subset of tables. Each row has an id value, and a long list of other column values for that id. I would want to check:
Both cursors are returning the same set of ids (I already checked this)
Both cursors have the same list of values for each id they have in common
If it was just one or two columns, I could concatenate them and find a hash and then sum it up over the cursor. Or another way might be to create a parent program that inserted the cursor results into a global temp table and compared the results. But since it's several dozen columns I'm trying to find a less brute force approach to doing the comparison.
Also it would be nice if the solution was scalable for other situations that involved different cursors, so it wouldn't have to be manually re-written each time, since this is a situation I'm running into more often.
I figured out a way to do this. It was a lot more complicated than I expected. I ended up using some DBMS_SQL procedures that allow converting REFCURSORs to defined cursors. Oracle has documentation on it here:
http://docs.oracle.com/cd/B28359_01/appdev.111/b28370/dynamic.htm#LNPLS00001
After that I concatenated the row values into a string and printed the hash. For bigger cursors, I will change concat_col_vals to use a CLOB to prevent it from overflowing.
p_testCursors returns a simple refcursor for example purposes.
declare
cx_1 sys_refcursor;
c NUMBER;
desctab DBMS_SQL.DESC_TAB;
colcnt NUMBER;
stringvar VARCHAR2(4000);
numvar NUMBER;
datevar DATE;
concat_col_vals varchar2(4000);
col_hash number;
h raw(32767);
n number;
BEGIN
p_testCursors(cx_1);
c := DBMS_SQL.TO_CURSOR_NUMBER(cx_1);
DBMS_SQL.DESCRIBE_COLUMNS(c, colcnt, desctab);
-- Define columns:
FOR i IN 1 .. colcnt LOOP
IF desctab(i).col_type = 2 THEN
DBMS_SQL.DEFINE_COLUMN(c, i, numvar);
ELSIF desctab(i).col_type = 12 THEN
DBMS_SQL.DEFINE_COLUMN(c, i, datevar);
-- statements
ELSE
DBMS_SQL.DEFINE_COLUMN(c, i, stringvar, 4000);
END IF;
END LOOP;
-- Fetch rows with DBMS_SQL package:
WHILE DBMS_SQL.FETCH_ROWS(c) > 0 LOOP
concat_col_vals := '~';
FOR i IN 1 .. colcnt LOOP
IF (desctab(i).col_type = 1) THEN
DBMS_SQL.COLUMN_VALUE(c, i, stringvar);
--Dbms_Output.Put_Line(stringvar);
concat_col_vals := concat_col_vals || '~' || stringvar;
ELSIF (desctab(i).col_type = 2) THEN
DBMS_SQL.COLUMN_VALUE(c, i, numvar);
--Dbms_Output.Put_Line(numvar);
concat_col_vals := concat_col_vals || '~' || to_char(numvar);
ELSIF (desctab(i).col_type = 12) THEN
DBMS_SQL.COLUMN_VALUE(c, i, datevar);
--Dbms_Output.Put_Line(datevar);
concat_col_vals := concat_col_vals || '~' || to_char(datevar);
-- statements
END IF;
END LOOP;
DBMS_OUTPUT.PUT_LINE(concat_col_vals);
col_hash := DBMS_UTILITY.GET_SQL_HASH(concat_col_vals, h, n);
DBMS_OUTPUT.PUT_LINE('Return Value: ' || TO_CHAR(col_hash));
DBMS_OUTPUT.PUT_LINE('Hash: ' || h);
END LOOP;
DBMS_SQL.CLOSE_CURSOR(c);
END;
/
This is not easy task for Oracle.
Very good article you can find on dba-oracle web:
Sql patterns symmetric diff
and Convert set to join sql parameter
If you need it often, you can:
add "hash column" and fill it always with insert using trigger, or
for each table in cursor output get unique value (create unique index) and compare only this column wiht anijoin
and you can find other possibilities in article.
My goal is to keep a table which contains bind values and arguments, which will later be used by dbms_sql. The below pl/sql example is basic, it's purpose is to illustrate the issue I am having with recalling values from prior loop objects.
The table account_table holds acccount information
CREATE TABLE account_table (account number, name varchar2(100)));
INSERT INTO mytest
(account, name)
VALUES
(1 ,'Test');
COMMIT;
The table MYTEST holds bind information
CREATE TABLE mytest (bind_value varchar2(100));
INSERT INTO mytest (bind_value) VALUES ('i.account');
COMMIT;
DECLARE
v_sql VARCHAR2(4000) := NULL;
v_ret VARCHAR2(4000) := NULL;
BEGIN
FOR I IN (
SELECT account
FROM account_table
WHERE ROWNUM = 1
) LOOP
FOR REC IN (
SELECT *
FROM mytest
) LOOP
v_sql := 'SELECT ' || rec.bind_value || ' FROM dual';
EXECUTE IMMEDIATE v_sql INTO v_ret;
dbms_output.put_line ('Account: ' || v_ret);
END LOOP;
END LOOP;
END;
/
I cannot store the name i.account and later use the value that object. My idea was to use NDS; but, while the value of v_sql looks ok (it will read "Select i.account from dual"), an exception of invalid identifier would be raised on i.account. Is there a way to get the value of the object? We are using Oracle 11g2.
Thanks!
Dynamic SQL will not have the context of your PL/SQL block. You cannot use identifiers that are defined in the PL/SQL block in your dynamic SQL statement. So you cannot reference i.account and reference the loop that you have defined outside of the dynamic SQL statement. You could, of course, dynamically generate the entire PL/SQL block but dynamic PL/SQL is generally a pretty terrible approach-- it is very hard to get that sort of thing right.
If you are trying to use the value of i.account, however, you can do something like
v_sql := 'SELECT :1 FROM dual';
EXECUTE IMMEDIATE v_sql
INTO v_ret
USING i.account;
That doesn't appear to help you, though, if you want to get the string i.account from a table.
Are you always trying to access the same table with the same where clause, but just looking for a different column each time? If so, you could do this:
p_what_I_want := 'ACCOUNT';
--
SELECT decode(p_what_I_want
,'ACCOUNT', i.account
, 'OTHER_THING_1', i.other_thing_1
, 'OTHER_THING_2', i.other_thing_2
, 'OTHER_THING_1', i.default_thing) out_thing
INTO l_thing
FROM table;
The above statement is not dynamic but returns a different column on demand...