How to convert code from Postgres to Oracle - oracle

I have a source table (T1):
ID1 | ID2
----------
1 | 2
1 | 5
4 | 7
7 | 8
9 | 1
I want to convert the data to this (T2):
ID1 | ID2 | LABEL
------------------
1 | 2 | 1
1 | 5 | 1
4 | 7 | 2
7 | 8 | 2
9 | 1 | 1
I found a solution for this in PostgreSQL:
with
recursive cte(id1, id2) as (
select id1, id2, 1 as level
from t
union all
select t.id1, cte.id2, cte.level + 1
from t join
cte
on t.id2 = cte.id1
)
select id1, id2,
dense_rank() over (order by grp) as label
from (select id1, id2,
least(min(id2) over (partition by id1), min(id1) over (partition by id2)) as grp,
level
from cte
) t
where level = 1;
I want to convert this code to Oracle. How I can convert this code from Postgres to Oracle?

Oracle 11.2 supports recursive CTEs. But it deviates from the standard in that the recursive keyword is not required (actually: must not be used). So if you remove the recursive keyword and get the definition of the CTE columns right the following should work. You also need to use something different than LEVEL as that is a reserved word either.
with cte (id1, id2, lvl) as (
select id1, id2, 1 as lvl
from t
union all
select t.id1, cte.id2, cte.lvl + 1
from t
join cte on t.id2 = cte.id1
)
select id1,
id2,
dense_rank() over (order by grp) as label
from (
select id1,
id2,
least(min(id2) over (partition by id1), min(id1) over (partition by id2)) as grp,
lvl
from cte
) t
where lvl = 1;
Here is an SQLFiddle example: http://sqlfiddle.com/#!4/deeb2/3
However I doubt that the original query was correct as you do not have a "starting condition" for the recursive CTE. The first part of the union retrieves all rows of the table. There should be a condition to restrict that to the "roots" of the hierarchy unless I'm mis-understanding the purpose of the query.
A recursive CTE can also be replaced with a CONNECT BY query, in your case this would be:
select id1, id2, level as lvl
from t
connect by prior id1 = id2;
You can combine that with the orginal query:
with cte (id1, id2, lvl) as (
select id1, id2, level as lvl
from t
connect by prior id1 = id2
)
select id1,
id2,
dense_rank() over (order by grp) as label
from (
select id1,
id2,
least(min(id2) over (partition by id1), min(id1) over (partition by id2)) as grp,
lvl
from cte
) t
where lvl = 1;
Although I think it should be the same, it seems that the hierarchy is traversed in a different order. Could be because the recursive CTE does a breadth first and the connect by a depth first recursion (or the other way round).
SQLFiddle example for the second version: http://sqlfiddle.com/#!4/deeb2/4

Related

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

Oracle give two parameters on multiple rows to a subquery (or from a query to another)

I'm trying to give two parameters from a first query to a 2nd query :
SELECT t1.FAM,t2.CODE FROM (SELECT t1.FAM, t2.CODE FROM (SELECT num_family FROM family) t1, (SELECT num_code FROM code) t2)
So I have my two parameters (multiples lines):
FAM1 | CODE1
FAM1 | CODE2
FAM1 | CODE3
FAM2 | CODE1
FAM2 | CODE2
FAM2 | CODE3
So it's my two parameters and I want to put those two parameters to a 3rd request :
SELECT count(*) FROM stats WHERE num_fam LIKE '%FAM1%' AND num_code LIKE '%FAM1%';
To have :
FAM1 | CODE1 | 5
FAM1 | CODE2 | 2
FAM1 | CODE3 | 0
FAM2 | CODE1 | 9
FAM2 | CODE2 | 4
FAM2 | CODE3 | 1
But I don't understand how to combine those two queries and to give the two parameters from the first query to the second : I've try with subquery and inner join, but it was not a success... !
Following is what I thought of...please check if this works:
SELECT COUNT (*)
FROM stats
WHERE (num_fam, num_code) IN
(SELECT t1.FAM, t2.CODE
FROM (SELECT num_family FROM family) t1,
(SELECT num_code FROM code) t2)
AND NUM_FAM LIKE '%FAM1%' --I assumed this is a required filter .
AND NUM_CODE LIKE '%FAM1%';
You could use cross join and inner join
SELECT t1.num_family, t2.CODE, count(*)
FROM family t1
CROSS JOIN code t2
INNER JOIN stats t3 ON t1.num_family LIKE '%FAM1%' AND t2.num_code LIKE '%FAM1%'
GROUP BY t1.num_family, t2.CODE
It sounds like you're cross-joining the first two tables, e.g. with sample data to match what you showed as output:
-- CTE for implied sample data
with family (num_family) as (
select 'FAM1' from dual
union all select 'FAM2' from dual
),
code (num_code) as (
select 'CODE1' from dual
union all select 'CODE2' from dual
union all select 'CODE3' from dual
)
-- actual query (edited from what you showed to be valid)
--SELECT t1.FAM,t2.CODE FROM (SELECT t1.FAM, t2.CODE FROM (SELECT num_family FROM family) t1, (SELECT num_code FROM code) t2)
SELECT t1.FAM, t2.CODE FROM (SELECT num_family as fam FROM family) t1, (SELECT num_code as code FROM code) t2
/
FAM CODE
---- -----
FAM1 CODE1
FAM1 CODE2
FAM1 CODE3
FAM2 CODE1
FAM2 CODE2
FAM2 CODE3
which doesn't even need those subqueries, as it's the same as:
select f.num_family, c.num_code from family f cross join code c
and you then want to get the count of matching liste values in your stats table, where both the family and code values are embedded in that string, preceded by a hash:
I have to do CASE WHEN LISTE LIKE '%#'|| t1.FAM ||'%' THEN ( CASE WHEN LISTE LIKE '%#'|| t2.CODE ||'%' THEN 1 ELSE 0 END) ELSE 0 END instead of SELECT COUNT (*) FROM stats
You can do what with an outer join and boolean logic in the join condition, rather than trying to use a case expression:
-- CTE for implied sample data
with family (num_family) as (
select 'FAM1' from dual
union all select 'FAM2' from dual
),
code (num_code) as (
select 'CODE1' from dual
union all select 'CODE2' from dual
union all select 'CODE3' from dual
),
stats (liste) as (
select 'abc #FAM1 def #CODE1 ghi' from dual connect by level <= 5
union all select 'jkl #CODE2#FAM1 mno' from dual connect by level <= 2
union all select '#FAM2#CODE1' from dual connect by level <= 9
union all select '#FAM2#CODE2' from dual connect by level <= 4
union all select '#FAM2#CODE3' from dual
union all select '#FAM2#CODE4' from dual -- no match
union all select '#FAM3#CODE1' from dual -- no match
)
-- actual query
select f.num_family, c.num_code, count(s.liste) as matches
from family f
cross join code c
left join stats s on s.liste like '%#'|| f.num_family ||'%'
and s.liste like '%#'|| c.num_code ||'%'
group by f.num_family, c.num_code
order by f.num_family, c.num_code;
NUM_ NUM_C MATCHES
---- ----- ----------
FAM1 CODE1 5
FAM1 CODE2 2
FAM1 CODE3 0
FAM2 CODE1 9
FAM2 CODE2 4
FAM2 CODE3 1
The like pattern is going to be problematic if the family or code values have overlapping roots, or unexpected partial matches. E.g. if you had a liste values of #FAM10#CODE20 it would still match against FAM1 and CODE2. You could maybe switch to regexp_like() if you can define a pattern to avoid those false matches (e.g. followed by space/punctuation/EOL).

Group by two fields, and having count() on first field

I have a table that stored users play list, a video can be viewed by multiple users for multiple times.
A records goes like this:
videoid, userid, time
123, abc , 2013-09-11
It means user(abc) has watched video(123) on 2013-09-11
Now I want to find distinct users watched video list (no duplication), and only show the users that have watched more than two videos.
SELECT videoid, userid
FROM table_play_list
WHERE SOME CONDICTION
GROUP BY userid, videoid
The sql only select distinct users watchlist, I also want to filter users that have watched more than two different videos.
I know I have to google and read the documentation first, some said 'HAVING' could solve this, unfortunately, I could not make it.
If I understand correctly, you are looking for users who watched more than two different videos. You can do this by using count(distinct) with a partition by clause:
select userid, videoid
from (SELECT userid, videoid, count(distinct videoid) over (partition by userid) as cnt
FROM table_play_list
WHERE <ANY CONDITION>
) t
where cnt > 2;
Try like this,
SELECT userid, count(*)
FROM table_play_list
--WHERE SOME CONDITION
GROUP BY user_id
having count(*) >2;
Try this if you need to get the count based on userid and videoid(users who watch the same video more than two times).
SELECT userid, videoid, count(*)
FROM table_play_list
--WHERE SOME CONDITION
GROUP BY user_id, video_id
having count(*) >2;
This is probably best handled with analytics (window functions). Without analytics you will probably need a self-join.
SQL> WITH table_play_list AS (
2 SELECT 123 videoid, 'a' userid FROM dual UNION ALL
3 SELECT 125 videoid, 'a' userid FROM dual UNION ALL
4 SELECT 123 videoid, 'b' userid FROM dual UNION ALL
5 SELECT 123 videoid, 'b' userid FROM dual UNION ALL
6 SELECT 123 videoid, 'c' userid FROM dual
7 )
8 SELECT videoid, userid,
9 COUNT(*) over(PARTITION BY userid) nb_video
10 FROM table_play_list;
VIDEOID USERID NB_VIDEO
---------- ------ ----------
123 a 2
125 a 2
123 b 2
123 b 2
123 c 1
This lists all user/video and the total number of videos watched by each user. As you can see user b has watched the same video twice, I don't know if it's possible in your system.
You can filter with a subquery:
SQL> WITH table_play_list AS (
2 SELECT 123 videoid, 'a' userid FROM dual UNION ALL
3 SELECT 125 videoid, 'a' userid FROM dual UNION ALL
4 SELECT 123 videoid, 'b' userid FROM dual UNION ALL
5 SELECT 123 videoid, 'b' userid FROM dual UNION ALL
6 SELECT 123 videoid, 'c' userid FROM dual
7 )
8 SELECT *
9 FROM (SELECT videoid, userid,
10 COUNT(*) over(PARTITION BY userid) nb_video
11 FROM table_play_list)
12 WHERE nb_video > 1;
VIDEOID USERID NB_VIDEO
---------- ------ ----------
123 a 2
125 a 2
123 b 2
123 b 2
The below will give users who have watched more than two different videos.
SELECT userid, count(distinct video_id)
FROM table_play_list
WHERE SOME CONDICTION
GROUP BY user_id
having count(distinct video_id) >2;
If you use Oracle PL/SQL you can use like this:
SELECT column1, column2
FROM
(
SELECT column1, column2, COUNT(column1)
OVER (PARTITION BY column1) AS cnt
FROM test
GROUP BY column1, column2
ORDER BY column1
)
WHERE cnt > 2
If you use standard SQL you can use like this:
SELECT column1, column2
FROM test
WHERE column1 IN
(
SELECT column1
FROM
(
SELECT column1, column2
FROM test
GROUP BY column1, column2
ORDER BY column1
)
GROUP BY column1
HAVING COUNT(column1) > 2
)
GROUP BY column1, column2
ORDER BY column1

duplicating entries in listagg function

i have a table in which two fields are id, controlflag.It looks like
Id CntrlFlag
121 SSSSSRNNNSSRSSNNR
122 SSSNNRRSSNNRSSSSS
123 RRSSSNNSSSSSSSSSSSSSSS
I have to get output in the following form( the occurences of R)
Id Flag
121 6,12,17
122 6,7,12
123 1,2
I tried oracle query( as i obtained from this forum):
select mtr_id,listagg(str,',') within group (order by lvl) as flags from
( select mtr_id, instr(mtr_ctrl_flags,'R', 1, level) as str, level as lvl
from mer_trans_reject
connect by level <= regexp_count(mtr_ctrl_flags, 'R'))group by mtr_id;
it gives the result but 2nd and 3rd occurrences(not 1st one) are duplicated a no. of times.
it looks like
id Flag
123 6,12,12,12,12,17,17,17,17,17.
Can anybody know what's wrong here?
It could be avoided by select distinct keyword.Is there any other way?
Yes, there is, but this one is a little bit heavier(distinct will cost you less):
with t1(Id1, CntrlFlag) as(
select 121, 'SSSSSRNNNSSRSSNNR' from dual union all
select 122, 'SSSNNRRSSNNRSSSSS' from dual union all
select 123, 'RRSSSNNSSSSSSSSSSSSSSS' from dual
)
select w.id1
, listagg(w.r_pos, ',') within group(order by w.id1) as R_Positions
from (select q.id1
, regexp_instr(q.CntrlFlag,'R', 1, t.rn) as r_pos
from t1 q
cross join (select rownum rn
from(select max (regexp_count(CntrlFlag, 'R')) ml
from t1
)
connect by level <= ml
) t
) w
where w.r_pos > 0
group by w.id1
Result:
ID1 R_POSITIONS
---------- -----------
121 12,17,6
122 12,6,7
123 1,2

Sql Query Problem

i have problem when joining tables(left join)
table1:
id1 amt1
1 100
2 200
3 300
table2:
id2 amt2
1 150
2 250
2 350
my Query:
select id1,amt1,id2,amt2 from table1
left join table2 on table2.id1=table1.id2
my supposed o/p is:
id1 amt1 id2 amt2
row1: 1 100 1 150
row2: 2 200 2 250
row3: 2 200 2 350
i want o/p in row3 as
2 null 2 350
ie i want avoid repetetion of data(amt1)
friends help!
Using LEAD and LAG gives acces to previous or following rows in oracle.
SELECT id1, decode(amt1, lag(amt1) over (order by id1, id2), '', amt1) amt1,
id2, amt2
FROM table1 left join table2 on table2.id1=table1.id2
ORDER BY id1, id2
The order of the query and the order given to the lag function should be the same.
Explanation:
If the current am1 is the same as the preceding amt1 (preceding in the given order) then omit the value.
EDIT
According to your comment, add an additional check for id changes.
SELECT id1,
decode(id1, lag(id1) over (order by id1, id2),
decode(amt1, lag(amt1) over (order by id1, id2), '', amt1),
amt1) amt1,
id2, amt2
FROM table1 left join table2 on table2.id1=table1.id2
ORDER BY id1, id2
Use the same LAG feature to check for id changes. The expression is a bit more complex, but its comparable with a nested if statement.
select distinct id1,amt1,id2,amt2 from table1 left join table2 on table2.id1=table1.id2
try this ?

Resources