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;
Related
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
I have table like this:
Table-1
Table-2
Table-3
Table-4
Table-5
each table is having many columns and one of the column name is employee_id.
Now, I want to write a query which will
1) return all the tables which is having this columns and
2) results should show the tables if the column is having values or empty values by passing employee_id.
e.g. show table name, column name from Table-1, Table-2,Table-3,... where employee_id='1234'.
If one of the table doesn't have this column, then it is not required to show.
I have verified with link, but it shows only table name and column name and not by passing some column values to it.
Also verified this, but here verifies from entire schema which I dont want to do it.
UPDATE:
Found a solution, but by using xmlsequence which is deprecated,
1)how do I make this code as xmltable?
2) If there are no values in the table, then output should have empty/null. or default as "YES" value
WITH char_cols AS
(SELECT /*+materialize */ table_name, column_name
FROM cols
WHERE data_type IN ('CHAR', 'VARCHAR2') and table_name in ('Table-1','Table-2','Table-3','Table-4','Table-5'))
SELECT DISTINCT SUBSTR (:val, 1, 11) "Employee_ID",
SUBSTR (table_name, 1, 14) "Table",
SUBSTR (column_name, 1, 14) "Column"
FROM char_cols,
TABLE (xmlsequence (dbms_xmlgen.getxmltype ('select "'
|| column_name
|| '" from "'
|| table_name
|| '" where upper("'
|| column_name
|| '") like upper(''%'
|| :val
|| '%'')' ).extract ('ROWSET/ROW/*') ) ) t ORDER BY "Table"
/
This query can be done in one step using the (non-deprecated) XMLTABLE.
Sample Schema
--Table-1 and Table-2 match the criteria.
--Table-3 has the right column but not the right value.
--Table-4 does not have the right column.
create table "Table-1" as select '1234' employee_id from dual;
create table "Table-2" as select '1234' employee_id from dual;
create table "Table-3" as select '4321' employee_id from dual;
create table "Table-4" as select 1 id from dual;
Query
--All tables with the column EMPLOYEE_ID, and the number of rows where EMPLOYEE_ID = '1234'.
select table_name, total
from
(
--Get XML results of dynamic query on relevant tables and columns.
select
dbms_xmlgen.getXMLType(
(
--Create a SELECT statement on each table, UNION ALL'ed together.
select listagg(
'select '''||table_name||''' table_name, count(*) total
from "'||table_name||'" where employee_id = ''1234'''
,' union all'||chr(10)) within group (order by table_name) v_sql
from user_tab_columns
where column_name = 'EMPLOYEE_ID'
)
) xml
from dual
) x
cross join
--Convert the XML data to relational.
xmltable('/ROWSET/ROW'
passing x.xml
columns
table_name varchar2(128) path 'TABLE_NAME',
total number path 'TOTAL'
);
Results
TABLE_NAME TOTAL
---------- -----
Table-1 1
Table-2 1
Table-3 0
Just try to use code below.
Pay your attention that may be nessecery clarify scheme name in loop.
This code works for my local db.
set serveroutput on;
DECLARE
ex_query VARCHAR(300);
num NUMBER;
emp_id number;
BEGIN
emp_id := <put your value>;
FOR rec IN
(SELECT table_name
FROM all_tab_columns
WHERE column_name LIKE upper('employee_id')
)
LOOP
num :=0;
ex_query := 'select count(*) from ' || rec.table_name || ' where employee_id = ' || emp_id;
EXECUTE IMMEDIATE ex_query into num;
if (num>0) then
DBMS_OUTPUT.PUT_LINE(rec.table_name);
end if;
END LOOP;
END;
I tried with the xml thing, but I get an error I cannot solve. Something about a zero size result. How difficult is it to solve this instead of raising exception?! Ask Oracle.
Anyway.
What you can do is use the COLS table to know what table has the employee_id column.
1) what table from table TABLE_LIKE_THIS (I assume column with table names is C) has this column?
select *
from COLS, TABLE_LIKE_THIS t
where cols.table_name = t
and cols.column_name = 'EMPLOYEE_ID'
-- think Oracle metadata/ think upper case
2) Which one has the value you are looking for: write a little chunk of Dynamic PL/SQL with EXECUTE IMMEDIATE to count the tables matching above condition
declare
v_id varchar2(10) := 'JP1829'; -- value you are looking for
v_col varchar2(20) := 'EMPLOYEE_ID'; -- column
n_c number := 0;
begin
for x in (
select table_name
from all_tab_columns cols
, TABLE_LIKE_THIS t
where cols.table_name = t.c
and cols.column_name = v_col
) loop
EXECUTE IMMEDIATE
'select count(1) from '||x.table_name
||' where Nvl('||v_col||', ''##'') = ''' ||v_id||'''' -- adding quotes around string is a little specific
INTO n_c;
if n_c > 0 then
dbms_output.put_line(n_C|| ' in ' ||x.table_name||' has '||v_col||'='||v_id);
end if;
-- idem for null values
-- ... ||' where '||v_col||' is null '
-- or
-- ... ||' where Nvl('||v_col||', ''##'') = ''##'' '
end loop;
dbms_output.put_line('done.');
end;
/
Hope this helps
This question already has answers here:
Can you SELECT everything, but 1 or 2 fields, without writer's cramp?
(12 answers)
Closed 6 years ago.
I want to write a query to fetch data from a table except some columns which start name like given in the wildcard criteria as bellow example pseudo code. Is it possible on oracle?
(this can be done as adding column names for the select clause. but assuming there will be new columns will add in future , i want to write a more generic code)
example
Employee(id , name , age, gender)
select *
from table_name
where column_name not like a%
after query it should display a table with
Employee(id , name . gender)
the age column is not there because we are not include in the result
You can try with some dynamic SQL:
declare
vSQL varchar2(32767);
vClob clob;
begin
/* build the query */
select distinct 'select ' || listagg(column_name, ',') within group (order by column_name) over (partition by table_name)|| ' from ' || table_name
into vSQL
from user_tab_columns
where table_name = 'EMPLOYEE'
and column_name not like 'A%';
/* print the query */
dbms_output.put_line(vSQL);
/* build an XML */
select DBMS_XMLGEN.getXML(vSQL)
into vClob
from dual;
dbms_output.put_line(vClob);
/* build a CLOB with all the columns */
vSQL := replace (vSQL, ',', ' || '' | '' || ' );
execute immediate vSQL into vClob;
dbms_output.put_line(vClob);
end;
In this way you can dynamically build a query that extracts all the columns exept those matching a pattern.
After building the query, the question is how to fetch it, given that you don't know in advance what columns you are fetching.
In the example I make an XML and a single row; you can use the query in different ways, depending on your needs.
Duplicate, but I like writing PL, so here is how you could, creating a temp table, then select * from it:
declare
your_table varchar2(40) := 'CHEMIN';
select_to_tmp varchar2(4000) := 'create table ttmp as select ';
begin
-- drop temporary table if exists
begin
execute immediate 'drop table ttmp';
Exception
When others Then
dbms_output.put_line(SQLERRM);
end;
for x in (
select column_name from all_tab_columns
where table_name=your_table
and column_name not in (
-- list columns you want to exclude
'COL_A'
, 'COL_B'
)
)
loop
select_to_tmp := select_to_tmp|| x.column_name ||',';
dbms_output.put_line(x.column_name);
end loop;
-- remove last ','
select_to_tmp := substr(select_to_tmp, 1, length(select_to_tmp) -1);
-- from your table
select_to_tmp := select_to_tmp||' from '||your_table;
-- add conditions if necessary
-- select_to_tmp := select_to_tmp|| ' where rownum < 1 '
dbms_output.put_line(select_to_tmp);
-- then create the temporary table using the query you generated:
execute immediate select_to_tmp;
end;
/
SELECT * FROM ttmp;
I would love to be able to use the system tables (Oracle in this case) to drive which fields are used in a SELECT statement. Something like:
SELECT
(
select column_name
from all_tab_cols
where table_Name='CLARITY_SER'
AND OWNER='CLARITY'
AND data_type='DATE'
)
FROM CLARITY_SER
This syntax doesn't work, as the subquery returns multiple rows, instead of one row with multiple columns.
Is it possible to generate a SQL statement dynamically by querying the table schema information in order to select only certain columns?
** edit **
Do this without using a function or procedure, if possible.
You can do this:
declare
l_sql varchar2(32767);
rc sys_refcursor;
begin
l_sql := 'select ';
for r in
( select column_name
from all_tab_cols
where table_Name='CLARITY_SER'
AND OWNER='CLARITY'
AND data_type='DATE'
)
loop
l_sql := l_sql || r.column_name || ',';
end loop;
l_sql := rtrim(l_sql,',') || ' from clarity_ser';
open rc for l_sql;
...
end;
No, it's not possible to specify a column list dynamically in SQL. You'll need to use a procedural language to run the first query, use that to construct a second query, then run the second query.
You could use dynamic SQL. Create a function that takes the table name, owner, data type, executes the inner query and returns a comma-separated list of column names, or an array table if you prefer. Then construct the outer query and execute it with execute immediate.
CREATE FUNCTION get_column_list(
table_name IN varchar2,
owner_name IN varchar2,
data_type IN varchar2)
RETURN varchar2
IS
BEGIN
...... (get columns and return comma-separated list)
END;
/
If your function returns a comma-separated list you can inline it:
execute immediate 'select ' || get_column_list(table_name, owner_name, datatype) || ' from ' || table_name
Admittedly it's a long time since I played with oracle so I may be a bit off but I'm pretty sure this is quite doable.
In SQLPlus you could do this:
COLUMN cols NEW_VALUE cols
SELECT max( ltrim( sys_connect_by_path( column_name, ',' ), ',' ) ) cols
FROM
(
select rownum rn, column_name
from all_tab_cols
where table_Name='CLARITY_SER'
and OWNER='CLARITY'
AND data_type='DATE'
)
start with rn = 1 connect by rn = prior rn +1
;
select &cols from clarity.clarity_ser;
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.