Dynamic Pivot in Oracle's SQL
You can't put a non constant string in the IN
clause of the pivot clause.
You can use Pivot XML for that.
From documentation:
subquery A subquery is used only in conjunction with the XML keyword.
When you specify a subquery, all values found by the subquery are used
for pivoting
It should look like this:
select xmlserialize(content t.B_XML) from t_aa
pivot xml(
sum(A) for B in(any)
) t;
You can also have a subquery instead of the ANY
keyword:
select xmlserialize(content t.B_XML) from t_aa
pivot xml(
sum(A) for B in (select cl from t_bb)
) t;
Here is a sqlfiddle demo
Use pivot for dynamically changing column headers using sql in oracle
What you can do
SELECT *
FROM data
PIVOT
(
MAX(value) FOR label IN ('A' AS "A", 'B' AS "B",'C' AS "C",'D' AS "D")
)
WHERE ID = 120 AND app_id = 1
as a static pivot statement might be converted to a function which contains two respective parameters
CREATE OR REPLACE FUNCTION Get_Pivoted_Labels( i_id data.id%type, i_app_id data.app_id%type )
RETURN SYS_REFCURSOR IS
v_recordset SYS_REFCURSOR;
v_sql VARCHAR2(32767);
v_cols VARCHAR2(32767);
BEGIN
SELECT LISTAGG( ''''||label||''' AS "'||label||'"' , ',' )
WITHIN GROUP ( ORDER BY label )
INTO v_cols
FROM ( SELECT DISTINCT label
FROM data
WHERE ID = i_id AND app_id = i_app_id );
v_sql :=
'SELECT *
FROM data
PIVOT
(
MAX(value) FOR label IN ( '|| v_cols ||' )
)
WHERE ID = :id AND app_id = :aid';
OPEN v_recordset FOR v_sql USING i_id, i_app_id;
RETURN v_recordset;
END;
/
in which an auxiliary query, in which the label
columns are distinctly selected, is used to determine the string(v_cols
for 'A' AS "A", 'B' AS "B",'C' AS "C",'D' AS "D"
) to be concatenated to the main SQL string in order to be used within the cursor which returns a value of type SYS_REFCURSOR
.
and is invoked by
VAR rc REFCURSOR
VAR v_id NUMBER
VAR v_app_id NUMBER
EXEC :rc := Get_Pivoted_Labels(:v_id,:v_app_id);
PRINT rc
from SQL developer's console.
Demonstration with generated SQL statements
If order of columns in the SELECT list matters, then use the code below in order to create the function
CREATE OR REPLACE FUNCTION Get_Pivoted_Labels( i_id data.id%type, i_app_id data.app_id%type )
RETURN SYS_REFCURSOR IS
v_recordset SYS_REFCURSOR;
v_sql VARCHAR2(32767);
v_cols_1 VARCHAR2(32767);
v_cols_2 VARCHAR2(32767);
BEGIN
SELECT LISTAGG( ''''||label||''' AS "'||label||'"' , ',' )
WITHIN GROUP ( ORDER BY label ),
LISTAGG( label , ',' )
WITHIN GROUP ( ORDER BY label )
INTO v_cols_1, v_cols_2
FROM ( SELECT DISTINCT label, value
FROM data
WHERE ID = i_id AND app_id = i_app_id );
v_sql :=
'SELECT ID, '|| v_cols_2 ||', app_id
FROM data
PIVOT
(
MAX(value) FOR label IN ( '|| v_cols_1 ||' )
)
WHERE ID = :id AND app_id = :aid';
OPEN v_recordset FOR v_sql USING i_id, i_app_id;
RETURN v_recordset;
END;
/
Pivot with Dynamic Columns in Oracle
SYS_REFCURSOR
within a stored function(eg. PL/SQL used instead of using SQL directly) might be used in order to get dynamically generated result set(eg. Dynamic Pivot). In this case, a string for Conditional Aggregation is generated :
CREATE OR REPLACE FUNCTION get_passengers_rs RETURN SYS_REFCURSOR IS
v_recordset SYS_REFCURSOR;
v_sql VARCHAR2(32767);
v_str VARCHAR2(32767);
BEGIN
SELECT LISTAGG('MAX(CASE WHEN rn = '||lvl||' THEN age||''(''||passengers||'')'' END)
AS "Age'||lvl||'"' ,',') WITHIN GROUP (ORDER BY 0)
INTO v_str
FROM ( SELECT level AS lvl
FROM dual
CONNECT BY level <= (SELECT MAX(COUNT(*)) FROM t GROUP BY ID ) ) t;
v_sql :=
'SELECT ID, '|| v_str ||'
FROM
(
SELECT t.*,
ROW_NUMBER() OVER (PARTITION BY ID ORDER BY 0) AS rn
FROM t
)
GROUP BY ID';
OPEN v_recordset FOR v_sql;
RETURN v_recordset;
END;
I also added the names of passengers in order to distinguish each data well.
Then run the below code :
VAR rc REFCURSOR
EXEC :rc := get_passengers_rs;
PRINT rc
from SQL Developer's Command Line in order to see the expected result set.
Above code generates this SQL string(v_sql) for the currently existing data
SELECT ID, MAX(CASE WHEN rn = 1 THEN age||'('||passengers||')' END) AS "Age1",
MAX(CASE WHEN rn = 2 THEN age||'('||passengers||')' END) AS "Age2",
MAX(CASE WHEN rn = 3 THEN age||'('||passengers||')' END) AS "Age3"
FROM
(
SELECT t.*,
ROW_NUMBER() OVER (PARTITION BY ID ORDER BY 0) AS rn
FROM t
)
GROUP BY ID
which yields
ID Age1 Age2 Age3
123456 58(Marie) 65(Ben)
123458 32(Aaron) 18(Caroline) 37(Stephanie)
as result set.
Oracle Dynamic Pivoting
Using dynamic sql for a result where the columns are unknown at the time of executing is a bit of a hassle in Oracle compared to certain other RDMBS.
Because the record type for the output is yet unknown, it can't be defined beforehand.
In Oracle 11g, one way is to use a nameless procedure that generates a temporary table with the pivoted result.
Then select the results from that temporary table.
declare
v_sqlqry clob;
v_cols clob;
begin
-- Generating a string with a list of the unique names
select listagg(''''||CCL||''' as "'||CCL||'"', ', ') within group (order by CCL)
into v_cols
from
(
select distinct CCL
from tableA
);
-- drop the temporary table if it exists
EXECUTE IMMEDIATE 'DROP TABLE tmpPivotTableA';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF;
-- A dynamic SQL to create a temporary table
-- based on the results of the pivot
v_sqlqry := '
CREATE GLOBAL TEMPORARY TABLE tmpPivotTableA
ON COMMIT PRESERVE ROWS AS
SELECT *
FROM (SELECT ID, CCL, Flag FROM TableA) src
PIVOT (MAX(Flag) FOR (CCL) IN ('||v_cols||')) pvt';
-- dbms_output.Put_line(v_sqlqry); -- just to check how the sql looks like
execute immediate v_sqlqry;
end;
/
select * from tmpPivotTableA;
Returns:
ID adam john rob terry
-- ---- ---- --- -----
1 x x x
2 x
You can find a test on db<>fiddle here
In Oracle 11g, another cool trick (created by Anton Scheffer) to be used can be found in this blog. But you'll have to add the pivot function for it.
The source code can be found in this zip
After that the SQL can be as simple as this:
select * from
table(pivot('SELECT ID, CCL, Flag FROM TableA'));
You'll find a test on db<>fiddle here
SQL Pivot with dynamic values for IN CLAUSE
An option which uses a substitution variable:
SQL> set verify off
SQL> clear columns
columns cleared
SQL> column llist new_value slist
SQL> select listagg(ym, ', ') within group (order by ym) as llist
2 from (select distinct
3 chr(39) || year_month ||chr(39) || ' as "' || year_month ||'"' ym
4 from details
5 );
LLIST
--------------------------------------------------------------------------------
'2020-05' as "2020-05", '2020-06' as "2020-06"
SQL> select * from
2 (select lob, clevel, classification, year_month, ccount from details)
3 pivot (sum(ccount)
4 for year_month in (&slist));
LOB CLEVEL CLASSIFIC 2020-05 2020-06
----------------- ------ --------- ---------- ----------
Finance level3 statement 65
Wealth Management level2 lending 23
Mergers and Acqu level3 statement 356
SQL>
Its drawback is that it'll fail if LISTAGG
's result is longer than 4000 characters (but, then again, what will you do with a result that contains more than 500 months?).
Passing dynamic Columns to Pivot in Oracle
- For the
ORA-00911
error you need to remove the;
appended to the end of dynamically generatedv_query
- To display the output you can fetch the results into a cursor. The entire block can go into your dynamic SQL. A sample is below:
set serveroutput on;
declare
v_cols varchar2(100);
v_query varchar2(4000);
begin
select listagg('''' ||product_code||'''',',') within group (order by product_code) into v_cols
from(
select distinct product_code
from pivot_test
);
v_query:='
declare
cur_pivot_test sys_refcursor;
a number;
b number;
c number;
d number;
begin
open cur_pivot_test for
select * from (select product_code, quantity from pivot_test)
pivot (sum (quantity)
as qunts
for product_code in ('|| v_cols ||'));
fetch cur_pivot_Test into a, b, c, d;
close cur_pivot_test;
dbms_output.put_line (rpad(''A_QTY'',10) || '' | '' || rpad(''B_QTY'',10) || '' | '' || rpad(''C_QTY'',10) || '' | '' || rpad(''D_QTY'',10) );
dbms_output.put_line (rpad(a,10) || '' | '' || rpad(b,10) || '' | '' || rpad(c,10) || '' | '' || rpad(d,10) );
end;
';
execute immediate v_query;
end;
Output:
A_QTY | B_QTY | C_QTY | D_QTY
210 | 90 | 160 | 90
Related Topics
How to Find the Last Modified Date, Modified User of an Stored Procedure in SQL Server 2008
Query SQL to Subtract Two Fields
Mysql: Select N Rows, But with Only Unique Values in One Column
Vb.Net Escape Reserved Keywords in SQL Statement
What's the Right Way to Compare an Ntext Column with a Constant Value
Which Table Exactly Is the "Left" Table and "Right" Table in a Join Statement (Sql)
SQL Server Return the Value of Identity Column After Insert Statement
Pivot a Table on a Value But Group the Data on One Line by Another
Select Top Distinct Results Ordered by Frequency
SQL Query to Add a New Column After an Existing Column in SQL Server 2005
Why Does SQL Server Return 0 for 1/2
How to Specify "Close Existing Connections" in SQL Script