cursor for loop & dynamic SQL - Snowflake - sql

I'm attempting to write a procedure that takes in a list of tables and date_column to create some row_counts by calendar dates for reconciliation purposes.
SELECT t.*
FROM (
VALUES ('tbl1', 'created_date')
, ('tbl2', 'modify_date')
, ('tbl3', 'last_seen_date')
) t(tbl, dt)
+----+--------------+
|TBL |DT |
+----+--------------+
|tbl1|created_date |
|tbl2|modify_date |
|tbl3|last_seen_date|
+----+--------------+
I'm connected to Snowflake via a JDBC connection using Datagrip - so I assume I need to follow the classic SnowSQL part of the documentation:
https://docs.snowflake.com/en/developer-guide/snowflake-scripting/loops.html#cursor-based-for-loops
EXECUTE IMMEDIATE $$
DECLARE
dt text
, tbl text;
c1 CURSOR FOR SELECT dt, tbl from t;
BEGIN
FOR record in c1 DO
dt := record.dt
tbl := record.tbl
stmt =: 'SELECT COUNT(*)' ||
CONCAT(', DAYOFMONTH(', $dt, ')') ||
CONCAT('\n FROM ', $tbl) ||
CONCAT('\n WHERE YEAR(', $dt, ')', ' = YEAR(CURRENT_DATE)') ||
CONCAT('\n AND MONTH(', $dt, ')', ' = MONTH(CURRENT_DATE)') ||
'\n GROUP BY' ||
CONCAT('\n DAYOFMONTH(', $dt, ')')
EXECUTE IMMEDIATE stmt -- will adapt this to be an update statement eventually.
END FOR
end;
$$
This returns a SQL Compilation error, I've tried a few different variations of this but I'm none the wiser on how to proceed.

Instead of concatenating the query string which makes it almost unreadable it could be rewritten using bind variables:
DECLARE
dt text;
tbl text;
stmt text;
c1 CURSOR FOR SELECT dt, tbl from t;
BEGIN
FOR record in c1 DO
dt := record.dt;
tbl := record.tbl;
stmt := 'INSERT INTO result(cnt, day_of_month)
SELECT COUNT(*), DAYOFMONTH(IDENTIFIER(?)) AS day_of_month
FROM TABLE(?)
WHERE YEAR(IDENTIFIER(?)) = YEAR(CURRENT_DATE)
AND MONTH(IDENTIFIER(?)) = MONTH(CURRENT_DATE)
GROUP BY day_of_month';
EXECUTE IMMEDIATE :stmt USING (dt, tbl, dt, dt);
RETURN stmt;
END FOR;
END;
If column or table is parameter it should be wrapped with IDENTIFIER/TABLE funtion.
For sample data:
CREATE OR REPLACE TABLE t AS
SELECT 'col1' AS dt, 'tab1' AS tbl UNION ALL
SELECT 'col2' AS dt, 'tab1' ;
CREATE TABLE tab1(col1 DATE, col2 DATE) AS
SELECT CURRENT_DATE(), CURRENT_DATE()-40;
CREATE TABLE result(cnt INT, day_of_month INT);
SELECT * FROM result;

There are lots of minor issues like missing semicolons etc. Here is the fixed script:
DECLARE
dt text;
tbl text;
stmt text;
c1 CURSOR FOR SELECT dt, tbl from t;
BEGIN
FOR record in c1 DO
dt := record.dt;
tbl := record.tbl;
stmt := 'SELECT COUNT(*)' ||
CONCAT(', DAYOFMONTH(', dt, ')') ||
CONCAT('\n FROM ', tbl) ||
CONCAT('\n WHERE YEAR(', dt, ')', ' = YEAR(CURRENT_DATE)') ||
CONCAT('\n AND MONTH(', dt, ')', ' = MONTH(CURRENT_DATE)') ||
'\n GROUP BY' ||
CONCAT('\n DAYOFMONTH(', dt, ')');
-- EXECUTE IMMEDIATE :stmt;
RETURN stmt;
END FOR;
END;

Related

execute immediate with $$

I have a long query that returns 1 column and 1 row:
Query 1:
select test_query from (
SELECT
LISTAGG('...') ... AS xx,
LISTAGG('"' || c.COLUMN_NAME || '"', ', ') WITHIN GROUP(ORDER BY c.COLUMN_NAME) AS column_list
.....
FROM INFORMATION_SCHEMA.COLUMNS c
WHERE TABLE_NAME = xx);
The output looks like this. It has a query that I want to run. Note that it consists of quotation marks. The original query 1 also has quotation marks in the LISTAGG.
output:
test_query
select "col1", "col2" from stg.new
I wanted to extend the Query A such that I can select and run the test_query and return the final output of the test_query instead of queryA. I tried using execute immediate with $$:
execute immediate
$$select test_query from (
SELECT
LISTAGG('...') ... AS xx,
LISTAGG('"' || c.COLUMN_NAME || '"', ', ') WITHIN GROUP(ORDER BY c.COLUMN_NAME) AS column_list
.....
FROM INFORMATION_SCHEMA.COLUMNS c
WHERE TABLE_NAME = xx)$$;
but instead of running the test_query it outputs the same thing as Query1.
What else can I try?
Here's an attempted conversion of your SQL template. You can extend it to add the entire SQL statement:
execute immediate
$$
declare
rs resultset default (
select test_query from (
SELECT
LISTAGG('...') ... AS xx,
LISTAGG('"' || c.COLUMN_NAME || '"', ', ') WITHIN GROUP(ORDER BY c.COLUMN_NAME) AS column_list
.....
FROM INFORMATION_SCHEMA.COLUMNS c
WHERE TABLE_NAME = xx);
);
c cursor for rs;
sqlStatement string;
rsFinal resultset;
begin
for rowContents in c do
sqlStatement := rowContents.SQL_STATEMENT;
end for;
rsFinal := (execute immediate :sqlStatement);
return table(rsFinal);
end;
$$
;
In case that's not working because of context, etc., here is a completely self-contained sample that's working:
create or replace transient table T1 as
select 'Hello world' as s;
create or replace transient table SQL_TO_RUN as
select 'select S from T1 SQL_STATEMENT' SQL_STATEMENT
;
execute immediate
$$
declare
rs resultset default (select SQL_STATEMENT from SQL_TO_RUN);
c cursor for rs;
sqlStatement string;
rsFinal resultset;
begin
for rowContents in c do
sqlStatement := rowContents.SQL_STATEMENT;
end for;
rsFinal := (execute immediate :sqlStatement);
return table(rsFinal);
end;
$$
;
Output:
S
Hello world

oracle dynamic pivot error: SQL command not properly ended

I have a table whose data is obtained according to the desired output with pivot. But I want to create the number of columns dynamically.
my table :
create table myTable(ROW_NAME varchar(10),COLUMN_NAME varchar(10),COLUMN_NAME_VALUE varchar(10));
table data :
insert into myTable (ROW_NAME,COLUMN_NAME,COLUMN_NAME_VALUE)
select 'ROW1','COL1','R1C1' from dual
union all select 'ROW1','COL2','R1C2' from dual
union all select 'ROW1','COL3','R1C3' from dual
union all select 'ROW2','COL1','R2C1' from dual
union all select 'ROW2','COL2','R2C2' from dual
union all select 'ROW2','COL3','R2C3' from dual
union all select 'ROW3','COL1','R3C1' from dual
union all select 'ROW3','COL2','R3C3' from dual
union all select 'ROW3','COL3','R3C3' from dual
my query :
select * from myTable
pivot (
max (COLUMN_NAME_VALUE)
for COLUMN_NAME
in (
'COL1' as COL1,'COL2' as COL2,'COL3' as COL3
)
)
ORDER BY ROW_NAME;
The above query works but I want to get the columns dynamically.
my dynamic query :
DECLARE
mycols VARCHAR2(1000);
sqlCommand varchar2(1000);
TYPE PivotCurTyp IS REF CURSOR;
pivot_cv PivotCurTyp;
piv_rec mytable%ROWTYPE;
BEGIN
select (select LISTAGG(COLUMN_NAME, ',') from myTable group by ROW_NAME FETCH FIRST 1 ROWS ONLY) into mycols from dual;
select Concat('select * from myTable pivot ( max (COLUMN_NAME_VALUE) for COLUMN_NAME in (',Concat(mycols,')) ORDER BY ROW_NAME;')) into sqlCommand from dual;
OPEN pivot_cv FOR sqlCommand;
LOOP
FETCH pivot_cv INTO piv_rec;
EXIT WHEN pivot_cv%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('ROW_NAME: ' || piv_rec.ROW_NAME || ' COL1: ' ||
piv_rec.COLUMN_NAME_VALUE || 'COL2: ' || piv_rec.COLUMN_NAME_VALUE || 'COL3: ' || piv_rec.COLUMN_NAME_VALUE);
END LOOP;
CLOSE pivot_cv;
END;
/
Note : The equivalent of the above query can be generated on SQL Server and I have created it.
demo in db<>fiddle
Thanks for any help
There are 3 problems in your script:
semicolon-terminated dynamic query (that's the cause of "SQL command not properly ended")
identifiers in in clause instead of string literals (you can use 'foo' or 'foo' as foo but not foo alone)
improper piv_rec type - use table format after pivot, not before pivot
Summary:
DECLARE
mycols VARCHAR2(1000);
sqlCommand varchar2(1000);
TYPE PivotCurTyp IS REF CURSOR;
pivot_cv PivotCurTyp;
type pivotted is record (row_name myTable.row_name%type, col1 myTable.column_name_value%type, col2 myTable.column_name_value%type, col3 myTable.column_name_value%type);
piv_rec pivotted;
BEGIN
select (select LISTAGG('''' || COLUMN_NAME || '''', ',') from myTable group by ROW_NAME FETCH FIRST 1 ROWS ONLY) into mycols from dual;
select Concat('select * from myTable pivot ( max (COLUMN_NAME_VALUE) for COLUMN_NAME in (',Concat(mycols,')) ORDER BY ROW_NAME')) into sqlCommand from dual;
DBMS_OUTPUT.PUT_LINE(sqlCommand);
OPEN pivot_cv FOR sqlCommand;
LOOP
FETCH pivot_cv INTO piv_rec;
EXIT WHEN pivot_cv%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('ROW_NAME: ' || piv_rec.ROW_NAME || ' COL1: ' ||
piv_rec.COL1 || ' COL2: ' || piv_rec.COL2 || ' COL3: ' || piv_rec.COL3);
END LOOP;
CLOSE pivot_cv;
END;
/
updated db fiddle (BTW composing fiddle was very motivating to help)

execute immediate not showing records of Dynamic Select statement

I created Anonymous block which is creating Select statement dynamically. when I execute block its only showing anonymous block completed but not showing SQL output.
declare
sql_stmt clob;
pivot_clause clob;
begin
select listagg('''' || TO_CHAR(PERIOD_NAME,'MON-YY') || ''' as "' || TO_CHAR(PERIOD_NAME,'MON-YY') || '"', ',')
within group (order by PERIOD_NAME)
into pivot_clause
from ( select TO_DATE(PERIOD_NAME,'MON-YYYY') PERIOD_NAME
from table1
where request_id=<id>
group by TO_DATE(PERIOD_NAME,'MON-YYYY')
order by TO_DATE(PERIOD_NAME,'MON-YYYY') ASC );
sql_stmt := 'select * from (select PERIOD_NAME, depreciation
from table1) pivot (sum(depreciation) for PERIOD_NAME in (' || pivot_clause || '))';
execute immediate sql_stmt;
end;
As you don't know the structure in advance, because of the dynamic pivot to an unknown number of columns in the result set, you could use a ref cursor to retrieve the result of the dynamic query.
This uses SQL*Plus/SQL Developer/SQLcl bind variables;
variable rc refcursor;
declare
sql_stmt clob;
pivot_clause clob;
begin
select listagg('''' || TO_CHAR(PERIOD_NAME,'MON-YY') || ''' as "' || TO_CHAR(PERIOD_NAME,'MON-YY') || '"', ',')
within group (order by PERIOD_NAME)
into pivot_clause from (select TO_DATE(PERIOD_NAME,'MON-YYYY') PERIOD_NAME
from table1
where request_id=<id>
GROUP BY TO_DATE(PERIOD_NAME,'MON-YYYY')
order by TO_DATE(PERIOD_NAME,'MON-YYYY') ASC);
sql_stmt := 'select * from (select PERIOD_NAME, depreciation
from table1) pivot (sum(depreciation) for PERIOD_NAME in (' || pivot_clause || '))';
open :rc for sql_stmt;
end;
/
print rc
The client variable command
variable rc refcursor;
declares the variable and data type of the client bind variable, as a reference cursor. Then rather than using execute immediate it does open for with your dynamic statement:
open :rc for sql_stmt;
which opens the ref cursor with the results of that query. (Notice the : at the start of :rc, indicating that is a bind variable reference not a local PL/SQL variable).
Then outside the block you can print the result set with:
print rc
Different clients/IDEs will need different syntax. You could do something similar over JDBC too. You could also have a function that returns a sys_refcursor. But it depends what your end goal for this is.
Incidentally, at the moment you'll get null for all the pivoted totals; your final query needs to get PERIOD_NAME in the same format the pivot clause is looking for, e.g.
sql_stmt := 'select * from (select to_char(to_date(PERIOD_NAME, ''MON-YYYY''), ''MON-YY'') as PERIOD_NAME, depreciation
from table1) pivot (sum(depreciation) for PERIOD_NAME in (' || pivot_clause || '))';
though it woudl be slightly simpler to leave the original format in the pivot clause instead:
declare
sql_stmt clob;
pivot_clause clob;
begin
select listagg('''' || PERIOD_NAME || ''' as "' || TO_CHAR(PERIOD_DATE,'MON-YY') || '"', ',')
within group (order by PERIOD_DATE)
into pivot_clause from (select distinct PERIOD_NAME, TO_DATE(PERIOD_NAME,'MON-YYYY') PERIOD_DATE
from table1
where request_id=<id>);
sql_stmt := 'select * from (select PERIOD_NAME, depreciation
from table1) pivot (sum(depreciation) for PERIOD_NAME in (' || pivot_clause || '))';
open :rc for sql_stmt;
end;
/
With a dummy table and data:
create table table1 (request_id, period_name, depreciation) as
select 1, 'JAN-2018', 42 from dual
union all select 1, 'FEB-2018', 11 from dual
union all select 1, 'MAR-2018', 22 from dual
union all select 1, 'MAR-2018', 33 from dual
union all select 2, 'MAR-2018', 44 from dual;
running either version and doing print rc shows:
JAN-18 FEB-18 MAR-18
---------- ---------- ----------
42 11 99
You can only select a dynamic sql into some variables.
Example:
declare
v_sql VARCHAR2(2000);
v_col1 varchar2(100);
v_col2 varchar2(100);
v_col3 varchar2(100);
begin
v_sql := 'SELECT 1, 2, 3 FROM DUAL';
EXECUTE IMMEDIATE v_sql INTO v_col1, v_col2, v_col3;
dbms_output.put_line('v_col1: ' || v_col1);
dbms_output.put_line('v_col2: ' || v_col2);
dbms_output.put_line('v_col3: ' || v_col3);
end;
If you got multiple rows you have to use a Cursor:
DECLARE
TYPE c IS REF CURSOR;
v_c c;
v_sql VARCHAR2(2000);
v_col1 VARCHAR2(100);
v_col2 VARCHAR2(100);
BEGIN
v_sql := 'SELECT 1, 2 FROM DUAL UNION ALL SELECT 3, 4 FROM DUAL';
OPEN v_c FOR v_sql;
LOOP
FETCH v_c INTO v_col1, v_col2;
EXIT WHEN v_c%NOTFOUND;
dbms_output.put_line('v_col1: ' || v_col1 || ', v_col2: ' || v_col2);
END LOOP;
CLOSE v_c;
END;
You need to give the EXECUTE IMMEDIATE a way to give the values back to your program so you need an INTO clause.
As you are returning a set of rows you will need a structure to store it.
For the sake of an example the following creates a dynamic query selecting from the ALL_OBJECTS view and puts the results into a collection.
DECLARE
sql_stmt CLOB;
TYPE my_rec_rt IS RECORD
( owner VARCHAR2(30),
object_name VARCHAR2(30) );
TYPE my_rec_t IS TABLE OF my_rec_rt;
obj_record my_rec_t;
BEGIN
sql_stmt :=
q'[select owner, object_name from all_objects where owner = 'ODS']';
EXECUTE IMMEDIATE sql_stmt BULK COLLECT INTO obj_record;
END;
How you go through the collection subsequently depends on your requirements.
(You can find this documented in the Oracle docs at https://docs.oracle.com/cd/B28359_01/appdev.111/b28370/executeimmediate_statement.htm#LNPLS01317 )
Based on your query it could be this:
declare
sql_stmt clob;
pivot_clause clob;
v_PERIOD_NAME DATE;
v_depreciation NUMBER;
begin
select listagg('''' || TO_CHAR(PERIOD_NAME,'MON-YY') || ''' as "' || TO_CHAR(PERIOD_NAME,'MON-YY') || '"', ',')
within group (order by PERIOD_NAME)
into pivot_clause
from ( select TO_DATE(PERIOD_NAME,'MON-YYYY') PERIOD_NAME
from table1
where request_id=<id>
group by TO_DATE(PERIOD_NAME,'MON-YYYY')
order by TO_DATE(PERIOD_NAME,'MON-YYYY') ASC );
sql_stmt := 'select * from (select PERIOD_NAME, depreciation
from table1) pivot (sum(depreciation) for PERIOD_NAME in (' || pivot_clause || '))';
execute immediate sql_stmt INTO v_PERIOD_NAME, v_depreciation;
dbms_output.put_line('v_PERIOD_NAME: ' || v_PERIOD_NAME);
dbms_output.put_line('v_depreciation: ' || v_depreciation);
end;
Note, this presumes that you will get only (and always) a single row back from the query. Otherwise you get exception NO_DATA_FOUND, resp. TOO_MANY_ROWS.

How to have XMLForest returning all columns of a table

I have a table with a lot of columns (e.g : Column1, Column 2, Column 3, Column 4, ...)
I'd like to use XMLElement and XMLForest function to generate an XML with each column being a tag.
I'm only able to do this by manually adding each column in the XMLForest :
e.g :
SELECT
XMLElement("ParentTag",
XMLForest(TABLE.Column1,
TABLE.Column2,
TABLE.Column2,
...)
)
FROM ...
Results :
<ParentTag> <Column1>Value1</Column1> <Column2>Value2</Column2> ...</ParentTag>
However i'd like to avoid typing each column as their number could increase in the future.
How can i do something like this ? :
SELECT
XMLElement("ParentTag",
XMLForest(TABLE.*)
)
FROM ...
You can use a PLSQL procedure to get your requirement done. Here in the PLSQL procedure, it would accept a Tablename and then generate the XMLForest and show the result. See below:
-- Creating a type of XMLTYPE
CREATE OR REPLACE TYPE Outpt IS TABLE OF XMLTYPE;
/
--Procedure with In parameter as Tablename and out parameter as resultset
CREATE OR REPLACE PROCEDURE XM_FOREST (tabnm VARCHAR2, v_out IN OUT Outpt)
AS
var VARCHAR2 (4000);
v_sql VARCHAR2 (4000);
BEGIN
FOR i IN (SELECT cname
FROM col
WHERE tname = tabnm)
LOOP
var := var || ',' || i.cname;
END LOOP;
var := LTRIM (var, ',');
v_sql :=
'select XMLElement("ParentTag",XMLForest('
|| var
|| ' ) ) from '
|| tabnm;
EXECUTE IMMEDIATE v_sql BULK COLLECT INTO v_out;
END;
--------------
--Execution
DECLARE
var_out Outpt := Outpt ();
LCLOB CLOB;
BEGIN
var_out.EXTEND;
XM_FOREST (tabnm => 'EMPLOYEE', v_out => var_out);
FOR i IN 1 .. var_out.COUNT
LOOP
LCLOB := var_out (i).getCLOBVAL ();
DBMS_OUTPUT.put_line (LCLOB);
END LOOP;
END;
------
--Result
SQL> /
<ParentTag><EMPLOYEE_ID>1</EMPLOYEE_ID><FIRST_NAME>XXX</FIRST_NAME></ParentTag>
<ParentTag><EMPLOYEE_ID>2</EMPLOYEE_ID><FIRST_NAME>YYY</FIRST_NAME></ParentTag>
PL/SQL procedure successfully completed.
How can i do something like this ? :
SELECT
XMLElement("ParentTag",
XMLForest(TABLE.*)
)
FROM ...
You cannot, you will have to type out all the names individually.
You could generate the query using dynamic SQL
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE table_name (
id NUMBER,
a NUMBER,
b NUMBER,
c NUMBER,
d NUMBER
);
Query 1:
SELECT '
SELECT XMLElement(
"ParentTag",
XMLForest( '
|| LISTAGG( '"' || column_name || '"', ',' )
WITHIN GROUP ( ORDER BY Column_id )
||' ) ) FROM ...' AS query
FROM user_tab_columns
WHERE table_name = 'TABLE_NAME'
Results:
| QUERY |
|-------------------------------------------------------|
| SELECT XMLElement( |
| "ParentTag", |
| XMLForest( "ID","A","B","C","D" ) ) FROM ... |

Count the number of rows in a schema based on a where condition

I have engine_id column in various tables in the schema. So I want to count the number of rows based on engine_id column in the whole schema where this column exists.
Select count(*)
from table_name
where table_name.engine_id = 8;
Without user-defined PL/SQL, using only built-in Oracle functionality...
Oracle 11g (and maybe even lower) query:
select TC.table_name, X.*
from user_tab_columns TC
cross join xmltable(
'/ROWSET/ROW/CNT'
passing
dbms_xmlgen.getXMLType('
select count(1) as cnt
from '||TC.table_name||'
where &columnName = &columnValueAsLiteral
')
columns
cnt integer
) X
where TC.column_name = '&columnName'
;
Oracle 12c+ query:
select TC.table_name, X.*
from user_tab_columns TC
cross apply xmltable(
'/ROWSET/ROW/CNT'
passing
dbms_xmlgen.getXMLType('
select count(1) as cnt
from '||TC.table_name||'
where &columnName = &columnValueAsLiteral
')
columns
cnt integer
) X
where TC.column_name = '&columnName'
;
In your case supply
&columnName as ENGINE_ID,
&columnValueAsLiteral as 8.
Note: It could also be possible with the with-PLSQL clause of 12c's but I somehow can't make it work, hence I'm not posting that solution here.
These are the steps which you should be following
Get all the tables which have the column.
select table_name from all_tab_columns
where column_name = 'ENGINE_ID';
Create the above as a cursor and run a loop on it
for records in above_cursor
loop
execute immediate 'select count(*) from ' || records.table_name || 'where engine_id = 8' into some_temp_number_var;
some_total_number_var := some_temp_number_var + some_total_number_var;
end loop;
You will need PL code that would query the data dictionary view USER_TABLES to find out which tables have the column you seek and then build a dynamic SQL query as per your filter requirements.
Your function would be something like this:
create or replace function find_count (p_column_name varchar2,
p_column_value number)
return number is
v_sql clob; HERE
v_count number;
begin
for i in (select table_name
from user_tab_cols
where column_name = upper(v_column_name)) loop
v_sql :=
v_sql
|| 'select count(*) as cnt from '
|| i.table_name
|| ' where '
|| p_column_name
|| ' = '
|| p_column_value;
v_sql := v_sql || chr (10) || 'union all ';
end loop;
v_sql := substr (v_sql, 1, length (v_sql) - 11);
v_sql := 'select sum(cnt) from (' || v_sql || ')';
execute immediate v_sql into v_count;
return v_count;
end find_count;
/
You can trigger this function using a query like this:
select find_count('ENTITY_ID', 1012) as engine_count from dual;