How to join id occurrences instead of simply showing count(*)? - sql

I did this snippet to demonstrate: http://sqlfiddle.com/#!6/ed243/2
Schema:
create table professional(
id int identity(1,3) primary key,
name varchar(20)
)
insert into professional values('professional A')
insert into professional values('professional B')
insert into professional values('professional C')
create table territory(
id int identity(2,3) primary key,
name varchar(20)
)
insert into territory values('territory A')
insert into territory values('territory B')
insert into territory values('territory C')
create table panel(
id int identity(3,3) primary key,
idProfessional int not null,
idTerritory int not null,
)
insert into panel values(1, 2)
insert into panel values(4, 5)
insert into panel values(7, 8)
insert into panel values(1, 5)
insert into panel values(7, 8)
insert into panel values(7, 2)
And the query I've got so far:
select
p.id, p.name, count(*) as Territories
from
(select distinct idProfessional, idTerritory from panel) panel
inner join
professional p
on p.id = panel.idProfessional
group by
p.id,
p.name
having count(*) > 1
order by p.id
The above query shows as result in how many territories each professional works filtering with distinct and by showing only professionals that work in more than one territory with having:
-------------------------------------------------------
| id | name | Territories |
-------------------------------------------------------
| 1 | professional A | 2 |
| 7 | professional C | 2 |
-------------------------------------------------------
Ok, but.. is it possible to show in Territories each idTerritory joined like "2, 5" instead of count(*) ?
Thanks in advance.

When it's necessary, I usually use the FOR XML function to do this kind of concatenation of multiple rows. I think this query does what you are looking for:
select
p.id, p.name, STUFF(
(select ', ' + CAST(t.id AS VARCHAR(10))
from panel panel2
inner join territory t
ON t.id = panel2.idTerritory
where panel2.idProfessional = p.id
order by t.name
for xml path(''), root('XMLVal'), type
).value('/XMLVal[1]','varchar(max)')
, 1, 2, '') as Territories
from panel
inner join
professional p
on p.id = panel.idProfessional
group by
p.id,
p.name
having count(*) > 1
order by p.id
I used this blog in creating my answer: http://blogs.lobsterpot.com.au/2010/04/15/handling-special-characters-with-for-xml-path/

Related

How to use write a SQL join when there exists empty cell in the join column?

Schema:
CREATE TABLE PRODUCT
(
PRODUCTID INT,
PRODUCTNAME VARCHAR(100),
PRODUCTUSER VARCHAR(100)
);
CREATE TABLE USER
(
USERID INT,
USERNAME VARCHAR(100),
USEREMAIL VARCHAR(100)
);
INSERT INTO PRODUCT(PRODUCTID,PRODUCTNAME,PRODUCTUSER)
VALUES (1, 'Product1', 'Chen'), (2, 'Product2', 'Bob'),
(3, 'Product3', ''), (4, 'Product4', '');
INSERT INTO USER (USERID, USERNAME, USEREMAIL)
VALUES (1, 'Chen', 'chen#email.com'),
(2, 'Bob', 'bob#email.com'),
(3, 'Paul', 'paul#email.com'),
(4, '', ''), (5, '', '');
Product table:
ProductID ProductName ProductUser
------------------------------------
1 Product1 Chen
2 Product2 Bob
3 Product3
4 Product4
User table:
UserID UserName UserEmail
--------------------------------
1 Chen chen#email.com
2 Bob bob#email.com
3 Paul paul#email.com
4
5
I want to join Product and User table to get all the Product Names and the User name (if available) as output. The challenge is that the common field Product.ProductUser and User.UserName both contain empty values as shown in example above. I know this is not a good schema design but I am cannot change the schema as it is out of my control.
Expected output:
PROCUTNAME USERNAME
----------------------
Product1 Chen
Product2 Bob
Product3
Product4
Sample query 1:
SELECT PRODUCTNAME, USERNAME
FROM PRODUCT P
JOIN USER U
ON P.PRODUCTUSER=U.USERNAME
Above query is an inner join so returns duplicates for Product3 and Product4 due to the join on empty. Adding a WHERE clause ProductUser<>'' results in exclusion of Product3 and Product4.
Usually if the ProductUser were NULL instead of empty then I know that a LEFT JOIN would be the solution. I think that I understand the difference between using the criteria in the ON clause and WHERE clause of the LEFT JOIN.
So trying a left join with criteria in the ON clause:
SELECT PRODUCTNAME, USERNAME
FROM PRODUCT P
LEFT JOIN USER U
ON P.PRODUCTUSER=U.USERNAME AND P.PRODUCTUSER<>''
So the above query works by 1st doing an inner join based on the given criteria in ON clause, and then pulls in all other rows from the product table and puts a null for username. (or in other words it first lists all products, and then joins on those records that satisfy the ON criteria.)
This gives me output as expected. But I am not sure whether my approach is correct so trying another approach:
SELECT PRODUCTNAME, USERNAME
FROM PRODUCT P
LEFT JOIN (SELECT * FROM USER WHERE USERNAME<>'') U
ON P.PRODUCTUSER=U.USERNAME
This also works.
Is the left join with criteria in the ON clause a correct way to approach the problem?
Can you just do a group by to remove the duplicates?
declare #prod TABLE (PRODUCTID INT, PRODUCTNAME VARCHAR(100), PRODUCTUSER VARCHAR(100));
declare #user table (USERID INT, USERNAME VARCHAR(100),USEREMAIL VARCHAR(100));
INSERT INTO #prod(PRODUCTID,PRODUCTNAME,PRODUCTUSER) values (1,'Product1','Chen'),
(2,'Product2','Bob'),(3,'Product3',''),(4,'Product4','');
INSERT INTO #user(USERID,USERNAME,USEREMAIL) VALUES (1,'Chen','chen#email.com'),
(2,'Bob','bob#email.com'),(3,'Paul','paul#email.com'),
(4,'',''),(5,'','');
select * from #prod
select * from #user
select
p.ProductId,
p.ProductName,
u.USERNAME
FROM #prod p
left join #user u on p.PRODUCTUSER = u.USERNAME
group by
p.PRODUCTID,
p.PRODUCTNAME,
u.USERNAME
select
main.PRODUCTNAME
, case
when sub.USERNAME is null
then ''
else sub.USERNAME
end USERNAME
from #PRODUCT main
left join
#USER sub
on main.PRODUCTUSER = sub.USERNAME
and sub.USERNAME like '%[A-Za-z]%'

SQL Server : select IF NOT EXISTS comparing date and time in 3 related tables

I have 3 tables, and I want to select all names, unless they have a block that covers a certain time frame of the game they have entered. For example, Jane has a block on game 2, which starts at 11:00, so she is not available for any game that starts at 11:00. She is available at 8:00, so she will be selected for game 1.
Officials tbl
RefId Name
---------------------
1 Jack
2 Sam
3 Jane
Games tbl Blocks tbl
GameId GameDate/Time BlockId RefId GameId
------------------------- ------------------------------
1 8/21/2021 8:00 1 2 1
2 8/21/2021 11:00 2 3 2
3 8/21/2021 11:00
Desired output
If Game 1 is selected: Jack Jane
If Game 2 is selected: Jack, Sam
If Game 3 is selected: Jack, Sam
I have tried similar SQL to the following and I am unable to get desired result:
Select a.GameId, a.GameDate o.Name
From Games a
Left Outer Join Blocks b On a.GameId = b.GameId
Left Outer Join Officials o On b.RefId = o.RefId
Where not exists ---the DateTime of the block = DateTime of the game
Schema:
create table Officials (
[Id] int identity not null,
[Name] nvarchar(255),
constraint PK_Officials primary key (Id)
)
create table Games (
Id int identity not null,
StartOn datetime,
constraint PK_Games primary key (Id)
)
create table Blocks (
Id int identity not null,
OfficialId int not null,
GameId int not null,
constraint PK_Block primary key (Id),
constraint AK_Block unique (OfficialId, GameId),
constraint FK_Block_Officials foreign key (OfficialId) references Officials (Id),
constraint FK_Block_Games foreign key (GameId) references Games (Id),
)
insert into Officials ([Name]) values
('Jack'),
('Sam'),
('Jane')
insert into Games (StartOn) values
('2021-08-21 08:00'),
('2021-08-21 11:00'),
('2021-08-21 11:00')
insert into Blocks (OfficialId, GameId) values
(2, 1),
(3, 2)
Blocks.Id is unnecessary, you can have composite primary key from foreign keys.
I removed unnecessary game prefixes from the game table
I renamed primary keys to be less confusing (for example at first look it is not clear if Officials.RefId is primary or foreign key so I renamed it to Officials.Id.
Query:
-- Game for which we want to display free officials.
-- 1: Jack, Jane
-- 2: Jack, Sam
-- 3: Jack, Sam
declare #gameId int = 3
-- Get datetime when the game starts.
declare #gameStartOn datetime =
(
select g.StartOn
from dbo.Games g
where g.Id = #gameId
)
-- Get all officicals not blocked for specified game start.
select * from dbo.Officials o
where o.Id not in (
-- Get all officials blocked for specified game start.
select o.Id
from dbo.Blocks b
join dbo.Officials o on b.OfficialId = o.Id
join dbo.Games g on b.GameId = g.Id
where g.StartOn = #gameStartOn
)
In common table expression part I have joined Games and Blocks table to get the list of games along with respective the blocked RefId.
select b.RefId,g2.GameId from Games g inner join Blocks b
on g.GameId=b.GameId
inner join Games g2
on g.GameDate_Time=g2.GameDate_Time
Then with not exists I have removed those RefId from Officials table for a particular game.
DB-Fiddle:
Schema and insert statements:
create table Officials( RefId int, Name varchar(50));
insert into Officials values(1, 'Jack');
insert into Officials values(2, 'Sam');
insert into Officials values(3, 'Jane');
create table Games(GameId int, GameDate_Time datetime);
insert into Games values(1, '8/21/2021 8:00');
insert into Games values(2, '8/21/2021 11:00');
insert into Games values(3, '8/21/2021 11:00');
create table Blocks(BlockId int, RefId int, GameId int);
insert into Blocks values(1, 2, 1);
insert into Blocks values(2, 3, 2);
Query for Game 1:
with BlockGames as
(
select b.RefId,g2.GameId from Games g inner join Blocks b
on g.GameId=b.GameId
inner join Games g2
on g.GameDate_Time=g2.GameDate_Time
)
Select * from Officials o
where not exists
(
select 1 from BlockGames bg
where o.RefId=bg.RefId and GameId=1
)
Output:
RefId
Name
1
Jack
3
Jane
Query for Game 2:
with BlockGames as
(
select b.RefId,g2.GameId from Games g inner join Blocks b
on g.GameId=b.GameId
inner join Games g2
on g.GameDate_Time=g2.GameDate_Time
)
Select * from Officials o
where not exists
(
select 1 from BlockGames bg
where o.RefId=bg.RefId and GameId=2
)
Output:
RefId
Name
1
Jack
2
Sam
Query for Game 3:
with BlockGames as
(
select b.RefId,g2.GameId from Games g inner join Blocks b
on g.GameId=b.GameId
inner join Games g2
on g.GameDate_Time=g2.GameDate_Time
)
Select * from Officials o
where not exists
(
select 1 from BlockGames bg
where o.RefId=bg.RefId and GameId=3
)
Output:
RefId
Name
1
Jack
2
Sam
db<>fiddle here
Just add your datetime check to the left join. If the joined table's fields are null, then no such pair exists.
Select a.GameId, a.GameDate o.Name From Games a
Left Outer Join Blocks b On a.GameId = b.GameId or a.DateTime = b.DateTime
Left Outer Join Officials o On b.RefId = o.RefId
where b.DateTime is null
and then there's this approach, to find which officials are available for which games, over all games and officials:
WITH cte1 AS (
SELECT g.GameId, g.GameDate, o.RefId, o.Name
FROM Games AS g
JOIN Officials AS o
ON NOT EXISTS (
SELECT 1 FROM Blocks AS b
JOIN Games AS g2
ON g2.GameDate = g.GameDate
AND b.RefId = o.RefId
AND b.GameId = g2.GameId
)
)
SELECT list.GameId, LEFT(names , LEN(names)-1) AS names
FROM cte1 AS list
CROSS APPLY (
SELECT Name + ','
FROM cte1 AS t
WHERE list.GameId = t.GameId
FOR XML PATH('')
) grp (names)
GROUP BY list.GameId, names
;
Just add WHERE GameId = <n> to the outer query expression to restrict the output to a specific game: WHERE GameId = 2 for just game 2.
Result (updated):
+--------+----------------+
| GameId | Available_Refs |
+--------+----------------+
| 1 | Jack,Jane |
| 2 | Jack,Sam |
| 3 | Jack,Sam |
+--------+----------------+
Working Example (updated)
You need to join Officials with LEFT joins to Blocks and 2 copies of Games and return the unmatched rows:
SELECT DISTINCT o.*
FROM Officials o
LEFT JOIN Blocks b ON b.RefId = o.RefId
LEFT JOIN Games g1 ON g1.GameId = b.GameId
LEFT JOIN Games g2 ON g2.GameDate = g1.GameDate AND g2.GameId = ?
WHERE g2.GameId IS NULL;
Or, with NOT EXISTS:
SELECT o.*
FROM Officials o
WHERE NOT EXISTS (
SELECT 1
FROM Blocks b
INNER JOIN Games g1 ON g1.GameId = b.GameId
INNER JOIN Games g2 ON g2.GameDate = g1.GameDate AND g2.GameId = ?
WHERE b.RefId = o.RefId
);
See the demo.

select all records with multiple category

I am trying to figure out how to select all records that are associated with all categories on a list.
For instance take this DB setup:
create table blog (
id integer PRIMARY KEY,
url varchar(100)
);
create table blog_category (
id integer PRIMARY KEY,
name varchar(50)
);
create table blog_to_blog_category (
blog_id integer,
blog_category_id integer
);
insert into blog values
(1, 'google.com'),
(2, 'pets.com'),
(3, 'petsearch.com');
insert into blog_category values
(1, 'search'),
(2, 'pets'),
(3, 'misc');
insert into blog_to_blog_category values
(1,1),
(2,2),
(3,1),
(3,2),
(3,3);
I can query on the main table like this:
select b.*, string_agg(bc.name, ', ') from blog b
join blog_to_blog_category btbc on b.id = btbc.blog_id
join blog_category bc on btbc.blog_category_id = bc.id
where b.url like '%.com'
group by b.id
But lets say I want to only return blogs that have BOTH category 1 & 2 connected with them how do I do that?
This would return just the petsearch.com domain as it is only record to have both of those categories.
Here you go:
Added a check to count the blog_category id (HAVING Clause) and if it is 2 then it should be either 1 or 2 (IN Clause),
select b.*, string_agg(bc.name, ', ') from blog b
join blog_to_blog_category btbc on b.id = btbc.blog_id
join blog_category bc on btbc.blog_category_id = bc.id
where b.url like '%.com' and bc.id in (1,2)
group by b.id
having count(distinct bc.id ) =2
here is one way:
select * from blog where id in (
select blog_id
from blog_to_blog_category bbc
where blog_category_id in (1, 2)
group by blog_id
having count(distinct blog_category_id) = 2
)

Get room members, room's owner and admin at the same time in one query with grouped by id (unique) on PostgreSQL 12

I want to get room's member list, room's owner member in case of he doesn't exists in other table and admin member at the same time. Currently i fetch them individually.
CREATE TABLE public.room_members (
id bigint NOT NULL,
member_id bigint,
room_id bigint,
group_id bigint
);
CREATE TABLE public.rooms (
id bigint NOT NULL,
member_id bigint,
group_id bigint,
name varchar(128)
);
CREATE TABLE public.members (
id bigint NOT NULL,
group_id bigint,
username varchar(128),
is_admin bool default false
);
CREATE TABLE public.groups (
id bigint NOT NULL,
name varchar(128)
);
-- My Group created
INSERT INTO "groups" (id, name) VALUES (1, 'My Group');
-- Create users for this group. We have 4 users/members
INSERT INTO "members" (id, group_id, username, is_admin) VALUES (1, 1, 'Pratha', true);
INSERT INTO "members" (id, group_id, username) VALUES (2, 1, 'John');
INSERT INTO "members" (id, group_id, username) VALUES (3, 1, 'Mike');
INSERT INTO "members" (id, group_id, username) VALUES (4, 1, 'April');
-- April creates a room and he is owner of this room
INSERT INTO "rooms" (id, group_id, member_id, name) VALUES (1, 1, 4, 'My Room'); -- 4 is April
-- April also adds Mike to the room members. But she does not add herself. As she is owner.
INSERT INTO "room_members" (id, group_id, room_id, member_id) VALUES (1, 1, 1, 3); -- 3 is Mike
What I want is:
room_members list of 'My Room' (Which is only Mike at the moment)
My Room's owner in case of he didn't add himself to room_members table. Because he is the owner of that room (Which is April)
Plus, admin member (Which is Pratha)
And this should be unique. For example, if user add himself to room_members and also owner then it should fetch member one time only.
What I tried so far?
select * from members
left outer join room_members cm on cm.member_id = members.id
left outer join rooms c on c.id = cm.room_id
where c.name = 'My Room' or members.id = 1
I couldn't use group by here either. Also i don't need the all fields. Just room_members table fields only.
See here: https://rextester.com/XWDS42043
Expected output for room_members:
+-------------+------------+------------+
| member_id | group_id | username |
+-------------+------------+------------+
| 1 | 1 | Pratha |
+-------------+------------+------------+
| 3 | 1 | Mike |
+-------------+------------+------------+
| 4 | 1 | April |
+-------------+------------+------------+
Pratha: Because he is ADMIN
Mike: Because he is member of My Room. MEMBER
April: Because she created that room. OWNER
room_members can be many. I just added only Mike but it can have multiple members including admins and owners.
You can address this with UNION:
-- list the admin(s) of the room group
select m.id, m.group_id, m.username
from rooms r
inner join members m on m.group_id = r.group_id and m.is_admin = true
where r.name = 'My Room'
union
-- list the members of the room
select m.id, m.group_id, m.username
from rooms r
inner join room_members rm on r.id = rm.room_id
inner join members m on rm.member_id = m.id
where r.name = 'My Room'
union
-- recover the room owner
select m.id, m.group_id, m.username
from rooms r
inner join members m on r.member_id = m.id
where r.name = 'My Room'
UNION eliminates duplicates accross queries, so if a user is both member and/or group admin and/or owner of the room, they will only appear once.
In your fiddle, this query returns:
id group_id username
1 4 1 April
2 3 1 Mike
3 1 1 Pratha

MySQL GROUP_CONCAT within GROUP_CONCAT different Group By values

I am trying to get a group_concat to work within another group_concat but grouped by different values.
3 Tables Products, Customers , and Product_Customer ( which holds what product each customer bought and what size )
#Creates the Customer Table
CREATE TABLE Customer
(
Cus_Code INT AUTO_INCREMENT PRIMARY KEY,
Cus_Name VARCHAR(20)
);
#Creates the Product Table
CREATE TABLE Product
(
Prod_Code INT AUTO_INCREMENT PRIMARY KEY,
Prod_Name VARCHAR(30)
);
#Creates the Product_Customer Table
CREATE TABLE Product_Customer
(
Cus_Code INT references Customer(Cus_Code),
Prod_Code INT references Product(Prod_Code),
Size INT,
);
Sample Data
#Inserts data into Customer Table
INSERT INTO Customer (Cus_Name)
VALUES
('Aaron')
('Bob')
('Charlie')
#Inserts data into Product Table
INSERT INTO Product (Prod_Name)
VALUES
('A')
('B')
('C')
#Inserts data into Product_Customer Table
INSERT INTO Product_Customer (Cus_Code, Prod_Code, Size)
VALUES
(1, 1, 1),
(1, 1, 2),
(1, 2, 1),
(2, 1, 1),
(2, 2, 1),
(2, 2, 2),
(3, 1, 1),
(3, 2, 1),
(3, 3, 1),
(3, 3, 2)
Desired Output Something like this
Customer Name | Product(Size)
Aaron | A(1,2), B(1)
Bob | A(1), B(1,2)
Charlie | A(1), B(1), C(1,2)
So i need the Size grouped by the product_code , then all that grouped by customer code
I have tried with variations of the following but to no avail
SELECT Customer.Cus_Name, GROUP_CONCAT(DISTINCT Product.Prod_Code, '(', s.list, ')' SEPARATOR ', ') AS 'Products'
FROM Product
JOIN (
SELECT Product.Prod_Code AS id, GROUP_CONCAT(DISTINCT Product_Customer.Size SEPARATOR ',') AS list
FROM Product
INNER JOIN Product_Customer ON Product.Prod_Code = Product_Customer.Prod_Code
GROUP BY id;
) AS s ON s.id = Product_Customer.Prod_Code
INNER JOIN Product_Customer ON Product.Prod_Code = Product_Customer.Prod_Code
INNER JOIN Customer ON Product_Customer.Cus_Code = Customer.Cus_Code
GROUP BY Customer.Cus_Code;
It seems to include all the sizes bought for that product, not what size each customer bought.
Any help would be appreciated
SELECT c.Cus_Name,
GROUP_CONCAT(cncat.Size ORDER BY cncat.Prod_Name SEPARATOR ', ') AS Size
FROM Customer c
INNER JOIN
(
SELECT pc.Cus_Code,
p.Prod_Name,
CONCAT(p.Prod_Name, '(', GROUP_CONCAT(pc.size), ')') Size
FROM Product_Customer pc
INNER JOIN Product p
ON pc.Prod_Code = p.Prod_Code
GROUP BY pc.Cus_Code,
p.Prod_Name
) AS cncat
ON c.Cus_Code = cncat.Cus_Code
GROUP BY c.Cus_Name
SQLFiddle Demo
I think the following version will do what you want:
SELECT c.Cus_Name, ps.prodsizes
FROM Customer c JOIN
(select cus_code, group_concat(prod_code, '(', sizes, ')' separator ', ') as prodsizes
from (select pc.cus_code, pc.prod_code, group_concat(distinct p.size separator ',') as sizes
from Product_Customer pc join
Product p
on pc.prod_code = p.prod_code
group by pc.cus_code, pc.prod_code
) cp
group by cus_code
) ps
on ps.cus_code = c.cus_code
GROUP BY c.Cus_Code;
Note that there are two levels of aggregation to get the products and sizes together, first at the customer product level then at the customer level.
I also introduces table aliases to make the query easier to write and read. There is no need for a distinct at the outer level, because duplicates are combined in the subquery.