Oracle Shuffle columns in rows - oracle

Based in the solution if this issue - Shuffle a column between rows - I need to apply to this, a new condition: the names must guarantee the gender: male, female or unknown.
So, my table have a column named gender. I need to shuffle the columns whit the same gender.
I've tried this, but in some cases I cannot guarantee the gender
merge into original_table o
using (
with
helper ( id, gender, rn, rand_rn ) as (
select id,
gender,
row_number() over (order by id),
row_number() over (order by dbms_random.value())
from original_table
)
select ot.name, ot.gender, h2.id
from original_table ot inner join helper h1 on (ot.id = h1.id and ot.gender = h1.gender)
inner join helper h2 on h1.rand_rn = h2.rn
) p
on (o.id = p.id)
when matched then update set o.name = p.name
;
And in cases that gender is not available (is null value), for example company's name, this merge not update anything.
I can split this query in 3 different merge statement. One for each gender that I have. But I'm looking for a better and simply statement. Because I need to apply the same solution in a different context and with different conditions.
Thanks.
EDIT: Sample data
ID GENDER NAME
3721 M MARK
3722 M JUSTIN
3723 F RUTH
3724 F MARY
3725 F ANNE
4639 CAMPANY SA
4640 M JOHN
4641 M LUCAS
4642 COMPANY HOLDER SA
One possible solution:
ID GENDER NAME
3721 M LUCAS
3722 M JOHN
3723 F MARY
3724 F ANNE
3725 F RUTH
4639 CAMPANY HOLDER SA
4640 M MARK
4641 M JUSTIN
4642 COMPANY SA

Include gender in the PARTITION BY clause in the analytic functions and in the JOIN clause. If you don't have a primary key to match on then you could use the ROWID pseudo-column.
Oracle Setup:
CREATE TABLE original_table ( id, gender, name, company ) AS
SELECT 1, 'F', 'Alice', CAST( NULL AS VARCHAR2(20) ) FROM DUAL UNION ALL
SELECT 2, 'M', 'Bobby', 'ACME' FROM DUAL UNION ALL
SELECT 3, 'F', 'Carol', 'XYZ' FROM DUAL UNION ALL
SELECT 4, 'M', 'David', NULL FROM DUAL UNION ALL
SELECT 5, 'M', 'Errol', 'ACME' FROM DUAL UNION ALL
SELECT 6, 'F', 'Fiona', 'XYZ' FROM DUAL;
Update:
MERGE INTO original_table dst
USING (
WITH rnd ( rid, rn, rnd_rn, gender, name ) AS (
SELECT ROWID,
ROW_NUMBER() OVER ( PARTITION BY gender ORDER BY id ),
ROW_NUMBER() OVER ( PARTITION BY gender ORDER BY DBMS_RANDOM.VALUE ),
gender,
name
FROM original_table
)
SELECT o.rid, r.name
FROM rnd o INNER JOIN rnd r
ON ( ( o.gender = r.gender OR ( o.gender IS NULL AND r.gender IS NULL ) )
AND o.rn = r.rnd_rn )
)
ON ( dst.ROWID = src.ROWID )
WHEN MATCHED THEN
UPDATE SET name = src.name;
Output:
SELECT * FROM original_table;
ID G NAME COMPANY
-- - ----- -------
1 F Fiona
2 M David ACME
3 F Alice XYZ
4 M Bobby
5 M Errol ACME
6 F Carol XYZ

Related

How to use GREATEST function with Over Partition by in Oracle

In the below code I want to select customer_name, location, gender and address along with customerid, aread_code.
select
customerid, aread_code, GREATEST(MAX(productid), MAX(itemid))
from
CUSTOMER C
inner join
ORDER O ON c.custid = o.custid
where
c.custtype = 'EXECUTIVE'
group
customerid, by aread_code;
I tried GREATEST function along with OVER PARTITION BY to display required columns. It's throwing an error.
Could you please help me to select the required columns.
Thank you.
DISCLAIMER:
When working with more than one table, qualify the columns with their table name. You haven't done so, so we don't know what of the two tables the aread_code resides in. In my answer here I assume it is the customer's area. If it isn't then you need a different answer.
ANSWER:
You group by customer_id and area code. This gives you one row per customer. And you want the maximum product/item ID from the orders table. (I suppose they are drawn from the same sequence, so you can use this ID somehow to go on from there.)
The easiest approach for this is to get the maximum ID in a subquery. Either directly in the select clause or in the from clause.
Here is how to do this in the SELECT clause:
select
c.*,
(
select greatest(max(productid), max(itemid))
from orders o
where o.custid = c.custid
) as max_id
from customer c
where c.custtype = 'EXECUTIVE';
Here is one way to do this in the FROM clause:
select
c.*,
agg.max_id
from customer c
outer apply
(
select greatest(max(productid), max(itemid)) as max_id
from orders o
where o.custid = c.custid
) agg
where c.custtype = 'EXECUTIVE';
And here is another way to do this in the FROM clause:
select
c.*,
agg.max_id
from customer c
left outer join
(
select
custid,
greatest(max(productid), max(itemid)) as max_id
from orders
group by custid
) agg on agg.custid = c.custid
where c.custtype = 'EXECUTIVE';
If you only want customers with at least one order, then I recommend the approach with the FROM clause. You'd have to turn the OUTER APPLY into a CROSS APPLY resp. the LEFT OUTER JOIN into an INNER JOIN for this.
There are several mistakes in your code. The main confusion is not using table alias prefix for columns. There is a group by mistake and a problem with your table name ORDER - if it is a name of a table. ORDER is a reserved word in Oracle and if it is the name of the table then you should use something like "YOUR_OWNER_NAME"."ORDER".... Here is the corected code with some sample data and result:
WITH
customers (CUSTID, PRODUCTID, AREAD_CODE, CUSTOMER_NAME, LOCATION, GENDER, ADDRESS, CUSTTYPE) AS
(
Select 1, 1, 63, 'Name 1', 'Location 1', 'M', 'Address 1', 'EXECUTIVE' From Dual Union All
Select 2, 1, 63, 'Name 1', 'Location 1', 'M', 'Address 1', 'EXECUTIVE' From Dual Union All
Select 3, 3, 63, 'Name 1', 'Location 1', 'M', 'Address 1', 'EXECUTIVE' From Dual Union All
Select 4, 7, 63, 'Name 1', 'Location 1', 'M', 'Address 1', 'EXECUTIVE' From Dual
),
orders (ORDER_ID, CUSTID, ITEMID, SOME_COLUMN) AS
(
Select 1, 1, 1, 'Some other data' From Dual Union All
Select 2, 2, 1, 'Some other data' From Dual Union All
Select 3, 3, 1, 'Some other data' From Dual Union All
Select 4, 3, 3, 'Some other data' From Dual Union All
Select 5, 4, 1, 'Some other data' From Dual Union All
Select 6, 4, 8, 'Some other data' From Dual
)
select
c.custid, c.aread_code, GREATEST(MAX(c.productid), MAX(o.itemid)) "MAX_ID"
from
CUSTOMERS C
inner join
ORDERS O ON c.custid = o.custid
where
c.custtype = 'EXECUTIVE'
group by
c.custid, c.aread_code
CUSTID AREAD_CODE MAX_ID
---------- ---------- ----------
1 63 1
4 63 8
3 63 3
2 63 1
There are different options to get the rest of the columns depending on your actual data you could use some or all of them.
Option 1 - select and group by as suggested in Beefstu's comment below
select Distinct
c.custid, c.customer_name, c.location, c.address, c.gender, c. custtype, c.aread_code,
GREATEST(MAX(c.productid), MAX(o.itemid)) "MAX_ID"
from
CUSTOMERS C
inner join
ORDERS O ON c.custid = o.custid
where
c.custtype = 'EXECUTIVE'
group by
c.custid, c.customer_name, c.location, c.address, c.gender, c. custtype, c.aread_code
order by c.custid
CUSTID CUSTOMER_NAME LOCATION ADDRESS GENDER CUSTTYPE AREAD_CODE MAX_ID
---------- ------------- ---------- --------- ------ --------- ---------- ----------
1 Name 1 Location 1 Address 1 M EXECUTIVE 63 1
2 Name 1 Location 1 Address 1 M EXECUTIVE 63 1
3 Name 1 Location 1 Address 1 M EXECUTIVE 63 3
4 Name 1 Location 1 Address 1 M EXECUTIVE 63 8
Option 2. - using analytic functions MAX() OVER() with Distinct keyword (could be performance costly with big datasets) - result is the same as above
select Distinct
c.custid, c.customer_name, c.location, c.address, c.gender, c. custtype, c.aread_code,
GREATEST(MAX(c.productid) OVER(Partition By c.custid), MAX(o.itemid) OVER(Partition By c.custid)) "MAX_ID"
from
CUSTOMERS C
inner join
ORDERS O ON c.custid = o.custid
where
c.custtype = 'EXECUTIVE'
order by c.custid
Option 3 - using left join to a subquery - see the solution offered by Thorsten Kettner

ORACLE - How to use LAG to display strings from all previous rows into current row

I have data like below:
group
seq
activity
A
1
scan
A
2
visit
A
3
pay
B
1
drink
B
2
rest
I expect to have 1 new column "hist" like below:
group
seq
activity
hist
A
1
scan
NULL
A
2
visit
scan
A
3
pay
scan, visit
B
1
drink
NULL
B
2
rest
drink
I was trying to solve with LAG function, but LAG only returns one row from previous instead of multiple.
Truly appreciate any help!
Use a correlated sub-query:
SELECT t.*,
(SELECT LISTAGG(activity, ',') WITHIN GROUP (ORDER BY seq)
FROM table_name l
WHERE t."GROUP" = l."GROUP"
AND l.seq < t.seq
) AS hist
FROM table_name t
Or a hierarchical query:
SELECT t.*,
SUBSTR(SYS_CONNECT_BY_PATH(PRIOR activity, ','), 3) AS hist
FROM table_name t
START WITH seq = 1
CONNECT BY
PRIOR seq + 1 = seq
AND PRIOR "GROUP" = "GROUP"
Or a recursive sub-query factoring clause:
WITH rsqfc ("GROUP", seq, activity, hist) AS (
SELECT "GROUP", seq, activity, NULL
FROM table_name
WHERE seq = 1
UNION ALL
SELECT t."GROUP", t.seq, t.activity, r.hist || ',' || r.activity
FROM rsqfc r
INNER JOIN table_name t
ON (r."GROUP" = t."GROUP" AND r.seq + 1 = t.seq)
)
SEARCH DEPTH FIRST BY "GROUP" SET order_rn
SELECT "GROUP", seq, activity, SUBSTR(hist, 2) AS hist
FROM rsqfc
Which, for the sample data:
CREATE TABLE table_name ("GROUP", seq, activity) AS
SELECT 'A', 1, 'scan' FROM DUAL UNION ALL
SELECT 'A', 2, 'visit' FROM DUAL UNION ALL
SELECT 'A', 3, 'pay' FROM DUAL UNION ALL
SELECT 'B', 1, 'drink' FROM DUAL UNION ALL
SELECT 'B', 2, 'rest' FROM DUAL;
All output:
GROUP
SEQ
ACTIVITY
HIST
A
1
scan
null
A
2
visit
scan
A
3
pay
scan,visit
B
1
drink
null
B
2
rest
drink
db<>fiddle here
To aggregate strings in Oracle we use LISAGG function.
In general, you need a windowing_clause to specify a sliding window for analytic function to calculate running total.
But unfortunately LISTAGG doesn't support it.
To simulate this behaviour you may use model_clause of the select statement. Below is an example with explanation.
select
group_
, activity
, seq
, hist
from t
model
/*Where to restart calculation*/
partition by (group_)
/*Add consecutive numbers to reference "previous" row per group.
May use "seq" column if its values are consecutive*/
dimension by (
row_number() over(
partition by group_
order by seq asc
) as rn
)
measures (
/*Other columnns to return*/
activity
, cast(null as varchar2(1000)) as hist
, seq
)
rules update (
/*Apply this rule sequentially*/
hist[any] order by rn asc =
/*Previous concatenated result*/
hist[cv()-1]
/*Plus comma for the third row and tne next rows*/
|| presentv(activity[cv()-2], ',', '') /**/
/*lus previous row's value*/
|| activity[cv()-1]
)
GROUP_ | ACTIVITY | SEQ | HIST
:----- | :------- | --: | :---------
A | scan | 1 | null
A | visit | 2 | scan
A | pay | 3 | scan,visit
B | drink | 1 | null
B | rest | 2 | drink
db<>fiddle here
Few more variants (without subqueries):
SELECT--+ NO_XML_QUERY_REWRITE
t.*,
regexp_substr(
listagg(activity, ',')
within group(order by SEQ)
over(partition by "GROUP")
,'^([^,]+,){'||(row_number()over(partition by "GROUP" order by seq)-1)||'}'
)
AS hist1
,xmlcast(
xmlquery(
'string-join($X/A/B[position()<$Y]/text(),",")'
passing
xmlelement("A", xmlagg(xmlelement("B", activity)) over(partition by "GROUP")) as x
,row_number()over(partition by "GROUP" order by seq) as y
returning content
)
as varchar2(1000)
) hist2
FROM table_name t;
DBFIddle: https://dbfiddle.uk/?rdbms=oracle_21&fiddle=9b477a2089d3beac62579d2b7103377a
Full test case with output:
with table_name ("GROUP", seq, activity) AS (
SELECT 'A', 1, 'scan' FROM DUAL UNION ALL
SELECT 'A', 2, 'visit' FROM DUAL UNION ALL
SELECT 'A', 3, 'pay' FROM DUAL UNION ALL
SELECT 'B', 1, 'drink' FROM DUAL UNION ALL
SELECT 'B', 2, 'rest' FROM DUAL
)
SELECT--+ NO_XML_QUERY_REWRITE
t.*,
regexp_substr(
listagg(activity, ',')
within group(order by SEQ)
over(partition by "GROUP")
,'^([^,]+,){'||(row_number()over(partition by "GROUP" order by seq)-1)||'}'
)
AS hist1
,xmlcast(
xmlquery(
'string-join($X/A/B[position()<$Y]/text(),",")'
passing
xmlelement("A", xmlagg(xmlelement("B", activity)) over(partition by "GROUP")) as x
,row_number()over(partition by "GROUP" order by seq) as y
returning content
)
as varchar2(1000)
) hist2
FROM table_name t;
GROUP SEQ ACTIV HIST1 HIST2
------ ---------- ----- ------------------------------ ------------------------------
A 1 scan
A 2 visit scan, scan
A 3 pay scan,visit, scan,visit
B 1 drink
B 2 rest drink, drink

Query to join Oracle tables

I have a 'MASTER' table as shown below:
Entity Cat1 Cat2
A Mary;Steve Jacob
B Alex;John Sally;Andrew
Another table 'PERSON' has associations of person's name (this could be InfoID as well) with emails.
Name Email InfoID
Mary maryD#gmail.com mryD
Steve steveR#gmail.com stvR
Jacob jacobB#gmail.com jacbb
Sally sallyD#gmail.com sallD
Alex AlexT#gmail.com alexT
John JohnP#gmail.com johP
Andrew AndrewV#gmail.com andV
I want to join the person table with master such as:
Entity Cat1 EmailCat1 Cat2 EmailCat2
A Mary;Steve maryD#gmail.com;steveR#gmail.com Jacob jacobB#gmail.com
B Alex;John AlexT#gmail.com;JohnP#gmail.com Sally;Andrew sallyD#gmail.com;AndrewV#gmail.com
any insights on how to go about it?
Honestly, your master table design needs to be normalized. But, in the meantime you could try this query below :
with
needed_rows_for_cat1_tab (lvl) as (
select level from dual
connect by level <= (select max(regexp_count(Cat1, ';')) from Your_bad_master_tab) + 1
)
, needed_rows_for_cat2_tab (lvl) as (
select level from dual
connect by level <= (select max(regexp_count(Cat2, ';')) from Your_bad_master_tab) + 1
)
, split_cat1_val_tab as (
select Entity, Cat1
, substr(Cat1||';'
, lag(pos, 1, 0)over(partition by Entity order by lvl) + 1
, pos - lag(pos, 1, 0)over(partition by Entity order by lvl) - 1
) val
, lvl
, pos
, 1 cat
from (
select Entity, Cat1, instr(Cat1||';', ';', 1, r1.lvl)pos, r1.lvl
from Your_bad_master_tab c1
join needed_rows_for_cat1_tab r1 on r1.lvl <= regexp_count(Cat1, ';') + 1
)
)
, split_cat2_val_tab as (
select Entity, Cat2
, substr(Cat2||';'
, lag(pos, 1, 0)over(partition by Entity order by lvl) + 1
, pos - lag(pos, 1, 0)over(partition by Entity order by lvl) - 1
) val
, lvl
, pos
, 2 cat
from (
select Entity, Cat2, instr(Cat2||';', ';', 1, r2.lvl)pos, r2.lvl
from Your_bad_master_tab c1
join needed_rows_for_cat2_tab r2 on r2.lvl <= regexp_count(Cat2, ';') + 1
)
)
select ENTITY
, max(decode(cat, 1, CAT1, null)) CAT1
, listagg(decode(cat, 1, EMAIL, null), ';')within group (order by lvl) EmailCat1
, max(decode(cat, 2, CAT1, null)) CAT2
, listagg(decode(cat, 2, EMAIL, null), ';')within group (order by lvl) EmailCat2
from (
select c.*, p.Email
from split_cat1_val_tab c join Your_person_tab p on (c.val = p.name)
union all
select c.*, p.Email
from split_cat2_val_tab c join Your_person_tab p on (c.val = p.name)
)
group by ENTITY
;
Here are your sample data
--drop table Your_bad_master_tab purge;
create table Your_bad_master_tab (Entity, Cat1, Cat2) as
select 'A', 'Mary;Steve', 'Jacob' from dual union all
select 'B', 'Alex;John', 'Sally;Andrew' from dual
;
--drop table Your_person_tab purge;
create table Your_person_tab (Name, Email, InfoID) as
select 'Mary', 'maryD#gmail.com' ,'mryD' from dual union all
select 'Steve', 'steveR#gmail.com' ,'stvR' from dual union all
select 'Jacob', 'jacobB#gmail.com' ,'jacbb' from dual union all
select 'Sally', 'sallyD#gmail.com' ,'sallD' from dual union all
select 'Alex', 'AlexT#gmail.com' ,'alexT' from dual union all
select 'John', 'JohnP#gmail.com' ,'johP' from dual union all
select 'Andrew', 'AndrewV#gmail.com' ,'andV' from dual
;
DB<>fiddle

coalesce with subquery in a where clause in Oracle

Hy,
I have the next sql query in Oracle:
Imagine I have a table "items" with "id" and "name" fields and other table "prices_items" which have three fields named "id, itemId, category". The category may have three values: "1,2,3". So the query I need to do is get the price of an item from the table "prices_items" but the item can have until three prices because of the category field. So, in priotiry order I need to get the price of an item which has category 1, if the item doesnt have this category I have to find the price for category 2 and so on.
from items
left join prices_items on prices_items.itemId = items.itemId
where prices_items.id = coalesce(select id
from prices_items
where itemId= items.itemId and category=1,
select id
from prices_items
where itemId= items.itemId and category=2,
select id
from prices_items
where itemId= items.itemId and category=3)
The query I am using is like this but I dont know how its working because coalesce is being executed on each join?. How is this being executed?
Thanks
The coalesce() is going to keep the first prices_items.id found in order of the categories listed. Instead of individual subqueries you could write it this way and it will probably give a better plan.
select ...
from items inner join prices_items on prices_items.itemId = items.itemId
where prices_items.category = (
select min(pi2.category) from prices_items pi2
where pi2.itemId = items.itemId
);
If the priority of categories doesn't happen to follow an ascending sequence you could handle it with a case expression:
select ...
from items inner join prices_items on prices_items.itemId = items.itemId
where
case prices_items.category
when 2 then 1
when 3 then 2
when 1 then 3
end = (
select
min(case pi2.category
when 2 then 1
when 3 then 2
when 1 then 3
end)
from prices_items pi2
where pi2.itemId = items.itemId
);
As far as how your current query is actually running it may or may not be materializing all the subquery results. From an end results perspective all you really need to know is that only the first non-null value from the coalesce() arguments is the one kept. The reality is that it is probably more efficient to re-write the query so you don't need them.
There are other ways to write this. The one that's most common these days seems to be the row_number() approach:
with data as (
select *,
row_number() over (partition by pi.itemId order by pi.category) as rn
from items inner join prices_items pi on pi.itemId = items.itemId
)
select ...
from data
where rn = 1;
Here's another Oracle-specific solution:
select *
from
items inner join
(
select itemId, min(price) keep (dense_rank first order by category) as price
from prices_items
group by itemId
) pi on pi.itemId = items.itemId;
Oracle Setup:
CREATE TABLE items (
itemid NUMBER PRIMARY KEY,
name VARCHAR2(20)
);
CREATE TABLE prices_items (
itemId NUMBER REFERENCES items ( itemid ),
category INT,
price NUMBER,
CHECK ( category IN ( 1, 2, 3 ) ),
PRIMARY KEY ( itemid, category )
);
INSERT INTO items
SELECT 1, 'A' FROM DUAL UNION ALL
SELECT 2, 'B' FROM DUAL UNION ALL
SELECT 3, 'C' FROM DUAL;
INSERT INTO prices_items
SELECT 1, 1, 32.5 FROM DUAL UNION ALL
SELECT 1, 2, 23.9 FROM DUAL UNION ALL
SELECT 1, 3, 19.99 FROM DUAL UNION ALL
SELECT 2, 1, 42.42 FROM DUAL UNION ALL
SELECT 2, 3, 99.99 FROM DUAL UNION ALL
SELECT 3, 2, 0.02 FROM DUAL UNION ALL
SELECT 3, 3, 10 FROM DUAL;
Query:
SELECT i.itemid,
name,
category,
price
FROM items i
INNER JOIN
( SELECT itemid,
MIN( category ) AS category,
MAX( price ) KEEP ( DENSE_RANK FIRST ORDER BY category ) AS price
FROM prices_items
GROUP BY itemid
) p
ON ( i.itemid = p.itemid );
Output:
ID NAME CATEGORY PRICE
-- ---- -------- -----
1 A 1 32.50
2 B 1 42.42
3 C 2 0.02

Only join two columns when it is not null in one table oracle

I have a table a
ID | Name | City
1 |Jack | Null
2 |Tom | Null
And table b
ID | Name | City
1 |Jack | Dever
2 |Tom | Dallas
I need to write a query to join these two tables by id, name and city if they are not null in table a. But any of these three column could be null for each row.
I wrote one below but the performance is bad when data grows
Select * from a, b
Where (a.id is not null and a.id=b.id or a.id is null) and
(a.name is not null and a.name=b.name or a.name is null) and
(a.city is not null and a.city=b.city or a.city is null)
Basically, I need to join on the column when it is not null in table a.
Could you shed some light on this?
Thanks a lot!
ATTEMPT I:
Would this be what you need? It seems to do what I can read out from your question.
with a as (select 1 id, 'Jack' name, null city from dual
union all
select 2 id, 'Tom' name, null city from dual
union all
select 3 id, 'Mike' name, 'Miami' city from dual)
,b as (select 1 id, 'Jack' name, 'Dever' city from dual
union all
select 2 id, 'Tom' name, 'Dallas' city from dual
union all
select 3 id, 'Mike' name, 'Boise' city from dual)
select b.*
from b
left outer join a
on a.id = b.id
and a.name = b.name
where b.city = nvl(a.city, b.city);
If not, please advisa as to what would need to change in the result or possibly in the indata.
UPDATE I:
To allow all columns to have the possibility of being null, this could be one way of doing it. I've added testdata for the conditions I think your are describing. It gives the result I think you are looking for.
with a as (select 1 id, 'Jack' name, null city from dual
union all
select 2 id, 'Tom' name, null city from dual
union all
select 3 id, 'Mike' name, 'Miami' city from dual
union all
select 4 id, 'Don' name, null city from dual
union all
select 5 id, null name, 'London' city from dual
union all
select null id, 'Erin' name, 'Berlin' city from dual
)
,b as (select 1 id, 'Jack' name, 'Dever' city from dual
union all
select 2 id, 'Tom' name, 'Dallas' city from dual
union all
select 3 id, 'Mike' name, 'Boise' city from dual
union all
select 4 id, 'Don' name, 'Dover' city from dual
union all
select 5 id, 'Lis' name, 'London' city from dual
union all
select 6 id, 'Erin' name, 'Berlin' city from dual
)
select b.*, a.*
from b
inner join a
on b.id = nvl(a.id, b.id)
and b.name = nvl(a.name, b.name)
and b.city = nvl(a.city, b.city)
order by 1;

Resources