Divide by couples of elements in a table - sql

I have this table:
ELEMENTO VALOR
-------------------------
ELEMENT1_SUFFIX1 2
ELEMENT1_SUFFIX2 4
ELEMENT2_SUFFIX1 5
ELEMENT2_SUFFIX2 15
I would like to generate new entries by dividing each couple of elements and suffixes. For each partition of element, I would like to divide suffix2/suffix1 and generate an entry with element_result. Like the below.
ELEMENTO VALOR
-------------------------
ELEMENT1_SUFFIX1 2
ELEMENT1_SUFFIX2 4
ELEMENT2_SUFFIX1 5
ELEMENT2_SUFFIX2 15
ELEMENT1_RESULT 2
ELEMENT1_RESULT 3
The table can be generated with the below code.
with aux (elemento, valor) as
( select 'ELEMENT1_SUFFIX1', 2 from dual
UNION ALL
select 'ELEMENT1_SUFFIX2', 4 from dual
UNION ALL
select 'ELEMENT2_SUFFIX1', 5 from dual
UNION ALL
select 'ELEMENT2_SUFFIX2', 10 from dual
)
select aux.* from aux;
This is what I've tried so far, but I am pretty sure there must be a better solution.
with aux (elemento, valor) as
( select 'ELEMENT1_SUFFIX1', 2 from dual
UNION ALL
select 'ELEMENT1_SUFFIX2', 4 from dual
UNION ALL
select 'ELEMENT2_SUFFIX1', 5 from dual
UNION ALL
select 'ELEMENT2_SUFFIX2', 15 from dual
),
aux2 as
(select aux.*,
valor/max(
case
when regexp_substr(elemento, '[^_]+', 1, 2) = 'SUFFIX1'
then valor
end) over (partition by REGEXP_SUBSTR(elemento,'[^_]+',1,1)) new_value
from aux
)
select * from aux
union all
select REGEXP_SUBSTR(elemento,'[^_]+',1,1)
|| '_RESULT',
new_value valor
from aux2
where regexp_substr(elemento, '[^_]+', 1, 2) = 'SUFFIX2';

Yet another variation.
Sample data:
SQL> WITH
2 aux (elemento, valor)
3 AS
4 (SELECT 'ELEMENT1_SUFFIX1', 2 FROM DUAL
5 UNION ALL
6 SELECT 'ELEMENT1_SUFFIX2', 4 FROM DUAL
7 UNION ALL
8 SELECT 'ELEMENT2_SUFFIX1', 5 FROM DUAL
9 UNION ALL
10 SELECT 'ELEMENT2_SUFFIX2', 15 FROM DUAL
11 )
Query:
12 -- existing rows
13 select elemento, valor
14 from aux
15 union all
16 -- calculated rows
17 select
18 substr(elemento, 1, instr(elemento, '_') - 1) ||'_RESULT' as elemento,
19 max(case when substr(elemento, -1) = 2 then valor end) /
20 max(case when substr(elemento, -1) = 1 then valor end) as valor
21 from aux
22 group by substr(elemento, 1, instr(elemento, '_') - 1);
ELEMENTO VALOR
----------------------- ----------
ELEMENT1_SUFFIX1 2
ELEMENT1_SUFFIX2 4
ELEMENT2_SUFFIX1 5
ELEMENT2_SUFFIX2 15
ELEMENT1_RESULT 2
ELEMENT2_RESULT 3
6 rows selected.
SQL>

We can use an aggregation/pivot approach along with a union here:
WITH cte AS (
SELECT SUBSTR(ELEMENTO, 1, INSTR(ELEMENTO, '_') - 1) || '_RESULT' AS ELEMENTO,
MAX(CASE WHEN SUBSTR(ELEMENTO, INSTR(ELEMENTO, '_') + 1) = 'SUFFIX2'
THEN VALOR END) /
MAX(CASE WHEN SUBSTR(ELEMENTO, INSTR(ELEMENTO, '_') + 1) = 'SUFFIX1'
THEN VALOR END) AS VALOR
FROM yourTable
GROUP BY SUBSTR(ELEMENTO, 1, INSTR(ELEMENTO, '_') - 1)
)
SELECT ELEMENTO, VALOR
FROM
(
SELECT ELEMENTO, VALOR, 1 AS pos FROM yourTable
UNION ALL
SELECT ELEMENTO, VALOR, 2 FROM cte
) t
ORDER BY pos, ELEMENTO;

Try this:
with aux (elemento, valor) as
( select 'ELEMENT1_SUFFIX1', 2 from dual
UNION ALL
select 'ELEMENT1_SUFFIX2', 4 from dual
UNION ALL
select 'ELEMENT2_SUFFIX1', 5 from dual
UNION ALL
select 'ELEMENT2_SUFFIX2', 15 from dual
), rawdata (elemento, valor, group_id, row_id) AS
(
select aux.*
,DENSE_RANK() OVER (ORDER BY SUBSTR(elemento, 1, INSTR(elemento, '_')-1))
,ROW_NUMBER() OVER (PARTITION BY SUBSTR(elemento, 1, INSTR(elemento, '_')-1) ORDER BY SUBSTR(elemento, INSTR(elemento, '_')+1))
from aux
)
SELECT *
FROM aux
UNION ALL
SELECT CONCAT(CONCAT('ELEMENT', A.group_id),'_RESULT')
,B.valor / a.valor
FROM rawdata A
INNER JOIN rawdata B
ON A.group_id = b.group_id
AND A.row_id = b.row_id -1

Related

Get parent id from level with Oracle SQL

I have a hierarchical structure defined by level and order of elements. Is it possible to create "parent_id" column with Oracle SQL without using procedures?
I need to generate red values:
test data:
with t as
(
select 1 id, 'element1' name, 1 level_ from dual union all
select 2 id, 'element2' name, 2 level_ from dual union all
select 3 id, 'element3' name, 3 level_ from dual union all
select 4 id, 'element4' name, 3 level_ from dual union all
select 5 id, 'element5' name, 3 level_ from dual union all
select 6 id, 'element6' name, 3 level_ from dual union all
select 7 id, 'element7' name, 2 level_ from dual union all
select 8 id, 'element8' name, 3 level_ from dual union all
select 9 id, 'element9' name, 4 level_ from dual union all
select 10 id, 'element10' name, 4 level_ from dual union all
select 11 id, 'element11' name, 1 level_ from dual union all
select 12 id, 'element12' name, 2 level_ from dual union all
select 13 id, 'element13' name, 3 level_ from dual union all
select 14 id, 'element14' name, 4 level_ from dual union all
select 15 id, 'element15' name, 4 level_ from dual union all
select 16 id, 'element16' name, 3 level_ from dual union all
select 17 id, 'element17' name, 4 level_ from dual union all
select 18 id, 'element18' name, 4 level_ from dual union all
select 19 id, 'element19' name, 1 level_ from dual
)
select * from t
From Oracle 12, you can use MATCH_RECOGNIZE:
select *
from t
MATCH_RECOGNIZE (
ORDER BY id DESC
MEASURES
child.id AS id,
child.name AS name,
child.lvl AS lvl,
parent.id AS parent_id
ONE ROW PER MATCH
AFTER MATCH SKIP TO NEXT ROW
PATTERN (child ancestors*? (parent | $))
DEFINE
parent AS lvl = child.lvl - 1
)
ORDER BY id
Or, again from Oracle 12, a LATERAL join:
select *
from t c
LEFT OUTER JOIN LATERAL(
SELECT p.id AS parent_id
FROM t p
WHERE c.id > p.id
AND c.lvl = p.lvl + 1
ORDER BY id DESC
FETCH FIRST ROW ONLY
)
ON (1 = 1)
ORDER BY id
Or, in earlier versions:
SELECT id, name, lvl, parent_id
FROM (
SELECT c.*,
p.id AS parent_id,
ROW_NUMBER() OVER (PARTITION BY c.id ORDER BY p.id DESC) AS rn
FROM t c
LEFT OUTER JOIN t p
ON (c.id > p.id AND c.lvl = p.lvl + 1)
)
WHERE rn = 1
ORDER BY id
Which, for the sample data:
CREATE TABLE t (id, name, lvl ) as
select 1, 'element1', 1 from dual union all
select 2, 'element2', 2 from dual union all
select 3, 'element3', 3 from dual union all
select 4, 'element4', 3 from dual union all
select 5, 'element5', 3 from dual union all
select 6, 'element6', 3 from dual union all
select 7, 'element7', 2 from dual union all
select 8, 'element8', 3 from dual union all
select 9, 'element9', 4 from dual union all
select 10, 'element10', 4 from dual union all
select 11, 'element11', 1 from dual union all
select 12, 'element12', 2 from dual union all
select 13, 'element13', 3 from dual union all
select 14, 'element14', 4 from dual union all
select 15, 'element15', 4 from dual union all
select 16, 'element16', 3 from dual union all
select 17, 'element17', 4 from dual union all
select 18, 'element18', 4 from dual union all
select 19, 'element19', 1 from dual;
All output:
ID
NAME
LVL
PARENT_ID
1
element1
1
null
2
element2
2
1
3
element3
3
2
4
element4
3
2
5
element5
3
2
6
element6
3
2
7
element7
2
1
8
element8
3
7
9
element9
4
8
10
element10
4
8
11
element11
1
null
12
element12
2
11
13
element13
3
12
14
element14
4
13
15
element15
4
13
16
element16
3
12
17
element17
4
16
18
element18
4
16
19
element19
1
null
db<>fiddle here

ORACLE SQL | If a column contains a value, then it will exclude a different value from the same column

I have this query that returns the data below it
select LISTAGG(d.DOCUMENT_TYPE_CD, ',') WITHIN GROUP (ORDER BY D.DOCUMENT_TYPE_CD) as value
from test_table d;
VALUE
---------
CI,ECI,POA
now I'm trying to add a condition whenever 'ECI' value is present, it should exclude 'CI' in the result like this one below
VALUE
---------
ECI,POA
I tried using case statement in where condition it prompted an error
select LISTAGG(d.DOCUMENT_TYPE_CD, ',')
WITHIN GROUP (ORDER BY D.DOCUMENT_TYPE_CD) as value
from test_table d
where CASE d.DOCUMENT_TYPE_CD
WHEN 'ECI' THEN d.DOCUMENT_TYPE_CD <> 'CI'
END;
ORA-00905: missing keyword
00905. 00000 - "missing keyword"
*Cause:
*Action:
Error at Line: 7 Column: 36
is there any other way I could resolve this?
See if this helps; read comments within code.
SQL> with
2 test (id, document_type_cd) as
3 -- sample data
4 (select 1, 'ECI' from dual union all
5 select 1, 'CI' from dual union all
6 select 1, 'POA' from dual union all
7 --
8 select 2, 'CI' from dual union all
9 select 2, 'POA' from dual union all
10 --
11 select 3, 'XYZ' from dual union all
12 select 3, 'ABC' from dual
13 ),
14 temp as
15 -- see whether CI and ECI exist per each ID
16 (select id,
17 sum(case when document_type_cd = 'CI' then 1 else 0 end) sum_ci,
18 sum(case when document_type_cd = 'ECI' then 1 else 0 end) sum_eci
19 from test
20 group by id
21 ),
22 excl as
23 -- exclude CI rows if ECI exist for that ID
24 (select a.id,
25 a.document_type_cd
26 from test a join temp b on a.id = b.id
27 where a.document_type_cd <> case when b.sum_ci > 0 and b.sum_eci > 0 then 'CI'
28 else '-1'
29 end
30 )
31 -- finally:
32 select e.id,
33 listagg(e.document_type_cd, ',') within group (order by e.document_type_cd) result
34 from excl e
35 group by e.id;
ID RESULT
---------- --------------------
1 ECI,POA
2 CI,POA
3 ABC,XYZ
SQL>
Something like this:
select LISTAGG(d.DOCUMENT_TYPE_CD, ',')
WITHIN GROUP (ORDER BY D.DOCUMENT_TYPE_CD) as value
from test_table d,
(select sum (case when DOCUMENT_TYPE_CD = 'CI' then 1 else 0 end) C
from test_table) A
where d.DOCUMENT_TYPE_CD <> case when A.c > 0 then 'CI' when A.c = 0 then ' ' end;
DEMO
You may identify the presence of both the values with two conditional aggregations in the same group by and then replace CI inside the result of listagg in one pass.
with a(id, cd) as (
select 1, 'ABC' from dual union all
select 1, 'ECI' from dual union all
select 1, 'CI' from dual union all
select 1, 'POA' from dual union all
select 2, 'XYZ' from dual union all
select 2, 'ECI' from dual union all
select 2, 'CI' from dual union all
select 2, 'POA' from dual union all
select 3, 'CI' from dual union all
select 3, 'POA' from dual union all
select 4, 'ABC' from dual union all
select 4, 'DEF' from dual
)
select
id,
ltrim(
/*Added comma in case CI will be at the beginning*/
replace(
',' || listagg(cd, ',') within group (order by cd asc),
decode(
/*If both are present, then replace CI. If not, then do not replace anything*/
max(decode(cd, 'CI', 1))*max(decode(cd, 'ECI', 1)),
1,
',CI,'
),
','
),
','
) as res
from a
group by id
ID | RES
-: | :----------
1 | ABC,ECI,POA
2 | ECI,POA,XYZ
3 | CI,POA
4 | ABC,DEF
db<>fiddle here
Instead of using GROUP BY, you can also use windowing (aka analytic) functions to check the presence of ECI per group (test data shamelessly stolen from #littlefoot):
with
test (id, document_type_cd) as
-- sample data
(select 1, 'ECI' from dual union all
select 1, 'CI' from dual union all
select 1, 'POA' from dual union all
--
select 2, 'CI' from dual union all
select 2, 'POA' from dual union all
--
select 3, 'XYZ' from dual union all
select 3, 'ABC' from dual
),
temp as
(select id,
document_type_cd,
sum(case when document_type_cd = 'ECI' then 1 else 0 end) over (partition by id) as sum_eci
from test
)
select a.id,
listagg(a.document_type_cd, ',') within group (order by a.document_type_cd) result
from temp a
where a.document_type_cd != 'CI' or sum_eci = 0
group by a.id;

Find rows with consecutive ones

I've two integer columns and need to display the rows with consecutive one's in the NUM column.
Sample data:
CREATE TABLE table_name ( ID, NUM ) AS
SELECT 1, 1 FROM DUAL UNION ALL
SELECT 2, 1 FROM DUAL UNION ALL
SELECT 3, 1 FROM DUAL UNION ALL
SELECT 4, 2 FROM DUAL UNION ALL
SELECT 5, 1 FROM DUAL UNION ALL
SELECT 6, 2 FROM DUAL UNION ALL
SELECT 7, 2 FROM DUAL;
Expected Output:
ID NUM
-- ---
1 1
2 1
3 1
I have tried using self-joins and achieved the result:
WITH TAB (ID, NUM) AS
(
SELECT 1, 1 FROM DUAL UNION ALL
SELECT 2, 1 FROM DUAL UNION ALL
SELECT 3, 1 FROM DUAL UNION ALL
SELECT 4, 2 FROM DUAL UNION ALL
SELECT 5, 1 FROM DUAL UNION ALL
SELECT 6, 2 FROM DUAL UNION ALL
SELECT 7, 2 FROM DUAL
)
SELECT DISTINCT
T.ID,
T.NUM
FROM
TAB T
JOIN (
SELECT
T1.ID ID1,
T2.ID ID2,
T1.NUM,
COUNT(1) OVER(
PARTITION BY T1.NUM
) RN
FROM
TAB T1
JOIN TAB T2 ON ( T1.NUM = T2.NUM
AND T1.ID = T2.ID + 1 )
) T_IN ON ( ( T.ID = T_IN.ID1
OR T.ID = T_IN.ID2 )
AND T.NUM = T_IN.NUM
AND RN >= 2 ) -- THIS CONDITION IS TO RESTRICT CONSECUTIVES LESS THAN 3
ORDER BY
1
output:
db<>fiddle demo
Use analytic functions LAG or LEAD:
Oracle Setup:
CREATE TABLE table_name ( ID, NUM ) AS
SELECT 1, 1 FROM DUAL UNION ALL
SELECT 2, 1 FROM DUAL UNION ALL
SELECT 3, 1 FROM DUAL UNION ALL
SELECT 4, 2 FROM DUAL UNION ALL
SELECT 5, 1 FROM DUAL UNION ALL
SELECT 6, 2 FROM DUAL UNION ALL
SELECT 7, 2 FROM DUAL;
Query:
SELECT id,num
FROM (
SELECT id,
num,
LAG( num ) OVER ( ORDER BY id ) AS prev_num,
LEAD( num ) OVER ( ORDER BY id ) AS next_num
FROM table_name
)
WHERE num = 1
AND ( num = prev_num
OR num = next_num )
Output:
ID | NUM
-: | --:
1 | 1
2 | 1
3 | 1
db<>fiddle here

SQL query to find the length of the serie of values

I need a query that evaluates the longest uninterrupted series of subsequent "1"'s in the column FL_SUCC_EXEC. For the following data in table TEST(row_no number, fl_succ_exec number(1)), the result of the query should be "6".
Rows are ordered by row_no.
ROW_NO FL_SUCC_EXEC
---------- ------------
1 1
2 1
3 1
4 0
5 1
6 1
7 1
8 1
9 1
10 1
11 0
12 1
13 1
14 1
15 1
I can do this in PL/SQL :
declare
temp_cnt pls_integer default 0;
total_cnt pls_integer default 0;
begin
for rec in (select row_no, fl_succ_exec from test order by row_no)
loop
if temp_cnt > total_cnt
then
total_cnt:=temp_cnt;
end if;
if rec.fl_succ_exec!=0
then
temp_cnt:=temp_cnt+rec.fl_succ_exec;
else
temp_cnt:=0;
end if;
end loop;
dbms_output.put_line(total_cnt);
end;
But I'm still hoping for SQL solution. Is there any?
Try:
SELECT max( count(*) ) As longest_uninterrupted_series
FROM (
select fl_succ_exec,
sum( case when fl_succ_exec = 1 then 0 else 1 end )
over ( order by row_no ) xx
from test
)
WHERE fl_succ_exec = 1
GROUP BY xx;
Oracle Setup:
CREATE TABLE test ( row_no, fl_succ_exec ) AS
SELECT 1, 1 FROM DUAL UNION ALL
SELECT 2, 1 FROM DUAL UNION ALL
SELECT 3, 1 FROM DUAL UNION ALL
SELECT 4, 0 FROM DUAL UNION ALL
SELECT 5, 1 FROM DUAL UNION ALL
SELECT 6, 1 FROM DUAL UNION ALL
SELECT 7, 1 FROM DUAL UNION ALL
SELECT 8, 1 FROM DUAL UNION ALL
SELECT 9, 1 FROM DUAL UNION ALL
SELECT 10, 1 FROM DUAL UNION ALL
SELECT 11, 0 FROM DUAL UNION ALL
SELECT 12, 1 FROM DUAL UNION ALL
SELECT 13, 1 FROM DUAL UNION ALL
SELECT 14, 1 FROM DUAL UNION ALL
SELECT 15, 1 FROM DUAL;
Query:
SELECT MAX( num_1s ) AS num_1s
FROM (
SELECT COALESCE(
row_no - LAST_VALUE( CASE fl_succ_exec WHEN 0 THEN row_no END )
IGNORE NULLS OVER ( ORDER BY row_no ),
ROWNUM
) AS num_1s
FROM test
);
Output:
NUM_1S
------
6

Oracle 11g split text column to rows

I have table:
ID |Values
-----+--------------------------------
1 |AB,AD
2 |AG, ... ,BD
3 |AV
How can i transform it to:
ID |Value
-----+------
1 |AB
1 |AD
2 |AG
... |...
2 |BD
3 |AV
Using the built-in XML functions, you can do it like that:
with sample_data as
(
select 1 id, 'AB,AD' vals from dual union all
select 2, 'AG,AK,AJ,BA,BD' from dual union all
select 3, 'AV' from dual
)
select id, cast(t.column_value.extract('//text()') as varchar2(10)) val
from sample_data,
table( xmlsequence( xmltype(
'<x><x>' || replace(vals, ',', '</x><x>') || '</x></x>'
).extract('//x/*'))) t;
Result:
ID VAL
--- -----
1 AB
1 AD
2 AG
2 AK
2 AJ
2 BA
2 BD
3 AV
Using recursive common table expression, the same query looks like this:
with sample_data as
(
select 1 id, 'AB,AD' vals from dual union all
select 2, 'AG,AK,AJ,BA,BD' from dual union all
select 3, 'AV' from dual
),
split_first(id, val, rem) as
(
select id,
coalesce(substr(vals, 1, instr(vals, ',') - 1), vals) val,
case when instr(vals, ',') > 0 then substr(vals, instr(vals, ',') + 1) end rem
from sample_data
union all
select id,
coalesce(substr(rem, 1, instr(rem, ',') - 1), rem) val,
case when instr(rem, ',') > 0 then substr(rem, instr(rem, ',') + 1) end rem
from split_first
where rem is not null
)
select id, val from split_first
order by id;
Or a slightly different approach:
with sample_data as
(
select 1 id, 'AB,AD' vals from dual union all
select 2, 'AG,AK,AJ,BA,BD' from dual union all
select 3, 'AV' from dual
),
pos(id, seq, vals, sta, stp) as
(
select id, 1, vals, 1, instr(vals, ',') from sample_data
union all
select id, seq + 1, vals, stp + 1, instr(vals, ',', stp + 1) from pos
where stp > 0
)
select id, substr(vals, sta, case when stp > 0 then stp - sta else length(vals) end) from pos
order by id, seq;