dynamic sql in plsql - sql

I have some code like this:
declare
p_vara varchar2;
p_varb varchar2;
p_varc varchar2;
begin
INSERT INTO MY_INSERTABLE_TABLE
SELECT a.id,b.id,c.id
FROM table_a a, table_b b, table_c c
WHERE a.id is not null
and a.id = b.id
and c.id = 'cat'
end;
Now based on the the variable to make it conditional so that only certain parts of the query get called based on the variable.
declare
p_vara varchar2;
p_varb varchar2;
p_varc varchar2;
begin
INSERT INTO MY_INSERTABLE_TABLE
SELECT a.id, -- Show only if p_vara = 'yes'
b.id, -- Show only if p_varb = 'yes'
c.id -- Show only if p_varc = 'yes'
FROM table_a a, -- Use only if p_vara = 'yes'
table_b b, -- Use only if p_varb = 'yes'
table_c c -- Use only if p_varc = 'yes'
WHERE a.id is not null -- Use only if p_vara = 'yes'
and a.id = b.id -- Use only if p_vara = 'yes' and p_varb = 'yes'
and c.id = 'cat' -- Use only if p_varc = 'yes'
end;
So for example if the variables are set as this:
p_vara = 'yes'
p_varb = 'no'
p_varc = 'no'
Then the query should look like this:
SELECT a.id
FROM table_a
WHERE a.id is null;

As written, your requirements do not appear to be complete. If all three variables are yes, for example, your full statement would have three table joins but only one join condition so you'd generate a Cartesian product with table_c. If p_vara = 'yes' and p_varc = 'yes', you'd have two tables joined with no join condition so you'd again have a Cartesian product. It seems unlikely to me that you really want to generate a Cartesian product.
In general, you can build a SQL statement in a string variable and then pass that to EXECUTE IMMEDIATE. If you have 25 boolean variables, that implies that your code could generate a total of 33.55 million distinct SQL statements. Just verifying that none of those paths generate a statement with syntax errors would be non-trivial. Combined with the fact that resorting to dynamic SQL generally makes code harder to read, maintain, and support in addition to creating opportunities for performance and security issues, I would tend to push back on any design that contemplates something as complex as what you are describing.
That said, you could do something like this (I'm not building the WHERE clause out completely but I trust you get the jist)
declare
l_vara boolean;
l_varb boolean;
l_varc boolean;
l_sql_stmt varchar(4000);
begin
l_sql_stmt := 'INSERT INTO my_insertable_table( col1, col2, col3 ) ';
l_sql_stmt := l_sql_stmt || ' SELECT ' ||
(case when l_vara then ' a.id, ' else ' null, ' end) ||
(case when l_varb then ' b.id, ' else ' null, ' end) ||
(case when l_varc then ' c.id, ' else ' null, ' end);
l_sql_stmt := rtrim( l_sql_stmt, ',' ); -- remove the extra trailing comma
l_sql_stmt := l_sql_stmt || ' FROM ';
if( l_vara )
then
l_sql_stmt := l_sql_stmt || ' table_a a, ';
end if;
if( l_varb )
then
l_sql_stmt := l_sql_stmt || ' table_b b, ';
end if;
if( l_varc )
then
l_sql_stmt := l_sql_stmt || ' table_c c, ';
end if;
-- again remove the extra trailing comma
l_sql_stmt := rtrim( l_sql_stmt, ',' );
<<build out the WHERE clause similarly>>
-- Log the SQL statement so you can debug it when it fails
insert into some_log_table( sql_stmt ) values( l_sql_stmt );
EXECUTE IMMEDIATE l_sql_stmt;
end;

Related

PL/SQL multiply Sql statements with if and then

Im trying to write a PL/SQL Code which should ask if something is there count it and
if tmp > 0
so if the object = 'VALID' then execute the rest of the sql statements if not skip everything in the Then case. When I Execute my code it dont execute the code.
DECLARE
tmp INT;
BEGIN
SELECT COUNT(*)
INTO tmp
FROM CDB_registry
WHERE Status = 'VALID';
IF tmp > 0 THEN
Col c.Status Format A6
Col Containers Format A20
Select c.Status, v.name as Containers
From Cdb_Ols_Status c, V\$Containers v
Where c.con_id = v.con_id
And c.Name = 'OLS_DIRECTORY_STATUS'
Order By v.Con_ID;
prompt ****************************************************************************************
DECLARE
tmp INT;
BEGIN
SELECT COUNT(*)
INTO tmp
FROM Cdb_Ols_Status
WHERE Status = 'TRUE'
AND name = 'OLS_DIRECTORY_STATUS';
IF tmp > 0 THEN
DBMS_Output.put_line('The Output is true');
ELSE
DBMS_Output.put_line('The Output is wrong');
END IF;
END;
/
ELSE
DBMS_Output.put_line('There is no value');
END IF;
END;
/
Here's one options; I presumed some things (read comments within code) & agree with #gsalem's comment.
You commented that
cdb_ols_status table does not exist when tmp < 0
It means that you'll have to use dynamic SQL (execute immediate) because - if table doesn't exist, code won't even compile. I guess that's why you said that code I previously posted won't work.
DECLARE
tmp INT;
BEGIN
SELECT COUNT (*)
INTO tmp
FROM cdb_registry
WHERE status = 'VALID';
IF tmp > 0
THEN
-- If this SELECT return only 1 row, you could declare local variables to fetch
-- those values INTO them.
-- Otherwise, it depends on what you want to do with them; if you'd just want to
-- DISPLAY their values (which is what your SQL*Plus COL ... FORMAT commands suggest),
-- then see whether a LOOP helps.
FOR cur_r IN ( SELECT c.status, v.name AS containers
FROM cdb_ols_status c, v$containers v
WHERE c.con_id = v.con_id
AND c.name = 'OLS_DIRECTORY_STATUS'
ORDER BY v.con_id)
LOOP
DBMS_OUTPUT.put_line (cur_r.status || ' - ' || cur_r.containers);
END LOOP;
-- The rest of the script should be ran because TMP value is greater than 0.
EXECUTE IMMEDIATE 'select count(*) from cdb_ols_status '
|| q'[where status = 'TRUE' and name = 'OLS_DIRECTORY-STATUS']'
INTO tmp;
IF tmp > 0
THEN
DBMS_OUTPUT.put_line ('The Output is true');
ELSE
DBMS_OUTPUT.put_line ('The Output is wrong');
END IF;
ELSE
DBMS_OUTPUT.put_line ('There is no value');
END IF;
END;
/

List columns of table on which there is at least one row with a non null value for a specific query

I'm trying to find the proper query to :
Get all the column names of a table on which there is at least one row with a non null value for a specific query.
Meaning : I will see which columns have at least one value set in the record returned by my given query.
I hope I'm clear enough.
I think you need something as the following:
SELECT CASE WHEN MAX(col1) IS NOT NULL THEN 'COL1' END ||','
|| CASE WHEN MAX(col2) IS NOT NULL THEN 'COL2' END ||','
...
FROM T
Then use REGEXP_REPLACE to replace duplicated ,. You can use user_tab_columns to generate this query dynamically as mentioned.
Please check below anonymous block for your case , tablename should be given at 3 places in below query
Declare
v_columnlist varchar2(32627);
v_noofcolumns number;
Columnnm varchar2(1000);
Finalcolumns varchar2(32627);
v_count number:=0;
plsql_block varchar2(32627);
Begin
select LISTAGG(column_name,',') within group (order by column_id),max(column_id)
into v_columnlist , v_noofcolumns
from user_tab_columns
where table_name='tablename';
FOR Lcntr IN 1..v_noofcolumns
LOOP
select REGEXP_SUBSTR(v_columnlist,'[^,]+',1,Lcntr)
into Columnnm
from dual;
plsql_block := 'select count(*) from tablename where '|| Columnnm || ' is not null ';
EXECUTE IMMEDIATE plsql_block into v_count;
IF v_count > 1 THEN
Finalcolumns:= LTRIM(Finalcolumns ||','||Columnnm,',');
END IF;
END LOOP;
plsql_block := 'select ' || Finalcolumns ||' from tablename ';
DBMS_OUTPUT.PUT_LINE(plsql_block);
END;

Errors in PLSQL -

Morning,
I'm trying to write a script that will convert Unload tables (UNLD to HDL files) creating a flat file using PLSQL. I keep getting syntax errors trying to run it and would appreciate some help from an expert out there!
Here are the errors:
Error(53,21): PLS-00330: invalid use of type name or subtype name
Error(57,32): PLS-00222: no function with name 'UNLDTABLE' exists in this scope
Our guess is that the unldTable variable is being treated as a String, rather than a database table object (Not really expereinced in PLSQL)
CREATE OR REPLACE PROCEDURE UNLD_TO_HDL (processComponent IN VARCHAR2)
IS
fHandle UTL_FILE.FILE_TYPE;
concatData VARCHAR2(240);
concatHDLMetaTags VARCHAR2(240);
outputFileName VARCHAR2(240);
TYPE rowArrayType IS TABLE OF VARCHAR2(240);
rowArray rowArrayType;
emptyArray rowArrayType;
valExtractArray rowArrayType;
hdlFileName VARCHAR2(240);
unldTable VARCHAR2(240);
countUNLDRows Number;
dataType VARCHAR2(240);
current_table VARCHAR2(30);
value_to_char VARCHAR2(240);
BEGIN
SELECT HDL_FILE_NAME
INTO hdlFileName
FROM GNC_HDL_CREATION_PARAMS
WHERE PROCESS_COMPONENT = processComponent;
SELECT UNLD_TABLE
INTO unldTable
FROM GNC_HDL_CREATION_PARAMS
WHERE PROCESS_COMPONENT = processComponent
FETCH NEXT 1 ROWS ONLY;
SELECT LISTAGG(HDL_META_TAG,'|')
WITHIN GROUP(ORDER BY HDL_META_TAG)
INTO concatHDLMetaTags
FROM GNC_MIG_CONTROL
WHERE HDL_COMP = processComponent;
SELECT DB_FIELD
BULK COLLECT INTO valExtractArray
FROM GNC_MIG_CONTROL
WHERE HDL_COMP = processComponent
ORDER BY HDL_META_TAG;
fHandle := UTL_FILE.FOPEN('./', hdlFileName, 'W');
UTL_FILE.PUTF(fHandle, concatHDLMetaTags + '\n');
SELECT num_rows INTO countUNLDRows FROM user_tables where table_name = unldTable;
FOR row in 1..countUNLDRows LOOP
rowArray := emptyArrayType;
FOR value in 1..valExtractArray.COUNT LOOP
rowArray.extend();
SELECT data_type INTO dataType FROM all_tab_columns where table_name = unldTable AND column_name = valExtractArray(value);
IF dataType = 'VARCHAR2' THEN (SELECT valExtractArray(value) INTO value_to_char FROM current_table WHERE ROWNUM = row);
ELSIF dataType = 'DATE' THEN (SELECT TO_CHAR(valExtractArray(value),'YYYY/MM/DD') INTO value_to_char FROM current_table WHERE ROWNUM = row);
ELSIF dataType = 'NUMBER' THEN (SELECT TO_CHAR(valExtractArray(value)) INTO value_to_char FROM current_table WHERE ROWNUM = row);
ENDIF;
rowArray(value) := value_to_char;
END LOOP;
concatData := NULL;
FOR item in 1..rowArray.COUNT LOOP
IF item = rowArray.COUNT
THEN concatData := (COALESCE(concatData,'') || rowArray(item));
ELSE concatData := (COALESCE(concatData,'') || rowArray(item) || '|');
END IF;
END LOOP;
UTL_FILE.PUTF(fHandle, concatData + '/n');
END LOOP;
UTL_FILE.FCLOSE(fHandle);
END;
Thanks,
Adam
I believe it is just an overlook in your code. You define unldTable as a varchar, which is used correctly until you try to access it as if it were a varray on line 51
rowArray(value) := unldTable(row).valExtractArray(value);
Given that you have not defined it as a varray, unldTable(row) is making the interpreter believe that you are referring to a function.
EDIT
Now that you have moved on, you should resolve the problem of invoking SELECT statements on tables that are unknown at runtime. To do so you need to make use of Dynamic SQL; you can do it in several way, the most direct being an Execute immediate statement in your case:
mystatement := 'SELECT valExtractArray(value) INTO :value_to_char FROM ' || current_table || ' WHERE ROWNUM = ' || row;
execute immediate mystatement USING OUT value_to_char;
It looks like you need to generate a cursor as
select [list of columns from GNC_MIG_CONTROL.DB_FIELD]
from [table name from GNC_HDL_CREATION_PARAMS.UNLD_TABLE]
Assuming setup like this:
create table my_table (business_date date, id integer, dummy1 varchar2(1), dummy2 varchar2(20));
create table gnc_hdl_creation_params (unld_table varchar2(30), process_component varchar2(30));
create table gnc_mig_control (db_field varchar2(30), hdl_comp varchar2(30), hdl_meta_tag integer);
insert into my_table(business_date, id, dummy1, dummy2) values (date '2018-01-01', 123, 'X','Some more text');
insert into gnc_hdl_creation_params (unld_table, process_component) values ('MY_TABLE', 'XYZ');
insert into gnc_mig_control (db_field, hdl_comp, hdl_meta_tag) values ('BUSINESS_DATE', 'XYZ', '1');
insert into gnc_mig_control (db_field, hdl_comp, hdl_meta_tag) values ('ID', 'XYZ', '2');
insert into gnc_mig_control (db_field, hdl_comp, hdl_meta_tag) values ('DUMMY1', 'XYZ', '3');
insert into gnc_mig_control (db_field, hdl_comp, hdl_meta_tag) values ('DUMMY2', 'XYZ', '4');
You could build a query like this:
select unld_table, listagg(expr, q'[||'|'||]') within group (order by hdl_meta_tag) as expr_list
from ( select t.unld_table
, case tc.data_type
when 'DATE' then 'to_char('||c.db_field||',''YYYY-MM-DD'')'
else c.db_field
end as expr
, c.hdl_meta_tag
from gnc_hdl_creation_params t
join gnc_mig_control c
on c.hdl_comp = t.process_component
left join user_tab_columns tc
on tc.table_name = t.unld_table
and tc.column_name = c.db_field
where t.process_component = 'XYZ'
)
group by unld_table;
Output:
UNLD_TABLE EXPR_LIST
----------- --------------------------------------------------------------------------------
MY_TABLE to_char(BUSINESS_DATE,'YYYY-MM-DD')||'|'||ID||'|'||DUMMY1||'|'||DUMMY2
Now if you plug that logic into a PL/SQL procedure you could have something like this:
declare
processComponent constant gnc_hdl_creation_params.process_component%type := 'XYZ';
unloadSQL long;
unloadCur sys_refcursor;
text long;
begin
select 'select ' || listagg(expr, q'[||'|'||]') within group (order by hdl_meta_tag) || ' as text from ' || unld_table
into unloadSQL
from ( select t.unld_table
, case tc.data_type
when 'DATE' then 'to_char('||c.db_field||',''YYYY/MM/DD'')'
else c.db_field
end as expr
, c.hdl_meta_tag
from gnc_hdl_creation_params t
join gnc_mig_control c
on c.hdl_comp = t.process_component
left join user_tab_columns tc
on tc.table_name = t.unld_table
and tc.column_name = c.db_field
where t.process_component = processComponent
)
group by unld_table;
open unloadCur for unloadSQL;
loop
fetch unloadCur into text;
dbms_output.put_line(text);
exit when unloadCur%notfound;
end loop;
close unloadCur;
end;
Output:
2018/01/01|123|X|Some more text
2018/01/01|123|X|Some more text
Now you just have to make that into a procedure, change dbms_output to utl_file and add your meta tags etc and you're there.
I've assumed there is only one distinct unld_table per process component. If there are more you'll need a loop to work through each one.
For a slightly more generic approach, you could build a cursor-to-csv generator which could encapsulate the datatype handling, and then you'd only need to build the SQL as select [columns] from [table]. You might then write a generic cursor to file processor, where you pass in the filename and a cursor and it does the lot.
Edit: I've updated my cursor-to-csv generator to provide file output, so you just need to pass it a cursor and the file details.

Trigger created with compilation errors in oracle

I am learning ORACLE and haven't been able to figure out why i am getting compilation errors when trying to create this trigger. Thank you for any help!
CREATE OR REPLACE TRIGGER TR_HISTORY
BEFORE INSERT ON HISTORY
FOR EACH ROW
DECLARE name_d varchar2(50), breed_d varchar2(50), area_d varchar2(50)
BEGIN
SELECT NAME INTO name_d FROM ANIMALS A WHERE A.ID = :NEW.ID;
SELECT BREED INTO breed_d FROM ANIMALS A WHERE A.ID = :NEW.ID;
SELECT AREA INTO area_d FROM STORE S WHERE S.STORE_ID = :NEW.STORE_ID;
IF (:NEW.DONE ='T')
THEN
:NEW.MSG = 'Hi , your animal ' || name_d || ' breed: ' || breed_d || 'is
at ' || area_d || '.';
ELSE
UPDATE :NEW.MSG = 'Not finished';
END IF;
END;
/
There were few issues with your Trigger code syntax.
we use semicolons ';' after each expression in declare not comma
','
Check the proper syntax for IF Condition.
THEN UPDATE
:NEW.MSG =
Is not a valid statement. It is simply :NEW.MSG :=
CREATE OR replace TRIGGER tr_history
BEFORE INSERT ON history
FOR EACH ROW
DECLARE
name_d VARCHAR2(50);
breed_d VARCHAR2(50);
area_d VARCHAR2(50);
BEGIN
SELECT name
INTO name_d
FROM animals A
WHERE A.id = :NEW.id;
SELECT breed
INTO breed_d
FROM animals A
WHERE A.id = :NEW.id;
SELECT area
INTO area_d
FROM store S
WHERE S.store_id = :NEW.store_id;
IF :NEW.done = 'T' THEN
:NEW.msg := 'Hi , your animal '
|| name_d
|| ' breed: '
|| breed_d
|| 'is at '
|| area_d
|| '.';
ELSE
:NEW.msg := 'Not finished';
END IF;
END;
/

Count the number of null values into an Oracle table?

I need to count the number of null values of all the columns in a table in Oracle.
For instance, I execute the following statements to create a table TEST and insert data.
CREATE TABLE TEST
( A VARCHAR2(20 BYTE),
B VARCHAR2(20 BYTE),
C VARCHAR2(20 BYTE)
);
Insert into TEST (A) values ('a');
Insert into TEST (B) values ('b');
Insert into TEST (C) values ('c');
Now, I write the following code to compute the number of null values in the table TEST:
declare
cnt number :=0;
temp number :=0;
begin
for r in ( select column_name, data_type
from user_tab_columns
where table_name = upper('test')
order by column_id )
loop
if r.data_type <> 'NOT NULL' then
select count(*) into temp FROM TEST where r.column_name IS NULL;
cnt := cnt + temp;
END IF;
end loop;
dbms_output.put_line('Total: '||cnt);
end;
/
It returns 0, when the expected value is 6.
Where is the error?
Thanks in advance.
Counting NULLs for each column
In order to count NULL values for all columns of a table T you could run
SELECT COUNT(*) - COUNT(col1) col1_nulls
, COUNT(*) - COUNT(col2) col2_nulls
,..
, COUNT(*) - COUNT(colN) colN_nulls
, COUNT(*) total_rows
FROM T
/
Where col1, col2, .., colN should be replaced with actual names of columns of T table.
Aggregate functions -like COUNT()- ignore NULL values, so COUNT(*) - COUNT(col) will give you how many nulls for each column.
Summarize all NULLs of a table
If you want to know how many fields are NULL, I mean every NULL of every record you can
WITH d as (
SELECT COUNT(*) - COUNT(col1) col1_nulls
, COUNT(*) - COUNT(col2) col2_nulls
,..
, COUNT(*) - COUNT(colN) colN_nulls
, COUNT(*) total_rows
FROM T
) SELECT col1_nulls + col1_nulls +..+ colN_null
FROM d
/
Summarize all NULLs of a table (using Oracle dictionary tables)
Following is an improvement in which you need to now nothing but table name and it is very easy to code a function based on it
DECLARE
T VARCHAR2(64) := '<YOUR TABLE NAME>';
expr VARCHAR2(32767);
q INTEGER;
BEGIN
SELECT 'SELECT /*+FULL(T) PARALLEL(T)*/' || COUNT(*) || ' * COUNT(*) OVER () - ' || LISTAGG('COUNT(' || COLUMN_NAME || ')', ' + ') WITHIN GROUP (ORDER BY COLUMN_ID) || ' FROM ' || T
INTO expr
FROM USER_TAB_COLUMNS
WHERE TABLE_NAME = T;
-- This line is for debugging purposes only
DBMS_OUTPUT.PUT_LINE(expr);
EXECUTE IMMEDIATE expr INTO q;
DBMS_OUTPUT.PUT_LINE(q);
END;
/
Due to calculation implies a full table scan, code produced in expr variable was optimized for parallel running.
User defined function null_fields
Function version, also includes an optional parameter to be able to run on other schemas.
CREATE OR REPLACE FUNCTION null_fields(table_name IN VARCHAR2, owner IN VARCHAR2 DEFAULT USER)
RETURN INTEGER IS
T VARCHAR2(64) := UPPER(table_name);
o VARCHAR2(64) := UPPER(owner);
expr VARCHAR2(32767);
q INTEGER;
BEGIN
SELECT 'SELECT /*+FULL(T) PARALLEL(T)*/' || COUNT(*) || ' * COUNT(*) OVER () - ' || listagg('COUNT(' || column_name || ')', ' + ') WITHIN GROUP (ORDER BY column_id) || ' FROM ' || o || '.' || T || ' t'
INTO expr
FROM all_tab_columns
WHERE table_name = T;
EXECUTE IMMEDIATE expr INTO q;
RETURN q;
END;
/
-- Usage 1
SELECT null_fields('<your table name>') FROM dual
/
-- Usage 2
SELECT null_fields('<your table name>', '<table owner>') FROM dual
/
Thank you #Lord Peter :
The below PL/SQL script works
declare
cnt number :=0;
temp number :=0;
begin
for r in ( select column_name, nullable
from user_tab_columns
where table_name = upper('test')
order by column_id )
loop
if r.nullable = 'Y' then
EXECUTE IMMEDIATE 'SELECT count(*) FROM test where '|| r.column_name ||' IS NULL' into temp ;
cnt := cnt + temp;
END IF;
end loop;
dbms_output.put_line('Total: '||cnt);
end;
/
The table name test may be replaced the name of table of your interest.
I hope this solution is useful!
The dynamic SQL you execute (this is the string used in EXECUTE IMMEDIATE) should be
select sum(
decode(a,null,1,0)
+decode(b,null,1,0)
+decode(c,null,1,0)
) nullcols
from test;
Where each summand corresponds to a NOT NULL column.
Here only one table scan is necessary to get the result.
Use the data dictionary to find the number of NULL values almost instantly:
select sum(num_nulls) sum_num_nulls
from all_tab_columns
where owner = user
and table_name = 'TEST';
SUM_NUM_NULLS
-------------
6
The values will only be correct if optimizer statistics were gathered recently and if they were gathered with the default value for the sample size.
Those may seem like large caveats but it's worth becoming familiar with your database's statistics gathering process anyway. If your database is not automatically gathering statistics or if your database is not using the default sample size those are likely huge problems you need to be aware of.
To manually gather stats for a specific table a statement like this will work:
begin
dbms_stats.gather_table_stats(user, 'TEST');
end;
/
select COUNT(1) TOTAL from table where COLUMN is NULL;