Recursive Teradata Query - sql

I'm trying to query the below table into a consolidated and sorted list, such as:
Beginning list:
GROUP_ID MY_RANK EMP_NAME
1 1 Dan
1 2 Bob
1 4 Chris
1 3 Steve
1 5 Cal
2 1 Britt
2 2 Babs
2 3 Beth
3 1 Vlad
3 3 Eric
3 2 Mike
Query Result:
1 Dan, Bob, Steve, Chris, Cal
2 Britt, Babs, Beth
3 Vlad, Mike, Eric
It needs to use a recursive query because the list is much longer. Also, I have to sort by my_rank to get them in sequential order. Thanks in advance. I've tried about 10 examples found on different forums, but I'm stuck. Also, don't worry about truncating the any trailing/leading commas.
CREATE TABLE MY_TEST (GROUP_ID INTEGER NOT NULL, MY_RANK INTEGER NOT NULL, EMP_NAME VARCHAR(18) NOT NULL);
INSERT INTO MY_TEST VALUES (1, 1, 'Dan');
INSERT INTO MY_TEST VALUES (1, 2, 'Bob');
INSERT INTO MY_TEST VALUES (1, 4, 'Chris');
INSERT INTO MY_TEST VALUES (1, 3, 'Steve');
INSERT INTO MY_TEST VALUES (1, 5, 'Cal');
INSERT INTO MY_TEST VALUES (2, 1, 'Britt');
INSERT INTO MY_TEST VALUES (2, 2, 'Babs');
INSERT INTO MY_TEST VALUES (2, 3, 'Beth');
INSERT INTO MY_TEST VALUES (3, 1, 'Vlad');
INSERT INTO MY_TEST VALUES (3, 3, 'Eric');
INSERT INTO MY_TEST VALUES (3, 2, 'Mike');

What's your Teradata release? Are XML-Services installed?
SELECT * FROM dbc.FunctionsV
WHERE FunctionName = 'XMLAGG';
If this function exists you can avoid recursion (which is not very efficient anyway):
SELECT GROUP_ID,
TRIM(TRAILING ',' FROM
CAST(XMLAGG(EMP_NAME || ',' ORDER BY MY_RANK) AS VARCHAR(10000)))
FROM MY_TEST
GROUP BY 1

Something like this should work:
WITH RECURSIVE employees(Group_ID , Employees, MY_RANK) AS
(
--Recursive Seed
SELECT
GROUP_ID,
--Cast as a big fat varchar so it can hold all of the employees in a list.
CAST(EMP_NAME as VARCHAR(5000)),
--We need to include MY_RANK so we can choose the next highest rank in the recursive term below.
MY_RANK
FROM
MY_TEST
WHERE
--filter for only my_rank = 1 because that's where we want to start
MY_RANK = 1
UNION ALL
--Recursive Term
SELECT
employees.GROUP_ID,
employees.EMP_NAME || ', ' || MY_TEST.EMP_NAME,
MY_TEST.MY_RANK
FROM
employees
INNER JOIN MY_TEST ON
--Joining on Group_id and Rank+1
employees.GROUP_ID = MY_TEST.GROUP_ID AND
employees.MY_RANK + 1 = MY_TEST.MY_RANK
)
SELECT GROUP_ID, Employees FROM employees;

Related

Find data by multiple Lookup table clauses

declare #Character table (id int, [name] varchar(12));
insert into #Character (id, [name])
values
(1, 'tom'),
(2, 'jerry'),
(3, 'dog');
declare #NameToCharacter table (id int, nameId int, characterId int);
insert into #NameToCharacter (id, nameId, characterId)
values
(1, 1, 1),
(2, 1, 3),
(3, 1, 2),
(4, 2, 1);
The Name Table has more than just 1,2,3 and the list to parse on is dynamic
NameTable
id | name
----------
1 foo
2 bar
3 steak
CharacterTable
id | name
---------
1 tom
2 jerry
3 dog
NameToCharacterTable
id | nameId | characterId
1 1 1
2 1 3
3 1 2
4 2 1
I am looking for a query that will return a character that has two names. For example
With the above data only "tom" will be returned.
SELECT *
FROM nameToCharacterTable
WHERE nameId in (1,2)
The in clause will return every row that has a 1 or a 3. I want to only return the rows that have both a 1 and a 3.
I am stumped I have tried everything I know and do not want to resort to dynamic SQL. Any help would be great
The 1,3 in this example will be a dynamic list of integers. for example it could be 1,3,4,5,.....
Filter out a count of how many times the Character appears in the CharacterToName table matching the list you are providing (which I have assumed you can convert into a table variable or temp table) e.g.
declare #Character table (id int, [name] varchar(12));
insert into #Character (id, [name])
values
(1, 'tom'),
(2, 'jerry'),
(3, 'dog');
declare #NameToCharacter table (id int, nameId int, characterId int);
insert into #NameToCharacter (id, nameId, characterId)
values
(1, 1, 1),
(2, 1, 3),
(3, 1, 2),
(4, 2, 1);
declare #RequiredNames table (nameId int);
insert into #RequiredNames (nameId)
values
(1),
(2);
select *
from #Character C
where (
select count(*)
from #NameToCharacter NC
where NC.characterId = c.id
and NC.nameId in (select nameId from #RequiredNames)
) = 2;
Returns:
id
name
1
tom
Note: Providing DDL+DML as shown here makes it much easier for people to assist you.
This is classic Relational Division With Remainder.
There are a number of different solutions. #DaleK has given you an excellent one: inner-join everything, then check that each set has the right amount. This is normally the fastest solution.
If you want to ensure it works with a dynamic amount of rows, just change the last line to
) = (SELECT COUNT(*) FROM #RequiredNames);
Two other common solutions exist.
Left-join and check that all rows were joined
SELECT *
FROM #Character c
WHERE EXISTS (SELECT 1
FROM #RequiredNames rn
LEFT JOIN #NameToCharacter nc ON nc.nameId = rn.nameId AND nc.characterId = c.id
HAVING COUNT(*) = COUNT(nc.nameId) -- all rows are joined
);
Double anti-join, in other words: there are no "required" that are "not in the set"
SELECT *
FROM #Character c
WHERE NOT EXISTS (SELECT 1
FROM #RequiredNames rn
WHERE NOT EXISTS (SELECT 1
FROM #NameToCharacter nc
WHERE nc.nameId = rn.nameId AND nc.characterId = c.id
)
);
A variation on the one from the other answer uses a windowed aggregate instead of a subquery. I don't think this is performant, but it may have uses in certain cases.
SELECT *
FROM #Character c
WHERE EXISTS (SELECT 1
FROM (
SELECT *, COUNT(*) OVER () AS cnt
FROM #RequiredNames
) rn
JOIN #NameToCharacter nc ON nc.nameId = rn.nameId AND nc.characterId = c.id
HAVING COUNT(*) = MIN(rn.cnt)
);
db<>fiddle

Oracle copying the tree in the table

I'm looking for some interesting simple algorithm to copy (clone) part of the tree to another part of the tree.
We have a table:
create table tree (
id integer not null,
parent_id integer,
value varchar2(255),
CONSTRAINT tab_pk PRIMARY KEY (id)
);
begin
insert into tree(id, parent_id, value) values (1, null, 'A');
insert into tree(id, parent_id, value) values (2, 1, 'B');
insert into tree(id, parent_id, value) values (3, 1, 'C');
insert into tree(id, parent_id, value) values (4, 2, 'BC');
insert into tree(id, parent_id, value) values (8, 4, 'BCX');
insert into tree(id, parent_id, value) values (5, 2, 'BD');
insert into tree(id, parent_id, value) values (6, 3, 'CA');
insert into tree(id, parent_id, value) values (7, 3, 'CD');
end;
/
https://dbfiddle.uk/?rdbms=oracle_18&fiddle=9fe802f144a3af0663754cfb3e8dc1ba
How to easily copy a tree "B" (ID = 2) with all children under "CA" (ID = 6)?
ORACLE 18-19c.
I tried to understand your question
Your original query provides this output
select level, id,
lpad ( ' ', level, ' ' ) || value name
from tree
start with parent_id is null
connect by prior id = parent_id;
LE ID NAME
1 1 A
2 2 B
3 4 BC
4 8 BCX
3 5 BD
2 3 C
3 6 CA
3 7 CD
Then you say you want the tree "B" (ID = 2) with all children under "CA" (ID = 6)
select level, id,
lpad ( ' ', level, ' ' ) || value name
from tree
start with id = 2
connect by prior id = parent_id
union all
-- tree to clone
select level, id,
lpad ( ' ', level, ' ' ) || value name
from tree
start with id = 6 or id=7
connect by prior id = parent_id;
LE ID NAME
1 2 B
2 4 BC
3 8 BCX
2 5 BD
1 6 CA
1 7 CD
db<>fiddle

How to generate batch number in SQL Server

Any idea on how to generate batch numbers into a batch_number column in SQL Server table from 1 to 3 and repeat it again as shown below in the script?
Thanks in advance
Declare #mTestTable table
(
id int,
city varchar(50),
batch_number int
)
--Insert some sample data
Insert into #mTestTable values (1, 'London')
Insert into #mTestTable values (2, 'Karachi')
Insert into #mTestTable values (3, 'New York')
Insert into #mTestTable values (4, 'Melbourne')
Insert into #mTestTable values (5, 'Beijing')
Insert into #mTestTable values (6, 'Tokyo')
Insert into #mTestTable values (7, 'Moscow')
I need to generate batch number from 1 to 3 and repeat it again.
Id City Batch_Number
1 London 1
2 Karachi 2
3 New York 3
4 Melbourne 1
5 Beijing 2
6 Tokyo 3
7 Moscow 1
Use row_number() and some arithmetic:
with toupdate as (
select t.*, 1 + ((row_number() over (order by id) - 1) % 3) as new_batch_no
from testtable
)
update toupdate
set batch_number = new_batch_number;
update #mTestTable
set batch_number = (id-1) % 3 + 1
If the IDs won't be sequential, then one way to do it is using a cursor passing through it. Simply count the rows one by one and update the appropriate batch number.

SQL: Test if Number Contains Digits of Another Number

I'm interested in using SQL to test if one number, stored in column DOW, contains the digits of another number, also stored in DOW, regardless of separation by other digits. Here are the current numbers with which I'm working, although I may have more to deal with in the future:
23
236
1234
12346
123456
67
If the query checks 123456 against 236, it needs to return true. Vice versa, 236 against 123456 returns false. Another example is 1234 returns true when checked against 23, but 67 returns false when checked against 12346. If I've not provided enough information in this question, please ask for clarification.
Simplified Version of my Query:
SELECT t1.DOW, t2.DOW
FROM table t1, table t2
WHERE /* t2.DOW contains all digits regardless of separation in t1.DOW */
Thanks!
I assume DOW is Day of Week and only has numbers 1-7
SELECT T1.DOW,T2.DOW
FROM T1,T2
WHERE NOT EXISTS(SELECT *
FROM (VALUES('%1%'),
('%2%'),
('%3%'),
('%4%'),
('%5%'),
('%6%'),
('%7%')) Nums(N)
WHERE (T2.DOW LIKE N AND T1.DOW NOT LIKE N))
SQL Server 2008 answer though AFAIK this is standard SQL.
This can be handled as a stored procedure which turns a value into a string and inspects the string for each of the digits in the pattern value.
The guts of the stored procedure would have something like
function has_digits (pattern, value)
string s = string (value)
count = 0
for each char in pattern
if instr (s, char) > 0 // for mysql
count = count + 1
return count == pattern.length()
(The specifics of this greatly depend on which SQL flavor is used.)
This would be used like
SELECT *
FROM sometable
WHERE has_digits (236, somefield);
First, I want to make sure everybody understands that I firmly believe this should be in a stored procedure. Also, that I will probably spend most of tomorrow trying to figure out a suitable punishment for what I'm about to write.
Let's assume we have two tables. The first table contains the digits we want to search for.
CREATE TABLE search_digits (
digit integer NOT NULL
);
INSERT INTO search_digits VALUES (2), (3);
We're going to search for the digits 2 and 3.
The second table contains the values we want to search within.
CREATE TABLE dow_digits (
dow integer NOT NULL,
digit integer NOT NULL,
CONSTRAINT dow_digits_pkey PRIMARY KEY (dow, digit),
);
INSERT INTO dow_digits VALUES (23, 2);
INSERT INTO dow_digits VALUES (23, 3);
INSERT INTO dow_digits VALUES (236, 2);
INSERT INTO dow_digits VALUES (236, 3);
INSERT INTO dow_digits VALUES (236, 6);
INSERT INTO dow_digits VALUES (1234, 1);
INSERT INTO dow_digits VALUES (1234, 2);
INSERT INTO dow_digits VALUES (1234, 3);
INSERT INTO dow_digits VALUES (1234, 4);
INSERT INTO dow_digits VALUES (12346, 1);
INSERT INTO dow_digits VALUES (12346, 2);
INSERT INTO dow_digits VALUES (12346, 3);
INSERT INTO dow_digits VALUES (12346, 4);
INSERT INTO dow_digits VALUES (12346, 6);
INSERT INTO dow_digits VALUES (123456, 1);
INSERT INTO dow_digits VALUES (123456, 2);
INSERT INTO dow_digits VALUES (123456, 3);
INSERT INTO dow_digits VALUES (123456, 4);
INSERT INTO dow_digits VALUES (123456, 5);
INSERT INTO dow_digits VALUES (123456, 6);
INSERT INTO dow_digits VALUES (67, 6);
INSERT INTO dow_digits VALUES (67, 7);
We can find at least some of the values for dow that contain the digits 2 and 3 with a simple query.
select d1.dow from dow_digits d1
inner join search_digits d2 on d1.digit = d2.digit
group by dow
having count(distinct d1.digit) = (select count(distinct digit)
from search_digits);
dow
--
23
236
1234
12346
123456
That seems to work. It's not clear what the OP expects if the search integer is 233, so I'm going to ignore that case for now. I want to finish this quickly, then step in front of a truck.
The next question is, can we build search_digits on the fly? In PostgreSQL, sort of.
SELECT UNNEST(ARRAY[2,3]) as digit;
digit
--
2
3
Drop the table search_digits, and wrap that in a CTE.
with search_digits as (
select unnest(array[2,3]) as digit
)
select d1.dow from dow_digits d1
inner join search_digits d2 on d1.digit = d2.digit
group by dow
having count(distinct d1.digit) = (select count(distinct digit)
from search_digits);
dow
--
23
236
1234
12346
123456
Next question. Can we build dow_digits on the fly? In PostgreSQL, sort of. Need to know how many digits in the longest number. Let's say no more than six.
select dow, digit
from (select dow, unnest(array[substring((dow)::text from 1 for 1),
substring((dow)::text from 2 for 1),
substring((dow)::text from 3 for 1),
substring((dow)::text from 4 for 1),
substring((dow)::text from 5 for 1),
substring((dow)::text from 6 for 1)]) digit
from dow ) d
where d.digit <> '';
dow digit
--
23 2
23 3
236 2
236 3
236 6
1234 1
1234 2
1234 3
1234 4
12346 1
12346 2
12346 3
12346 4
12346 6
123456 1
123456 2
123456 3
123456 4
123456 5
123456 6
67 6
67 7
233 2
233 3
233 3
Pulling all that together into a single statement . . .
with search_digits as (
select unnest(array[1,2,3,4,6]) digit
)
select dow
from (select dow, digit
from (select dow, unnest(array[substring((dow)::text from 1 for 1),
substring((dow)::text from 2 for 1),
substring((dow)::text from 3 for 1),
substring((dow)::text from 4 for 1),
substring((dow)::text from 5 for 1),
substring((dow)::text from 6 for 1)]) digit
from dow
) arr
where arr.digit <> ''
) d
inner join (select distinct digit from search_digits) sd
on sd.digit = d.digit::integer
group by dow
having count(distinct d.digit) = (select count(distinct digit)
from search_digits)
dow
--
12346
123456
Oh, I can feel karma points slipping away . . . where's that truck?
For MySQL only:
CREATE TABLE num --- a help table
( i INT PRIMARY KEY
) ;
INSERT INTO num
VALUES
(1), (2), (3), (4)
, (5), (6), (7), (8)
, (9),(10),(11),(12)
,(13),(14),(15),(16)
,(17),(18),(19),(20) ; --- maximum number of digits = 20
Then, you can use this (horrible) query, which first converts numbers to strings and add % between each digit (23 would be converted to '%2%3%') and then tests WHERE 23456 LIKE '%2%3% to match if 23456 contains 23:
SELECT t1.DOW
, t2.DOW
, (t1.DOW LIKE test) AS t1_contains_t2
FROM t1
JOIN
( SELECT t2.DOW
, CONCAT( '%'
, GROUP_CONCAT( SUBSTRING( t2.DOW, num.i, 1 )
ORDER BY num.i
SEPARATOR '%' )
, '%'
) AS test
FROM t2
JOIN num
ON (SUBSTRING(t2.DOW,num.i,1) != "" )
GROUP BY t2.DOW
) AS temp ;
The t1_contains_t2 column will have 1 or 0 depending on whether t1.DOW contains t2.DOW
Or you can use WHERE (t1.DOW LIKE test) in the query.

Select Records that match ALL groups in a many to many join table

I have 2 tables: sets and groups. Both are joined using a 3rd table set_has_groups.
I would like to get sets that have ALL groups that I specify
One way of doing it would be
SELECT column1, column2 FROM sets WHERE
id IN(SELECT set_id FROM set_has_group WHERE group_id = 1)
AND id IN(SELECT set_id FROM set_has_group WHERE group_id = 2)
AND id IN(SELECT set_id FROM set_has_group WHERE group_id = 3)
obviously this is not the most beautiful solution
I've also tried this:
SELECT column1, column2 FROM sets WHERE
id IN(SELECT set_id FROM set_has_group WHERE group_id IN(1,2,3) GROUP BY group_id
HAVING COUNT(*) = 3
This looks prettier but the problem is that it takes forever to execute.
While the first query runs in like 200ms the 2nd one takes more than 1 minute.
Any idea why that is?
===UPDATE:
I've played with this some more and I modified the 2nd query like this
SELECT columns FROM `set` WHERE id IN(
select set_id FROM
(
SELECT set_id FROM set_has_group
WHERE group_id IN(1,2,3)
GROUP BY set_id HAVING COUNT(*) = 3
) as temp
)
that is really fast
It's the same as the 2nd query before just that I wrap it in another temporary table
Pretty strange
I am suspecting a small mistyping in the second query.
Really, I am not sure. Probably, the second query is executed via full table scan. At the same time the first one "IN" is really transformed into "EXISTS". So, you can try to use "exists". For example:
...
where 3 = (select count(*) from set_has_group
where group_id in (1, 2, 3) and set_id = id
group by set_id)
Assuming SQL Server, here is a working example with a JOIN that should work better than the IN clauses you are using as long as you have your primary and foreign keys set correctly. I have built joined 5 sets to 3 groups, but set 4 and 5 are not a part of group 3 and will not show in the answer. However, this query is not scalable (for ex. find in group 4, 5, 7, 8 and 13 will require code modifications unless you parse input params into a table variable)
set nocount on
declare #sets table
(
Id INT Identity (1, 1),
Column1 VarChar (50),
Column2 VarChar (50)
)
declare #Set_Has_Group table
(
Set_Id Int,
Group_Id Int
)
insert into #sets values (newid(), newid())
insert into #sets values (newid(), newid())
insert into #sets values (newid(), newid())
insert into #sets values (newid(), newid())
insert into #sets values (newid(), newid())
update #sets set column1 = 'Column1 at Row ' + Convert (varchar, id)
update #sets set column2 = 'Column2 at Row ' + Convert (varchar, id)
insert into #Set_Has_Group values (1, 1)
insert into #Set_Has_Group values (1, 2)
insert into #Set_Has_Group values (1, 3)
insert into #Set_Has_Group values (2, 1)
insert into #Set_Has_Group values (2, 2)
insert into #Set_Has_Group values (2, 3)
insert into #Set_Has_Group values (3, 1)
insert into #Set_Has_Group values (3, 2)
insert into #Set_Has_Group values (3, 3)
insert into #Set_Has_Group values (4, 1)
insert into #Set_Has_Group values (4, 2)
insert into #Set_Has_Group values (5, 1)
insert into #Set_Has_Group values (5, 2)
/* your query with IN */
SELECT column1, column2 FROM #sets WHERE
id IN(SELECT set_id FROM #set_has_group WHERE group_id = 1)
AND id IN(SELECT set_id FROM #set_has_group WHERE group_id = 2)
AND id IN(SELECT set_id FROM #set_has_group WHERE group_id = 3)
/* my query with JOIN */
SELECT * -- Column1, Column2
FROM #sets sets
WHERE 3 = (
SELECT Count (1)
FROM #Set_Has_Group Set_Has_Group
WHERE 1=1
AND sets.Id = Set_Has_Group.Set_Id
AND Set_Has_Group.Group_ID IN (1, 2, 3)
Group by Set_Id
)
Here's a solution that uses a non-correlated subquery and no GROUP BY:
SELECT column1, column2
FROM sets
WHERE id IN (
SELECT g1.set_id FROM set_has_group g1
JOIN set_has_group g2 ON (g1.set_id = g3.set_id)
JOIN set_has_group g3 ON (g1.set_id = g3.set_id)
WHERE g1.group_id = 1 AND g2.group_id = 2 AND g3.group_id = 3);