Using 'AND' in a many-to-many relationship - sql

I have a Users table and a Groups table. Users can be in multiple groups via a 'UserInGroup' table and Groups can have a 'GroupTypeId'.
[User]
--------------
Id | Name
1 | Bob
2 | James
[UserInGroup]
-----------------
UserId | GroupId
1 1
1 2
[Group]
Id | Name | TypeId
------------------------
1 | Directors | 1
2 | IT | 1
3 | London | 2
I want to create a query to return for example users that are in both 'Directors' AND 'London' (rather than 'Directors' OR 'London'). However, I only want to AND groups of a different 'Type', I want to OR groups of the same type. I could do with having a separate table per group type but I can't as they are created dynamically.
Ideally I want to be able to query users who are in 'Directors' OR 'IT' AND 'London'.
What is the most efficient way of doing this?

This problem is commonly known as Relational Division.
SELECT a.Name
FROM [user] a
INNER JOIN UserInGroup b
ON a.ID = b.UserID
INNER JOIN [Group] c
ON b.groupID = c.TypeId
WHERE c.Name IN ('Directors','London')
GROUP BY a.Name
HAVING COUNT(*) = 2
SQLFiddle Demo
SQL of Relational Division
But if a UNIQUE constraint was not enforce on GROUP for every USER, DISTINCT keywords is needed to filter out unique groups:
SELECT a.Name
FROM [user] a
INNER JOIN UserInGroup b
ON a.ID = b.UserID
INNER JOIN [Group] c
ON b.groupID = c.TypeId
WHERE c.Name IN ('Directors','London')
GROUP BY a.Name
HAVING COUNT(DISTINCT c.Name) = 2
OUTPUT from both queries
╔══════╗
║ NAME ║
╠══════╣
║ Bob ║
╚══════╝

I arrived at the following solution (with help from J W and this article):
SELECT
u.Name UserName
FROM [User] u
INNER JOIN [UserInGroup] uig
ON uig.UserId = u.Id
INNER JOIN [Group] g
ON g.Id = uig.GroupId
WHERE
g.Id IN (1,2,3) -- these are the passed in groupids
GROUP BY
u.Name
having count(distinct g.TypeId)
= (select count(distinct g1.TypeId)
from [group] g1 where g1.Id IN (1,2,3))
This allows me to group the relational division by a discriminator field. An alternative would be this:
SELECT a.Name
FROM [User] a
INNER JOIN
(
SELECT b.UserID
FROM UserInGroup b
INNER JOIN [Group] c
ON b.groupID = c.Id
WHERE c.Name IN ('Directors','IT')
GROUP BY b.UserID
HAVING COUNT(DISTINCT c.Name) >= 1
) b ON a.ID = b.UserID
INNER JOIN
(
SELECT DISTINCT b.UserID
FROM UserInGroup b
INNER JOIN [Group] c
ON b.groupID = c.Id
WHERE c.Name = 'London'
) c ON a.ID = c.UserID
With an extra join for each GroupTypeId. Execution plans look similar, so I went with the first option.

Related

oracle join or union

I have a condition where i get the users data from table A or table B wherever the data exists.
I cannot write a condition in java but it has to be done totally in oracle and result set sent to UI.
How can i do this in oracle sql.
Users table:
userId firstName LastName MI
11 AAA 111 A1
12 BBB 222 B2
TableA
UserId ROLE firstName LastName MI Grade
11 MANAGER AAA 111 A1 A1
TableB
UserId ROLE firstName LastName MI Grade
12 LEAD BBB 222 B2 A4
OutPut should be:
UserId FirstName LastName MI ROLE Grade
11 AAA 111 A1 MANAGER A1
12 BBB 222 B2 LEAD A4
Select * from users u, tableA a, tableB b
where u.userId = a.userId
and u.userId = b.userId
and u.userId = :userId
Where :userId is the data passed from UI.
I have to get the data from tableA 1st and only when not found I have to get from tableB
Should I do a union or join which is more efficient?
You can try to use UNION ALL on tableA and tableB, then do JOIN instead of ,(CROSS JOIN)
SELECT u,*,t1.ROLE,t1.Grade
FROM (
SELECT UserId ,ROLE ,Grade
FROM tableA
UNION ALL
SELECT UserId ,ROLE,Grade
FROM tableB
) t1 INNER JOIN users u
ON u.UserId = t1.UserId
I would use left join:
select u.*,
(case when a.userid is not null then 'MANAGER' else 'LEAD' end) as role,
coalesce(a.grade, b.grade) as grade
from users u left join
tablea a
on a.userid = u.userid left join
tableb b
on b.userid = u.userid;

SQL Server: Subquery on a join

I have two tables with schema and data as below. Table A has an id and an associated name. Table B associates the id from Table A with a price and otherAttr. For each entry in Table A, there may be multiple entries with different prices in Table B, but otherAttr is the same for each entry.
Given an id for Table A, I would like to select the name, otherAttr, and the minimum price.
The below query returns multiple results, I need to write a query that will return a single result with the minimum price.
SELECT a.name, b.price, b.otherAttr
FROM A a
LEFT Join B b on b.idFromA = 1
WHERE a.id = 1
Table A Table B
id | name idFromA | price | otherAttr
-------- ---------------------------
1 | A 1 | 200 | abc
2 | B 1 | 300 | abc
1 | 400 | abc
2 | 20 | def
2 | 30 | def
2 | 40 | ef
I massively oversimplified my example. In addition to selecting the min price and otherAttr from Table B, I also have to select a bunch of other attributes from joins on other tables. Which is why I was thinking the Group By and Min should be a subquery of the join on Table B, as a way to avoid Grouping By all the attributes I am selecting (because the attributes being selected for vary programmatically).
The Actual query looks more like:
SELECT a.name, b.price, b.otherAttr, c.x, c.y, d.e, d.f, g.h....
FROM A a
LEFT Join B b on b.idFromA = 1
LEFT Join C c on something...
LEFT Join D d on something...
LEFT Join G g on something...
WHERE a.id = 1
To get this, you could use GROUP BY in an INNER query:
SELECT gd.name, gd.price,gd.otherAttr, c.x, c.y, d.e, d.f, g.h....
FROM
(SELECT a.id,a.name, MIN(b.price) as price, b.otherAttr
FROM A a
LEFT Join B b on b.idFromA = 1
WHERE a.id = 1
GROUP BY a.id,a.name,b.otherAttr) gd
LEFT Join B b on b.idFromA = 1
LEFT Join C c on something...
LEFT Join D d on something...
LEFT Join G g on something...
Try:-
SELECT a.name, MIN(b.price) minprice, b.otherAttr
FROM A a
LEFT Join B b ON a.Id = b.idFromA
GROUP BY a.name, b.otherAttr
HAVING a.id = 1
You could just do this instead:
SELECT a.name, MIN(b.price), MIN(b.otherAttr)
FROM TableA a
LEFT JOIN TableB b on b.idFromA = a.id
GROUP BY a.name
HAVING a.id = 1;
You need to inner join on price as well in addition to id on the subquery to intersect the right record(s) with the lowest price(s). Then TOP(1) will return only one of those records. You can avoid using TOP(1) if you can expand the conditions and group by fields in the subquery so you schema can assure only a single record is returned for that combination of attributes. Lastly, avoid left joins when intersecting sets.
SELECT TOP(1) p.id, p.price, b.OtherAttr
FROM B as b
INNER JOIN
(SELECT A.id, min(B.price) as price
FROM B
INNER JOIN A on A.id=B.idFromA and A.ID=1
GROUP BY A.id) as p on b.idFromA=p.id and b.price=p.price

Complex sql server query

I need some help with a SQL query I'm working on. Here is a simplified version of the data I'm working with. I have 3 tables:
Contacts:
- ContactID
- ContactName
Submissions:
- SubmissionID
- ContactID
- SubmissionTypeID
SubmissionTypes:
- SubmissionTypeID
- SubmissionType
I need to return all of the Contacts (joined to Submissions on ContactID) where there are SubmissionTypeIDs that match up with a list of SubmissionTypeIDs. The tricky part is that I only want results where a Contact has a Submission record with a SubmissionTypeID that matches each of the values in the list. So, for instance, if I had this data:
Contacts
----------------
1 | Jim Johnson
2 | Sally Anderson
SubmissionTypes
----------------------
1 | Contact Form
2 | Request Form
3 | Generic Form
Submissions
----------------------
1 | 1 | 1
2 | 1 | 2
3 | 2 | 1
If my SubmissionTypeID values are 1 and 2, I'd want to get the following results:
Jim Johnson | Contact Form
Jim Johnson | Request Form
I wouldn't want to see Sally Anderson because she doesn't have a record in Submissions for both values.
I'm guessing there are a few ways to do this. I'm excited to see your ideas.
Thank you!
Here's a convoluted way using double negation.
declare #list table (SubmissionTypeID int not null primary key);
insert into #list values (1), (2); -- values to search for.
with c as (
select
c.ContactID,
c.ContactName
from
Contacts c
where
not exists (
select
'x'
from
#list l
where
not exists (
select
'x'
from
Submissions s
where
s.ContactID = c.ContactID and
s.SubmissionTypeID = l.SubmissionTypeID
)
)
)
select
c.ContactName,
t.SubmissionType
from
c
inner join
Submissions s
on c.ContactId = s.ContactId
inner join
SubmissionTypes t
on s.SubmissionTypeID = t.SubmissionTypeID
inner join
#list l
on t.SubmissionTypeID = l.SubmissionTypeID;
Example SQLFiddle
One way is with INTERSECT:
select c.contactname, t.submissiontype
from contacts c
join submissions s
on c.contactid = s.contactid
join submissiontypes t
on s.submissiontypeid = t.submissiontypeid
join (select c.contactid
from contacts c
join submissions s
on c.contactid = s.contactid
where s.submissiontypeid = 1
intersect
select c.contactid
from contacts c
join submissions s
on c.contactid = s.contactid
where s.submissiontypeid = 2) v
on c.contactid = v.contactid
where s.submissiontypeid in (1, 2)
Fiddle: http://sqlfiddle.com/#!6/9ee4e/2/0
You can also COUNT where equal to 2 (you have 2 values you're checking for):
select c.contactname, t.submissiontype
from contacts c
join submissions s
on c.contactid = s.contactid
join submissiontypes t
on s.submissiontypeid = t.submissiontypeid
join (select c.contactid
from contacts c
join submissions s
on c.contactid = s.contactid
where s.submissiontypeid in (1, 2)
group by c.contactid
having count(distinct s.submissiontypeid) = 2) v
on c.contactid = v.contactid
where s.submissiontypeid in (1, 2)
Fiddle: http://sqlfiddle.com/#!6/9ee4e/1/0
Try this.. It works fine to me
DECLARE #list TABLE (SubmissionTypeID int not null primary key);
INSERT INTO #list VALUES(1); -- values to search for.
SELECT C.ContactName, ST.SubmissionTypeName
FROM
(
SELECT *, ROW_NUMBER() OVER(PARTITION BY S.ContactID ORDER BY S.ContactID) AS RCount
FROM
Submission S
WHERE EXISTS
(SELECT 1 FROM #list l WHERE l.SubmissionTypeID = S.SubmissionTypeID)) AS Result
INNER JOIN Submission S1 ON S1.ContactID = Result.ContactID
INNER JOIN Contacts C ON C.ContactID = Result.ContactID
INNER JOIN SubmissionTypes ST ON ST.SubmissionTypeID = S1.SubmissionTypeID
WHERE RCOunt = (SELECT COUNT(DISTINCT SubmissionTypeID) FROM #list)
AND EXISTS
(SELECT 1 FROM #list l WHERE l.SubmissionTypeID = ST.SubmissionTypeID)
;

Join a table with one table if condition1 is true and with another table if condition1 is false?

I have the following tables :
User_Group
id group_id group_type
------------------------
1 100 A
1 100 B
2 101 B
2 102 A
Group_A
id name
---------
100 A
101 B
102 C
Group_B
id name
---------
100 D
101 E
102 F
I want the group names of all users (using array.agg()). We have to get the group name from group A if the user's group type = A and from group B if the user's group type = B. The result should be :
userid groups
--------------
1 A,D
2 E,C
I have created a fiddle for this, and given a solution using union of 2 separate queries. Can it be done without the union, something in which I can decide on which table to pick the group name from with a single join of user_groups, group_A and group_B ?
select ug.id, array_agg(
case ug.group_type
when 'A' then g_a.name
when 'B' then g_b.name
else 'N/A'
end)
from user_groups ug
left outer join group_A g_a on ug.group_id = g_a.id
left outer join group_B g_b on ug.group_id = g_b.id
group by ug.id
SQL Fiddle Example
You can do this without union using left joins (I'd advise using explicit joins anyway since implicit joins are 20 years out of date Aaron Bertrand has written a good blog as to why). The Group_Type can become a join condition meaning the table is only joined when the right group type is found:
SELECT ug.ID, ARRAY_AGG(COALESCE(a.Name, b.Name))
FROM User_Groups ug
LEFT JOIN group_A a
ON a.ID = ug.group_ID
AND ug.Group_Type = 'A'
LEFT JOIN group_B b
ON b.ID = ug.group_ID
AND ug.Group_Type = 'B'
WHERE COALESCE(a.ID, b.ID) IS NOT NULL -- ENSURE AT LEAST ONE GROUP IS MATCHED
GROUP BY ug.ID;
However I would be inclined to use a UNION Still, but move it as follows:
SELECT ug.ID, ARRAY_AGG(Name)
FROM User_Groups ug
INNER JOIN
( SELECT 'A' AS GroupType, ID, Name
FROM Group_A
UNION ALL
SELECT 'B' AS GroupType, ID, Name
FROM Group_B
) G
ON g.GroupType = ug.Group_Type
AND g.ID = ug.Group_ID
GROUP BY ug.ID;
Your Fiddle with my queries added

SQL - Group By Distinct Values

My question, is there a faster way to the following query?
I'm using ORACLE 10g
Say i have a table Manufacturer and Car, and i want to count all occurrences of the column 'Car.Name'. here is How i'd do it:
SELECT manuf.Name, COUNT(car1.Name), COUNT(car2.Name), COUNT(car3.Name)
FROM Manufacturer manuf
LEFT JOIN (SELECT * FROM Car c where c.Name = 'Ferrari1') car1 ON manuf.PK = car1.ManufPK
LEFT JOIN (SELECT * FROM Car c where c.Name = 'Ferrari2') car2 ON manuf.PK = car2.ManufPK
LEFT JOIN (SELECT * FROM Car c where c.Name = 'Ferrari3') car3 ON manuf.PK = car3.ManufPK
GROUP BY manuf.Name
Wanted Results:
Manufacturer | Ferrari1 | Ferrari2 | Ferrari3
----------------------------------------------
Fiat | 1 | 0 | 5
Ford | 2 | 3 | 0
I tried this with few LEFT JOINs, and it worked fine. But when i added a lot (like 90+), it was ultra slow (more than 1 minute).
My question, is there a faster way to do this query?
If you are happy to see the cars counted down the page, try:
select m.Name manufacturer_name,
c.Name car_name,
count(*)
from Manufacturer m
left join Car c
on m.PK = c.ManufPK and c.Name in ('Ferrari1','Ferrari2','Ferrari3')
group by m.Name, c.Name
If you need to see individual cars across the page, try:
select m.Name manufacturer_name,
sum(case c.Name when 'Ferrari1' then 1 else 0 end) Ferrari1_Count,
sum(case c.Name when 'Ferrari2' then 1 else 0 end) Ferrari2_Count,
sum(case c.Name when 'Ferrari3' then 1 else 0 end) Ferrari3_Count
from Manufacturer m
left join Car c
on m.PK = c.ManufPK and c.Name in ('Ferrari1','Ferrari2','Ferrari3')
group by m.Name
SELECT manuf.Name, COUNT(DISTINCT c.Name)
FROM Manufacturer manuf
LEFT JOIN Car c ON manuf.PK = c.ManufPK
GROUP BY manuf.Name
OR depending on your needs
SELECT manuf.Name, c.Name, COUNT(*) Cnt
FROM Manufacturer manuf
LEFT JOIN Car c ON manuf.PK = c.ManufPK
GROUP BY manuf.Name, c.Name
PS: Your question is not very clear. Provide some wanted resultset to refine the answer
You can also try this:
SELECT manuf.Name
, car1.cnt AS Ferrari1
, car2.cnt AS Ferrari2
, car3.cnt AS Ferrari3
FROM
Manufacturer AS manuf
LEFT JOIN
( SELECT ManufPK, COUNT(*) AS cnt
FROM Car
WHERE Name = 'Ferrari1'
GROUP BY ManufPK
) AS car1
ON car1.ManufPK = manuf.PK
LEFT JOIN
( SELECT ManufPK, COUNT(*) AS cnt
FROM Car
WHERE Name = 'Ferrari2'
GROUP BY ManufPK
) AS car2
ON car2.ManufPK = manuf.PK
LEFT JOIN
( SELECT ManufPK, COUNT(*) AS cnt
FROM Car
WHERE Name = 'Ferrari3'
GROUP BY ManufPK
) AS car3
ON car3.ManufPK = manuf.PK
ORDER BY manuf.Name