Oracle PL/SQL How to store and fetch a dynamic multi column query - sql

I am trying hard dynamic PL/SQL thing here.
I don't manage to fetch a column dynamic Query.
I am iterating on the name of the column to concatenate a full query in order to be executed on another table.
sql_req := 'select ';
for c in (SELECT name_col from TAB_LISTCOL)
loop
sql_req := sql_req || 'sum(' || c.name_col || '),';
end loop;
sql_req := sql_req || ' from ANOTHER_TAB ';
And when i try to execute it with EXECUTE IMMEDIATE or cursors or INTO/BULK COLLECT thing or just to fetch, i don't manage to iterate on the result.
I tried a lot.
Can you help me plz ? Or maybe it is not possible ?
ps : i know the coma is wrong but my code is more complexe than this : i didn't want to put more things

If you only want to get string columns, you can use listagg
select listagg(name_col, ',') WITHIN GROUP (ORDER BY null) from TAB_LISTCOL

Please see if this helps
In the absence of actual table structure and requirement, I'm creating dummy tables and query to illustrate an example:
SQL> create table another_tab
as
select 10 dummy_value1, 100 dummy_value2, 1000 dummy_value3 from dual union all
select 11 dummy_value1, 101 dummy_value2, 1001 dummy_value3 from dual union all
select 12 dummy_value1, 102 dummy_value2, 1003 dummy_value3 from dual
;
Table created.
SQL> create table tab_listcol
as select column_name from dba_tab_cols where table_name = 'ANOTHER_TAB'
;
Table created.
To reduce complexity in the final block, I'm defining a function to generate the dynamic sql query. This is based on your example and will need changes according to your actual requirement.
SQL> create or replace function gen_col_based_query
return varchar2
as
l_query varchar2(4000);
begin
l_query := 'select ';
for cols in ( select column_name cname from tab_listcol )
loop
l_query := l_query || 'sum(' || cols.cname || '), ' ;
end loop;
l_query := rtrim(l_query,', ') || ' from another_tab';
return l_query;
end;
/
Function created.
Sample output from the function will be as follows
SQL> select gen_col_based_query as query from dual;
QUERY
--------------------------------------------------------------------------------
select sum(DUMMY_VALUE1), sum(DUMMY_VALUE2), sum(DUMMY_VALUE3) from another_tab
Below is a sample block for executing a dynamic cursor using DBMS_SQL. For your ease of understanding, I've added comments wherever possible. More info here.
SQL> set serveroutput on size unlimited
SQL> declare
sql_stmt clob;
src_cur sys_refcursor;
curid number;
desctab dbms_sql.desc_tab; -- collection type
colcnt number;
namevar varchar2 (50);
numvar number;
datevar date;
l_header varchar2 (4000);
l_out_rows varchar2 (4000);
begin
/* Generate dynamic sql from the function defined earlier */
select gen_col_based_query into sql_stmt from dual;
/* Open cursor variable for this dynamic sql */
open src_cur for sql_stmt;
/* To fetch the data, however, you cannot use the cursor variable, since the number of elements fetched is unknown at complile time.
Therefore you use DBMS_SQL.TO_CURSOR_NUMBER to convert a REF CURSOR variable to a SQL cursor number which you can then pass to DBMS_SQL subprograms
*/
curid := dbms_sql.to_cursor_number (src_cur);
/* Use DBMS_SQL.DESCRIBE_COLUMNS to describe columns of your dynamic cursor, returning information about each column in an associative array of records viz., desctab. The no. of columns is returned in colcnt variable.
*/
dbms_sql.describe_columns (curid, colcnt, desctab);
/* Define columns at runtime based on the data type (number, date or varchar2 - you may add to the list)
*/
for indx in 1 .. colcnt
loop
if desctab (indx).col_type = 2 -- number data type
then
dbms_sql.define_column (curid, indx, numvar);
elsif desctab (indx).col_type = 12 -- date data type
then
dbms_sql.define_column (curid, indx, datevar);
else -- assuming string
dbms_sql.define_column (curid, indx, namevar, 100);
end if;
end loop;
/* Print header row */
for i in 1 .. desctab.count loop
l_header := l_header || ' | ' || rpad(desctab(i).col_name,20);
end loop;
l_header := l_header || ' | ' ;
dbms_output.put_line(l_header);
/* Loop to retrieve each row of data identified by the dynamic cursor and print output rows
*/
while dbms_sql.fetch_rows (curid) > 0
loop
for indx in 1 .. colcnt
loop
if (desctab (indx).col_type = 2) -- number data type
then
dbms_sql.column_value (curid, indx, numvar);
l_out_rows := l_out_rows || ' | ' || rpad(numvar,20);
elsif (desctab (indx).col_type = 12) -- date data type
then
dbms_sql.column_value (curid, indx, datevar);
l_out_rows := l_out_rows || ' | ' || rpad(datevar,20);
elsif (desctab (indx).col_type = 1) -- varchar2 data type
then
dbms_sql.column_value (curid, indx, namevar);
l_out_rows := l_out_rows || ' | ' || rpad(namevar,20);
end if;
end loop;
l_out_rows := l_out_rows || ' | ' ;
dbms_output.put_line(l_out_rows);
end loop;
dbms_sql.close_cursor (curid);
end;
/
PL/SQL procedure successfully completed.
Output
| SUM(DUMMY_VALUE1) | SUM(DUMMY_VALUE2) | SUM(DUMMY_VALUE3) |
| 33 | 303 | 3004 |

You have to use EXECUTE IMMEDIATE with BULK COLLECT
Below is an example of the same. For more information refer this link
DECLARE
TYPE name_salary_rt IS RECORD (
name VARCHAR2 (1000),
salary NUMBER
);
TYPE name_salary_aat IS TABLE OF name_salary_rt
INDEX BY PLS_INTEGER;
l_employees name_salary_aat;
BEGIN
EXECUTE IMMEDIATE
q'[select first_name || ' ' || last_name, salary
from hr.employees
order by salary desc]'
BULK COLLECT INTO l_employees;
FOR indx IN 1 .. l_employees.COUNT
LOOP
DBMS_OUTPUT.put_line (l_employees (indx).name);
END LOOP;
END;

If I understand correctly, you want to create a query and execute it and return the result to another function or some calling app. As the resulting query's columns are note before-known, I'd return a ref cursor in this case:
create function get_sums return sys_refcur as
declare
my_cursor sys_refcursor;
v_query varchar2(32757);
begin
select
'select ' ||
listagg('sum(' || name_col || ')', ', ') within group (order by name_col) ||
' from another_tab'
into v_query
from tab_listcol;
open my_cursor for v_query;
return v_query;
end get_sums;

Related

Dynamic sql with for loop PL/SQL

The following query needs to convert to dynamic SQL without hard code cursor SQL,
using l_query, I do not know the l_query it will come as a parameter.
Inside the loop, I need to execute another insert query ( l_insert_query) that also comes as a parameter.
Your counsel would be much appreciated
DECLARE
CURSOR cust
IS
SELECT *
FROM customer
WHERE id < 500;
BEGIN
l_query := 'SELECT * FROM customer WHERE id < 5';
l_insert_query :=
'insert into data ( name, mobile) values ( cust.name,cust.mobile)';
FOR r_cust IN cust
LOOP
EXECUTE IMMEDIATE l_insert_query;
END LOOP;
END;
You could do this with a dynamic PL/SQL block:
declare
l_query varchar2(100) := 'SELECT * FROM customer WHERE id < 5';
l_insert varchar2(100) := 'insert into data ( name, mobile) values ( cust.name,cust.mobile)';
l_plsql varchar2(4000);
begin
l_plsql := '
begin
for cust in (' || l_query || ') loop
' || l_insert || ';
end loop;
end;
';
dbms_output.put_line(l_plsql);
execute immediate l_plsql;
end;
/
The l_plsql statement ends up as a generated PL/SQL block using the cursor query and insert statement:
begin
for cust in (SELECT * FROM customer WHERE id < 5) loop
insert into data ( name, mobile) values ( cust.name,cust.mobile);
end loop;
end;
db<>fiddle
But that you can do this doesn't mean you should. This is vulnerable to SQL injection, and doesn't seem like a very safe, sensible or efficient way to handle data manipulation in your system.

How stored procedure returns from multiple cursor

I have Oracle Stored Procedure below and I understood most of the logic inside except for the part where the caller of the procedure will get all the values from different cursors.
Did some reading that SP returns the OUT part in a procedure parameters shown in the sample below. But i fail to get any reference as to how p_returnCode can store resultsets from queries inside the stored procedure.
Procedure retrieveX(p_date date, p_loc varchar2, p_returnCode out integer)
The stored procedure below have 3 cursors cur1, cur2 and cur3. How or where does it store the values? Cur1 contains 2 columns with multiple rows and Cur2 and Cur3 contains one column with multiple rows.
Can anyone clarify this part?
Caller from cgi script
report.retrieveX(p_date,p_loc,p_return)
Full Stored Procedure
PROCEDURE retrieveX(
p_date DATE,
p_loc VARCHAR2,
p_returnCode OUT INTEGER
)
AS
TYPE cur_typ IS REF CURSOR;
cur1 cur_typ;
cur2 cur_typ;
cur3 cur_typ;
query_str VARCHAR2(2000) := '';
query_str2 VARCHAR2(2000) := '';
query_str3 VARCHAR2(2000) := '';
v_an VARCHAR2(20);
v_tn VARCHAR2(20);
v_sOID varchar2(20);
BEGIN
sqlRouteDT := 'AND sp.ROUTE_DT = TO_DATE(''' || TO_CHAR(p_date, 'YYYY/MM/DD') || ''',''YYYY/MM/DD'')';
IF p_loc IS NOT NULL THEN
sqlLocation := 'AND act.location_cd = ''' || p_loc || '''';
END IF;
p_returnCode := 0;
query_str := '
SELECT distinct
sp.ab,
y.track,
FROM ship sp
inner join activ act on sp.soid=act.on
inner join peace y on act.on=y.soid
where
sp.man is not null
' || sqlLocation || '
' || sqlRouteDT || '
ORDER BY sp.ab asc
';
OPEN cur1 FOR query_str1;
LOOP
FETCH cur1
INTO
v_AN,
v_FN
EXIT WHEN cur1%NOTFOUND;
query_str2 := '
SELECT DISTINCT INTER_CD
FROM TBL_INTR
WHERE AF = ''Y''
AND sOID = ''' || v_sOID || '''
ORDER BY INTER_CD
';
OPEN cur2 FOR query_str2;
LOOP
FETCH cur2
INTO v_intr_cd;
EXIT WHEN cur2%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('INTER_CD|' || v_intr_cd);
END LOOP;
CLOSE cur2;
query_str3 := '
SELECT DISTINCT hi_cd
FROM tbl_hi
WHERE AF = ''Y''
AND sOID = ''' || v_sOID || '''
ORDER BY hi_cd
';
OPEN cur3 FOR query_str3;
LOOP
FETCH cur3
INTO v_hi_c;
EXIT WHEN cur3%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('hi_cd|' || v_hi_c);
END LOOP;
CLOSE cur3;
END LOOP;
CLOSE cur1;
EXCEPTION WHEN OTHERS THEN
p_returnCode := 1;
DBMS_OUTPUT.PUT_LINE(SQLERRM);
END retrieveX;
I don't think possible to do that you want.
Maybe you should to considerate the use of pipelined function, please take a look at https://oracle-base.com/articles/misc/pipelined-table-functions
With this technique, you can write a pl/sql code to access to complex data and relationship table and get the result as a simple
select * from StoredProcedure(Parameter_1...);

Query for particular integer value from multiple columns with number datatype

I try to search a number from multiple columns (datatype number), but get ORA-01722: invalid number error.
My Query:
SELECT *
FROM CAMPAIGN
WHERE 1481125 IN (select column_name
from all_tab_columns
where table_name = 'CAMPAIGN'
AND data_type = 'NUMBER');
What is wrong with it?
You are comparing your number 1481125 with the names of the each column, not the values of each column in your table.
To go from a column's name (from dba_tab_columns) to the values in that column, you need to use some form of dynamic SQL. Here's a relatively simple example:
DECLARE
-- Since I don't have your CAMPAIGN table or data, I'm using DBA_OBJECTS in it's place.
l_table_name VARCHAR2 (30) := 'DBA_OBJECTS';
l_search_number NUMBER := 20; -- 1481125 in your example
l_record dba_objects%ROWTYPE;
l_sql VARCHAR2 (32000);
l_column_number NUMBER := 0;
l_cur SYS_REFCURSOR;
BEGIN
-- First: build dynamic SQL statement of the form:
-- SELECT * FROM table_name WHERE
-- ( ( col_name_a = 20 ) OR ( col_name_b = 20 ) OR ... )
l_sql := 'SELECT * FROM dba_objects WHERE ( ';
FOR r_number_column IN (SELECT column_name
FROM dba_tab_columns
WHERE table_name = l_table_name
AND data_type = 'NUMBER'
ORDER BY column_id) LOOP
IF l_column_number > 0 THEN
l_sql := l_sql || ' OR ';
END IF;
l_column_number := l_column_number + 1;
l_sql := l_sql || '(' || r_number_column.column_name || ' = ' || l_search_number || ')';
END LOOP;
IF l_column_number = 0 THEN
-- No number columns in table, so there should be no matches
l_sql := l_sql || ' 1=0';
END IF;
l_sql := l_sql || ')';
DBMS_OUTPUT.put_line (l_sql);
OPEN l_cur FOR l_sql;
LOOP
FETCH l_cur INTO l_record;
EXIT WHEN l_cur%NOTFOUND;
DBMS_OUTPUT.put_line ('Object Name ' || l_record.object_name || ' has search number ' || l_search_number);
END LOOP;
END;
Your query is:
SELECT * FROM CAMPAIGN WHERE 1481125 IN
(select column_name from all_tab_columns where table_name = 'CAMPAIGN' AND data_type='NUMBER')
Breaking that down we have:
SELECT * FROM CAMPAIGN WHERE 1481125 IN (<a set of numbers>)
and the subquery:
select column_name from all_tab_columns
where table_name = 'CAMPAIGN'
AND data_type='NUMBER'
That subquery is going to return a list of column names e.g.
CAMPAIGN_COUNT
CAMPAIGN_ID
CAMPAIGN_NUMBER_OF_SOMETHINGS
Your query is thus equivalent to:
SELECT * FROM CAMPAIGN WHERE 1481125 IN
('CAMPAIGN_COUNT', 'CAMPAIGN_ID', 'CAMPAIGN_NUMBER_OF_SOMETHINGS')
You can see why you would get the ORA-01722 error there?
You would need to write dynamic SQL to achieve your aim.

oracle xmltable with columns from another table

with oracle xmltable
SELECT u.*
FROM table1
, XMLTable('/abc/def[contract = $count]'
PASSING xmlcol, 1 as "count"
COLUMNS contract integer path 'contract',
oper VARCHAR2(20) PATH 'oper' ) u
This is normally what we do.
Now I need to have "COLUMNS" in above query selected from another tables column for Xpath
something like
{
SELECT u.*
FROM table1
, XMLTable('/abc/def[contract = $count]'
PASSING xmlcol, 1 as "count"
COLUMNS (select xpath from xpath_metadeta )) u
}
Please let me know if this is possible and how?
One option that comes to my mind is dynamic sql and ref cursor.
Something like this:
DECLARE
columnParameters SYS.ODCIVARCHAR2LIST :=
SYS.ODCIVARCHAR2LIST(
'TITLE VARCHAR2(1000) PATH ''title''',
'SUMMARY CLOB PATH ''summary''',
'UPDATED VARCHAR2(20) PATH ''updated''',
'PUBLISHED VARCHAR2(20) PATH ''published''',
'LINK VARCHAR2(1000) PATH ''link/#href'''
);
ref_cursor SYS_REFCURSOR;
cursor_id NUMBER;
table_description DBMS_SQL.DESC_TAB;
column_count NUMBER;
string_value VARCHAR2(4000);
clob_value CLOB;
FUNCTION DYNAMIC_XMLTABLE(xml_columns SYS.ODCIVARCHAR2LIST) RETURN SYS_REFCURSOR
IS
result SYS_REFCURSOR;
statementText VARCHAR2(32000) := Q'|SELECT * FROM
XMLTABLE(
XMLNAMESPACES (DEFAULT 'http://www.w3.org/2005/Atom'),
'for $entry in /feed/entry return $entry'
PASSING
HTTPURITYPE('http://stackoverflow.com/feeds/tag?tagnames=oracle&sort=newest').getxml()
COLUMNS
{column_definition}
)|';
BEGIN
SELECT REPLACE(statementText, '{column_definition}', LISTAGG(COLUMN_VALUE, ', ') WITHIN GROUP (ORDER BY ROWNUM)) INTO statementText FROM TABLE(xml_columns);
DBMS_OUTPUT.PUT_LINE('Statement: ' || CHR(10) || statementText);
OPEN result FOR statementText;
RETURN result;
END;
BEGIN
DBMS_OUTPUT.ENABLE(NULL);
ref_cursor := dynamic_xmltable(columnParameters);
cursor_id := DBMS_SQL.TO_CURSOR_NUMBER(ref_cursor);
DBMS_SQL.DESCRIBE_COLUMNS(cursor_id, column_count, table_description);
FOR i IN 1..column_count LOOP
IF table_description(i).col_type = 1 THEN
DBMS_SQL.DEFINE_COLUMN(cursor_id, i, string_value, 4000);
ELSIF table_description(i).col_type = 112 THEN
DBMS_SQL.DEFINE_COLUMN(cursor_id, i, clob_value);
END IF;
END LOOP;
WHILE DBMS_SQL.FETCH_ROWS(cursor_id) > 0 LOOP
FOR i IN 1..column_count LOOP
DBMS_OUTPUT.PUT_LINE(table_description(i).col_name || ': datatype=' || table_description(i).col_type);
IF (table_description(i).col_type = 1) THEN
BEGIN
DBMS_SQL.COLUMN_VALUE(cursor_id, i, string_value);
DBMS_OUTPUT.PUT_LINE('Value: ' || string_value);
END;
ELSIF (table_description(i).col_type = 112) THEN
BEGIN
DBMS_SQL.COLUMN_VALUE(cursor_id, i, clob_value);
DBMS_OUTPUT.PUT_LINE('Value: ' || clob_value);
END;
-- add other data types
END IF;
END LOOP;
END LOOP;
DBMS_SQL.CLOSE_CURSOR(cursor_id);
END;
I depends how the cursor is consumed. It's much simple if by an application, a bit more difficult if using PL/SQL.

query each column of a table in a loop - Oracle Database

I'm working with an oracle database and what I basically need to do is to count the number of NULL fields per column in a certain table.
something like that:
DECLARE
BlankCount number(20);
i number(2) := 1;
BEGIN
loop that would take each column individualy and exit after the last one
SELECT COUNT(*) INTO BlankCount FROM name_of_my_table
DBMS_OUTPUT.PUT_LINE('Column '||i||' has '||BlankCount||' empty cells');
i := i + 1;
END LOOP;
END;
I just couldn't find anything that would do the loop part.
It would also be nice if instead of just numbering them (with the i) I could display the column name (but that is not very important).
Thank you!
Something like this:
declare
mytable varchar(32) := 'MY_TABLE';
cursor s1 (mytable varchar2) is
select column_name
from user_tab_columns
where table_name = mytable
and nullable = 'Y';
mycolumn varchar2(32);
query_str varchar2(100);
mycount number;
begin
open s1 (mytable);
loop
fetch s1 into mycolumn;
exit when s1%NOTFOUND;
query_str := 'select count(*) from ' || mytable || ' where ' || mycolumn || ' is null';
execute immediate query_str into mycount;
dbms_output.put_line('Column ' || mycolumn || ' has ' || mycount || ' null values');
end loop;
end;
Try using cursor approach and Dynamic SQL as mentioned in this thread: How to loop through columns in an oracle pl/sql cursor
HTH.