Pivot with dynamic columns in oracle 11g - sql

I have 3 tables (Potions and Reagents and relation of both) and I want to pivot the results, pass to explain:
TABLE POTIONS
ID DESCRIPTION
1 Heal
2 Mana
3 Explosion
TABLE REAGENTS
ID DESCRIPTION
1 Base
2 Mandrake
3 Tulip
4 Sunflower
5 Powder
TABLE POTION_REAGENTS
ID_POTION ID_REAGENT
1 1
1 3
2 1
2 2
2 5
3 4
3 5
I want to obtain the result like this but I dont know how pivot with dynamic columns
POTION REAG_1 REAG_2 REAG_3
Heal Base Tulip NULL
Mana Base Mandrake Powder
Explosion Sunflower Powder NULL
I want if new potion have 4 Reagent the select return's REAG_4 column. Its possible?
Thx for your time!

You cannot use a dynamic pivot; you must have a pre-defined set of values in the PIVOT clause. However, you can use the ROW_NUMBER analytic function to pre-generate a numeric index for potion reagents:
SELECT potion,
reagent1,
reagent2,
reagent3,
reagent4
FROM (
SELECT p.id,
p.description AS potion,
r.description AS reagent,
ROW_NUMBER() OVER (PARTITION BY p.id ORDER BY r.description) AS rn
FROM potions p
INNER JOIN potion_reagents pr
ON (p.id = pr.id_potion)
INNER JOIN reagents r
ON (pr.id_reagent = r.id)
)
PIVOT (
MAX(reagent) FOR rn IN (1 AS reagent1, 2 AS reagent2, 3 AS reagent3, 4 AS reagent4)
)
Which, for your sample data:
CREATE TABLE POTIONS (ID, DESCRIPTION) AS
SELECT 1, 'Heal' FROM DUAL UNION ALL
SELECT 2, 'Mana' FROM DUAL UNION ALL
SELECT 3, 'Explosion' FROM DUAL;
CREATE TABLE REAGENTS (ID, DESCRIPTION) AS
SELECT 1, 'Base' FROM DUAL UNION ALL
SELECT 2, 'Mandrake' FROM DUAL UNION ALL
SELECT 3, 'Tulip' FROM DUAL UNION ALL
SELECT 4, 'Sunflower' FROM DUAL UNION ALL
SELECT 5, 'Powder' FROM DUAL;
CREATE TABLE POTION_REAGENTS (ID_POTION, ID_REAGENT) AS
SELECT 1, 1 FROM DUAL UNION ALL
SELECT 1, 3 FROM DUAL UNION ALL
SELECT 2, 1 FROM DUAL UNION ALL
SELECT 2, 2 FROM DUAL UNION ALL
SELECT 2, 5 FROM DUAL UNION ALL
SELECT 3, 4 FROM DUAL UNION ALL
SELECT 3, 5 FROM DUAL;
Outputs:
POTION
REAGENT1
REAGENT2
REAGENT3
REAGENT4
Explosion
Powder
Sunflower
null
null
Heal
Base
Tulip
null
null
Mana
Base
Mandrake
Powder
null
fiddle

To me it seems you're a making a game or something of a kind. That means there is another "external" programming is being used to process data you're getting from the database.
For that case there is a possibility to obtain a pivot result with dynamical set of columns in XML format. You need the "pivot xml"
select *
from (select p.description p_desc,
r.description r_desc,
'REAG_' || row_number() over (partition by p.description order by r.description) reag_num
from potion_reagents pr
join reagents r
on r.id = pr.id_reagent
join potions p
on p.id = pr.id_potion)
pivot xml(
min(r_desc)
for reag_num in (ANY)
)
As I said, the answer you'll get will be in XML format which is quite simple to parse. Here is an example for "Mana" potion with a fourth reagent I named "Secret"
<PivotSet>
<item>
<column name = "REAG_NUM">REAG_1</column>
<column name = "MIN(R_DESC)">Base</column>
</item>
<item>
<column name = "REAG_NUM">REAG_2</column>
<column name = "MIN(R_DESC)">Mandrake</column>
</item>
<item>
<column name = "REAG_NUM">REAG_3</column>
<column name = "MIN(R_DESC)">Powder</column>
</item>
<item>
<column name = "REAG_NUM">REAG_4</column>
<column name = "MIN(R_DESC)">Secret</column>
</item>
</PivotSet>
Here's a dbfiddle with an example based on data you've provide us with

Related

Looping through string splitted in Oracle

I have a query which returns a data from a column[datatype varchar2] in string format.
select AuditEvents.BeforeValue from AuditEvents where condition......
Now , AuditEvents.BeforeValue has a mix type of data like null or "1,2,3,4,5" or "2,3,1,4" or "3,14" or "11:10" or "Security" --without quotes. These "1,2,3,4,5" are eventId in another table 'EventDesc'.
AuditEvents.BeforeValue column data depends on another column, AuditEvents.EventyType which is varchar. In EventyType column, if data is "ExcludedEvents" then BeforeValue should fetch data from another table EventDesc which has eventId description. Like 1=Event1 2=Event2 etc.So in case of "1,2,3" it should fetch data as "Event1,Event2,Event3"
My query is is it possible to fetch data based on above scenario directly in oracle query rather than doing in c# side(as I'm having alot of trouble when Ui asks for searching or sorting)
Is it possible? Yes, with some trouble because not all values exist in your events table, nor are all of them valid.
See this example, read comments within code.
SQL> with
2 -- sample data
3 events (id, name) as
4 (select 1, 'Event 1' from dual union all
5 select 2, 'Event 2' from dual union all
6 select 3, 'Event 3' from dual
7 ),
8 auditevents (beforevalue) as
9 (select '2,3,1' from dual union all
10 select 'security' from dual union all
11 select '11:10' from dual
12 ),
13 -- query you might need begins here
14 -- split AUDITEVENTS to rows
15 ae_split as
16 (select beforevalue,
17 regexp_substr(beforevalue, '[^,]+', 1, column_value) id
18 from auditevents cross join
19 table(cast(multiset(select level from dual
20 connect by level <= regexp_count(beforevalue, ',') + 1
21 ) as sys.odcinumberlist))
22 )
23 -- join split AUDITEVENTS with the EVENTS table
24 select s.beforevalue, listagg(e.name, ', ') within group (order by e.id) events
25 from ae_split s join events e on to_char(e.id) = s.id
26 where exists (select null from events a where to_char(a.id) = s.id)
27 group by s.beforevalue;
BEFOREVALUE EVENTS
----------- ------------------------------
2,3,1 Event 1, Event 2, Event 3
SQL>

Process distinct comma-separated value in oracle

I have a column with the following data:
Brand
-------------
Audi, Opel, Ford
Skoda, Renault
Audi, BMW
Audi, Volkswagen, Opel
Toyota, Hyundai
I would like to have query which automates assign the data into group as following:
Brand
-------------------
Audi, Opel, Ford, BMW, Volkwagen
Skoda, Renault
Toyota, Hyundai
Note that if we insert another record into the table like this ...
Toyota, BMW
... the required output would be:
Brand
-------------------
Audi, Opel, Ford, BMW, Volkwagen, Toyota, Hyundai
Skoda, Renault
This is an interesting and difficult problem, obscured by your poor data model (which violates First Normal Form). Normalizing the data - and de-normalizing at the end - is trivial, it's just an annoyance (and it will make the query much slower). The interesting part: the input groups are the nodes of a graph, two nodes are connected if they have a "make" in common. You need to find the connected components of the graph; this is the interesting problem.
Here is a complete solution (creating the testing data on the fly, in the first factored subquery in the with clause). Question for you though: even assuming that this solution works for you and you put it in production, who is going to maintain it in the future?
EDIT It occurred to me that my original query can be simplified. Here is the revised version; you can click on the Edited link below the answer if you are curious to see the original version.
with
sample_data (brand) as (
select 'Audi, Opel, Ford' from dual union all
select 'Skoda, Renault' from dual union all
select 'Audi, BMW' from dual union all
select 'Audi, Volkswagen, Opel' from dual union all
select 'Toyota, Hyundai' from dual union all
select 'Tesla' from dual
)
, prep (id, brand) as (
select rownum, brand
from sample_data
)
, fnf (id, brand) as (
select p.id, ca.brand
from prep p cross apply
( select trim(regexp_substr(p.brand, '[^,]+', 1, level)) as brand
from dual
connect by level <= regexp_count(p.brand, '[^,]+')
) ca
)
, g (b1, b2) as (
select distinct fnf1.brand, fnf2.brand
from fnf fnf1 join fnf fnf2 on fnf1.id = fnf2.id
)
, cc (rt, brand) as (
select min(connect_by_root b1), b2
from g
connect by nocycle b1 = prior b2
group by b2
)
select listagg(brand, ', ') within group (order by null) as brand
from cc
group by rt;
Output:
BRAND
---------------------------------------------
Audi, BMW, Ford, Opel, Volkswagen
Hyundai, Toyota
Renault, Skoda
Tesla
That is standard Connected components problem. You can find fast pl/sql solution for production use here: http://orasql.org/2017/09/29/connected-components/
Or in case of just educational purposes, you can use SQL-only solution:
https://gist.github.com/xtender/b6e5cac4dec461c0121145b0e62c5cf5
with t(Brand) as (
select 'Audi, Opel, Ford' brand from dual union all
select 'Skoda, Renault' from dual union all
select 'Audi, BMW' from dual union all
select 'Audi, Volkswagen, Opel' from dual union all
select 'Toyota, Hyundai' from dual union all
select 'Tesla' from dual union all
select 'A'||level||', A'||(level+1) from dual connect by level<=500 union all
select 'B'||level||', B'||(level+1) from dual connect by level<=500 union all
select 'C'||level||', C'||(level+1) from dual connect by level<=500
)
,split_tab as (
select
dense_rank()over(order by t.brand) rn
,x.*
from t,
xmltable(
'ora:tokenize(concat(",",.),",")[position()>1]'
passing t.brand
columns
n for ordinality
,name varchar2(20) path 'normalize-space(.)'
) x
)
,pairs as (
select
t1.rn, t1.name name1, t2.name name2
from split_tab t1
,split_tab t2
where t1.rn=t2.rn
)
select listagg(x,',')within group(order by x)
from (
select x, min(root) grp
from (
select distinct connect_by_root(name1) root, name1 x
from pairs
connect by nocycle
prior name1 = name2
)
group by x
)
group by grp
/
PS. I've split my solution into smallest possible steps, so you can check each CTE separately step-by-step to view how to get results.

How would you find the 'GOOD' ID when cancellation is involved?

Suppose you have the following schema:
CREATE TABLE Data
(
ID INT,
CXL INT
)
INSERT INTO Data (ID, CXL)
SELECT 1, NULL
UNION
SELECT 2, 1
UNION
SELECT 3, 2
UNION
SELECT 5, 3
UNION
SELECT 6, NULL
UNION
SELECT 7, NULL
UNION
SELECT 8, 7
The column CXL is the ID that cancels a particular ID. So, for example, the first row in the table with ID:1 was good until it was cancelled by ID:2 (CXL column). ID:2 was good until it was cancelled by ID:3. ID:3 was good until it was cancelled by ID:5 so in this sequence the last "GOOD" ID was ID:5.
I would like to find all the "GOOD" IDs So in this example it would be:
Latest GOOD ID
5
6
8
Here's a fiddle if you want to play with this:
http://sqlfiddle.com/#!6/68ac48/1
SELECT D.ID
FROM Data D
WHERE NOT EXISTS(SELECT 1
FROM Data WHERE D.ID = CXL)
select Id
from data
where Id not in (select cxl from data where cxl is not null)

create view for items not in list

I have two tables. Table 1 is a master list of equipment with equipment_id and equipment_description. So, let's say for this table I have ten equipment_id's. 1,2,3....10 each with some description attached.
Table 2 logs when the equipment has been inspected:
equipment_id|inspection_date
1 | '1-22-2012'
2 | '1-22-2012'
4 | '1-22-2012'
2 | '1-23-2012'
3 | '1-23-2012'
I've created a view, v_dates which pulls out of table 2 all of the distinct inspection dates - not sure if I needed it but did it anyway.
I would like to create another view which shows all equipment that was NOT inspected for each date in the v_dates. So it would show:
3 | '1-22-2012'
5 | '1-22-2012'
and so on.
Rookie here and just not sure how to join these tables correctly. Can't get it to work and would appreciate any help.
Untested, but I think this should give the desired result:
SELECT i.id,d.date FROM
( SELECT DISTINCT inspection_date AS date FROM inspections ORDER BY 1 ) d
LEFT JOIN
inspections i
ON d.date=i.date
WHERE i.date IS NULL
GROUP BY 1,2
ORDER BY 1,2
Like mentioned in the comments would a table with inspection dates really help.
The following appears to work based on my test data using SQL SERVER 2005. I am using a CROSS JOIN of distinct dates along with a LEFT JOIN to throw out EQUIPMENT_ID records that exist for those dates.
Sorry, I am having problems getting my code formatting correct with tabs and spaces...
IF OBJECT_ID('tempdb..#EQUIPMENT') IS NOT NULL
DROP TABLE #EQUIPMENT
CREATE TABLE #EQUIPMENT
( EQUIPMENT_ID smallint,
EQUIPMENT_DESC varchar(32)
)
INSERT INTO #EQUIPMENT
( EQUIPMENT_ID, EQUIPMENT_DESC )
SELECT 1, 'AAA'
UNION SELECT 2, 'BBB'
UNION SELECT 3, 'CCC'
UNION SELECT 4, 'DDD'
UNION SELECT 5, 'EEE'
UNION SELECT 6, 'FFF'
UNION SELECT 7, 'GGG'
UNION SELECT 8, 'HHH'
UNION SELECT 9, 'III'
UNION SELECT 10, 'JJJ'
IF OBJECT_ID('tempdb..#INSPECTION') IS NOT NULL
DROP TABLE #INSPECTION
CREATE TABLE #INSPECTION
( EQUIPMENT_ID smallint,
INSPECTION_DATE smalldatetime
)
INSERT INTO #INSPECTION
( EQUIPMENT_ID, INSPECTION_DATE )
SELECT 1, '1-22-2012'
UNION SELECT 1, '1-27-2012'
UNION SELECT 3, '1-27-2012'
UNION SELECT 5, '1-29-2012'
UNION SELECT 7, '1-22-2012'
UNION SELECT 7, '1-27-2012'
UNION SELECT 7, '1-29-2012'
SELECT E.EQUIPMENT_ID, D.INSPECTION_DATE
FROM #EQUIPMENT E
CROSS JOIN ( SELECT DISTINCT INSPECTION_DATE
FROM #INSPECTION
) D
LEFT JOIN #INSPECTION I2
ON E.EQUIPMENT_ID = I2.EQUIPMENT_ID
AND D.INSPECTION_DATE = I2.INSPECTION_DATE
WHERE I2.EQUIPMENT_ID IS NULL
ORDER BY E.EQUIPMENT_ID, D.INSPECTION_DATE
As per my comment to the question, you really need a table of valid inspection dates. It makes the sql much more sensible, and besides it's the only way to do it if you want to see all items listed for dates when inspections were supposed to be done, but no inspections were done.
So, assuming the two tables:
create table inspections (equipment_id int, inspection_date date);
create table inspection_dates (id int, inspection_date date);
Then a join to get all the equipment that does not have an inspection on a date when an inspection should have taken place would be:
select i.equipment_id, id.inspection_date
from inspection_dates id,
(select distinct equipment_id from inspections) i
where not exists (select * from inspections i2
where i2.inspection_date = id.inspection_date
and i2.equipment_id = i.equipment_id);
You want the combos that do not exist. Thus the not exists predicate.
Note again, that presumably you would have a table for all the unique equipment_ids, but not knowing that I had to construct it myself in place.

Combining data from 2 tables in to 1 dynamic query

I have two tables:
table 1
id item itemType
-----------------------
1 book1 1
2 book2 1
3 laptop1 2
table 2
id itemId name value
------------------------------------------
1 1 author enid blyton
2 1 title five 1
3 2 author enid blyton
4 2 title five 2
5 3 cpu i7-940
6 3 ram 4 GB
7 3 vcard nvidia quadro
When I query with filter itemType = 1, the result should be:
query 1
id item author title
--------------------------------------------------------
1 book1 enid blyton five 1
2 book2 enid blyton five 2
and with filter itemType = 2
query 2
id item cpu ram vcard
----------------------------------------------
1 laptop1 i7-940 4 GB nvidia quadro
and without filter
query 3
id item author title cpu ram vcard
---------------------------------------------------------------------------
1 book1 enid blyton five 1
2 book2 enid blyton five 2
1 laptop1 i7-940 4 GB nvidia quadro
The reason I use table 2 is because the parameter of each itemType is created during the fly, so it is not possible to have a table like in query 3.
At this moment I can solve this in C# by rebuilding the table programmatically (using a lot of linq call). With a small size of table 1 (1K rows) and 2 (10K rows), the performance is good, but now the size of table 1 is already more than 100K rows and table 2 is more than 1M rows, and the performance is very low.
Is there any function using SQL query that can solve this problem?
Not exactly dynamic but if your name's are all known upfront, you can use PIVOT to retrieve your data.
PIVOT rotates a table-valued expression by turning the unique values
from one column in the expression into multiple columns in the output,
and performs aggregations where they are required on any remaining
column values that are wanted in the final output.
SQL Statement
SELECT t1.Id
, t1.item
, t2.author
, t2.title
, t2.cpu
, t2.ram
, t2.vcard
FROM table1 t1
INNER JOIN (
SELECT *
FROM (
SELECT itemId
, name
, value
FROM table2
) s
PIVOT (
MAX(Value)
FOR name IN (title, author, cpu, ram, vcard)
) p
) t2 ON t2.itemId = t1.Id
Test script
;WITH table1 (id, item, itemtype) AS (
SELECT 1, 'book1', 1
UNION ALL SELECT 2, 'book2', 1
UNION ALL SELECT 3, 'laptop1', 2
)
, table2 (id, itemId, name, value) AS (
SELECT 1, 1, 'author', 'enid blyton'
UNION ALL SELECT 2, 1, 'title', 'five 1'
UNION ALL SELECT 3, 2, 'author', 'enid blyton'
UNION ALL SELECT 4, 2, 'title', 'five 2'
UNION ALL SELECT 5, 3, 'cpu', 'i7 940'
UNION ALL SELECT 6, 3, 'ram', '4 GB'
UNION ALL SELECT 7, 3, 'vcard', 'nvidia quadro'
)
SELECT t1.Id
, t1.item
, t2.author
, t2.title
, t2.cpu
, t2.ram
, t2.vcard
FROM table1 t1
INNER JOIN (
SELECT *
FROM (
SELECT itemId
, name
, value
FROM table2
) s
PIVOT (
MAX(Value)
FOR name IN (title, author, cpu, ram, vcard)
) p
) t2 ON t2.itemId = t1.Id
I suggest running a query to return all possible names from table2 for the specified itemtype, like so:
select distinct name
from table2 t2
where exists (select null
from table1 t1
where t1.itemtype = #itemtype and
t1.id = t2.item_id)
In C#, concatenate the names into a single comma-separated string, then construct a new query string similar to Lieven's answer, like so:
SELECT t1.item
, t2.*
FROM table1 t1
INNER JOIN (SELECT *
FROM (SELECT itemId,
name,
value
FROM table2) s
PIVOT (MAX(Value)
FOR name IN (/*insert names string here*/)) p
) t2 ON t2.itemId = t1.Id
WHERE t1.itemtype = #itemtype;
(with the names string replacing the comment inside the brackets).
Incidentally, if possible, I suggest separating the names from Table 2 into a separate lookup table, like so:
name_table
----------
name_id
name
itemtype
- this would mean that the first query would only have to query a small lookup table rather than all of table 2; it could also be used for consistency in name values at data entry.