I have a table with several hundred columns that I need to unpivot. All of the columns that need to be unpivoted start with 'SIM_'. I know how to do this statically (example below), but I'd like a dynamic solution - as the number of columns that need be be unpivot is both long and may change over time.
SELECT
*
FROM
(SELECT
ID,
NAME,
SIM_1,
SIM_2,
SIM_3
FROM
SAMPLE_TABLE
) T UNPIVOT(SIM_RESULT FOR SIM IN (SIM_1, SIM_2, SIM_3))
Maybe you could (re)create a view when there is a need to refresh the list of columns. This way you will have the accurate list of columns and a way to show real data. It does the unpivoting of all columns (like SIM_%) in the time of execution.
/* sample table and data
CREATE TABLE AA_TST
(ID NUMBER(6), SIM_A VARCHAR2(20), SIM_B VARCHAR2(20), SIM_C VARCHAR2(20), SIM_D VARCHAR2(20), SIM_E VARCHAR2(20));
INSERT INTO AA_TST VALUES(1, 'A', 'B', 'C', 'D', 'E');
INSERT INTO AA_TST VALUES(2, 'AA', 'BB', 'CC', 'DD', 'EE');
INSERT INTO AA_TST VALUES(3, 'AAA', 'BBB', 'CCC', 'DDD', 'EEE');
*/
Declare
col_list VarChar2(1000);
myViewSQL VarChar2(1000);
Begin
SELECT LISTAGG(column_name, ', ') WITHIN GROUP (ORDER BY column_name)
INTO col_list
FROM all_tab_columns
WHERE table_name = 'AA_TST' And
column_name LIKE 'SIM_%';
--
myViewSQL := 'SELECT VALUE_NAME, VALUE_OF FROM AA_TST UNPIVOT (VALUE_OF FOR VALUE_NAME IN(' || col_list || '))';
execute immediate 'CREATE or replace VIEW AA_TST_VIEW AS ' || myViewSQL;
End;
/
SELECT * FROM AA_TST_VIEW;
--
-- R e s u l t
--
-- anonymous block completed
-- VALUE_NAME VALUE_OF
-- ---------- --------------------
-- SIM_A A
-- SIM_B B
-- SIM_C C
-- SIM_D D
-- SIM_E E
-- SIM_A AA
-- SIM_B BB
-- SIM_C CC
-- SIM_D DD
-- SIM_E EE
-- SIM_A AAA
-- SIM_B BBB
-- SIM_C CCC
-- SIM_D DDD
-- SIM_E EEE
--
-- 15 rows selected
How to print different output within LISTAGG() depending on number of aggregated elements?
Is it possible to get number of aggreated elements without additional COUNT(*) query?
There is an example DDL:
create table shepherds (
SHEPHERD_ID NUMBER(19),
SHEPHERD_NAME VARCHAR2(50 CHAR)
);
create table sheeps (
SHEEP_ID VARCHAR2(10 CHAR),
SHEEP_NAME VARCHAR2(50 CHAR),
SHEEP_SHEPHERD_ID NUMBER(19)
);
-- insert shepherds
insert into shepherds VALUES (111, 'Asher');
insert into shepherds VALUES (222, 'Joseph');
insert into shepherds VALUES (333, 'Nicodemus');
-- first shepherd (one sheep)
insert into sheeps VALUES ('A', 'Mark', 111);
-- second shepherd (two sheeps)
insert into sheeps VALUES ('A', 'Andres', 222);
insert into sheeps VALUES ('B', 'Jeffrey', 222);
-- third shepherd (three sheeps)
insert into sheeps VALUES ('B', 'Jeffrey', 333);
insert into sheeps VALUES ('A', 'Andres', 333);
insert into sheeps VALUES ('D', 'Andres', 333);
Now I want to display all shepherds with new-line separated sheep names in the following way:
SELECT
SHEPHERD_NAME,
(SELECT
listagg(SHEEP_ID || ': ' || SHEEP_NAME, CHR(10)) WITHIN GROUP (ORDER BY SHEEP_ID)
FROM SHEEPS
WHERE SHEEP_SHEPHERD_ID = SHEPHERD_ID)
FROM SHEPHERDS;
The result is: http://sqlfiddle.com/#!4/881a7/3
However, I want to hide sheep's ID letter for those shepherds who have only one sheep.
I tried the following:
SELECT
SHEPHERD_NAME,
(SELECT
listagg(
CASE WHEN COUNT(*) > 1 THEN SHEEP_ID || ': ' ELSE '' END
|| SHEEP_NAME, CHR(10)) WITHIN GROUP (ORDER BY SHEEP_ID)
FROM SHEEPS
WHERE SHEEP_SHEPHERD_ID = SHEPHERD_ID)
FROM SHEPHERDS;
However, I get error:
ORA-00978: nested group function without GROUP BY
http://sqlfiddle.com/#!4/881a7/7
Is it possible to return different string from LISTAGG() if there is only one element to aggregate?
How to detect number of aggregated elements without slowing down query performance in Oracle 11g or higher?
A conditional expression in the subquery should do what you want:
SELECT sh.SHEPHERD_NAME,
(SELECT (CASE WHEN COUNT(*) = 1 THEN MAX(s.SHEEP_NAME)
ELSE LISTAGG(s.SHEEP_ID || ': ' || s.SHEEP_NAME, CHR(10)) WITHIN GROUP (ORDER BY s.SHEEP_ID)
END) as SHEEPS
FROM SHEEPS s
WHERE s.SHEEP_SHEPHERD_ID = sh.SHEPHERD_ID
) as SHEEPS
FROM SHEPHERDS sh;
Here is a db<>fiddle.
The solution without a subquery use a simple GROUP BY, COUNT(*) = 1 to distinct the sheep count and two different LISTAGG statements
SELECT
s.SHEPHERD_NAME,
case when count(*) = 1 then
listagg(SHEEP_NAME, CHR(10)) WITHIN GROUP (ORDER BY SHEEP_ID)
else
listagg(SHEEP_ID || ': ' || SHEEP_NAME, CHR(10)) WITHIN GROUP (ORDER BY SHEEP_ID) end as SHEEPS
FROM SHEPHERDS s
JOIN SHEEPS sh on s.SHEPHERD_ID = sh.SHEEP_SHEPHERD_ID
GROUP BY s.SHEPHERD_NAME /* add SHEPHERD_ID in GROUP BY if the name is not unique */
returns
SHEPHERD_NAME, SHEEPS
Asher Mark
Joseph A: Andres
B: Jeffrey
Nicodemus A: Andres
B: Jeffrey
D: Andres
I have a question about running a Oracle DB query on multiple tables. Is there a way to make the table names variables to be iterated as opposed to having to state each table name?
Background Example
There are a large number of tables (ex. TABLE_1...TABLE_100).
Each of these tables are listed in the NAME column of another table (ex. TABLE_LIST) listing an even larger number of tables along with TYPE (ex. "Account")
Each of these tables has columnn VALUE a boolean column, ACTIVE.
Requirements
Query the TABLE_LIST by TYPE = 'Account'
For Each table found, query that table for all records where column ACTIVE = 'N'
Results show table NAME and VALUE from each table row where ACTIVE = 'N'.
Any tips would be appreciated.
There is a low tech and a high tech way. I'll put them in separate answers so that people can vote for them. This is the high tech version.
Set up: Same as in low tech version.
CREATE TYPE my_row AS OBJECT (name VARCHAR2(128), value NUMBER)
/
CREATE TYPE my_tab AS TABLE OF my_row
/
CREATE OR REPLACE FUNCTION my_fun RETURN my_tab PIPELINED IS
rec my_row := my_row(null, null);
cur SYS_REFCURSOR;
BEGIN
FOR t IN (SELECT name FROM table_list WHERE table_type='Account') LOOP
rec.name := dbms_assert.sql_object_name(t.name);
OPEN cur FOR 'SELECT value FROM '||t.name||' WHERE active=''N''';
LOOP
FETCH cur INTO rec.value;
EXIT WHEN cur%NOTFOUND;
PIPE ROW(rec);
END LOOP;
CLOSE cur;
END LOOP;
END my_fun;
/
SELECT * FROM TABLE(my_fun);
NAME VALUE
TABLE_1 1
TABLE_3 3
There is a low tech and a high tech way. I'll put them in separate answers so that people can vote for them. This is the low tech version.
Set up:
CREATE TABLE table_1 (value NUMBER, active VARCHAR2(1) CHECK(active IN ('Y','N')));
CREATE TABLE table_2 (value NUMBER, active VARCHAR2(1) CHECK(active IN ('Y','N')));
CREATE TABLE table_3 (value NUMBER, active VARCHAR2(1) CHECK(active IN ('Y','N')));
INSERT INTO table_1 VALUES (1, 'N');
INSERT INTO table_1 VALUES (2, 'Y');
INSERT INTO table_3 VALUES (3, 'N');
INSERT INTO table_3 VALUES (4, 'Y');
CREATE TABLE table_list (name VARCHAR2(128 BYTE) NOT NULL, table_type VARCHAR2(10));
INSERT INTO table_list (name, table_type) VALUES ('TABLE_1', 'Account');
INSERT INTO table_list (name, table_type) VALUES ('TABLE_2', 'Something');
INSERT INTO table_list (name, table_type) VALUES ('TABLE_3', 'Account');
The quick and easy way is to use a query to generate another query. I do that quite often, especially for one off jobs:
SELECT 'SELECT '''||name||''' as name, value FROM '||name||
' WHERE active=''N'' UNION ALL' as sql
FROM table_list
WHERE table_type='Account';
SELECT 'TABLE_1' as name, value FROM TABLE_1 WHERE active='N' UNION ALL
SELECT 'TABLE_3' as name, value FROM TABLE_3 WHERE active='N' UNION ALL
You'll have to remove the last UNION ALL and execute the rest of the query. The result is
NAME VALUE
TABLE_1 1
TABLE_3 3
I'm working on an Oracle Stored procedure.
I need to iterate over rows of a table . I can do that using:
FOR eachrow IN table_name
LOOP
END LOOP;
But i need the table_name to be dynamic.
For example the table names are stored in some other table.
So, i can do FOR loop on that table and inside the loop ,i want to iterate through the rows of the new table.
Please suggest how i can achieve that.
Thanks,
Sash
This is not possible with a FOR eachrow IN table_name LOOP, you have to use a ref cursor instead:
First of all some sample data. These tables have some column names in common, in your inner for loop you can only access these columns, this is what I wrote in my comment above. Result type should be the same in the inner loop.
create table froc_a(id number, name varchar2(10), row_added date);
insert into froc_a values (1, '1', sysdate);
insert into froc_a values (2, '2', sysdate - 2);
insert into froc_a values (4, '4', sysdate - 4);
create table froc_b(id number, name2 varchar2(10), row_added date);
insert into froc_b values (1, 'b1', sysdate);
insert into froc_b values (2, 'b2', sysdate - 2);
insert into froc_b values (4, 'b4', sysdate - 4);
create table froc_c(id number, txt varchar2(10), row_added date);
insert into froc_c values (1, 'c1', sysdate);
insert into froc_c values (2, 'c2', sysdate - 2);
insert into froc_c values (4, 'c4', sysdate - 4);
Here is a first approach how to write it:
declare
TYPE curtype IS REF CURSOR;
l_cursor curtype;
l_param_id number;
l_id number;
l_val varchar2(100);
begin
l_param_id := 1;
-- Loop over your table names
for l_rec in (with tabnames(name) as
(select 'froc_a'
from dual
union all
select 'froc_b'
from dual
union all
select 'froc_c'
from dual)
select * from tabnames) loop
dbms_output.put_line(l_rec.name);
-- Open cursor for current table
open l_cursor for 'select id, row_added from ' || l_rec.name || ' where id = :1'
using l_param_id;
-- Loop over rows of current table
loop
fetch l_cursor
into l_id, l_val;
exit when l_cursor%notfound;
dbms_output.put_line(l_id || ', ' || l_val);
end loop;
end loop;
end;
Output:
froc_a
1, 06-APR-16
froc_b
1, 06-APR-16
froc_c
1, 06-APR-16
Let's say I have the following tables:
create table student(
id number not null,
name varchar2(80),
primary key(id)
);
create table class(
id number not null,
subject varchar2(80),
primary key(id)
);
create table class_meeting(
id number not null,
class_id number not null,
meeting_sequence number,
primary key(id),
foreign key(class_id) references class(id)
);
create table meeting_attendance(
id number not null,
student_id number not null,
meeting_id number not null,
present number not null,
primary key(id),
foreign key(student_id) references student(id),
foreign key(meeting_id) references class_meeting(id),
constraint meeting_attendance_uq unique(student_id, meeting_id),
constraint present_ck check(present in(0,1))
);
I want a query for each class, which has a column for the student name, one column for every class_meeting for this class and for every class meeting the cells would show the present attribute, which should be 1 if the student was present at that meeting and 0 if the student was absent in that meeting. Here is a picture from excel for reference:
Is it possible to make an apex report like that?
From googling I figured I must use Pivot, however I'm having a hard time understanding how it could be used here. Here is the query I have so far:
select * from(
select s.name, m.present
from student s, meeting_attendance m
where s.id = m.student_id
)
pivot(
present
for class_meeting in ( select a.meeting_sequence
from class_meeting a, class b
where b.id = a.class_id )
)
However I'm sure it's way off. Is it even possible to do this with one query, or should I use pl sql htp and htf packages to create an html table?
Pretty inexperienced oracle developer here, so any help is very appreciated.
It took a while to answer, but I had to write this all up and test it!
Data I've worked with:
begin
insert into student(id, name) values (1, 'Tom');
insert into student(id, name) values (2, 'Odysseas');
insert into class(id, subject) values (1, 'Programming');
insert into class(id, subject) values (2, 'Databases');
insert into class_meeting (id, class_id, meeting_sequence) values (1, 1, 10);
insert into class_meeting (id, class_id, meeting_sequence) values (2, 1, 20);
insert into class_meeting (id, class_id, meeting_sequence) values (3, 2, 10);
insert into class_meeting (id, class_id, meeting_sequence) values (4, 2, 20);
insert into meeting_attendance (id, student_id, meeting_id, present) values (1, 1, 1, 1); -- Tom was at meeting 10 about programming
insert into meeting_attendance (id, student_id, meeting_id, present) values (2, 1, 2, 1); -- Tom was at meeting 20 about programming
insert into meeting_attendance (id, student_id, meeting_id, present) values (3, 1, 3, 0); -- Tom was NOT at meeting 10 about databases
insert into meeting_attendance (id, student_id, meeting_id, present) values (4, 1, 4, 0); -- Tom was NOT at meeting 20 about databases
insert into meeting_attendance (id, student_id, meeting_id, present) values (5, 2, 1, 0); -- Odysseas was NOT at meeting 10 about programming
insert into meeting_attendance (id, student_id, meeting_id, present) values (6, 2, 2, 1); -- Odysseas was at meeting 20 about programming
insert into meeting_attendance (id, student_id, meeting_id, present) values (7, 2, 3, 0); -- Odysseas was NOT at meeting 10 about databases
insert into meeting_attendance (id, student_id, meeting_id, present) values (8, 2, 4, 1); -- Odysseas was at meeting 20 about databases
end;
PIVOT , as it stands right now, does not allow a dynamic number of columns in a simple way. It only allows this with the XML keyword, resulting in an xmltype column.
Here are some excellent docs. http://www.oracle-base.com/articles/11g/pivot-and-unpivot-operators-11gr1.php
It always pays off to read those first.
How to, then?
You'll literally find tons of questions about the same thing once you start searching.
Dynamic SQL
https://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:4471013000346257238
Dynamically pivoting a table Oracle
Dynamic Oracle Pivot_In_Clause
A classic report can take a function body returning a sql statement as return. An interactive report can not. As it stands, an IR is out of the question as it is too metadata dependent.
For example, with these queries/plsql in a classic report region source:
static pivot
select *
from (
select s.name as student_name, m.present present, cm.meeting_sequence||'-'|| c.subject meeting
from student s
join meeting_attendance m
on s.id = m.student_id
join class_meeting cm
on cm.id = m.meeting_id
join class c
on c.id = cm.class_id
)
pivot ( max(present) for meeting in ('10-Databases' as "10-DB", '20-Databases' as "20-DB", '10-Programming' as "10-PRM", '20-Programming' as "20-PRM") );
-- Results
STUDENT_NAME '10-Databases' 20-DB 10-PRM 20-PRM
Tom 0 0 1 1
Odysseas 0 1 0 1
function body returning statement
DECLARE
l_pivot_cols VARCHAR2(4000);
l_pivot_qry VARCHAR2(4000);
BEGIN
SELECT ''''||listagg(cm.meeting_sequence||'-'||c.subject, ''',''') within group(order by 1)||''''
INTO l_pivot_cols
FROM class_meeting cm
JOIN "CLASS" c
ON c.id = cm.class_id;
l_pivot_qry :=
'select * from ( '
|| 'select s.name as student_name, m.present present, cm.meeting_sequence||''-''||c.subject meeting '
|| 'from student s '
|| 'join meeting_attendance m '
|| 'on s.id = m.student_id '
|| 'join class_meeting cm '
|| 'on cm.id = m.meeting_id '
|| 'join class c '
|| 'on c.id = cm.class_id '
|| ') '
|| 'pivot ( max(present) for meeting in ('||l_pivot_cols||') )' ;
RETURN l_pivot_qry;
END;
Take note however of the settings in the region source.
Use Query-Specific Column Names and Validate Query
This is the standard setting. It will parse your query and then store the columns found in the query in the report metadata. If you go ahead and create a report with the above plsql code, you can see that apex has parsed the query and has assigned the correct columns. What is wrong with this approach is that that metadata is static. The report's metadata is not refreshed every time the report is being ran.
This can be proven quite simply by adding another class to the data.
begin
insert into class(id, subject) values (3, 'Watch YouTube');
insert into class_meeting (id, class_id, meeting_sequence) values (5, 3, 10);
insert into meeting_attendance (id, student_id, meeting_id, present) values (10, 1, 5, 1); -- Tom was at meeting 10 about watching youtube
end;
Run the page without editing the report! Editing and saving will regenerate the metadata, which is clearly not a viable method. The data will change anyway, and you cannot go in and save the report metadata every time.
--cleanup
begin
delete from class where id = 3;
delete from class_meeting where id = 5;
delete from meeting_attendance where id = 10;
end;
Use Generic Column Names (parse query at runtime only)
Setting the source to this type will allow you to use a more dynamic approach. By changing the settings of the report to this type of parsing, apex will just generate an amount of columns in its metadata without being directly associated with the actual query. There'll just be columns with 'COL1', 'COL2', 'COL3',...
Run the report. Works fine. Now insert some data again.
begin
insert into class(id, subject) values (3, 'Watch YouTube');
insert into class_meeting (id, class_id, meeting_sequence) values (5, 3, 10);
insert into meeting_attendance (id, student_id, meeting_id, present) values (10, 1, 5, 1); -- Tom was at meeting 10 about watching youtube
end;
Run the report. Works fine.
However, the kink here are the column names. They're not really all that dynamic, with their ugly names. You can edit the columns, surely, but they're not dynamic. There is no class being displayed or anything, nor can you reliably set their headers to one. Again this makes sense: the metadata is there, but it is static. It could work for you if you're happy with this approach.
You can however deal with this. In the "Report Attributes" of the report, you can select a "Headings Type". They're all static, expect for "PL/SQL" of course! Here you can write a function body (or just call a function) which'll return the column headers!
DECLARE
l_return VARCHAR2(400);
BEGIN
SELECT listagg(cm.meeting_sequence||'-'||c.subject, ':') within group(order by 1)
INTO l_return
FROM class_meeting cm
JOIN "CLASS" c
ON c.id = cm.class_id;
RETURN l_return;
END;
Third party solution
https://asktom.oracle.com/pls/apex/f?p=100:11:0::::P11_QUESTION_ID:4843682300346852395#5394721000346803830
https://stackoverflow.com/a/16702401/814048
http://technology.amis.nl/2006/05/24/dynamic-sql-pivoting-stealing-antons-thunder/
In APEX: though the dynamic pivot is more straightforward after installing, the setup in apex remains the same as if you'd want to use dynamic SQL. Use a classic report with generic column names.
I'm not going to go into much detail here. I don't have this package installed atm. It's nice to have, but in this scenario it may not be that helpful. It purely allows you to write a dynamic pivot in a more concise way, but doesn't help much on the apex side of things. As I've demonstrated above, the dynamic columns and the static metadata of the apex reports are the limiting factor here.
Use XML
I myself have opted to use the XML keyword before. I use pivot to make sure I have values for all rows and columns, then read it out again with XMLTABLE, and then creating one XMLTYPE column, serializing it to a CLOB.
This may be a bit advanced, but it's a technique I've used a couple of times so far, with good results. It's fast, provided the base data is not too big, and it's just one sql call, so not a lot of context switches. I've used it with CUBE'd data aswell, and it works great.
(note: the classes I've added on the elements correspond with classes used on classic reports in theme 1, simple red)
DECLARE
l_return CLOB;
BEGIN
-- Subqueries:
-- SRC
-- source data query
-- SRC_PIVOT
-- pivoted source data with XML clause to allow variable columns.
-- Mainly used for convenience because pivot fills in 'gaps' in the data.
-- an example would be that 'Odysseas' does not have a relevant record for the 'Watch Youtube' class
-- PIVOT_HTML
-- Pulls the data from the pivot xml into columns again, and collates the data
-- together with xmlelments.
-- HTML_HEADERS
-- Creates a row with just header elements based on the source data
-- HTML_SRC
-- Creates row elements with the student name and the collated data from pivot_html
-- Finally:
-- serializes the xmltype column for easier-on-the-eye markup
WITH src AS (
SELECT s.name as student_name, m.present present, cm.meeting_sequence||'-'||c.subject meeting
FROM student s
JOIN meeting_attendance m
ON s.id = m.student_id
JOIN class_meeting cm
ON cm.id = m.meeting_id
JOIN class c
ON c.id = cm.class_id
),
src_pivot AS (
SELECT student_name, meeting_xml
FROM src pivot xml(MAX(NVL(present, 0)) AS is_present_max for (meeting) IN (SELECT distinct meeting FROM src) )
),
pivot_html AS (
SELECT student_name
, xmlagg(
xmlelement("td", xmlattributes('data' as "class"), is_present_max)
ORDER BY meeting
) is_present_html
FROM src_pivot
, xmltable('PivotSet/item'
passing meeting_xml
COLUMNS "MEETING" VARCHAR2(400) PATH 'column[#name="MEETING"]'
, "IS_PRESENT_MAX" NUMBER PATH 'column[#name="IS_PRESENT_MAX"]')
GROUP BY (student_name)
),
html_headers AS (
SELECT xmlelement("tr",
xmlelement("th", xmlattributes('header' as "class"), 'Student Name')
, xmlagg(xmlelement("th", xmlattributes('header' as "class"), meeting) order by meeting)
) headers
FROM (SELECT DISTINCT meeting FROM src)
),
html_src as (
SELECT
xmlagg(
xmlelement("tr",
xmlelement("td", xmlattributes('data' as "class"), student_name)
, ah.is_present_html
)
) data
FROM pivot_html ah
)
SELECT
xmlserialize( content
xmlelement("table"
, xmlattributes('report-standard' as "class", '0' as "cellpadding", '0' as "cellspacing", '0' as "border")
, xmlelement("thead", headers )
, xmlelement("tbody", data )
)
AS CLOB INDENT SIZE = 2
)
INTO l_return
FROM html_headers, html_src ;
htp.prn(l_return);
END;
In APEX: well, since the HTML has been constructed, this can only be a PLSQL region which calls the package function and prints it using HTP.PRN.
(edit) There's also this post on the OTN forum which does the same in a large part, but does not generate headings etc, rather using the apex functionalities:
OTN: Matrix report
PLSQL
Alternatively, you can just opt to go the good ol' plsql route. You could take the body from the dynamic sql above, loop over it, and put out a table structure by using htp.prn calls. Put out headers, and put out whatever else you want. For good effect, add classes on the elements which correspond with the theme you're using.
Disclaimer: I don't know apex specifically.
Here's a correct pivot query, assuming the class you want has an ID = 1, and that the meeting_id's for that class are 1,2,3.
select * from(
select s.name, a.present,m.id meeting_id
from student s, meeting_attendance a, class_meeting m, class c
where s.id = a.student_id
and m.id = a.meeting_id
and c.id = m.class_id
and c.id = 1
)
pivot(
sum(present)
for meeting_id in(1,2,3)
);
I don't believe you can use a sub-query to return the values for the "for in" of the pivot.