Oracle Dynamic Pivoting - sql

I have the below table. I need to create columns based off the column CCL. The values in column CCL are unknown. I'm not sure where to begin here. Any help would be appreciated.
TABLEA
ID CCL Flag
1 john x
1 adam x
1 terry
1 rob x
2 john x
Query:
SELECT *
FROM TABLEA
Output:
ID John Adam Terry Rob
1 x x x
2 x

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

Oracle must know all the column in select list on PARSING stage.
This has a couple of consequences
It's not possible for Oracle to change the column list of the query without re-parsing it. Regardless what is supposed to impact that - whether it's distinct list of values in some column or something else. In other words you cannot expect Oracle to add new columns to output if you added new value to CCL column in your example.
In each and every query you must specify explicitly all the columns in select list unless you use "*" with table alias. If you use "*" then Oracle gets column list from metadata and if you modify metadata (i.e. run DDL on a table) then Oracle re-parses query.
So the best option to deal with "Dynamic Pivoting" is to pivot and format result in the UI. However, there are still some options in database which you may want to consider.
Generating XML with pivoted result and parsing it.
Do pivot for XML and then parse results. In this case, eventually, you have to specify pivoted columns one way or another.
create table tablea(id, ccl, flag) as
(
select 1, 'john', 'x' from dual
union all select 1, 'adam', 'x' from dual
union all select 1, 'terry', null from dual
union all select 1, 'rob', 'x' from dual
union all select 2, 'john', 'x' from dual
);
In below example you do NOT have to provide list of the values for CCL, the only literals you specify are:
pivoted expression (FLAG) and column used for pivoting (CCL).
SQL> select id, x.*
2 from tablea t
3 pivot xml (max(flag) flag for ccl in(any))
4 -- parsing output
5 , xmltable('/PivotSet' passing ccl_xml
6 columns
7 name1 varchar2(30) path '/PivotSet/item[1]/column[#name="CCL"]/text()',
8 value1 varchar2(30) path '/PivotSet/item[1]/column[#name="FLAG"]/text()',
9 name2 varchar2(30) path '/PivotSet/item[2]/column[#name="CCL"]/text()',
10 value2 varchar2(30) path '/PivotSet/item[2]/column[#name="FLAG"]/text()',
11 name3 varchar2(30) path '/PivotSet/item[3]/column[#name="CCL"]/text()',
12 value3 varchar2(30) path '/PivotSet/item[3]/column[#name="FLAG"]/text()',
13 name4 varchar2(30) path '/PivotSet/item[4]/column[#name="CCL"]/text()',
14 value4 varchar2(30) path '/PivotSet/item[4]/column[#name="FLAG"]/text()') x;
ID NAME1 VALUE NAME2 VALUE NAME3 VALUE NAME4 VALUE
---------- ----- ----- ----- ----- ----- ----- ----- -----
1 adam x john x rob x terry
2 john x
You may have noticed 2 important details
In fact, each pivoted column is represented using two columns in result - one for caption and one for value
Names are ordered so you cannot preserver order like in your example ('john', 'adam', 'terry', 'rob'),
moreover one column may represent different names like NAME1 represents values for 'adam' in first row and 'john' in second row.
It's possible to use only indices to get the same output.
select id, x.*
from tablea
pivot xml (max(flag) flag for ccl in(any))
-- parsing output
, xmltable('/PivotSet' passing ccl_xml
columns
name1 varchar2(30) path '/PivotSet/item[1]/column[1]',
value1 varchar2(30) path '/PivotSet/item[1]/column[2]',
name2 varchar2(30) path '/PivotSet/item[2]/column[1]',
value2 varchar2(30) path '/PivotSet/item[2]/column[2]',
name3 varchar2(30) path '/PivotSet/item[3]/column[1]',
value3 varchar2(30) path '/PivotSet/item[3]/column[2]',
name4 varchar2(30) path '/PivotSet/item[4]/column[1]',
value4 varchar2(30) path '/PivotSet/item[4]/column[2]') x;
But still there are two columns for each pivoted column in the output.
Below query returns exactly the same data as in your example
SQL> select id, x.*
2 from tablea
3 pivot xml (max(flag) flag for ccl in(any))
4 -- parsing output
5 , xmltable('/PivotSet' passing ccl_xml
6 columns
7 john varchar2(30) path '/PivotSet/item[column="john"]/column[2]',
8 adam varchar2(30) path '/PivotSet/item[column="adam"]/column[2]',
9 terry varchar2(30) path '/PivotSet/item[column="terry"]/column[2]',
10 rob varchar2(30) path '/PivotSet/item[column="rob"]/column[2]') x;
ID JOHN ADAM TERRY ROB
---------- ----- ----- ----- -----
1 x x x
2 x
But wait... all the values for CCL are specified in the query. This is because column caption cannot depend on the data in the table. So what is the point in pivoting for XML if you could have just hardcoded all values in for clause with the same success? One of the ideas is that Oracle SQL engine transposes query result and the tool which displays output just has to properly parse XML. So you split pivoting logic into two layers. XML parsing can be done outside SQL, say, in your application.
ODCI table interface
There is already a link in another answer to Anton's solution.
You can also check an example here.
And, of course, it's explained in detail in Oracle Documentation.
Polymorphic Table Functions
One more advanced technology has been introduces in Oracle 18 - Polymorphic Table Functions.
But again, you should not expect that column list of your query will change after you added new value to CCL. It can change only after re-parsing. There is a way to force hard parse before each excution, but that is another topic.
Dynamic SQL
Finally, as also already pointed out in the comments, you can use good old DSQL.
First step - generate SQL statement based on the table contents. Second step - execute it.
SQL> var rc refcursor
SQL> declare
2 tmp clob;
3 sql_str clob := 'select * from tablea pivot (max(flag) for ccl in ([dynamic_list]))';
4 begin
5 select listagg('''' || ccl || ''' as ' || ccl, ',') within group(order by max(ccl))
6 into tmp
7 from tablea
8 group by ccl;
9 open :rc for replace(sql_str, '[dynamic_list]', tmp);
10 end;
11 /
PL/SQL procedure successfully completed.
SQL> print rc
ID ADAM JOHN ROB TERRY
---------- ----- ----- ----- -----
1 x x x
2 x

Related

use case for uuid in database

Let's say for example I have a table called test and the data is like that:
id name
1 John
2 Jay
3 Maria
Let's suppose this test gets updated and now the ids the names are for some reason allocated to different id , consider the name column as a unique column, it's just not the pprimary key of test but unique.
Next time I query test it may look like that:
id name
10 John
12 Jay
13 Maria
So in that case the id changed but the name is consistent can be traced back to the previous state of the test table. I believe this is bad practice to change id like that, but I don't have control over this table and this is how some folks handle right now the data. I would like to know if this is a good case for using uuid ? I'm not familiar with the concept of uuid, and how it's best to create something consistent as uniquely identifiable and also fast on search when I want to handle the data changes in this table. I would like to import this table on my end but create a key that is fast and that will not change during data imports.
I feel like the problem you're trying to solve isn't clear.
Problem 1: The id column keeps getting updated. This seems weird so getting to the root of why that is happening seems like the real issue to resolve.
Problem 2: Uniquely identifying rows. You would like to use the id column or a new uuid column to uniquely identify but you've already said you can uniquely identify rows with the name column so what problem are you trying to solve here.
Problem 3: Performance. You're going to get best performance using an indexed integer (preferably primary key) column. Most likely id in this case. uuid won't help with performance.
Problem 4: Data changing on imports. This is likely due to auto increments or initial values set differently in DDL. You need to get a better understanding of what exactly is going wrong with your import.
Problem 5: If you don't have control over the values of the id column how would you be able to add your own uuid?
uuid is just a way of creating a unique value.
Oracle has a function to create uuid random_uuid().
This is an XY-problem.
You have data in your table with a unique key in a given data type and when it gets reloaded then the unique key is being regenerated so that all the data gets given new unique values.
The data type you use does not matter; the issue is with the process of reloading the data and regenerating the unique keys. If you use a different data type and the unique keys are still regenerated then you have exactly the same problem.
Fix that problem first.
Putting aside the reasons for this question and does it make sense or not. If I got it right it is about generation of a unique key from NAME which is unique itself.
If that is the case then you could create your on function to do the job:
CREATE OR REPLACE FUNCTION NAME_2_ID(p_name VARCHAR2) RETURN NUMBER AS
BEGIN
--
Declare
mRet Number(16);
mAlpha VarChar2(64);
mWrk Number(16) := 0;
mANum VarChar2(4000) := '';
--
Begin
IF p_name Is Null Then
mRet := 0;
GOTO End_It;
END IF;
--
mAlpha := ' ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'',.!?"()[]{}';
-- ---------------- Replacing Alpha To Numeric -----------------------------------------------------------------------------------------------
For i In 1 .. Length(p_name) Loop
mANum := mANum || SubStr(p_name, i, 1) || To_Char(InStr(mAlpha, Upper(SubStr(p_name, i, 1)))) || '~';
mWrk := mWrk + InStr(mAlpha, Upper(SubStr(p_name, i, 1)));
End Loop;
mRet := mWrk * Length(mANum);
<<End_It>>
RETURN(mRet);
End;
END NAME_2_ID;
As your ID column in TEST table is changing like in sample data:
WITH
test_1 AS
(
Select 1 "ID", 'John' "A_NAME" From Dual Union All
Select 2 "ID", 'Jay' "A_NAME" From Dual Union All
Select 3 "ID", 'Maria' "A_NAME" From Dual
),
test_2 AS
(
Select 10 "ID", 'John' "A_NAME" From Dual Union All
Select 12 "ID", 'Jay' "A_NAME" From Dual Union All
Select 13 "ID", 'Maria' "A_NAME" From Dual
)
... you can get the same ID_2 whenever you query the table (if name didn't
change) ...
Select
ID,
A_NAME,
NAME_2_ID(A_NAME) "ID_2"
From
test_1
/*
ID A_NAME ID_2
---------- ------ ----------
1 John 765
2 Jay 429
3 Maria 846
*/
-- -------------------------
... ... ...
From
test_2
/*
ID A_NAME ID_2
---------- ------ ----------
10 John 765
12 Jay 429
13 Maria 846
*/

Why is this join not working in Oracle SQL

I am trying to output everything for table one where there is a mention like something from table two
table one is the aoldataleak from 2006 and table two is a created table of all contestants in a horse race at that time period
select query,
PFERDENAME
from AOLDATA.querydata,
Pferde
where query like ( '%' ||PFERDENAME||'%')
ORDER BY PFERDENAME;
Pferdename is a column in table 2 and query is a column in table one
both are chars and the output I get is just a blank table, but I know for a fact there are querys in the first table that are like Pferdenamen in the second one.
I tried this same statement with a dummy table with only a few entries and there it worked just fine
So here's error cause:
Pferdename CHAR (25) PRIMARY KEY
CHAR datatype right-pads values with spaces up to the max column length. So, if horse name is "Max", Oracle stores it as "Max" followed by 22 spaces (which alltogether make 3 + 22 = 25 characters).
You shouldn't
use CHAR in such a case; use VARCHAR2 instead
name is not the best choice for a primary key; it means that there can't be two horses whose names are "Max"
If it must be CHAR, then you'd better trim it, e.g.
select query, PFERDENAME
from AOLDATA.querydata, Pferde
where query like ( '%' || trim(PFERDENAME) ||'%')
ORDER BY PFERDENAME;
Example:
SQL> with
2 querydata (query) as
3 (select 'This is Max, my favorite horse' from dual),
4 pferde (pferdename) as
5 (select 'Max ' from dual)
6 select query, pferdename
7 from querydata, pferde
8 where query like ( '%' || trim(pferdename) ||'%')
9 order by pferdename;
QUERY PFERDENAME
------------------------------ -----------
This is Max, my favorite horse Max
SQL>

Unable to add null columns in sql query via sql developer in 12 c

I am trying to run the query in sql developer, Oracle 12c.
Select id, phone_number, null as contact,null as name from emp;
I need to add 2 extra columns with null data for now.
The above query throws error : from keyword not found where expected.
What am i doing wrong here?
Yes, because column name can't be number - it is reserved for datatype.
Rename the column to something else.
As you commented, column name is - actually - phone_number. If that's so, query works OK:
SQL> select * from emp;
ID PHONE_NUMB
---------- ----------
1 1234-5678
SQL> select id, phone_number, null as contact, null as name from emp;
ID PHONE_NUMB C N
---------- ---------- - -
1 1234-5678
SQL>

Why multiset union is not working when I'm trying to concatenate a null with some number in plsql?

So i have two nested tables and i want to make a new one with the elements from both of them but the first nested table have an null value and the second one an number and i want the result to be the number in the second one but he print the null value. It is possible to make a union between a null and an number with multiset union ?
To answer your question, yes, it is possible to "make a union between a null and an number with multiset union". But what you end up with is **two entries in the nested table:
SQL> update test
2 set marks = numberlist(null) multiset union all numberlist(42)
3 where id_std = 1
4 /
SQL> select id_std
2 , t2.column_value as mark
3 from test t1
4 , table(t1.marks) t2
5 /
ID_STD MARK
------ ----
1
1 42
SQL>
I suspect this affect is actually what you're complaining about. However, the null mark is still a valid entry. If you want to overwrite it you need to provide different logic.

How to split the columns?

Name Nameid
P,q,r,s,t One
A,b,c Two
D,e Three
This is my source table, but i want my target table like this
Name Nameid
P One
Q One
R One
S One
T One
A Two
B Two
C Two
D three
In a case like this, I think it would be more elegant to store the data in a different way.
If you are inserting your rows from a program, try splitting your string there, and insert a few more rows instead.
Let me give you a pseudo code example.
number = "One"
many_letters = "P,Q,R,S,T".split(",")
for(letter in many_letters) {
insert_values(letter, number)
}
Here's one way, lifted from here:
SQL> CREATE TABLE t (name VARCHAR2(20), nameid VARCHAR2(10));
Table created.
SQL> INSERT INTO t VALUES ('P,q,r,s,t','One');
1 row created.
SQL> INSERT INTO t VALUES ('A,b,c' ,'Two');
1 row created.
SQL> INSERT INTO t VALUES ('D,e' ,'Three');
1 row created.
SQL> SELECT nameid
2 , REGEXP_SUBSTR (name, '[^,]+', 1, LEVEL) AS token
3 FROM t
4 CONNECT BY PRIOR nameid = nameid
5 AND REGEXP_INSTR (name, '[^,]+', 1, LEVEL) > 0
6 AND PRIOR DBMS_RANDOM.STRING ('p', 10) IS NOT NULL
7 ;
NAMEID TOKEN
---------- --------------------
One P
One q
One r
One s
One t
Three D
Three e
Two A
Two b
Two c
10 rows selected.
SQL>
Sorry I didn't understand the question it was posted. It's been reformatted since.
Oracle does not have a built in Split function, which is what you need here. The following link shows how to create a custom Split function:
http://srinisreeramoju.blogspot.com/2010/03/oracle-custom-split-function.html
Once you've created the types and the function you would simply call it as:
SELECT
Split(Name, ',') AS Name,
NameID
FROM
YourTable