Tables for recursive hierarchial data in SQL [duplicate] - sql

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Simplest way to do a recursive self-join in SQL Server?
I have to create a table in SQL that will comprise of groups of items/products. Each new group made will be made under one of the pre-defined groups or the groups previously formed. I want to keep all this data in a SQL Table. So far, I have though of creating a table like this:
Group ID
Group Name
Group Under (This will store the ID of the group under which this group is from
But this can only refer to just the next level, how will I get to know who is the super-parent of this group.
For example:
I have groups A, B, C.
A has further subgroups A1, A2, A3.
A1 has further subgroups, A11, A12, A13.
I will I have the information about super-parent group i.e A from A11 or A22 or A33?
Let me know if the problem is not clear..

Assuming T-SQL and MSSQLServer (you didn't specify), and given that your Group table should look something like this:
Id | Name | ParentId
---+------+---------
1 | A | NULL
2 | B | NULL
3 | C | NULL
4 | A1 | 1
5 | A2 | 1
6 | A3 | 1
7 | A11 | 4
8 | A12 | 4
9 | A13 | 4
You can use the following recursive CTE to find the top level a given group, say 'A12':
WITH [Group](Id, Name, ParentId) AS
(
SELECT 1, 'A' , NULL UNION
SELECT 2, 'B' , NULL UNION
SELECT 3, 'C' , NULL UNION
SELECT 4, 'A1' , 1 UNION
SELECT 5, 'A2' , 1 UNION
SELECT 6, 'A3' , 1 UNION
SELECT 7, 'A11', 4 UNION
SELECT 8, 'A12', 4 UNION
SELECT 9, 'A13', 4
), q AS
(
SELECT
*
FROM
[Group]
WHERE
[Name] = 'A12' -- Given 'A12' as the child
UNION ALL
SELECT
g.*
FROM
[Group] g
JOIN
q
ON
q.ParentId = g.Id
)
SELECT
*
FROM
q
WHERE
ParentId IS NULL
This query returns:
Id | Name | ParentId
---+------+---------
1 | A | NULL

Related

Find top parent of child, multiple levels

ENTRY TABLE
__________________
| ID | PARENT_ID |
| 1 | null |
| 2 | 1 |
| 3 | 2 |
| 4 | null |
| 5 | 4 |
| 6 | 5 |
...
I make copies of the entries in some cases and they are conneted by parent ID.
Each entry can have one copy:
THIS WONT HAPPEN
__________________
| ID | PARENT_ID |
| 1 | null |
| 2 | 1 |
| 3 | 1 |
...
Sometimes I need to take a copy and query for it's top level parent. I need to find the top parent entries for all the entries I search for.
For example, if I query for the parents of ID 6 and 3, I would get ID 4 and 1.
If I query for the parents of ID 5 and 2, I would get ID 4 and 1.
But also If I query for ID 5 and 1, it should return ID 4 and 1 because the entry ID 1 is already the top parent itself.
I don't know where to begin since I don't know how to recursively query in such case.
Can anyone point me in the right direction?
I know that the query below will just return the child elemements (ID 6 and 3), but I don't know where to go from here honestly.
I am using OracleSQL by the way.
SELECT * FROM entry WHERE id IN (6, 3);
You can use a hierarchical query and CONNECT_BY_ROOT.
Either starting at the root of the hierarchy and working down:
SELECT id,
CONNECT_BY_ROOT(id) AS root_id
FROM entry
WHERE id IN (6, 3)
START WITH parent_id IS NULL
CONNECT BY PRIOR id = parent_id;
Or, from the entry back up to the root:
SELECT CONNECT_BY_ROOT(id) AS id,
id AS root_id
FROM entry
WHERE parent_id IS NULL
START WITH id IN (6, 3)
CONNECT BY PRIOR parent_id = id;
Which, for the sample data:
CREATE TABLE entry( id, parent_id ) AS
SELECT 1, NULL FROM DUAL UNION ALL
SELECT 2, 1 FROM DUAL UNION ALL
SELECT 3, 2 FROM DUAL UNION ALL
SELECT 4, NULL FROM DUAL UNION ALL
SELECT 5, 4 FROM DUAL UNION ALL
SELECT 6, 5 FROM DUAL UNION ALL
SELECT 7, 6 FROM DUAL
Both output:
ID
ROOT_ID
3
1
6
4
db<>fiddle here
You can use recursive CTE to walk the graph and find the initial parent. For example:
with
n (starting_id, current_id, parent_id, v) as (
select id, id, parent_id, 0 from entry where id in (6, 3)
union all
select n.starting_id, e.id, e.parent_id, n.v - 1
from n
join entry e on e.id = n.parent_id
)
select starting_id, current_id as initial_id
from (
select n.*, row_number() over(partition by starting_id order by v) as rn
from n
) x
where rn = 1
Result:
STARTING_ID INITIAL_ID
------------ ----------
3 1
6 4
See running example at db<>fiddle.

Sequence generation for new ID

I have a requirement where there are 2 tables A and B. There is a column named ID which is a primary key for A and foreign key for table B where the tables have one to many relationship between them. In table A for ID column we have entries 1,2,3 and for them we have corresponding multiple entries for one ID and another column in table B named SEQ where a sequence has been created for entries in it. Now for entries for ID 1, I have 3 entries in table B with SEQ 1,2,3 but when new ID entry will be there then i need the sequence again to start from 1 for that ID.**Can you please help me to do that.
TABLE A
I'd suggest you not to store the SEQ value. Why would you? It is easy to calculate it whenever needed. How? Like this, using row_number analytic function:
SQL> with b (id, name) as
2 (select 1, 'TRI' from dual union all
3 select 1, 'TRI' from dual union all
4 select 1, 'TRI' from dual union all
5 select 2, 'ROHIT' from dual union all
6 select 2, 'ROHIT' from dual union all
7 select 3, 'RAVI' from dual
8 )
9 select id,
10 name,
11 row_number() over (partition by id order by null) seq
12 from b;
ID NAME SEQ
---------- ----- ----------
1 TRI 1
1 TRI 2
1 TRI 3
2 ROHIT 1
2 ROHIT 2
3 RAVI 1
6 rows selected.
SQL>
If you still want to store it, now you know how.
Don't have multiple sequences. Just use a single sequence in the B table and accept that there will be gaps for each ID and then if you need sequential values you can calculate them as needed using the ROW_NUMBER() analytic function and, if you need to, put it in a view. Also, don't duplicate the name from table A in table B; keep your data normalised.
CREATE TABLE A (
id NUMBER(8,0)
GENERATED ALWAYS AS IDENTITY
CONSTRAINT A__id__pk PRIMARY KEY,
name VARCHAR2(20)
);
CREATE TABLE B (
id NUMBER(8,0)
CONSTRAINT B__id__nn NOT NULL
CONSTRAINT B__id__fk REFERENCES A(id),
seq NUMBER(8,0)
GENERATED ALWAYS AS IDENTITY
CONSTRAINT B__seq__pk PRIMARY KEY
);
Then you can create your sample data:
INSERT INTO A ( name )
SELECT 'TRI' FROM DUAL UNION ALL
SELECT 'ROHIT' FROM DUAL UNION ALL
SELECT 'RAVI' FROM DUAL;
INSERT INTO B ( id )
SELECT 1 FROM DUAL UNION ALL
SELECT 1 FROM DUAL UNION ALL
SELECT 2 FROM DUAL UNION ALL
SELECT 3 FROM DUAL UNION ALL
SELECT 2 FROM DUAL UNION ALL
SELECT 1 FROM DUAL;
And:
SELECT *
FROM B
Outputs:
ID | SEQ
-: | --:
1 | 1
1 | 2
2 | 3
3 | 4
2 | 5
1 | 6
If you want your output then create a view:
CREATE VIEW B_view ( id, name, seq ) AS
SELECT b.id,
a.name,
ROW_NUMBER() OVER ( PARTITION BY b.id ORDER BY seq )
FROM B
INNER JOIN A
ON ( B.id = A.id )
Then:
SELECT *
FROM b_view
Outputs:
ID | NAME | SEQ
-: | :---- | --:
1 | TRI | 1
1 | TRI | 2
1 | TRI | 3
2 | ROHIT | 1
2 | ROHIT | 2
3 | RAVI | 1
db<>fiddle here

To count a column based on another column's repeating(same) entry

I want to create a report of calls last made based on weeks from last call and call-Group
Actual Data is like below with call id, date of call and call grouping
callid | Date | Group
----------------------------
1 | 6-1-18 | a1
2 | 6-1-18 | a2
3 | 7-1-18 | a3
4 | 8-1-18 | a1
5 | 9-1-18 | a2
6 | 9-1-18 | a4
Expected data is to display the number of calls for each call group corresponding to the number of week from last call
week | |
from | |
last |Group|Group
call | a1 | a2
--------------------
1 | 2 | 2 ->number of calls
2 | - | -
3 | 1 | -
4 | 2 | -
5 | - | 3
6 | - | -
Can anyone please tell me some solution for this
Although you data provided was a very small set and not rich enough to cover all cases, here is an sql that will calculate the number of weeks difference between each call and last call within a group and count the number of calls for each group for the particular week difference.
with your_table as (
select 1 as "callid", to_date('6-1-18','dd-mm-rr') as "date", 'a1' as "group" from dual
union select 2, to_date('6-1-18','mm-dd-rr'), 'a2' from dual
union select 3, to_date('7-1-18','mm-dd-rr'), 'a3' from dual
union select 4, to_date('8-1-18','mm-dd-rr'), 'a1' from dual
union select 5, to_date('9-1-18','mm-dd-rr'), 'a2' from dual
union select 6, to_date('6-1-18','mm-dd-rr'), 'a4' from dual
),
data1 as (
select t.*, max(t."date") over (partition by t."group") last_call_dt from your_table t
),
data2 as (select t.*, round((last_call_dt-t."date")/7,0) as weeks_diff from data1 t)
select * from (
select t.weeks_diff, t."callid", t."group" from data2 t
)
PIVOT
(
COUNT("callid")
FOR "group" IN ('a1', 'a2', 'a3','a4')
)
order by weeks_diff
to try it out with your table just make the following change:
with your_table as (select * from my_table), ....
let me know how it goes :)

Select unique combinations (unique on both sides)

EDIT: added a link to Fiddle for a more comprehensive sample (actual dataset)
I wonder if the below is possible in SQL, in BigQuery in particular, and in one SELECT statement.
Consider following input:
Key | Value
-----|-------
a | 2
a | 3
b | 2
b | 3
b | 5
c | 2
c | 5
c | 7
Logic: select the lowest value "available" for each key. Available meaning not yet assigned/used. See below.
Key | Value | Rule
-----|-------|--------------------------------------------
a | 2 | keep
a | 3 | ignore because key "a" has a value already
b | 2 | ignore because value "2" was already used
b | 3 | keep
b | 5 | ignore because key "b" has a value already
c | 2 | ignore because value "2" was already used
c | 5 | keep
c | 7 | ignore because key "c" has a value already
Hence expected outcome:
Key | Value
-----|-------
a | 2
b | 3
c | 5
Here the SQL to create the dummy table:
with t as ( select
'a' key, 2 value UNION ALL select 'a', 3
UNION ALL select 'b', 2 UNION ALL select 'b', 3 UNION ALL select 'b', 5
UNION ALL select 'c', 2 UNION ALL select 'c', 5 UNION ALL select 'c', 7
)
select * from t
EDIT: here another dataset
Not sure what combination of FULL JOIN, DISTINCT, ARRAY or WINDOW functions I can use.
Any guidance is appreciated.
EDIT: This is an incorrect answer that worked with the original example dataset, but has issues (as seen with comprehensive sample). I'm leaving it here for now to maintain comment history.
I don't have a specific BigQuery answer, but here is one SQL solution using a Common Table Expression and recursion.
WITH MyCTE AS
(
/* ANCHOR SUBQUERY */
SELECT MyKey, MyValue
FROM MyTable t
WHERE t.MyKey = (SELECT MIN(MyKey) FROM MyTable)
UNION ALL
/* RECURSIVE SUBQUERY */
SELECT t.MyKey, t.MyValue
FROM MyTable t
INNER JOIN MyCTE c
ON c.MyKey < t.MyKey
AND c.MyValue < t.MyValue
)
SELECT MyKey, MIN(MyValue)
FROM MyCTE
GROUP BY MyKey
;
Results:
Key | Value
-----|-------
a | 2
b | 3
c | 5
SQL Fiddle

SQL: How do dynamically parse ranges from min to next min?

I am trying to figure out how (or see if possible) I can grab the minimum value of 'TEST' and get just the next minimum value of 'TEST', then join again and go after the 2nd minimum value, etc, etc.
Table1
ID TEST
A12 1
A12 2
A12 3
A12 5
A12 8
B35 1
B35 3
Results I'm after:
ID RANGE1 RANGE2
A12 1 2
A12 2 3
A12 3 5
A12 5 8
B35 1 3
table code I'm using:
WITH FRED AS
(
SELECT 'A12' AS ID
, 1 AS TEST
UNION
SELECT 'A12' AS ID
, 2 AS TEST
UNION
SELECT 'A12' AS ID
, 3 AS TEST
UNION
SELECT 'B35' AS ID
, 1 AS TEST
UNION
SELECT 'B35' AS ID
, 2 AS TEST
)
SELECT *
FROM FRED F
This works for me:
--Construct sample data.
WITH FRED AS
(
SELECT 'A12' AS ID
, 1 AS TEST
UNION
SELECT 'A12' AS ID
, 2 AS TEST
UNION
SELECT 'A12' AS ID
, 3 AS TEST
UNION
SELECT 'A12' AS ID
, 5 AS TEST
UNION
SELECT 'A12' AS ID
, 8 AS TEST
UNION
SELECT 'B35' AS ID
, 1 AS TEST
UNION
SELECT 'B35' AS ID
, 3 AS TEST
),
--This is the sample data
test_data AS
(
SELECT *
FROM FRED F
ORDER BY ID, TEST
)
SELECT td.ID, td.TEST as RANGE1, MIN(td2.TEST) as RANGE2
FROM test_data td
cross join test_data td2
WHERE td.ID = td2.ID
AND
td.TEST < td2.TEST
GROUP BY td.ID, td.TEST
ORDER BY td.ID, td.TEST
test_data gives the sample you provided. I can try to handle repeated value cases once you let us all know how you want those to be handled. The current code above doesn't do that.
Assuming this is mysql (but most other DBMS will do the same) you can join the table with itself and use the limit in the ON clause.
Minima and grouping helps you from there.
e.g.:
mysql> SELECT m.id, m.test AS RANGE1 ,min(mm.test) AS RANGE2
FROM table1 AS m
INNER JOIN table1 AS mm ON m.id=mm.id AND mm.test > m.test
GROUP BY m.id,m.test;
+------+--------+--------+
| id | RANGE1 | RANGE2 |
+------+--------+--------+
| a12 | 1 | 2 |
| a12 | 2 | 3 |
| a12 | 3 | 5 |
| a12 | 5 | 8 |
| b35 | 1 | 3 |
+------+--------+--------+
5 rows in set (0.00 sec)
Take care with the efficiency of these things.
You might need to look into how to deal with edge cases where no second value is present in the table.
See also here: https://dba.stackexchange.com/questions/24014/how-do-i-get-the-current-and-next-greater-value-in-one-select