SQL Dynamic Self Cross Join using TSql - sql

I have a table that has data like this
Group Value
1 A
1 B
1 C
2 F
2 G
3 J
3 K
I want to join all members of one group to all members of each of the other groups into a single column like this:
AFJ
AFK
AGJ
AGK
BFJ
BFK
BGJ
BGK
CFJ
CFK
CGJ
CGK
There can be n number of groups and n number of values
Thank you

SQL does not offer many options for such a query. The one standard method is a recursive CTE. Other methods would involve recursive functions or stored procedures.
The following is an example of a recursive CTE that solves this problem:
with groups as (
select v.*
from (values (1, 'a'), (1, 'b'), (1, 'c'), (2, 'f'), (2, 'g'), (3, 'h'), (3, 'k')
) v(g, val)
),
cte as (
select 1 as groupid, val, 1 as lev
from groups
where g = 1
union all
select cte.groupid + 1, cte.val + g.val, lev + 1
from cte join
groups g
on g.g = cte.groupid + 1
)
select val
from (select cte.*, max(lev) over () as max_lev
from cte
) cte
where lev = max_lev
order by 1;
Some databases that support recursive CTEs don't use the recursive keyword.
Here is a db<>fiddle.

If you need to consider gaps between the groups, i.e. Group 1, then no 2, then 3.
with t(Grp, val) as (
select 1, 'A' from dual
union all
select 1, 'B' from dual
union all
select 1, 'C' from dual
union all
select 2, 'F' from dual
union all
select 2, 'G' from dual
union all
select 3, 'J' from dual
union all
select 3, 'K' from dual
)
, grpIndexes as (
SELECT ROWNUM indx
, grp
FROM (select distinct
grp
from t)
)
, Grps as (
select t.*
, grpIndexes.indx
from t
inner join grpIndexes
on grpIndexes.grp = t.grp
)
, rt(val, indx, lvl) as (
select val
, indx
, 0 as lvl
from Grps
where indx = (select min(indx) from Grps)
union all
select previous.val || this.val as val
, this.indx
, previous.lvl + 1 as lvl
from rt previous
, Grps this
where this.indx = (select min(indx) from Grps where indx > previous.indx)
and previous.indx <> (select max(indx) from Grps)
)
select val
from rt
where lvl = (select max(lvl) from rt)
order by val
;
Note that I renamed the columns because Group and Value are reserved words.

Related

Oracle SQL - Count based on a condition to include distinct rows with zero matches

Is there a "better" way to refactor the query below that returns the number occurrences of a particular value (e.g. 'A') for each distinct id? The challenge seems to be keeping id = 2 in the result set even though the count is zero (id = 2 is never related to 'A'). It has a common table expression, NVL function, in-line view, distinct, and left join. Is all of that really needed to get this job done? (Oracle 19c)
create table T (id, val) as
select 1, 'A' from dual
union all select 1, 'B' from dual
union all select 1, 'A' from dual
union all select 2, 'B' from dual
union all select 2, 'B' from dual
union all select 3, 'A' from dual
;
with C as (select id, val, count(*) cnt from T where val = 'A' group by id, val)
select D.id, nvl(C.cnt, 0) cnt_with_zero from (select distinct id from T) D left join C on D.id = C.id
order by id
;
ID CNT_WITH_ZERO
---------- -------------
1 2
2 0
3 1
A simple way is conditional aggregation:
select id,
sum(case when val = 'A' then 1 else 0 end) as num_As
from t
group by id;
If you have another table with one row per id, you I would recommend:
select i.id,
(select count(*) from t where t.id = i.id and t.val = 'A') as num_As
from ids i;

insert random strings from a list using sql

i've created a table like this : region(id, name) and i want to insert into this table some rows , the name should be choosed randomly from a list like ['east', 'west'], how can i do that ?
Insert 10 random rows with...
Oracle:
INSERT INTO region(name)
SELECT rndm.name
FROM (SELECT 1 n
FROM dual
CONNECT BY LEVEL <= 365
) gen
CROSS
JOIN (SELECT 'west' name FROM dual
UNION ALL
SELECT 'east' FROM dual
UNION ALL
SELECT 'north' FROM dual
) rndm
WHERE rownum <= 10
ORDER BY DBMS_RANDOM.VALUE
SQL Server:
INSERT INTO region (name)
SELECT TOP 10 rndm.name
FROM sys.all_objects a1
CROSS
JOIN (VALUES ('east'),
('west')) rndm(name)
ORDER BY CHECKSUM(NEWID())
MySQL:
INSERT INTO region (name)
SELECT rndm.name
FROM information_schema.columns v
CROSS
JOIN (SELECT 'east' as name UNION ALL
SELECT 'west') rndm
ORDER BY RAND()
LIMIT 10
Postgres:
INSERT INTO region(NAME)
SELECT unnest(ARRAY['west','east'])
FROM generate_series(1, 100)
ORDER BY random()
LIMIT 10
This is an Oracle solution. The first subquery generates ten random integers between 1 and 3, using Oracle's CONNECT BY syntax. The second subquery associates the region name with a number between 1 and 3. These are joined in the main query to populate the table:
insert into region (id, name)
with rnd as (
select level as lvl, round(DBMS_RANDOM.VALUE(0.5,3.4999999999), 0) as rnd
from dual
connect by level <= 10
) , regn as (
select 1 as rno, 'west' as rname from dual union all
select 2 as rno, 'east' as rname from dual union all
select 3 as rno, 'north' as rname from dual
)
select rnd.lvl
, regn.rname
from rnd
join regn on rnd.rnd = regn.rno
/

Oracle SQL Replace multiple characters in different positions

I'm using Oracle 11g and I'm having trouble replacing multiple characters based on positions mentioned in a different table. For example:
Table 1
PRSKEY POSITION CHARACTER
123 3 ć
123 9 ć
Table 2
PRSKEY NAME
123 Becirovic
I have to replace the NAME in Table 2 to Bećirović.
I've tried regexp_replace but this function doesn't provide replacing more then 1 position, is there an easy way to fix this?
Here's another way to do it.
with tab1 as (select 123 as prskey, 3 as position, 'ć' as character from dual
union select 123, 9, 'ć' from dual),
tab2 as (select 123 as prskey, 'Becirovic' as name from dual)
select listagg(nvl(tab1.character, namechar)) within group(order by lvl)
from
(select prskey, substr(name, level, 1) as namechar, level as lvl
from tab2
connect by level <= length(name)
) splitname
left join tab1 on position = lvl and tab1.prskey = splitname.prskey
;
Simple solution using cursor ...
create table t1 (
prskey int,
pos int,
character char(1)
);
create table t2
(
prskey int,
name varchar2(100)
);
insert into t1 values (1, 1, 'b');
insert into t1 values (1, 3, 'e');
insert into t2 values (1, 'dear');
begin
for t1rec in (select * from t1) loop
update t2
set name = substr(name, 1, t1rec.pos - 1) || t1rec.character || substr(name, t1rec.pos + 1, length(name) - t1rec.pos)
where t2.prskey = t1rec.prskey;
end loop;
end;
/
I would prefer approach via PL/SQL, but in your tag only 'sql', so I made this monster:
with t as (
select 123 as id, 3 as pos, 'q' as new_char from dual
union all
select 123 as id, 6 as pos, 'z' as new_char from dual
union all
select 123 as id, 9 as pos, '1' as new_char from dual
union all
select 456 as id, 1 as pos, 'A' as new_char from dual
union all
select 456 as id, 4 as pos, 'Z' as new_char from dual
),
t1 as (
select 123 as id, 'Becirovic' as str from dual
union all
select 456 as id, 'Test' as str from dual
)
select listagg(out_text) within group (order by pos)
from(
select id, pos, new_char, str, prev, substr(str,prev,pos-prev)||new_char as out_text
from(
select id, pos, new_char, str, nvl(lag(pos) over (partition by id order by pos)+1,1) as prev
from (
select t.id, pos, new_char, str
from t, t1
where t.id = t1.id
) q
) a
) w
group by id
Result:
Beqirzvi1
AesZ

Finding sequence in data and grouping by it

Data in Phone_number column of my Temp_table looks like this
1234560200
1234560201
1234560202
2264540300
2264540301
2264540302
2264540303
2264540304
2264540305
2264540306
I want it to find sequence of last 4 digits and and find First and Last number of sequence of it. For eg.
There is sequence of first 3 rows as 0200, 0201, 0202, so First = 0200 and Last = 0202
Final Output of this query should be
First Last
0200 0202
0300 0306
I tried below query, but not sure about this approach.
WITH get_nxt_range AS
(
select substr(a.PHONE_NUMBER,7,4) range1,
LEAD(substr(a.PHONE_NUMBER,7,4)) OVER (ORDER BY a.PHONE_NUMBER ) nxt_range
from Temp_table a
)
SELECT range1,nxt_range FROM get_nxt_range
WHERE nxt_range = range1 +1
ORDER BY range1
One method to get sequences is to use the difference of row numbers approach. This works in your case as well:
select substr(phone_number, 1, 6),
min(substr(phone_number, 7, 4)), max(substr(phone_number, 7, 4))
from (select t.*,
(row_number() over (order by phone_number) -
row_number() over (partition by substr(phone_number, 1, 6) order by phone_number)
) as grp
from temp_table t
) t
group by substr(phone_number, 1, 6), grp;
I think something like this might work:
select
min (substr (phone_number, -4, 4)) as first,
max (substr (phone_number, -4, 4)) as last
from temp_table
group by
substr (phone_number, -4, 2)
SELECT DISTINCT
COALESCE(
first_in_sequence,
LAG( first_in_sequence ) IGNORE NULLS OVER ( ORDER BY phone_number )
) AS first_in_sequence,
COALESCE(
last_in_sequence,
LAG( last_in_sequence ) IGNORE NULLS OVER ( ORDER BY phone_number )
) AS last_in_sequence
FROM (
SELECT phone_number,
CASE phone_number
WHEN LAG( phone_number ) OVER ( ORDER BY phone_number ) + 1
THEN NULL
ELSE phone_number
END AS first_in_sequence,
CASE phone_number
WHEN LEAD( phone_number ) OVER ( ORDER BY phone_number ) - 1
THEN NULL
ELSE phone_number
END AS last_in_sequence
FROM temp_table
);
Update:
CREATE TABLE phone_numbers ( phone_number ) AS
select 1234560200 from dual union all
select 1234560201 from dual union all
select 1234560202 from dual union all
select 2264540300 from dual union all
select 2264540301 from dual union all
select 2264540302 from dual union all
select 2264540303 from dual union all
select 2264540304 from dual union all
select 2264540305 from dual union all
select 2264540306 from dual;
SELECT MIN( phone_number ) AS first_in_sequence,
MAX( phone_number ) AS last_in_sequence
FROM (
SELECT phone_number,
phone_number - ROW_NUMBER() OVER ( ORDER BY phone_number ) AS grp
FROM phone_numbers
)
GROUP BY grp;
Output:
FIRST_IN_SEQUENCE LAST_IN_SEQUENCE
----------------- ----------------
2264540300 2264540306
1234560200 1234560202
If 1234560201 1234560203 1234560204 are two instances then this should work:
with tt as (
select substr(PHONE_NUMBER,7,4) id from Temp_table
),
t as (
select
t1.id,
case when t3.id is null then 1 else 0 end start,
case when t2.id is null then 1 else 0 end "end"
from tt t1
-- no next adjacent element - we have an end of interval
left outer join tt t2 on t2.id - 1 = t1.id
-- not previous adjacent element - we have a start of interval
left outer join tt t3 on t3.id + 1 = t1.id
-- select starts and ends only
where t2.id is null or t3.id is null
)
-- find nearest end record for each start record (it may be the same record)
select t1.id, (select min(id) from t where id >= t1.id and "end" = 1)
from t t1
where t1.start = 1
I see guys already have answered for your question.
I just want to propose my variant how resolve this task:
with list_num (phone_number) as (
select 1234560200 from dual union all
select 1234560201 from dual union all
select 1234560202 from dual union all
select 2264540300 from dual union all
select 2264540301 from dual union all
select 2264540302 from dual union all
select 2264540303 from dual union all
select 2264540304 from dual union all
select 2264540305 from dual union all
select 2264540306 from dual)
select root as from_value,
max(phone_number) keep (dense_rank last order by lvl) as to_value
from
(select phone_number, level as lvl, CONNECT_BY_ROOT phone_number as root
from
(select phone_number,
decode(phone_number-lag (phone_number) over(order by phone_number),1,1,0) as start_value
from list_num) b
connect by nocycle phone_number = prior phone_number + 1
start with start_value = 0)
group by root
having count(1) > 1
If you need only last 4 numbers just substr it.
substr(root,7,4) as from_value,
substr(max(phone_number) keep (dense_rank last order by lvl),7,4) as to_value
Thanks.

select where a and next is c, skipping b

I am wondering if it is possible in SQL to return a single row to show, using the table below as an example, only a row for id 2:
table1 ( id 2 and 4 are missing value b)
id value
1 a
1 b
1 c
1 d
2 a
2 c
2 d
3 a
3 b
3 c
3 d
4 a
4 c
4 d
i basically want to find all instances where 'b' does not exist but 'a' still does exist for any id and return a single row for that any given id. i have tried something like this, but its not working as i would want it to:
select * from table1
where not exists (select distinct value from table1 where value b)
i would like the end result to be something this, identifying the values where 'b' does not exist but 'a' does(not showing the value, is unneeded for final goal):
result table
id
2
4
SELECT id
FROM table1 t1
WHERE
value = 'a'
AND NOT EXISTS (
SELECT *
FROM table1 sub
WHERE sub.id = t1.id AND sub.value = 'b'
)
This should do the job:
select distinct id
from table1 t
where not exists (
select 1
from table1 tt
where t.id = tt.id and tt.vallue = 'b'
)
and exists (
select 1
from table1 tt
where t.id = tt.id and tt.vallue = 'a'
)
Below you have shorter form. It may perform better and distinct keyword may be unnecessary if the pair (id, value) is unique.
select distinct id
from table1 t
left join table1 tt
on t.id = tt.id and tt.value = 'b'
where t.value = 'a'
and tt.id is null
Haven't tested, but I think something like this would work.
SELECT id FROM table1
WHERE value='a' AND id NOT IN(SELECT id FROM table1 WHERE value='b')
GROUP BY id;
EDIT: Apologies to Dooh. I just noticed that this answer is essentially a duplicate of Dooh's second query. I'll leave it as a runnable example.
It may be enlightening to compare execution plans for the various queries.
declare #table1 as table ( id int, value varchar(10) )
insert into #table1 ( id, value ) values
( 1, 'a' ), ( 1, 'b' ), ( 1, 'c' ), ( 1, 'd' ),
( 2, 'a' ), ( 2, 'c' ), ( 2, 'd' ),
( 3, 'a' ), ( 3, 'b' ), ( 3, 'c' ), ( 3, 'd' ),
( 4, 'a' ), ( 4, 'c' ), ( 4, 'd' ),
( 5, 'a' ), ( 5, 'a' ), ( 5, 'b' ), -- Duplicate 'a's.
( 6, 'a' ), ( 6, 'a' ) -- Duplicate 'a's.
select distinct L.id
from #table1 as L left outer join
#table1 as R on R.id = L.id and R.value = 'b'
where R.id is NULL and L.value = 'a'