SQL Server : query multivalued table recursive - sql

I have a SQL problem which I cannot solve
There are 2 tables mms and mms_mv which are linked via object_id.
The mms_mv is a multivalue table and the content is group memberships and group manager which also can be an other group.
This runs on SQL Server
mms:
|object_id|attribute_type|objectSid|
| 1 |user | a |
| 2 | group | b |
| 3 | group | c |
| 4 | group | d |
| 5 | group | f
mms_mv:
|object_id|attribute_name|reference_id|
| 2 | member | 1 |
| 3 | manager | 1 |
| 4 | manager | 2 |
I am trying to find out which groups a user can manage either directly or indirectly via nested groups.
In the example above the user (1) is member of group Number 2 and group 2 is Manager of group 4
user 1 is manager of group 3 directly.
Which groups can be managed by the user?
So the output I need is group 3 and 4
select
accountname, objectsid, mms1.reference_id as ManagerID,
mms2.object_id
from
dbo.mms_mv_link as mms1 with (nolock)
inner join
dbo.mms_metaverse as mms2 with (nolock) on mms1.object_id = mms2.object_id
where
mms2.object_type ='group'
and mms1.attribute_name = 'manager'
and mms1.reference_id in (1, 3)
This is the best I came up with to find out which of all Group id's and user id I submitted are Manager of a Group. I used an other lookup to get the groups a user is in.
My problem are the nested groups, by long thinking and googling I am not sure if it is even possible to create such a query.
I can find out all groups a user is member of, but I also need the Groups in which these groups are members.
Well I am happy if anyone has some ideas or hints for me to figure this one out.
I am even happy if you have a recommendation for a good sql book which covers such complex queries.
Thank you all for helping me.

I think that the following recursive CTE will give you what you want:
;WITH cte AS
(
SELECT m2.object_id AS groupID, m2.attribute_name
FROM #mms AS m1
INNER JOIN #mms_mv AS m2 ON m1.object_id = m2.reference_id
INNER JOIN #mms m3 ON m2.object_id = m3.object_id
WHERE m1.attribute_type = 'user' AND m3.attribute_type = 'group'
UNION ALL
SELECT m.object_id AS groupID, m.attribute_name
FROM cte AS c
INNER JOIN #mms_mv AS m ON c.groupID = m.reference_id
)
SELECT *
FROM cte
WHERE attribute_name <> 'member'
The so-called 'anchor' query of the CTE returns all groups that every user either manages or is member of. Using recursion we get all other groups managed by either the groups of the original set or by any 'intermediate' set.
With these data as input:
DECLARE #mms TABLE (object_id INT, attribute_type VARCHAR(10), objectSid VARCHAR(10))
DECLARE #mms_mv TABLE (object_id INT, attribute_name VARCHAR(10), reference_id INT)
INSERT #mms VALUES
( 1, 'user', 'a'),
( 2, 'group', 'b'),
( 3, 'group', 'c'),
( 4, 'group', 'd'),
( 5, 'group', 'f')
INSERT #mms_mv VALUES
( 2, 'member', 1),
( 3, 'manager', 1),
( 4, 'manager', 2),
( 5, 'manager', 3)
the above query yields the following output:
groupID attribute_name
----------------------
3 manager
5 manager
4 manager

Related

SQL - How to check if users are in the same hierarchy?

I want to find out if users are directly in a parent child relation.
Given my user table schema
User_id | Parent_ID | Name
For example, I have a list of user_id's and I want to know if they are all in the same hierarchical tree.
I have tried using CTE recursive.
Sample data
User_id | Parent_ID | Name
1 | | A
2 | 1 | B
3 | 2 | C
4 | 3 | D
5 | 2 | E
6 | | F
7 | 6 | G
user_id varchar(100)
parent_id varchar(100)
Desired result: Input [2,3,4] => Same Team
Input [2,3,7] => Not same team
Use the top-level parents' parent_id as the hierarchy identifier:
with recursive hierarchies as (
select user_id, user_id as hierarchy_id
from ttable
where parent_id is null
union all
select c.user_id, p.hierarchy_id
from hierarchies p
join ttable c on c.parent_id = p.user_id
)
select * from hierarchies;
With that mapping of each user_id to a single hierarchy_id, you can join to your list of users.
EDIT BEGINS
Since you added sample data and example results that do not match your original question, here is an example of how any minimally competent programmer could slightly tweak the above to match the newly added contradictory examples:
with recursive subhierarchies as (
select user_id, array[user_id] as path
from ttable
where parent_id is null
union all
select c.user_id, p.path||c.user_id as path
from subhierarchies p
join ttable c on c.parent_id = p.user_id
)
select d.user_ids, count(s.path) > 0 as same_team
from (values (array[2, 3, 4]), (array[2, 3, 6])) as d(user_ids)
left join subhierarchies s
on s.path #> d.user_ids
group by d.user_ids
;

How to convert JSONB array of pair values to rows and columns?

Given that I have a jsonb column with an array of pair values:
[1001, 1, 1002, 2, 1003, 3]
I want to turn each pair into a row, with each pair values as columns:
| a | b |
|------|---|
| 1001 | 1 |
| 1002 | 2 |
| 1003 | 3 |
Is something like that even possible in an efficient way?
I found a few inefficient (slow) ways, like using LEAD(), or joining the same table with the value from next row, but queries take ~ 10 minutes.
DDL:
CREATE TABLE products (
id int not null,
data jsonb not null
);
INSERT INTO products VALUES (1, '[1001, 1, 10002, 2, 1003, 3]')
DB Fiddle: https://www.db-fiddle.com/f/2QnNKmBqxF2FB9XJdJ55SZ/0
Thanks!
This is not an elegant approach from a declarative standpoint, but can you please see whether this performs better for you?
with indexes as (
select id, generate_series(1, jsonb_array_length(data) / 2) - 1 as idx
from products
)
select p.id, p.data->>(2 * i.idx) as a, p.data->>(2 * i.idx + 1) as b
from indexes i
join products p on p.id = i.id;
This query
SELECT j.data
FROM products
CROSS JOIN jsonb_array_elements(data) j(data)
should run faster if you just need to unpivot all elements within the query as in the demo.
Demo
or even remove the columns coming from products table :
SELECT jsonb_array_elements(data)
FROM products
OR
If you need to return like this
| a | b |
|------|---|
| 1001 | 1 |
| 1002 | 2 |
| 1003 | 3 |
as unpivoting two columns, then use :
SELECT MAX(CASE WHEN mod(rn,2) = 1 THEN data->>(rn-1)::int END) AS a,
MAX(CASE WHEN mod(rn,2) = 0 THEN data->>(rn-1)::int END) AS b
FROM
(
SELECT p.data, row_number() over () as rn
FROM products p
CROSS JOIN jsonb_array_elements(data) j(data)) q
GROUP BY ceil(rn/2::float)
ORDER BY ceil(rn/2::float)
Demo

Join one table with two other ones by id

I am trying to join one table with two others that are unrelated to each other but are linked to the first one by an id
I have the following tables
create table groups(
id int,
name text
);
create table members(
id int,
groupid int,
name text
);
create table invites(
id int,
groupid int,
status int \\ 2 for accepted, 1 if it's pending
);
Then I inserted the following data
insert into groups (id, name) values(1,'group');
insert into members(id, groupid, name) values(1,1,'admin'),(1,1,'other');
insert into invites(id, groupid, status) values(1,1,2),(2,1,1),(3,1,1);
Obs:
The admin does not has an invite
The group has an approved invitation with status 2 (because the member 'other' joined)
The group has two pending invites with status 1
I am trying to do a query that gets the following result
groupid | name | inviteId
1 | admin | null
1 | other | null
1 | null | 2
1 | null | 3
I have tried the following querys with no luck
select g.id, m.name, i.id from groups g
left join members m ON m.groupid = g.id
left join invites i ON i.groupid = g.id and i.status = 1;
select g.id, m.name, i.id from groups g
join (select groupid, name from members) m ON m.groupid = g.id
join (select groupid, id from invites where status = 1) i ON i.groupid = g.id;
Any ideas of what I am doing wrong?
Because members and invites are not related, you need to use two separate queries and use UNION (automatically removes duplicates) or UNION ALL (keeps duplicates) to get the output you desire:
select g.id as groupid, m.name, null as inviteid from groups g
join members m ON m.groupid = g.id
union all
select g.id, null, i.id from groups g
join invites i ON (i.groupid = g.id and i.status = 1);
Output:
groupid | name | inviteid
---------+-------+----------
1 | admin |
1 | other |
1 | | 3
1 | | 2
(4 rows)
Without a UNION, your query implies that the tables have some sort of relationship, so the columns are joined side-by-side. Since you want to preserve the null values, implying that the tables are not related, you need to concatenate/join them vertically with UNION
Disclosure: I work for EnterpriseDB (EDB)

Recursive CTE with three tables

I'm using SQL Server 2008 R2 SP1.
I would like to recursively find the first non-null manager for a certain organizational unit by "walking up the tree".
I have one table containing organizational units "ORG", one table containing parents for each org. unit in "ORG", lets call that table "ORG_PARENTS" and one table containing managers for each organizational unit, lets call that table "ORG_MANAGERS".
ORG has a column ORG_ID:
ORG_ID
1
2
3
ORG_PARENTS has two columns.
ORG_ID, ORG_PARENT
1, NULL
2, 1
3, 2
MANAGERS has two columns.
ORG_ID, MANAGER
1, John Doe
2, Jane Doe
3, NULL
I'm trying to create a recursive query that will find the first non-null manager for a certain organizational unit.
Basically if I do a query today for the manager for ORG_ID=3 I will get NULL.
SELECT MANAGER FROM ORG_MANAGERS WHERE ORG_ID = '3'
I want the query to use the ORG_PARENTS table to get the parent for ORG_ID=3, in this case get "2" and repeat the query against the ORG_MANAGERS table with ORG_ID=2 and return in this example "Jane Doe".
In case the query also returns NULL I want to repeat the process with the parent of ORG_ID=2, i.e. ORG_ID=1 and so on.
My CTE attempts so far have failed, one example is this:
WITH BOSS (MANAGER, ORG_ID, ORG_PARENT)
AS
( SELECT m.MANAGER, m.ORG_ID, p.ORG_PARENT
FROM dbo.MANAGERS m INNER JOIN
dbo.ORG_PARENTS p ON p.ORG_ID = m.ORG_ID
UNION ALL
SELECT m1.MANAGER, m1.ORG_ID, b.ORG_PARENT
FROM BOSS b
INNER JOIN dbo.MANAGERS m1 ON m1.ORG_ID = b.ORG_PARENT
)
SELECT * FROM BOSS WHERE ORG_ID = 3
It returns:
Msg 530, Level 16, State 1, Line 4
The statement terminated. The maximum recursion 100 has been exhausted before statement completion.
MANAGER ORG_ID ORG_PARENT
NULL 3 2
You need to keep track of the original ID you start with. Try this:
DECLARE #ORG_PARENTS TABLE (ORG_ID INT, ORG_PARENT INT )
DECLARE #MANAGERS TABLE (ORG_ID INT, MANAGER VARCHAR(100))
INSERT #ORG_PARENTS (ORG_ID, ORG_PARENT)
VALUES (1, NULL)
, (2, 1)
, (3, 2)
INSERT #MANAGERS (ORG_ID, MANAGER)
VALUES (1, 'John Doe')
, (2, 'Jane Doe')
, (3, NULL)
;
WITH BOSS
AS
(
SELECT m.MANAGER, m.ORG_ID AS ORI, m.ORG_ID, p.ORG_PARENT, 1 cnt
FROM #MANAGERS m
INNER JOIN #ORG_PARENTS p
ON p.ORG_ID = m.ORG_ID
UNION ALL
SELECT m1.MANAGER, b.ORI, m1.ORG_ID, OP.ORG_PARENT, cnt +1
FROM BOSS b
INNER JOIN #ORG_PARENTS AS OP
ON OP.ORG_ID = b.ORG_PARENT
INNER JOIN #MANAGERS m1
ON m1.ORG_ID = OP.ORG_ID
)
SELECT *
FROM BOSS
WHERE ORI = 3
Results in:
+----------+-----+--------+------------+-----+
| MANAGER | ORI | ORG_ID | ORG_PARENT | cnt |
+----------+-----+--------+------------+-----+
| NULL | 3 | 3 | 2 | 1 |
| Jane Doe | 3 | 2 | 1 | 2 |
| John Doe | 3 | 1 | NULL | 3 |
+----------+-----+--------+------------+-----+
General tips:
Don't predefine the columns of a CTE; it's not necessary, and makes maintenance annoying.
With recursive CTE, always keep a counter, so you can limit the recursiveness, and you can keep track how deep you are.
edit:
By the way, if you want the first not null manager, you can do for example (there are many ways) this:
SELECT BOSS.*
FROM BOSS
INNER JOIN (
SELECT BOSS.ORI
, MIN(BOSS.cnt) cnt
FROM BOSS
WHERE BOSS.MANAGER IS NOT NULL
GROUP BY BOSS.ORI
) X
ON X.ORI = BOSS.ORI
AND X.cnt = BOSS.cnt
WHERE BOSS.ORI IN (3)

SQL Ignore duplicate primary keys

Imagine you have a string of results from a SELECT statement:
ID (pk) Name Address
1 a b
1 c d
1 e f
2 a b
3 a d
2 a d
Is it possible to alter the SQL statement to get one record ONLY for the record with ID 1?
I have a SELECT statement that displays multiple values which can have the same primary key. I want to only take one of those records, if say, I have 5 records with the same primary key.
SQL: http://pastebin.com/cFCBA2Uy
Screenshot: http://i.imgur.com/UlMBZhC.png
What I want is to show only one file which is for e.g. File Number: 925, 890
You stated that no matter which row to choose when there are more than one row for the same Id, you just want one row for each id.
The following query does what you asked for:
DECLARE #T table
(
id int,
name varchar(50),
address varchar(50)
)
INSERT INTO #T VALUES
(1, 'a', 'b'),
(1, 'c', 'd'),
(1, 'e', 'f'),
(2, 'a', 'b'),
(3, 'a', 'd'),
(2, 'a', 'd');
WITH A AS
(
SELECT
t.id, t.name, t.address,
ROW_NUMBER() OVER (PARTITION BY id ORDER BY (SELECT NULL)) AS RowNumber
FROM
#T t
)
SELECT
A.id, A.name, A.address
FROM
A
WHERE
A.RowNumber = 1
But I think there should be a criteria. If you find one, express your criteria as the ORDER BY inside the OVER clause.
EDIT:
Here you have the result:
+----+------+---------+
| id | name | address |
+----+------+---------+
| 1 | a | b |
| 2 | a | b |
| 3 | a | d |
+----+------+---------+
Disclaimer: the query I wrote is non-deterministic, different conditions (indexes, statistics, etc) might lead to different results.