pivot does not work when using a nested select in SQL - sql

I have a requirement to pivot the columns of a particular table specified from a user.
the problem is the number of columns from each table to pivot is dynamic.
so the code below gets the name of the columns from the table.
SELECT DISTINCT
LISTAGG('''' || column_name || '''', ',')
WITHIN GROUP (ORDER BY column_name) AS temp_in_statement
FROM (SELECT DISTINCT column_name FROM all_tab_columns WHERE table_name = 'DIM_XYZ')
the code above returns the columns in the following format:
col1, col2
I have to use a pivot for this requirement and plug the code above in the code below in order to pivot the columns.
SELECT * FROM
(
SELECT table_name, column_name
FROM ALL_TAB_COLUMNS
WHERE
table_name = 'DIM_XYZ'
)
PIVOT
(
MIN(column_name)
FOR column_name IN (
-- values added manually
'col1','col2'
-- values added manually
)
)
ORDER BY table_name;
The code works fine in this case but when replacing 'col1','col2' by the select statement to retrieve columns names the system throws the following error:
ORA-00936: missing expression
00936. 00000 - "missing expression"
*Cause:
*Action:
Error at Line: 39 Column: 40
CODE:
SELECT * FROM
(
SELECT table_name, column_name
FROM ALL_TAB_COLUMNS
WHERE
table_name = 'DIM_XYZ'
)
PIVOT
(
MIN(column_name)
FOR column_name IN (
--code below does not work when plugged in the statement above
SELECT DISTINCT
LISTAGG('''' || column_name || '''', ',')
WITHIN GROUP (ORDER BY column_name) AS temp_in_statement
FROM (SELECT DISTINCT column_name FROM all_tab_columns WHERE table_name = 'DIM_XYZ')
--code above does not work
)
)
ORDER BY table_name;
----------------
do you guys have any idea how to solve this problem?

You cannot directly add dynamic expressions as an input to Pivot table,
You can try something like this where we retrieve all the columns of the table in a variable via a PL/SQL Block and then pass it in a way to the expected by the Oracle Pivot Table Functionality.
SET serveroutput ON;
DECLARE
sqlquery VARCHAR(32767);
cols VARCHAR2(32767);
BEGIN
SELECT listagg('''' || column_name || '''', ',') within
GROUP(
ORDER BY column_name)
INTO cols
FROM
(SELECT DISTINCT column_name
FROM all_tab_columns
WHERE TABLE_NAME = 'TABLE_NAME')
;
sqlquery := '
SELECT * FROM
(
SELECT table_name, column_name
FROM ALL_TAB_COLUMNS
WHERE
table_name = ''TABLE_NAME''
)
PIVOT
(
MIN(column_name)
FOR column_name IN (
''||cols||''
)
)
ORDER BY table_name';
DBMS_OUTPUT.PUT_LINE(sqlquery);
EXECUTE IMMEDIATE sqlquery;
END;
/

Related

sqlplus SELECT from the selected result

I am using Oracle sqlplus and am trying to use the result being selected from the codes below and select the column again from Table_1
SELECT *
FROM
(
SELECT Column_Name
FROM All_Tab_columns
WHERE Table_Name=UPPER('Table_1')
)
INTERSECT
(
SELECT Column_Name
FROM All_Tab_columns
WHERE Table_Name=UPPER('Table_2')
);
How can I perform something like that:
SELECT <Columns_That_Intersected>
FROM Table_1;
Is it possible to store the Columns_That_Intersected in a variable/function/procedure and use it again for other select statement?
Yes, it's possible. Just save the aggregated result of the query in a substitution variable. Something like this:
create table items1 as
select rownum id, 'me '||rownum name, 1 dummy
from xmlTable ('1 to 3');
create table items2 as select id, name from items1;
set verify off
col Columns_That_Intersected new_value Columns_That_Intersected noprint
select listagg (column_name, ',') within group (order by null) Columns_That_Intersected
from (
select column_name
from All_Tab_Columns
where Table_Name=UPPER('items1')
intersect
select column_name
from All_Tab_columns
where Table_Name=UPPER('items2')
);
prompt Columns_That_Intersected=&Columns_That_Intersected
select &Columns_That_Intersected
from items1;
Output:
Columns_That_Intersected=ID,NAME
ID NAME
---------- -------------------------------------------
1 me 1
2 me 2
3 me 3
About col[umn] command
Just to fetch the column names which exists in both the table table1 and table2, you can use below query:
SELECT Column_Name
FROM all_tab_columns t1
WHERE table_name = 'Table1'
AND EXISTS (SELECT 1
FROM all_tab_columns t2
WHERE table_name = 'Table2'
AND t2.Column_Name = t1.Column_Name);
Then to fetch these column values from Table1, you can use below PL/SQL construct:
DECLARE
v_sql_statement VARCHAR2(2000);
v_cols VARCHAR2(2000);
BEGIN
FOR cn IN (SELECT Column_Name
FROM all_tab_columns t1
WHERE table_name = 'Table1'
AND EXISTS (SELECT 1
FROM all_tab_columns t2
WHERE table_name = 'Table2'
AND t2.Column_Name = t1.Column_Name))
LOOP
v_cols := v_cols || ', ' || cn.column_name;
END LOOP;
v_cols := ltrim(v_cols, ',');
v_sql_statement := 'SELECT ' || v_cols || ' FROM Table1';
EXECUTE IMMEDIATE v_sql_statement;
END;
you can, for example, work with a dynamic sql.
The result of your selects can be aggregated to a list using the listagg function
SELECT listagg( Column_Name,',') WITHIN GROUP (ORDER BY 1) cols
FROM
(
SELECT Column_Name
FROM All_Tab_columns
WHERE Table_Name=UPPER('tab1')
INTERSECT
SELECT Column_Name
FROM All_Tab_columns
WHERE Table_Name=UPPER('tab2')
);
As result you get then eg. col1, col2, col3
with the help of this list you can then create a cursor in a plsql block and then loop through it to print out wat you want.
declare
v_cols VARCHAR2(2000);
v_select_string varchar(2000);
csr SYS_REFCURSOR;
v1 VARCHAR2(2000);
BEGIN
SELECT listagg( Column_Name,',') WITHIN GROUP (ORDER BY 1) cols
into v_cols
FROM
(
SELECT Column_Name
FROM All_Tab_columns
WHERE Table_Name=UPPER('tab1')
INTERSECT
SELECT Column_Name
FROM All_Tab_columns
WHERE Table_Name=UPPER('tab2')
);
v_select_string := 'SELECT ' || v_cols ||
' FROM tab1';
OPEN csr FOR v_select_string;
LOOP
FETCH csr INTO v1;
EXIT WHEN csr%NOTFOUND;
DBMS_OUTPUT.PUT_LINE( v1);
END LOOP;
CLOSE csr;
END P1;

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

Oracle: Count non-null fields for each column in a table

I need a query to count the total number of non-null values for each column in a table. Since my table has hundreds of columns I'm looking for a solution that only requires me to input the table name.
Perhaps using the result of:
select COLUMN_NAME from ALL_TAB_COLUMNS where TABLE_NAME='ORDERS';
to get the column names and then a subquery to put counts against each column name? The additional complication is that I only have read-only access to the DB so I can't create any temp tables.
Slightly out of my league with this one so any help is appreciated.
Construct the query in SQL or using a spreadsheet. Then run the query.
For instance, assuming that your column names are simple and don't have special characters:
select replace('select ''[col]'', count([col]) from orders union all ',
'[col]', COLUMN_NAME
) as sql
from ALL_TAB_COLUMNS
where TABLE_NAME = 'ORDERS';
(Of course, this can be adapted for more complex column names, but I'm trying to show the idea.)
Then copy the code, remove the final union all and run it.
You can put this in one string if there are not too many columns:
select listagg(replace('select ''[col]'', count([col]) from orders',
'[col]', COLUMN_NAME
), ' union all '
) within group (order by column_name) as sql
from ALL_TAB_COLUMNS
where TABLE_NAME = 'ORDERS';
You can also use execute immediate using the same query, but that seems like overkill.
If you're happy with the results row-ar rather than column-ar:
SELECT 'SELECT ''dummy'', 0 FROM DUAL' FROM DUAL
UNION ALL
SELECT
' UNION ALL SELECT ''' ||
column_name ||
''', COUNT(' ||
column_name ||
') FROM ' ||
TABLE_NAME
FROM
all_tab_columns
WHERE
table_name = 'ORDERS'
This is an "SQL that writes an SQL" that you can then copy and run to get your answers. Should make a resultset that looks like:
SELECT 'dummy', 0 FROM dual
UNION ALL SELECT 'col1', COUNT(col1) FROM ORDERS
UNION ALL SELECT 'col2', COUNT(col2) FROM ORDERS
...
If you want your results column-ar:
SELECT 'SELECT '
UNION ALL
SELECT
'COUNT(' ||
column_name ||
') as count_' ||
column_name ||
', ' ||
TABLE_NAME
FROM
all_tab_columns
WHERE
table_name = 'ORDERS'
UNION ALL
SELECT 'null as dummy_column FROM ORDERS'
Should make a resultset that looks like:
SELECT
COUNT(col1) as count_col1,
COUNT(col2) as count_col2,
...
null as dummycoll FROM orders
Caveat: I don't have oracle installed anywhere I can test these, it's written from memory and may need some debugging
This will generate the SQL to get the counts in columns and will handle case sensitive column names and column names with non-alpha-numeric characters:
SELECT 'SELECT '
|| LISTAGG(
'COUNT("' || column_name || '") AS "' || column_name || '"',
', '
) WITHIN GROUP ( ORDER BY column_id )
|| ' FROM "' || table_name || '"' AS sql
FROM ALL_TAB_COLUMNS
WHERE TABLE_NAME = 'ORDERS'
GROUP BY TABLE_NAME;
or, if you have a large number of columns that is generating a string longer than 4000 characters you can use a custom aggregation function to aggregate VARCHAR2s into a CLOB and then do:
SELECT 'SELECT '
|| CLOBAgg( 'COUNT("' || column_name || '") AS "' || column_name || '"' )
|| ' FROM "' || table_name || '"' AS sql
FROM ALL_TAB_COLUMNS
WHERE TABLE_NAME = 'ORDERS'
GROUP BY TABLE_NAME;
In Oracle 19 (I used similar code in Ora 12, maybe that works too), this works without generating another select to execute:
select * from
(
select table_name, column_name,
to_number( extractvalue( xmltype(dbms_xmlgen.getxml('select count(to_char(substr('||column_name||',1,1))) c from '||table_name)) ,'/ROWSET/ROW/C')) count
from all_tab_columns where owner = user
)
--where table_name = 'MY_TABLE'
;
It will create XML with count, from which it extracts the current count. The substr and to_char functions here are used to extract first character, so it will works with CLOB columns also

Get column names in subquery, and then return values for those columns?

Seems like this is impossible, but I'm so close - maybe someone can take me the last step...
I have a bunch of dynamic code and I don't always know the tables and columns I'm going to be dealing with, but I do know that VARCHAR2 columns with data_lengths of 2000 result in errors. I'd love to be able to identify these 'bad' columns dynamically, and remove them from my results in 1 shot.
This code:
SELECT LISTAGG(probs.column_name, ', ')
WITHIN GROUP (ORDER BY column_name) FROM
(select 1 grp, column_name
from all_tab_columns
where TABLE_NAME = 'MYTABLE' AND
DATA_TYPE <> 'VARCHAR2' AND
DATA_LENGTH < 2000
) probs
GROUP BY GRP
Gives me a nice comma, separated list of all of my acceptable column names like this:
FIELD1, FIELD2, FIELD3, FIELD4...
And I am hopeful that there's a way a can simply do something to drop that list of field names into a select statement like this:
SELECT (<my subquery, above>)
FROM MYTABLE;
Is this possible?
Assuming this situation
create table mytable ( a number, b number, c number)
insert into mytable values (10, 20, 30)
insert into mytable values (1, 2, 3)
and that only exists one table with that name (otherwise you should specify the owner in the query from all_tab_columns), your query could be simplified this way:
SELECT 'select ' || LISTAGG(column_name, ', ') WITHIN GROUP (ORDER BY column_name) || ' from ' || table_name
FROM all_tab_columns
WHERE TABLE_NAME = 'MYTABLE'
AND DATA_TYPE <> 'VARCHAR2'
AND DATA_LENGTH < 2000
GROUP BY table_name
this would give: select A, B, C from MYTABLE.
The problem here is that you can not simply run a statement that returns a variable number of columns; one way to use this could be building an xml:
SELECT xmltype(
DBMS_XMLGEN.getxml(
( SELECT 'select ' || LISTAGG(column_name, ', ') WITHIN GROUP (ORDER BY column_name) || ' from ' || table_name
FROM all_tab_columns
WHERE TABLE_NAME = 'MYTABLE'
AND DATA_TYPE <> 'VARCHAR2'
AND DATA_LENGTH < 2000
GROUP BY table_name)
)
)
FROM DUAL
<?xml version="1.0"?>
<ROWSET>
<ROW>
<A>10</A>
<B>20</B>
<C>30</C>
</ROW>
<ROW>
<A>1</A>
<B>2</B>
<C>3</C>
</ROW>
</ROWSET>
Another way could be using some PLSQL and dynamic SQL, with a little modification of yur query to concatenate the fields, to build the result in a unique string:
declare
type tTabResults is table of varchar2(1000);
vSQL varchar2(1000);
vTabResults tTabResults;
begin
SELECT 'select ' || LISTAGG( column_name, '|| '', '' ||') WITHIN GROUP (ORDER BY column_name) || ' from ' || table_name
into vSQL
FROM all_tab_columns
WHERE TABLE_NAME = 'MYTABLE'
AND DATA_TYPE <> 'VARCHAR2'
AND DATA_LENGTH < 2000
GROUP BY table_name;
--
execute immediate vSQL bulk collect into vTabResults;
--
for i in vTabResults.first .. vTabResults.last loop
dbms_output.put_line(vTabResults(i));
end loop;
end;
10, 20, 30
1, 2, 3
Notice that I oversimplified the problem, treating numbers as strings and not using any conversion, by simply printing the values in your table, no matter their type; in a real solution you should handle the possible types of your columns and modify the initial query to add some type conversions.

Sort result from xmlagg(xmlelement(...)

I got this SQL query:
select rtrim(extract(xmlagg(xmlelement(e, column_name || ',')),
'/E/text()').getclobval(), ',') from all_tab_columns
where OWNER = 'TESTER' AND TABLE_NAME = 'H4_POSIT';
I using this instead of LISTAGG(column_name, ',') because the result is going to exceed the limit of varchar2 (>4000).
Now I am asking myself whether it is possible to sort the result like LISTAGG does it.
So when having columns FERA, BAUT, CHECK_ID, ... I'd like them to be returned as: BAUT,CHECK_ID,FERA, ...
I am using Oracle Server and my framework doesn't allow me to work with PL/SQL.
XMLAGG supports ordering on its own (see https://docs.oracle.com/database/121/SQLRF/functions251.htm):
SELECT
rtrim(
extract(
xmlagg(
xmlelement(e, column_name || ',') ORDER BY column_name
),
'/E/text()')
.getclobval (),
',')
FROM
all_tab_columns
WHERE
owner = 'TESTER' AND table_name = 'H4_POSIT'
You can use a subquery and simply sort the columns before you pass it to the xml function. A simple solution.
select rtrim(extract(xmlagg(xmlelement(e, column_name || ',')),
'/E/text()').getclobval(), ',') from
(select * from all_tab_columns
where OWNER != 'TESTER' AND TABLE_NAME=upper('H4_POSIT')
order by COLUMN_NAME );