Append oracle associative array in a loop to another associative array within that loop - sql

I am trying to do to a bulk collect inside a loop which have dynamic SQL and execute multiple times based on input from loop then inserting into a table (and it is taking time approx. 4 mins to insert 193234 records).
So as to try different different approach I think of using the bulk collect on select inside the loop and fill up a collection with each iteration of that loop lets say 1st iteration gives 10 rows then second gives 0 rows and 3rd returns 15 rows then the collection should hold 15 records at end of the loop.
After exiting the loop I will use forall with collection I filled up inside loop to do an Insert at one go instead to doing insert for each iteration inside loop.
below is a sample code which is similar to application procedure I just use different tables to simplify question.
create table test_tab as select owner, table_name, column_name from all_tab_cols where 1=2;
create or replace procedure p_test
as
l_sql varchar2(4000);
type t_tab is table of test_tab%rowtype index by pls_integer;
l_tab t_tab;
l_tab1 t_tab;
l_cnt number := 0;
begin
for i in (with tab as (select 'V_$SESSION' table_name from dual
union all
select 'any_table' from dual
union all
select 'V_$TRANSACTION' from dual
union all
select 'test_table' from dual
)
select table_name from tab )
loop
l_sql := 'select owner, table_name, column_name from all_tab_cols where table_name = '''||i.table_name||'''';
-- dbms_output.put_line(l_sql );
execute immediate l_sql bulk collect into l_tab;
dbms_output.put_line(l_sql ||' > '||l_tab.count);
l_cnt := l_cnt +1;
if l_tab.count<>0
then
l_tab1(l_cnt) := l_tab(l_cnt);
end if;
end loop;
dbms_output.put_line(l_tab1.count);
forall i in indices of l_tab1
insert into test_tab values (l_tab1(i).owner, l_tab1(i).table_name, l_tab1(i).column_name);
end;
It is inserting only 2 rows in test_tab table whereas as per my system it should insert 150 rows.
select owner, table_name, column_name from all_tab_cols where table_name = 'V_$SESSION' > 103
select owner, table_name, column_name from all_tab_cols where table_name = 'any_table' > 0
select owner, table_name, column_name from all_tab_cols where table_name = 'V_$TRANSACTION' > 47
select owner, table_name, column_name from all_tab_cols where table_name = 'test_table' > 0
2
Above is DBMS_OUTPUT from my system you may change the table names in loop if the example table names does not exists in your DB.
Oracle Version --
Oracle Database 19c Standard Edition 2 Release 19.0.0.0.0 - Production
EDIT
Below screenshot shows highlighted timings from PLSQL_PROFILER with Actual insert...select... written in procedure at line# 114 and with bulk collect and forall with nested table at line# 132 and multiset and seems like we are saving atleast 40 secs here with bulk collect, multiset and forall.

Firstly, do not use associative array collection for this, just use a nested-table collection type. You can concatenate nested-table collections using the MULTISET UNION ALL operator (avoiding needing to use loops).
CREATE TYPE test_type IS OBJECT(
owner VARCHAR2(30),
table_name VARCHAR2(30),
column_name VARCHAR2(30)
);
CREATE TYPE test_tab_type IS TABLE OF test_type;
Then:
create procedure p_test
as
l_sql CLOB := 'select test_type(owner, table_name, column_name) from all_tab_cols where table_name = :table_name';
l_table_names SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST(
'V_$SESSION',
'ANY_TABLE',
'V_$TRANSACTION',
'TEST_TABLE'
);
l_tab test_tab_type := test_tab_type();
l_temp test_tab_type;
l_cnt number := 0;
BEGIN
FOR i IN 1 .. l_table_names.COUNT LOOP
EXECUTE IMMEDIATE l_sql BULK COLLECT INTO l_temp USING l_table_names(i);
dbms_output.put_line(
l_sql || ': ' || l_table_names(i) || ' > '||l_temp.count
);
l_cnt := l_cnt +1;
l_tab := l_tab MULTISET UNION ALL l_temp;
END LOOP;
dbms_output.put_line(l_tab.count);
insert into test_tab
SELECT *
FROM TABLE(l_tab);
end;
/
Secondly, don't do multiple queries if you can do it all in one query and use an IN statement; and if you do it all in a single statement then you do not need to worry about concatenating collections.
create or replace procedure p_test
as
l_table_names SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST(
'V_$SESSION',
'ANY_TABLE',
'V_$TRANSACTION',
'TEST_TABLE'
);
l_tab test_tab_type;
BEGIN
select test_type(owner, table_name, column_name)
bulk collect into l_tab
from all_tab_cols
where table_name IN (SELECT column_value FROM TABLE(l_table_names));
dbms_output.put_line(l_tab.count);
insert into test_tab
SELECT *
FROM TABLE(l_tab);
end;
/
Thirdly, if you can do INSERT ... SELECT ... in a single statement the it will be much faster than using SELECT ... INTO ... and then a separate INSERT; and doing that means you do not need to use any collections.
create or replace procedure p_test
as
begin
INSERT INTO test_tab (owner, table_name, column_name)
select owner, table_name, column_name
from all_tab_cols
where table_name IN (
'V_$SESSION',
'ANY_TABLE',
'V_$TRANSACTION',
'TEST_TABLE'
);
end;
/
fiddle

For Oracle 19c I would suggest to use SQL_MACRO(table) to build dynamic SQL in place and use plain SQL. Below is an example that builds dynamic SQL based on all_tab_cols but it may be any other logic to build such SQL (with known column names and column order). Then you may use insert ... select ... without PL/SQL, because SQL macro is processed at the query parsing time.
Setup:
create table t1
as
select level as id, mod(level, 2) as val, mod(level, 2) as col
from dual
connect by level < 4;
create table t2
as
select level as id2, mod(level, 2) as val2, mod(level, 2) as col2
from dual
connect by level < 5;
create table t3
as
select level as id3, mod(level, 2) as val3, mod(level, 2) as col3
from dual
connect by level < 6;
Usage:
create function f_union_tables
return varchar2 sql_macro(table)
as
l_sql varchar2(4000);
begin
/*
Below query emulates dynamic SQL with union
of counts per columns COL* and VAL* per table,
assuming you have only one such column in a table
*/
select
listagg(
replace(replace(
/*Get a count per the second and the third column per table*/
q'{
select '$$table_name$$' as table_name, $$agg_cols$$, count(*) as cnt
from $$table_name$$
group by $$agg_cols$$
}',
'$$table_name$$', table_name),
'$$agg_cols$$', listagg(column_name, ' ,') within group(order by column_name asc)
),
chr(10) || ' union all ' || chr(10)
) within group (order by table_name) as result_sql
into l_sql
from user_tab_cols
where regexp_like(table_name, '^T\d+$')
and (
column_name like 'VAL%'
or column_name like 'COL%'
)
group by table_name;
return l_sql;
end;/
select *
from f_union_tables()
TABLE_NAME
COL
VAL
CNT
T1
1
1
2
T1
0
0
1
T2
1
1
2
T2
0
0
2
T3
1
1
3
T3
0
0
2
fiddle

Related

Oracle- creating dynamic function for deleting tables based on cursor

I'm trying to build a dynamic function in Oracle using a cursor for all the tables that need to be dropped and re-created again. For example, I have the following example table structure:
CREATE TABLE All_tmp_DATA AS
(SELECT 'T_tmp_test1' As Table_NM, 'TEST1' As Process_name FROM DUAL UNION ALL
SELECT 'T_tmp_test2' As Table_NM, 'TEST1' As Process_name FROM DUAL UNION ALL
SELECT 'T_tmp_test3' As Table_NM, 'TEST1' As Process_name FROM DUAL)
The above tables starting with "T_tmp" represent all the tables in the database which needs to be dropped if their counts are >1 when starting the TEST1 process. I really need a function to pass in the parameter Process_name where I can input "TEST1", and build a loop using a cursor by binding it to the Table_NM from All_tmp_DATA and inserting it into table_name in the following code:
BEGIN
SELECT count(*)
INTO l_cnt
FROM user_tables
WHERE table_name = 'MY_TABLE';
IF l_cnt = 1 THEN
EXECUTE IMMEDIATE 'DROP TABLE my_table';
END IF;
END;
In the beginning, I'd suggest you not to use mixed case when naming Oracle objects.
Test case:
SQL> select * From all_tmp_data;
TABLE_NM PROCE
----------- -----
T_tmp_test1 TEST1
T_tmp_test2 TEST1
T_tmp_test3 TEST1
SQL> create table "T_tmp_test1" as select * From dept;
Table created.
SQL> -- I don't have "T_tmp_test2"
SQL> create table "T_tmp_test3" as select * From emp;
Table created.
SQL>
SQL> select table_name From user_Tables where upper(table_name) like 'T_TMP%';
TABLE_NAME
------------------------------
T_tmp_test3
T_tmp_test1
Procedure which drops tables contained in ALL_TMP_DATA:
as opposed to your code, I concatenated table name with DROP
as you use table names with mixed case, you have to enclose their names into double quotes, always (did I say not do use that?)
As the final select shows, those tables don't exist any more.
SQL> declare
2 l_cnt number;
3 begin
4 for cur_r in (select table_nm from all_tmp_data) loop
5 select count(*) into l_cnt
6 from user_tables
7 where table_name = cur_r.table_nm;
8
9 if l_cnt > 0 then
10 execute immediate ('drop table "' || cur_r.table_nm || '"');
11 end if;
12 end loop;
13 end;
14 /
PL/SQL procedure successfully completed.
SQL> select table_name From user_Tables where upper(table_name) like 'T_TMP%';
no rows selected
SQL>
As of the process column: I have no idea what is it used for so I did exactly that - didn't use it.
You can use the exception handling to handle such scenario directly as follows:
DECLARE
TABLE_DOES_NOT_EXIST EXCEPTION;
PRAGMA EXCEPTION_INIT ( TABLE_DOES_NOT_EXIST, -00942 );
BEGIN
FOR CUR_R IN (
SELECT TABLE_NM
FROM ALL_TMP_DATA
) LOOP
BEGIN
EXECUTE IMMEDIATE 'drop table "' || cur_r.table_nm || '"';
DBMS_OUTPUT.PUT_LINE('"' || cur_r.table_nm || '" table dropped.');
EXCEPTION
WHEN TABLE_DOES_NOT_EXIST THEN
DBMS_OUTPUT.PUT_LINE('"' || cur_r.table_nm || '" table does not exists');
END;
END LOOP;
END;
/

Iterate through row's columns

I have table with 100 columns with not correlated names (ABC1, DA23, EE123 - there is no common pattern there).
I want to iterate through every row and every column in this table.
My current script:
BEGIN
FOR single_row IN (
SELECT *
FROM MY_TABLE)
LOOP
--iterate through columns of 'single_row'
--for each nullable column do insert with real current column name and column value)
--I assume each column is nullable except of ID
INSERT INTO ANOTHER_TABLE VALUES (single_row.id, column_name, column_value);
END LOOP;
END;
So for example, if MY_TABLE contains 2 rows:
ID|ABC1|DA23|EE123|...
1|123|456|789|...
2|321|654|987|...
After running my script, my ANOTHER_TABLE will contain:
MY_TABLE_ID|COLUMN_NAME|COLUMN_VALUE
1|ABC1|123
1|DA23|456
1|EE123|789
... other columns from row 1
2|ABC1|321
2|DA23|654
2|EE123|987
... other columns from row 2
How I can do this?
I'm using Oracle 11g
EDIT
#vkp provided great solution, but there is one more thing to solve. I don't want to specify all columns in in clause. I would love to use some kind of query there or * or anything else, just to not be forced to list all of them.
I have tried something like this:
select *
from MY_TABLE t
unpivot (
column_value for column_name in (select column_name
from user_tab_columns
where table_name = 'MY_TABLE'
and nullable = 'Y')
) u
but it returns error:
ORA-00904: : invalid identifier
00904. 00000 - "%s: invalid identifier"
This is an application of unpivot.
select *
from my_table m
unpivot (column_value for column_name in (ABC1,DA23,EE123)) u
null values for any of the columns for an id won't be shown in the result.
If you have to include null values in the output, use the option INCLUDE NULLS.
select *
from my_table m
unpivot include nulls (column_value for column_name in (ABC1,DA23,EE123)) u
Edit: To include column names dynamically, use
DECLARE
sql_stmt VARCHAR2(4000);
var_columns VARCHAR2(4000); --use clob datatype if the column names can't fit in with this datatype
BEGIN
SELECT LISTAGG(column_name,',') WITHIN GROUP(ORDER BY column_name)
INTO var_columns
FROM user_tab_columns
WHERE table_name='MY_TABLE' AND column_name<>'ID';
sql_stmt:='select * from my_table m
unpivot
(column_value for column_name in (' || var_columns || ')) u';
EXECUTE IMMEDIATE sql_stmt;
END;
/
First option. With dynamic sql.
declare
v_ctx number;
v_query varchar2(500);
v_total NUMBER;
v_desctab DBMS_SQL.DESC_TAB;
v_column_cnt NUMBER;
v_value varchar2(32767);
v_result clob := '';
v_rownum number := 0;
begin
v_ctx := dbms_sql.open_cursor;
v_query := 'select * from user_objects where rownum < 100';
dbms_sql.parse(v_ctx,v_query,dbms_sql.v7);
v_total := dbms_sql.execute(v_ctx);
DBMS_SQL.DESCRIBE_COLUMNS(v_ctx, v_column_cnt, v_desctab);
for i in 1 .. v_column_cnt loop
dbms_sql.define_column(v_ctx, i, v_value /* data_type varchar2*/, 32767 /* max_length*/);
end loop;
loop
exit when dbms_sql.fetch_rows(v_ctx) = 0;
v_rownum := v_rownum +1;
for i in 1 .. v_column_cnt loop
dbms_sql.column_value(v_ctx, i, v_value);
dbms_output.put_line(v_rownum||' - '||v_desctab(i).col_name||' - '||v_value);
end loop;
end loop;
dbms_sql.close_cursor(v_ctx);
exception
when others then
dbms_sql.close_cursor(v_ctx);
raise;
end;
/
2nd option with xquery.
select t1.id,t2.* from xmltable('for $i in ora:view("<you_table_here>")/ROW
return $i'
columns id FOR ORDINALITY
, row_value xmltype path'.'
) t1
,xmltable('for $i in $row_value/ROW/* return $i'
passing t1.row_value as "row_value"
columns col_index for ORDINALITY ,
column_name varchar2(100) path 'name()',
column_value varchar2(100) path 'text()'
) t2
Here is a simple solution using REF CURSOR.
I've tried this code and it's working at my end.
DECLARE
query_2 VARCHAR2(1000);
TYPE icur IS REF CURSOR;
ic icur;
col_val VARCHAR2(100);
BEGIN
FOR j IN
(SELECT * FROM user_tab_cols WHERE table_name = UPPER('MY_TABLE'))
LOOP
dbms_output.put_line(j.column_name);
query_2 := 'SELECT ' || j.column_name|| ' FROM MY_TABLE';
OPEN ic FOR query_2;
LOOP
FETCH ic INTO col_val;
EXIT WHEN ic%NOTFOUND;
INSERT INTO ANOTHER_TABLE VALUES( j.column_name, col_val);
END LOOP;
END LOOP;
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;

How select tablename from user_table

I have dinamics tables on my system, it change one number on the name so I do not have a fixed name. I wish to select the lasted table but only the name of the table to make a select to this table.
SELECT * FROM ((SELECT * FROM (select a.tablespace_name || '.' || a.table_name AS TABLE_NAME
from user_tables a
wherE a.tablespace_name='USERNAME'
and a.table_name like '%_DIN'
ORDER BY TABLE_NAME DESC)
WHERE ROWNUM = '1'))
for example.
the table name of
SELECT * FROM (select a.tablespace_name || '.' || a.table_name AS TABLE_NAME
from user_tables a
wherE a.tablespace_name='USERNAME'
and a.table_name like '%_DIN'
ORDER BY TABLE_NAME DESC)
WHERE ROWNUM = '1'
return:
7_DIN
is a name of one table in my tablespace
I want to take this name and make a select of this table.
something like
select * from 7_DIN
All in one statement
Below is code step-by-step, you can test it in this SQLFiddle.
Sample tables used for test:
create table t1(n number)
/
create table t2(n number)
/
create table t13(n number)
/
insert into t1(n) values(1)
/
insert into t2(n) values(2)
/
insert into t13(n) values(13)
/
Declare types used as selection result, same as row type of tables with dynamic names:
create or replace type t_row as object(n number)
/
create or replace type t_rowlist as table of t_row
/
Function which searches for last table and selects data from them into collection, then returns collection as table data:
create or replace function get_last_data return t_rowlist
as
v_table varchar2(30);
v_rows t_rowlist;
begin
select table_name into v_table from (
select * from user_tables where table_name like 'T%'
order by lpad(substr(table_name,2),40,'0') desc
)
where rownum = 1;
execute immediate 'select t_row(n) from '|| v_table
bulk collect into v_rows;
return v_rows;
end;
/
Create view based on function data:
create or replace view last_data_view as
select * from table(get_last_data)
/
This works good only if dynamic tables don't have a big amount of data.
Otherwise it's better to use pipelined functions. To do so, just replace function implementation with code below:
create or replace function get_last_data_pipe
return t_rowlist pipelined
as
v_table varchar2(30);
v_row t_row;
v_cursor sys_refcursor;
begin
select table_name into v_table from (
select * from user_tables where table_name like 'T%'
order by lpad(substr(table_name,2),40,'0') desc
)
where rownum = 1;
open v_cursor for 'select t_row(n) from '|| v_table;
loop
fetch v_cursor into v_row;
exit when v_cursor%notfound;
pipe row(v_row);
end loop;
close v_cursor;
return;
end;
Link to test SQLFiddle.

How to select columns from a table which have non null values?

I have a table containing hundreds of columns many of which are null, and I would like have my select statement so that only those columns containing a value are returned. It would help me analyze data better. Something like:
Select (non null columns) from tablename;
I want to select all columns which have at least one non-null value.
Can this be done?
Have a look as statistics information, it may be useful for you:
SQL> exec dbms_stats.gather_table_stats('SCOTT','EMP');
PL/SQL procedure successfully completed.
SQL> select num_rows from all_tables where owner='SCOTT' and table_name='EMP';
NUM_ROWS
----------
14
SQL> select column_name,nullable,num_distinct,num_nulls from all_tab_columns
2 where owner='SCOTT' and table_name='EMP' order by column_id;
COLUMN_NAME N NUM_DISTINCT NUM_NULLS
------------------------------ - ------------ ----------
EMPNO N 14 0
ENAME Y 14 0
JOB Y 5 0
MGR Y 6 1
HIREDATE Y 13 0
SAL Y 12 0
COMM Y 4 10
DEPTNO Y 3 0
8 rows selected.
For example you can check if NUM_NULLS = NUM_ROWS to identify "empty" columns.
Reference: ALL_TAB_COLUMNS, ALL_TABLES.
Use the below:
SELECT *
FROM information_schema.columns
WHERE table_name = 'Table_Name' and is_nullable = 'NO'
Table_Name has to be replaced accordingly...
select column_name
from user_tab_columns
where table_name='Table_name' and num_nulls=0;
Here is simple code to get non null columns..
I don't think this can be done in a single query. You may need some plsql to first test what columns contain data and put together a statement based on that information. Of course, if the data in your table changes you have to recreate the statement.
declare
l_table varchar2(30) := 'YOUR_TABLE';
l_statement varchar2(32767);
l_test_statement varchar2(32767);
l_contains_value pls_integer;
-- select column_names from your table
cursor c is
select column_name
,nullable
from user_tab_columns
where table_name = l_table;
begin
l_statement := 'select ';
for r in c
loop
-- If column is not nullable it will always contain a value
if r.nullable = 'N'
then
-- add column to select list.
l_statement := l_statement || r.column_name || ',';
else
-- check if there is a row that has a value for this column
begin
l_test_statement := 'select 1 from dual where exists (select 1 from ' || l_table || ' where ' ||
r.column_name || ' is not null)';
dbms_output.put_line(l_test_statement);
execute immediate l_test_statement
into l_contains_value;
-- Yes, add column to select list
l_statement := l_statement || r.column_name || ',';
exception
when no_data_found then
null;
end;
end if;
end loop;
-- create a select statement
l_statement := substr(l_statement, 1, length(l_statement) - 1) || ' from ' || l_table;
end;
select rtrim (xmlagg (xmlelement (e, column_name || ',')).extract ('//text()'), ',') col
from (select column_name
from user_tab_columns
where table_name='<table_name>' and low_value is not null)
This block determines all columns in the table, loops through them in dynamic SQL and checks if they are null, then constructs a DBMS output query of the non-null query.
All you have to do is run the returned query.
I've included the exclusion of PKs and BLOB columns.
Obviously, this is quite slow as going through columns one by one, and it's not going to be great for very hot tables, as data may change too quickly, but this works for me as I control traffic in dev env.
DECLARE
l_table_name VARCHAR2(255) := 'XXXX';
l_counter NUMBER;
l_sql CLOB;
BEGIN
FOR r_col IN (SELECT *
FROM user_tab_columns tab_col
WHERE table_name = l_table_name
AND data_type NOT IN ('BLOB')
AND column_name NOT IN (SELECT column_name
FROM user_cons_columns con_col
JOIN user_constraints cons ON con_col.constraint_name = cons.constraint_name AND con_col.table_name = cons.table_name
WHERE con_col.table_name = tab_col.table_name
AND constraint_type = 'P')
ORDER BY column_id)
LOOP
EXECUTE IMMEDIATE 'SELECT COUNT(1) FROM '||l_table_name||' WHERE '||r_col.column_name||' IS NOT NULL'
INTO l_counter;
IF l_counter > 0 THEN
IF l_sql IS NULL THEN
l_sql := r_col.column_name;
ELSE
l_sql := l_sql||','||r_col.column_name;
END IF;
END IF;
END LOOP;
l_sql := 'SELECT '||l_sql||CHR(10)
||'FROM '||l_table_name;
----------
DBMS_OUTPUT.put_line(l_sql);
END;
What you're asking to do is establish a dependency on each row in the whole result. This is in fact not ever what you want. Just think of the ramifications if in one row every column had a value of '0' -- suddenly the schema of your result set grows to include all of those previously "empty" columns. You're effectively growing the badness of '*' exponentially, now your result set is not dependent on just the table's meta-data -- but your whole result set is dependent on the plain data.
What you want to do is just select the fields that have what you want, and not deviate from this simple plan.