How select tablename from user_table - sql

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.

Related

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

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

dynamic SQL ERROR: column "age" does not exist

postgres 12
I am trying to loop through a table which has schema , table_names and columns
I want to do various things like finding nulls ,row count etc. I failed at the first hurdle trying to update the col records.
table i am using
CREATE TABLE test.table_study (
table_schema text,
table_name text,
column_name text,
records int,
No_Nulls int,
No_Blanks int,
per_pop int
);
I populate the table with some schema names ,tables and columns from information_schema.columns
insert into test.table_study select table_schema, table_name, column_name
from information_schema.columns
where table_schema like '%white'
order by table_schema, table_name, ordinal_position;
I want to populate the rest with a function
function :-
CREATE OR REPLACE PROCEDURE test.insert_data_population()
as $$
declare s record;
declare t record;
declare c record;
BEGIN
FOR s IN SELECT distinct table_schema FROM test.table_study
LOOP
FOR t IN SELECT distinct table_name FROM test.table_study where table_schema = s.table_schema
loop
FOR c IN SELECT column_name FROM test.table_study where table_name = t.table_name
LOOP
execute 'update test.table_study set records = (select count(*) from ' || s.table_schema || '.' || t.table_name || ') where table_study.table_name = '|| t.table_name ||';';
END LOOP;
END LOOP;
END LOOP;
END;
$$
LANGUAGE plpgsql;
I get this error SQL Error [42703]: ERROR: column "age" does not exist. the table age does exist.
when I take out the where clause
execute 'update referralunion.testinsert ti set records = (select count(*) from ' || s.table_schema || '.' || t.table_name || ') ;';
it works, I just cant figure out whats wrong?
Your procedure is structured entirely wrong. What it results in is an attempt to get every column name for every table name in every schema. I would guess results in your column does not exist error. Further is shows procedural thinking. SQL requires think in terms of sets. Below I use basically your query to demonstrate then a revised version which uses a single loop.
-- setup (dropping schema references)
create table table_study (
table_schema text,
table_name text,
column_name text,
records int,
no_nulls int,
no_blanks int,
per_pop int
);
insert into table_study(table_schema, table_name, column_name)
values ('s1','t1','age')
, ('s2','t1','xyz');
-- procedure replacing EXECUTE with Raise Notice.
create or replace procedure insert_data_population()
as $$
declare
s record;
t record;
c record;
line int = 0;
begin
for s in select distinct table_schema from table_study
loop
for t in select distinct table_name from table_study where table_schema = s.table_schema
loop
for c in select column_name from table_study where table_name = t.table_name
loop
line = line+1;
raise notice '%: update table_study set records = (select count(*) from %.% where table_study.table_name = %;'
, line, s.table_schema, t.table_name, c.column_name;
end loop;
end loop;
end loop;
end;
$$
language plpgsql;
Run procedure
do $$
begin
call insert_data_population();
end;
$$;
RESULTS
1: update table_study set records = (select count(*) from s2.t1 where table_study.table_name = age; 2: update table_study set records = (select count(*) from s2.t1 where table_study.table_name = xyz; 3: update table_study set records = (select count(*) from s1.t1 where table_study.table_name = age; 4: update table_study set records = (select count(*) from s1.t1 where table_study.table_name = xyz;
Notice lines 2 and 3. Each references a column name that does not exist in the table. This results from the FOR structure with the same table name in different schema.
Revision for Single Select statement with Single For loop.
create or replace
procedure insert_data_population()
language plpgsql
as $$
declare
s record;
line int = 0;
begin
for s in select distinct table_schema, table_name, column_name from table_study
loop
line = line+1;
raise notice '%: update table_study set records = (select count(*) from %.% where table_study.table_name = %;'
, line, s.table_schema, s.table_name, s.column_name;
end loop;
end;
$$;
do $$
begin
call insert_data_population();
end;
$$;
RESULTS
1: update table_study set records = (select count(*) from s2.t1 where table_study.table_name = xyz;
2: update table_study set records = (select count(*) from s1.t1 where table_study.table_name = age;
Note: In Postgres DECLARE begins a block. It is not necessary to declared each variable. I would actually consider it bad practice. In theory it could require an end for each declare as each could be considered a nested block. Fortunately Postgres does not require this.

Select from all_tab_columns where table has > 0 rows

I need to search in a large DB a table that matches with a column name, but this table must have more than 0 rows.
Here is the query by the way:
SELECT * FROM all_tab_columns WHERE column_name LIKE '%ID_SUPPORT%';
You could use single query to filter names and get actual number of rows:
SELECT owner, table_name, cnt
FROM all_tab_columns, XMLTABLE('/ROWSET/ROW' passing
(dbms_xmlgen.getxmltype(REPLACE(REPLACE(
'select COUNT(*) AS cnt from <owner>.<table_name>', '<owner>', owner)
, '<table_name>', table_name))) COLUMNS cnt INT)
WHERE column_name LIKE '%ID_SUPPORT%' AND cnt > 0;
DBFiddle Demo
Any chance this can be expanded/tweaked to yield the values of the first few rows for all tables?
Yes, by flattening row using JSON_ARRAYAGG(JSON_OBJECT(*)) Oracle 19c:
-- generic approach Oracle 19c
SELECT owner, table_name, cnt, example
FROM all_tab_columns, XMLTABLE('/ROWSET/ROW' passing
(dbms_xmlgen.getxmltype(REPLACE(REPLACE(
'select COUNT(*) AS cnt,
MAX((SELECT JSON_ARRAYAGG(JSON_OBJECT(*))
FROM <owner>.<table_name>
WHERE rownum < 10) -- taking up to 10 rows as example
) as example
from <owner>.<table_name>', '<owner>', owner)
, '<table_name>', table_name)))
COLUMNS cnt INT
, example VARCHAR2(1000))
WHERE column_name LIKE '%ID_SUPPORT%'
AND cnt > 0;
Demo contains hardcoded column list inside JSON_OBJECT. Oracle 19c and JSON_OBJECT(*) would allow any column list per table.
db<>fiddle demo
How it works:
find all tables that have column named '%ID_SUPPORT'
run query per table using dbms_xml_gen.getxmltype
in sub query count the rows, flatten few rows an example to JSON
return rows that have at least one record table
One way:
SELECT * FROM all_tables WHERE num_rows > 0
AND table_name in (SELECT table_name FROM all_tab_columns WHERE column_name LIKE '%ID_SUPPORT%')
If your DB is periodically analyzed the direct way is to use the following SQL :
SELECT *
FROM all_tables t
WHERE t.table_name LIKE '%ID_SUPPORT%'
and t.num_rows > 0;
More precise way to determine is using the following :
declare
v_val pls_integer := 0;
begin
for c in (
SELECT *
FROM all_tables t
WHERE t.table_name LIKE '%ID_SUPPORT%'
)
loop
execute immediate 'select count(1) from '||c.owner||'.'||c.table_name into v_val;
if v_val > 0 then
dbms_output.put_line('Table Name : '||c.table_name||' with '||v_val||' rows ');
end if;
end loop;
end;
I'm confused with the word matches. If you mean column, but not table, you may use the following routine to get the desired tables with columns whose names are like ID_SUPPORT :
declare
v_val pls_integer := 0;
begin
for c in (
SELECT t.*
FROM all_tab_columns c
JOIN all_tables t on ( c.table_name = t.table_name )
WHERE c.column_name LIKE '%ID_SUPPORT%'
)
loop
execute immediate 'select count(1) from '||c.owner||'.'||c.table_name into v_val;
if v_val > 0 then
dbms_output.put_line('Table Name : '||c.table_name||' with '||v_val||' rows ');
end if;
end loop;
end;

CREATE AS SELECT * but with one column obtained from another table

I need to 'recreate' over 50 tables (in Oracle) with CREATE AS SELECT statements. However, all this tables will have one column modified using data from another table. Is there a way to achieve this without declaring each column in the SELECT statement?
Something like:
CREATE TABLE table_name_copy AS SELECT *, (SELECT col_name FROM other_table WHERE other_table.col_id = table_name.col_id) AS col_name FROM table_name`
Basically on all tables I have a column which needs to be replaced with the data in the other_table column.
Generate the SQL string as such:
SELECT 'CREATE TABLE table_name_copy AS SELECT '
|| LISTAGG (column_name, ', ') WITHIN GROUP (ORDER BY column_name)
|| ', (SELECT col_name FROM other_table
WHERE other_table.col_id = table_name.col_id) AS col_name'
|| ' FROM table_name'
FROM all_tab_cols
WHERE owner = 'OWNER'
AND table_name = 'TABLE_NAME'
AND column_name != 'COL_NAME'
If you want to run the above statement, you could use EXECUTE IMMEDIATE:
DECLARE
v_sql VARCHAR2(10000);
BEGIN
SELECT 'CREATE TABLE table_name_copy AS SELECT '
|| LISTAGG (column_name, ', ') WITHIN GROUP (ORDER BY column_name)
|| ', (SELECT col_name FROM other_table
WHERE other_table.col_id = table_name.col_id) AS col_name'
|| ' FROM table_name'
INTO v_sql
FROM all_tab_cols
WHERE owner = 'OWNER'
AND table_name = 'TABLE_NAME'
AND column_name != 'COL_NAME';
EXECUTE IMMEDIATE v_sql;
END;
/
If col_id column is fixed for both of the joined tables,
you may use user_tab_columns and user_tables dictionary views through the schema to produce new tables named as "table_name_copy" by using the following mechanism :
declare
v_ddl varchar2(4000);
v_cln varchar2(400);
begin
for c in ( select *
from user_tables t
where t.table_name in
( select c.table_name
from user_tab_columns c
where c.column_name = 'COL_ID' )
order by t.table_name )
loop
v_ddl := 'create table '||c.table_name||'_copy as
select ';
for d in ( select listagg('t1.'||column_name, ',') within group ( order by column_name ) cln
from user_tab_columns
where table_name = c.table_name
and column_name != 'COL_ID' )
loop
v_cln := v_cln||d.cln;
end loop;
v_ddl := v_ddl||v_cln;
v_ddl := v_ddl||', t2.col_id t2_id
from '||c.table_name||' t1
left outer join other_table t2 on ( t1.col_id = t2.col_id )';
execute immediate v_ddl;
v_ddl := null;
v_cln := null;
end loop;
end;
Maybe you can use a simple join and an asterisk to return all columns from the first table, like that:
CREATE TABLE table_name_copy AS
SELECT * FROM (
SELECT tab1.*, tab2.column_name
FROM table_name tab1 LEFT JOIN other_table tab2 ON tab1.col_id = tab2.col_id
);
I would try this (but I don't have Oracle SQL to test on so please leave me the benefit of the doubt)
CREATE TABLE table_name_copy AS
SELECT * FROM (
SELECT *, (SELECT col_name FROM other_table WHERE other_table.col_id = table_name.col_id) as col_name
FROM table_name`
)
edit:
then run
ALTER TABLE table_name_copy DROP COLUMN <old column>
to remove the column you don't need any more

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;