Oracle SQL to Mimic Nested Window Function - sql

My actual problem involves a larger row source and more involved math, but this is a small example that still exhibits the challenge faced. Using Oracle 19c.
Suppose we have a table X with four rows of data as follows.
x
-
1
2
3
4
Further, suppose we want to derive two columns, a and b, from X such that
a = x + sum(previous rows of b ordered by x)
b = a - 1.
Where if there are no previous rows, then the sum is 0.
Therefore, the new table would have rows like the following.
x a b
- - -
1 1 0
2 2 1
3 4 3
4 8 7
The following is invalid SQL, but provides an example of what is being attempted.
with
X AS
(
select 1 x from dual
union all select 2 from dual
union all select 3 from dual
union all select 4 from dual
)
, A AS
(
select
x
, x + sum(b) over (order by x range between unbounded preceding and 1 preceding) AS a
, a - 1 AS b
from x
)
select * from A
;
Perhaps a hierarchical query may help, but not sure on what it is to connect by.
Any ideas would be appreciated. Thanks in advance.

You can do this using a recursive CTE:
with X AS (
select 1 x from dual
union all select 2 from dual
union all select 3 from dual
union all select 4 from dual
),
cte(x, a, b, b_sum) as (
select x, x as a, x - 1 as b, x - 1 as b_sum
from x
where x = 1
union all
select x.x, x.x + cte.b_sum, x.x + cte.b_sum - 1, cte.b_sum + (x.x + cte.b_sum - 1)
from cte join
x
on x.x = cte.x + 1
)
select *
from cte;
Here is a db<>fiddle.

Related

Complex SQL query - Count always return 0

I want to count how much users created account each month, I fount here a query - author says it worked him, but i have problem - as i said - 3rd argument is always 0.
I was trying to fix it in many ways (delete, add args) but result was the same or SQL error.
SELECT y, m, Count(ytskagamerpl_userinfo.creationdate)
FROM (
SELECT y, m
FROM
(SELECT YEAR(CURDATE()) y UNION ALL SELECT YEAR(CURDATE())-1) years,
(SELECT 1 m UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4
UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8
UNION ALL SELECT 9 UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12) months
) ym
LEFT JOIN ytskagamerpl_userinfo
ON ym.y = YEAR(FROM_UNIXTIME(ytskagamerpl_userinfo.creationdate))
AND ym.m = MONTH(FROM_UNIXTIME(ytskagamerpl_userinfo.creationdate))
WHERE (y=YEAR(CURDATE()) AND m<=MONTH(CURDATE()))
OR
(y<YEAR(CURDATE()) AND m>MONTH(CURDATE()))
GROUP BY y, m;
DATABASE:
uid | creationdate (TIMESTAMP):
1 | 20.10.2018
2 | 12.11.2018
... | ..........
X | 15.07.2019
PHP VAR DUMP _ https://pastebin.com/ABT5PCZn
I expect count of users created on X month 201X year, but it always returns 0,
years & months are displayed correctly.

Gradually aggregating a string column in Oracle SQL

I would like to gradually aggregate a string column in Oracle sql.
From this table:
col_1 | col_2
-------------
1 | A
1 | B
1 | C
2 | C
2 | D
to:
col_1 | col_2
-------------
1 | A
1 | A,B
1 | A,B,C
2 | C
2 | C,D
I tried LISTAGG but it won't return all rows due to group by. I have about 2 million rows in the table.
Oracle doesn't support accumulating string concatenation with a single listagg() expression. However, you can use a subquery.
Just one note: SQL tables represent unordered sets. You seem to have an ordering in mind. The following code adds an ordering column:
with t as (
select 1 as id, 1 as x, 'A' as y from dual union all
select 2, 1 as x, 'B' as y from dual union all
select 3, 1 as x, 'C' as y from dual union all
select 4, 2 as x, 'C' as y from dual union all
select 5, 2 as x, 'D' as y from dual
)
select t.*,
(select listagg(t2.y, ',') within group (order by t2.id)
from t t2
where t2.x = t.x and t2.id <= t.id
)
from t;
A hierarchical query option looks like this:
SQL> with t as (
2 select 1 as id, 1 as x, 'A' as y from dual union all
3 select 2, 1 as x, 'B' as y from dual union all
4 select 3, 1 as x, 'C' as y from dual union all
5 select 4, 2 as x, 'C' as y from dual union all
6 select 5, 2 as x, 'D' as y from dual
7 )
8 select x,
9 ltrim(sys_connect_by_path(y, ','), ',') result
10 from (select x,
11 y,
12 row_number() over (partition by x order by y) rn
13 from t
14 )
15 start with rn = 1
16 connect by prior rn = rn - 1 and prior x = x;
X RESULT
---------- --------------------
1 A
1 A,B
1 A,B,C
2 C
2 C,D
SQL>

Oracle - Split records based on overlapping ranges

Data sample:
id lowerlimt upperlimit
1 5 10 ---Master Record
2 8 12
3 3 8
4 8 9
5 11 15
In the above table, Let us assume record with id=1 as Master Record. I want Compare the other records with the first record in the same table, and Split each record if it overlaps the range and assign a flag. If it overlaps assign 'Y', else 'N'.
If it overlaps partially split the record in to two, one for overlapping range and other for the non-overlapping range.
id lowerlimt upperlimit flag
2 8 10 y
2 10 12 n
3 3 5 n
3 5 8 y
4 8 9 y
5 11 15 n
I added more "test data" for testing and illustration. The main computation is to break down the input ranges into up to three pieces (some of those don't make sense and are eliminated in the final stage - the maximum of three is reached when the input range is strictly overlapping the Master Record in both directions).
For efficiency, it is best if each input row is accessed just once. So, instead of using union all (the easiest route), I prefer to create all three subranges and corresponding flags simultaneously, with the result having nine columns instead of three for the subranges and flags. Then I use unpivot to put them into separate rows.
with
test_data ( id, lowerlimit, upperlimit ) as (
select 1, 5, 10 from dual union all ---Master Record
select 2, 8, 12 from dual union all
select 3, 3, 8 from dual union all
select 4, 8, 9 from dual union all
select 5, 11, 15 from dual union all
select 6, 2, 5 from dual union all
select 7, 1, 14 from dual
)
-- end of test data (not part of the solution)
-- SQL query begins BELOW THIS LINE (use your actual table name)
select id, lowerlimit, upperlimit, flag
from (
select id,
t.lowerlimit as x1, least(t.upperlimit, m.ll) as y1, 'n' as f1,
greatest(t.lowerlimit, m.ll) as x2, least(t.upperlimit, m.ul) as y2, 'y' as f2,
greatest(t.lowerlimit, m.ul) as x3, t.upperlimit as y3, 'n' as f3
from test_data t cross join
( select lowerlimit ll, upperlimit ul
from test_data
where id = 1
) m
where t.id != 1
)
unpivot ( ( lowerlimit, upperlimit, flag )
for ( x, y, f ) in ( ( x1, y1, f1), (x2, y2, f2), (x3, y3, f3) ) )
where lowerlimit < upperlimit
order by id, lowerlimit -- if needed
;
Output:
ID LOWERLIMIT UPPERLIMIT FLAG
-- ---------- ---------- ----
2 8 10 y
2 10 12 n
3 3 5 n
3 5 8 y
4 8 9 y
5 11 15 n
6 2 5 n
7 1 5 n
7 5 10 y
7 10 14 n
10 rows selected.
One method is to split this into three overlapping conditions, essentially "before", "during", and "after". Because you want multiple different rows for each existing row, you can do this using union all:
select t.id, t.lowerlimit,
least(tm.lowerlimit, t.upperlimit) as upperlimit,
'n' as overlaps
from t join
t tm
on t.id <> 1 and tm.id = 1 and
t.lowerlimit < tm.lowerlimit
union all
select t.id,
greatest(t.lowerlimit, tm.lowerlimit),
least(t.upperlimit, tm.upperlimit), 'y' as overlaps
from t join
t tm
on t.id <> 1 and tm.id = 1 and
t.lowerlimit <= tm.lowerlimit and
t.upperlimit >= tm.upperlimit
union all
select t.id, greatest(tm.upperlimit, t.upperlimit),
t.upperlimit, 'n'
from t join
t tm
on t.id <> 1 and tm.id = 1 and
t.upperlimit > tm.upperlimit;

Sql query to print values starting from column A till column B

New to SQL so looking for help
I'm trying to write a query which would print values starting from column A till the column B excluding the value present in column 'ANS' of second table.
Like here are the two tables X and Y
Table1
A FROM TO
a 6 9
b 3 6
c 0 3
d 2 3
Table2
A ANS
a 7
b 5
c 1
And I want the output as
A ANS
a 6
a 8
a 9
b 3
b 4
b 6
c 0
c 2
c 3
d 2
d 3
I've tried to write something like this but it doesn't work
WITH y(n) AS
(SELECT 1 AS n
FROM dual
UNION ALL
SELECT n + 1 AS n
FROM y, table1 T
WHERE n <= T.TO AND n>= T.FROM )
SELECT * FROM y;
Which prints 5000+ rows (that's why I am not attaching output)
Thanks in advance
After you get all the numbers between from and to with a recursive cte, left join on the generated table and get only those numbers which don't exist in table2 using not exists.
--Get the maximum value of `to` column and generate all numbers between 0 and that value
WITH maxto(maxt) as (SELECT MAX(TO) FROM TABLE1)
,y(n) AS
(SELECT 0 AS n FROM dual
UNION ALL
SELECT n + 1 AS n FROM y WHERE n < (SELECT maxt FROM maxto))
SELECT * FROM
(SELECT t1.a, y.n
FROM y
LEFT JOIN table1 t1 on y.n between t1.from and t1.to
WHERE t1.a IS NOT NULL) x
WHERE NOT EXISTS (SELECT 1 FROM table2 WHERE x.a = a and x.n = ans)
ORDER BY 1,2
Sample demo
WITH y(n) AS
(SELECT level - 1 FROM dual connect by level <= select max(TO- FROM) +2 from table1)
SELECT t1.a, t1.from + y.n FROM table1 t1
JOIN y on 1 = 1
left JOIN table2 on y.n + t1.FROM = t2.ANS and t2.a = t1.a
where y.n < t1.TO-t1.FROM
and t2.ANS is null;
You can use a "hierarchical query" and a MINUS operation and avoid joins altogether. MINUS is easy to understand if you are somewhat familiar with set theory. Generating numbers using hierarchical queries is somewhat unnatural (and may only be available in Oracle, I don't know any other db products), but it is used very often and it works very fast.
I changed the column names to from_n and to_n; I don't remember if "from" and/or "to" are reserved words in Oracle, but why take the risk.
with
table1 ( a, from_n, to_n ) as (
select 'a', 6, 9 from dual union all
select 'b', 3, 6 from dual union all
select 'c', 0, 3 from dual union all
select 'd', 2, 3 from dual
),
table2 ( a, ans ) as (
select 'a', 7 from dual union all
select 'b', 5 from dual union all
select 'c', 1 from dual
)
-- everything above this point is for testing only and can be removed
-- solution (SQL query) begins below
select a, from_n + level - 1 as ans
from table1
connect by level <= 1 + to_n - from_n
and prior a = a
and prior sys_guid() is not null
minus
select a, ans
from table2
;
Output:
A ANS
- ----------
a 6
a 8
a 9
b 3
b 4
b 6
c 0
c 2
c 3
d 2
d 3
11 rows selected

Repeat rows dynamically in Oracle based on a condition

Heres what i have
Query
with temp as (
select 11 as x from dual
union
select 222 as x from dual
)
select x from temp t,(SELECT 1
FROM DUAL
CONNECT BY ROWNUM <= 3)
Output
X
--
11
222
11
222
11
222
Output i desire
X
--
11
11
222
222
222
basically i would like the row to repeat itself based on the length of the column 'X' value.
so if value is 'abc'
then that row should repeat 3 times
This works:
with temp as (
select '11' as x from dual
union
select '222' as x from dual
)
SELECT x, LEVEL from temp t
CONNECT BY LEVEL <= LENGTH(t.x)
and prior x = x
and prior sys_guid() is not null;
Last line does the trick. Same can be achieved with other reference to PRIOR:
with temp as (
select '11' as x from dual
union
select '222' as x from dual
)
SELECT x, LEVEL from temp t
CONNECT BY LEVEL <= LENGTH(t.x)
and PRIOR x = x
and PRIOR DBMS_RANDOM.VALUE () IS NOT NULL;
Since you're on 11g you can use recursive subquery factoring to achieve this:
with t as (
select 11 as x from dual
union all
select 222 as x from dual
),
r (x, idx, len) as (
select x, 1, length(x)
from t
union all
select r.x, r.idx + 1, r.len
from r
where r.idx < r.len
)
select x from r
order by x;
X
-----
11
11
222
222
222
The anchor member gets the original rows and the length of the value.The recursive member adds one to idx until it reaches the length.
SQL Fiddle.
You can do it with a hierarchical query too:
with t as (
select 11 as x from dual
union all
select 222 as x from dual
)
select x
from t
connect by level <= length(x)
and prior x = x
and prior sys_guid() is not null;
The combination of the two prior clauses - one restricitng duplicates, the other involing a non-deterministic function to prevent cycling when you do that - gives you the desired rows:
X
-----
11
11
222
222
222
SQL Fiddle.
with temp as (
select 11 as x from dual
union
select 222 as x from dual
)
select x from temp t,(
SELECT 1
FROM DUAL
CONNECT BY ROWNUM <= 3)
order by 1