How to transpose a table from a wide format to narrow, using the values as a filter? - sql

I get a table X (with 1 row):
COL_XA COL_VG COL_LF COL_EQ COL_PP COL_QM ...
1 0 0 0 1 1
Each column COL_x can have only values 0 or 1.
I want to transform this table into this form Y:
NAME
"COL_XA"
"COL_PP"
"COL_QM"
...
This table should print only those columns from table X that the first (and only) row has value 1.
This question is related to any other question about transposition, with the difference that I don't want the actual values, but the column names, which are not known in advance.
I could use Excel or PL/SQL to create a list of strings of the form
MIN(CASE WHEN t.COL_XA = 1 THEN 'COL_XA' ELSE null END) as NAME, but this solution is inefficient (EXECUTE IMMEDIATE) and difficult to maintain. And the string passed to EXECUTE IMMEDIATE is limited to 32700 characters, which can be easily exceeded in production, where the table X can have well over 500 fields.

To completly automate the query you must be able to read the column names of the actual cursor. In PL/SQL this is possible using DBMS_SQL (other way would be in JDBC). Based on this OTN thread here a basic table function.
The importent parts are
1) dbms_sql.parse the query given as a text string and dbms_sql.execute it
2) dbms_sql.describe_columns to get the list of the column names returned from the query on table x
3) dbms_sql.fetch_rows to fetch the first row
4) loop the columns and checking the dbms_sql.column_value if equals to 1 output column_name (with PIPE)
create or replace type str_tblType as table of varchar2(30);
/
create or replace function get_col_name_on_one return str_tblType
PIPELINED
as
l_theCursor integer default dbms_sql.open_cursor;
l_columnValue varchar2(2000);
l_columnOutput varchar2(4000);
l_status integer;
l_colCnt number default 0;
l_colDesc dbms_sql.DESC_TAB;
begin
dbms_sql.parse( l_theCursor, 'SELECT * FROM X', dbms_sql.native );
for i in 1 .. 1000 loop
begin
dbms_sql.define_column( l_theCursor, i,
l_columnValue, 2000 );
l_colCnt := i;
exception
when others then
if ( sqlcode = -1007 ) then exit;
else
raise;
end if;
end;
end loop;
dbms_sql.define_column( l_theCursor, 1, l_columnValue, 2000 );
l_status := dbms_sql.execute(l_theCursor);
dbms_sql.describe_columns(l_theCursor,l_colCnt, l_colDesc);
if dbms_sql.fetch_rows(l_theCursor) > 0 then
for lColCnt in 1..l_colCnt
loop
dbms_sql.column_value( l_theCursor, lColCnt, l_columnValue );
--DBMS_OUTPUT.PUT_LINE( l_columnValue);
IF (l_columnValue = '1') THEN
DBMS_OUTPUT.PUT_LINE(Upper(l_colDesc(lColCnt).col_name));
pipe row(Upper(l_colDesc(lColCnt).col_name));
END IF;
end loop;
end if;
return;
end;
/
select * from table(get_col_name_on_one);
COLUMN_LOOOOOOOOOOOOOONG_100
COLUMN_LOOOOOOOOOOOOOONG_200
COLUMN_LOOOOOOOOOOOOOONG_300
COLUMN_LOOOOOOOOOOOOOONG_400
COLUMN_LOOOOOOOOOOOOOONG_500
COLUMN_LOOOOOOOOOOOOOONG_600
COLUMN_LOOOOOOOOOOOOOONG_700
COLUMN_LOOOOOOOOOOOOOONG_800
COLUMN_LOOOOOOOOOOOOOONG_900
COLUMN_LOOOOOOOOOOOOOONG_1000
You should not get in troubles with wide tables using this solution, I tested with a 1000 column tables with long column names.

Here is solution but I have to break it in two parts
First you extract all the column names of table. I have used LISTAGG to collect column names separated by ,
I will use the output of first query in second query.
select listagg(column_name,',') WITHIN GROUP (ORDER BY column_name )
from user_tab_cols where upper(table_name)='X'
The output of above query will be like COL_XA,COL_VG,COL_LF,COL_EQ,COL_PP,COL_QM ... and so on.
Copy above output and use in below query replacing
select NAME from X
unpivot ( bit for NAME in (<outputvaluesfromfirstquery>))
where bit=1
I am trying to merge above two, but I have option for pivot xml but not for unpivot xml.

You can do this with a bunch of union alls:
select 'COL_XA' as name from table t where col_xa = 1 union all
select 'COL_VG' as name from table t where col_vg = 1 union all
. . .
EDIT:
If you have only one row, then you do not need:
MIN(CASE WHEN t.COL_XA = 1 THEN 'COL_XA' ELSE null END) as NAME
You can simply use:
(CASE WHEN t.COL_XA = 1 THEN 'COL_XA' END)
The MIN() isn't needed for one row and the ELSE null is redundant.

Related

How to store multiple rows in a variable in pl/sql function?

I'm writing a pl/sql function. I need to select multiple rows from select statement:
SELECT pel.ceid
FROM pa_exception_list pel
WHERE trunc(pel.creation_date) >= trunc(SYSDATE-7)
if i use:
SELECT pel.ceid
INTO v_ceid
it only stores one value, but i need to store all values that this select returns. Given that this is a function i can't just use simple select because i get error, "INTO - is expected."
You can use a record type to do that. The below example should work for you
DECLARE
TYPE v_array_type IS VARRAY (10) OF NUMBER;
var v_array_type;
BEGIN
SELECT x
BULK COLLECT INTO
var
FROM (
SELECT 1 x
FROM dual
UNION
SELECT 2 x
FROM dual
UNION
SELECT 3 x
FROM dual
);
FOR I IN 1..3 LOOP
dbms_output.put_line(var(I));
END LOOP;
END;
So in your case, it would be something like
select pel.ceid
BULK COLLECT INTO <variable which you create>
from pa_exception_list
where trunc(pel.creation_Date) >= trunc(sysdate-7);
If you really need to store multiple rows, check BULK COLLECT INTO statement and examples. But maybe FOR cursor LOOP and row-by-row processing would be better decision.
You may store all in a rowtype parameter and show whichever column you want to show( assuming ceid is your primary key column, col1 & 2 are some other columns of your table ) :
SQL> set serveroutput on;
SQL> declare
l_exp pa_exception_list%rowtype;
begin
for c in ( select *
from pa_exception_list pel
where trunc(pel.creation_date) >= trunc(SYSDATE-7)
) -- to select multiple rows
loop
select *
into l_exp
from pa_exception_list
where ceid = c.ceid; -- to render only one row( ceid is primary key )
dbms_output.put_line(l_exp.ceid||' - '||l_exp.col1||' - '||l_exp.col2); -- to show the results
end loop;
end;
/
SET SERVEROUTPUT ON
BEGIN
FOR rec IN (
--an implicit cursor is created here
SELECT pel.ceid AS ceid
FROM pa_exception_list pel
WHERE trunc(pel.creation_date) >= trunc(SYSDATE-7)
)
LOOP
dbms_output.put_line(rec.ceid);
END LOOP;
END;
/
Notes from here:
In this case, the cursor FOR LOOP declares, opens, fetches from, and
closes an implicit cursor. However, the implicit cursor is internal;
therefore, you cannot reference it.
Note that Oracle Database automatically optimizes a cursor FOR LOOP to
work similarly to a BULK COLLECT query. Although your code looks as if
it fetched one row at a time, Oracle Database fetches multiple rows at
a time and allows you to process each row individually.

Need to build a query with column names stored in another table

I have a table as shown below. This table will be generated dynamically and I have no prior idea about what value it is going to hold.
------------------------------------------
TABLE_NAME COLUMN_NAME CHAR_LENGTH
------------------------------------------
EMPLOYEE COL1 100
EMPLOYEE COL2 200
EMPLOYEE COL3 300
EMPLOYEE COL4 400
Based on this table, I want to build a query in such a way that it would give me those columns, that contains data having char length greater than CHAR_LENGTH column value.
For example if COL2 contains data having char length 500 (>200), then query would give me COL2.
I don't have any draft code to show my attempt, as I have no idea how would I do this.
I don't think this is possible in pure SQL due to the dynamic nature of your requirement. You'll need some form of PL/SQL.
Assuming you're ok with simply outputting the desired results, here is a PL/SQL block that will get the job done:
declare
wExists number(1);
begin
for rec in (select * from your_dynamic_table)
loop
execute immediate 'select count(*)
from dual
where exists (select null
from ' || rec.table_name || ' t
where length(t.' || rec.column_name || ') > ' || rec.char_length || ')'
into wExists;
if wExists = 1 then
dbms_output.put_line(rec.column_name);
end if;
end loop;
end;
You'll also notice the use of the exists clause to optimize the query, so as not to iterate over the whole table unnecessarily, when possible.
Alternatively, if you want the results to be queryable, you can consider converting the code to a pipelined function.
select column_name
from (
select statement that builds the table output
) A
where char_length<length(column_name)
will that help?
You would need a procedure to achieve the same :
Here I am treating the all_tab_columns table in Oracle, which is a default table with much similar structure as your reference example.(Try select * from all_tab_columns). The structure of all_tab_columns is much like yours, except that you will never find a varchar record whose value has exceeded its data length(obvious database level constraint). Date fields may exceed data length, and do reflect in this procedure's output. I am searching all columns in EMPLOYEES whose size exceeds what is specified.
DECLARE
cursor c is select column_name,data_length,table_name from all_tab_columns where table_name=:Table_name;
V_INDEX_NAME all_tab_columns.column_name%type;
v_data_length all_tab_columns.data_length%type;
V_NUMBER PLS_INTEGER;
v_table_name all_tab_columns.table_name%type;
BEGIN
open c;
LOOP
FETCH c into v_index_name,v_data_length,v_table_name;
EXIT when c%NOTFOUND;
v_number :=0;
execute immediate 'select count(*) from '|| :Table_name ||' where length('||v_index_name||')>'||v_data_length into v_number;
if v_number>1 then
dbms_output.put_line(v_index_name||' has values greater than specified'||' '||V_INDEX_NAME||' '||v_data_length);
end if;
END LOOP;
close c;
END;
/
Replace all_tab_columns and its respective columns with the column name of your table.
DEFECTS : The table name is hardcoded. Trying to make the code generic execute immediate or any other trick. Will achieve soon.
EDIT : Defect fixed.

foreach rows of my table and update my ROW_NUMBER column

I would like to create a script pl/sql where I can modify the value of my column ROW_NUMBER (the first time the value of ROW_NUMBER equal NULL).
This is the structure of my table 'A' :
CREATE TABLE A
(
"NAME" VARCHAR2(25 BYTE),
"NUM" NUMBER(10,0)
)
I would like to foreach all rows of table A and increment my Column 'NUM' by 1 if Column 'NAME' equal 'DEB'.
I would like to get the result like :
I created one pl/sql script :
DECLARE
INcrmt NUMBER(4):=1;
line WORK_ODI.TEST_SEQ%ROWTYPE;--before fetch it returns 0
CURSOR c_select IS
SELECT ROW_NUMBER,VALUE FROM WORK_ODI.TEST_SEQ;
BEGIN
OPEN c_select;
LOOP
FETCH c_select INTO line;
DBMS_OUTPUT.PUT_LINE(line.VALUE);
if line.VALUE like '%DEB%'
then
UPDATE WORK_ODI.TEST_SEQ SET ROW_NUMBER = INcrmt WHERE VALUE=line.VALUE;
INcrmt := INcrmt + 1;
end if;
if line.VALUE not like '%DEB%'
then
UPDATE WORK_ODI.TEST_SEQ SET ROW_NUMBER = INcrmt WHERE VALUE=line.VALUE;
end if;
EXIT WHEN c_select%NOTFOUND;
END LOOP;
CLOSE c_select;
COMMIT;
END;
DECLARE
INcrmt NUMBER(4):=1;
line WORK_ODI.TEST_SEQ%ROWTYPE;--before fetch it returns 0
CURSOR c_select IS
SELECT ROW_NUMBER,VALUE FROM WORK_ODI.TEST_SEQ;
BEGIN
OPEN c_select;
LOOP
FETCH c_select INTO line;
DBMS_OUTPUT.PUT_LINE(line.VALUE);
if line.VALUE like '%DEB%'
then
UPDATE WORK_ODI.TEST_SEQ SET ROW_NUMBER = INcrmt WHERE VALUE=line.VALUE;
INcrmt := INcrmt + 1;
end if;
if line.VALUE not like '%DEB%'
then
UPDATE WORK_ODI.TEST_SEQ SET ROW_NUMBER = INcrmt WHERE VALUE=line.VALUE;
end if;
EXIT WHEN c_select%NOTFOUND;
END LOOP;
CLOSE c_select;
COMMIT;
END;
but this is not work well , please take a look at what it gives me as result :
please anybody can help me
First, you should have an Aid column of some sort. In Oracle 12+, you can use an identity. In earlier versions, you can use a sequence. This provides an ordering for the rows in the table, based on insert order.
Second, you can do what you want on output:
select a.*,
sum(case when a.name like 'DEB%' then 1 else 0 end) over (order by aid) as row_number
from a;
If you really need to keep the values in the table, then you can use a merge statement to assign values to existing rows (the aid column is very handy for this). You will need a trigger afterwards to maintain it.
My suggestion is to do the calculation on the data, rather than storing the value in the data. Maintaining the values with updates and deletes seems like a real pain.

Display Number of Rows based on input parameter

CREATE OR REPLACE PROCEDURE test_max_rows (
max_rows IN NUMBER DEFAULT 1000
)
IS
CURSOR cur_test ( max_rows IN number ) IS
SELECT id FROM test_table
WHERE user_id = 'ABC'
AND ROWNUM <= max_rows;
id test_table.id%TYPE;
BEGIN
OPEN cur_test(max_rows) ;
LOOP
FETCH cur_test INTO id;
EXIT WHEN cur_test%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('ID:' || id);
END LOOP;
END;
My requirement is to modify the above code so that when I pass -1 for max_rows, the proc should return all the rows returned by the query. Otherwise, it should limit the rows as per max_rows.
For example:
EXECUTE test_max_rows(-1);
This command should return all the rows returned by the SELECT statement above.
EXECUTE test_max_rows(10);
This command should return only 10 rows.
You can do this with a OR clause; change:
AND ROWNUM <= max_rows;
to:
AND (max_rows < 1 OR ROWNUM <= max_rows);
Then passing zero, -1, or any negative number will fetch all rows, and any positive number will return a restricted list. You could also replace the default 1000 clause with default null, and then test for null instead, which might be a bit more obvious:
AND (max_rows is null OR ROWNUM <= max_rows);
Note that which rows you get with a passed value will be indeterminate because you don't have an order by clause at the moment.
Doing this in a procedure also seems a bit odd, and you're assuming whoever calls it will be able to see the output - i.e. will have done set serveroutput on or the equivalent for their client - which is not a very safe assumption. An alternative, if you can't specify the row limit in a simple query, might be to use a pipelined function instead - you could at least then call that from plain SQL.
CREATE OR REPLACE FUNCTION test_max_rows (max_rows IN NUMBER DEFAULT NULL)
RETURN sys.odcinumberlist PIPELINED
AS
BEGIN
FOR r IN (
SELECT id FROM test_table
WHERE user_id = 'ABC'
AND (max_rows IS NULL OR ROWNUM <= max_rows)
) LOOP
PIPE ROW (r.id);
END LOOP;
END;
/
And then call it as:
SELECT * FROM TABLE(test_max_rows);
or
SELECT * FROM TABLE(test_max_rows(10));
Here's a quick SQL Fiddle demo. But you should still consider if you can do the whole thing in plain SQL and PL/SQL altogether.

nzsql - Converting a subquery into columns for another select

Goal: Use a given subquery's results (a single column with many rows of names) to act as the outer select's selection field.
Currently, my subquery is the following:
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'test_table' AND column_name not in ('colRemove');
What I am doing in this subquery is grabbing all the column names from a table (i.e. test_table) and outputting all except for the column name specified (i.e. colRemove). As stated in the "goal", I want to use this subquery as such:
SELECT (*enter subquery from above here*)
FROM actual_table
WHERE (*enter specific conditions*)
I am working on a Netezza SQL server that is version 7.0.4.4. Ideally, I would like to make the entire query executable in one line, but for now, a working solution would be much appreciated. Thanks!
Note: I do not believe that the SQL extensions has been installed (i.e. arrays), but I will need to double check this.
A year too late, here's the best I can come up with but, as you already noticed, it requires a stored procedure to do the dynamic SQL. The stored proc creates a view with the all the columns from the source table minus the one you want to exclude.
-- Create test data.
CREATE TABLE test (firstcol INTEGER, secondcol INTEGER, thirdcol INTEGER);
INSERT INTO test (firstcol, secondcol, thirdcol) VALUES (1, 2, 3);
INSERT INTO test (firstcol, secondcol, thirdcol) VALUES (4, 5, 6);
-- Install stored procedure.
CREATE OR REPLACE PROCEDURE CreateLimitedView (varchar(ANY), varchar(ANY)) RETURNS BOOLEAN
LANGUAGE NZPLSQL AS
BEGIN_PROC
DECLARE
tableName ALIAS FOR $1;
columnToExclude ALIAS FOR $2;
colRec RECORD;
cols VARCHAR(2000); -- Adjust as needed.
isfirstcol BOOLEAN;
BEGIN
isfirstcol := true;
FOR colRec IN EXECUTE
'SELECT ATTNAME AS NAME FROM _V_RELATION_COLUMN
WHERE
NAME=UPPER('||quote_literal(tableName)||')
AND ATTNAME <> UPPER('||quote_literal(columnToExclude)||')
ORDER BY ATTNUM'
LOOP
IF isfirstcol THEN
cols := colRec.NAME;
ELSE
cols := cols || ', ' || colRec.NAME;
END IF;
isfirstcol := false;
END LOOP;
-- Should really check if 'LimitedView' already exists as a view, table or synonym.
EXECUTE IMMEDIATE 'CREATE OR REPLACE VIEW LimitedView AS SELECT ' || cols || ' FROM ' || quote_ident(tableName);
RETURN true;
END;
END_PROC
;
-- Run the stored proc to create the view.
CALL CreateLimitedView('test', 'secondcol');
-- Select results from the view.
SELECT * FROM limitedView WHERE firstcol = 4;
FIRSTCOL | THIRDCOL
----------+----------
4 | 6
You could have the stored proc return a resultset directly but then you wouldn't be able to filter results with a WHERE clause.