Better way of doing multiple sub-selects? - sql

Pretty standard setup with a table of products, categories and products in categories, but I was wondering if there is a better/more effective way to get the several top categories for a product (I don't really want to bring them back as separate rows so an inner join isn't viable).
The SQL I have currently is:
SELECT p.*,
(SELECT TOP 1 category_name FROM categories c INNER JOIN products_and_categories pc ON pc.category_id = c.category_id WHERE p.product_id = pc.product_id AND category_parent = 100 ORDER BY sort) AS cat_1,
(SELECT TOP 1 category_name FROM categories c INNER JOIN products_and_categories pc ON pc.category_id = c.category_id WHERE p.product_id = pc.product_id AND category_parent = 200 ORDER BY sort) AS cat_2,
(SELECT TOP 1 category_name FROM categories c INNER JOIN products_and_categories pc ON pc.category_id = c.category_id WHERE p.product_id = pc.product_id AND category_parent = 500 ORDER BY sort) AS cat_3,
(SELECT TOP 1 category_name FROM (SELECT TOP 2 c.* FROM categories c INNER JOIN products_and_categories pc ON pc.category_id = c.category_id WHERE p.product_id = pc.product_id AND category_parent = 500 ORDER BY sort) c1 ORDER BY sort DESC) AS cat_4,
(SELECT TOP 1 category_name FROM categories c INNER JOIN products_and_categories pc ON pc.category_id = c.category_id WHERE p.product_id = pc.product_id AND category_parent = 50 ORDER BY sort) AS cat_5,
(SELECT TOP 1 category_name FROM (SELECT TOP 2 c.* FROM categories c INNER JOIN products_and_categories pc ON pc.category_id = c.category_id WHERE p.product_id = pc.product_id AND category_parent = 50 ORDER BY sort) c1 ORDER BY sort DESC) AS cat_6
FROM products AS p
I'm happy enough doing it this way for a couple of columns, but was wondering if there was any other way to do it? Either by joining on a PIVOT table or something else I've not thought of or if I should just accept doing it this way.
There are a couple of other restrictions (which may or may not be important):
Some of the sub queries may not bring back any results
Some of the rows I require the top 2 categories (as can be seen in the above code)

Since your correlated subqueries only have simple equals conditions you can move these to a join, then rather than using TOP 1 you can use ROW_NUMBER() to sort your categories, then only select the top 1 for each product_ID/Parent_category combination, or in the case of two of your fields, select the the second category.
I think this would work for you:
WITH ProductCategories AS
( SELECT pc.Product_ID,
c.category_name,
category_parent,
RowNum = ROW_NUMBER() OVER(PARTITION BY pc.Product_ID, category_parent ORDER BY Sort)
FROM categories c
INNER JOIN products_and_categories pc
ON pc.category_id = c.category_id
), MaxProductCategories AS
( SELECT Product_ID,
[Cat_1] = MAX(CASE WHEN RowNum = 1 AND category_parent = 100 THEN category_name END),
[Cat_2] = MAX(CASE WHEN RowNum = 1 AND category_parent = 200 THEN category_name END),
[Cat_3] = MAX(CASE WHEN RowNum = 1 AND category_parent = 500 THEN category_name END),
[Cat_4] = MAX(CASE WHEN RowNum = 2 AND category_parent = 500 THEN category_name END),
[Cat_5] = MAX(CASE WHEN RowNum = 1 AND category_parent = 50 THEN category_name END),
[Cat_6] = MAX(CASE WHEN RowNum = 2 AND category_parent = 50 THEN category_name END)
FROM ProductCategories
WHERE RowNum IN (1, 2)
AND category_parent IN (50, 100, 200, 500)
GROUP BY Product_ID
)
SELECT p.*,
mpc.Cat_1,
mpc.Cat_2,
mpc.Cat_3,
mpc.Cat_4,
mpc.Cat_5,
mpc.Cat_6
FROM products p
LEFT JOIN MaxProductCategories mpc
ON mpc.Product_ID = p.Product_ID;

Related

How to list products that belong to more than 3 private categories

What's the SQL to find the list of products that belong to more than 3 private categories.
I tried this:
SELECT
products.*
FROM
products
INNER JOIN
product_categories
ON
products.product_id = product_categories.product_id
INNER JOIN
categories
ON
product_categories.category_id = categories.category_id
WHERE
categories.is_private = 1
GROUP BY
categories.category_id
HAVING
COUNT(categories.category_id) > 3
Thanks!
Your query would be correct if you aggregated by products.product_id -- well, depending on the database, you might need to include other columns in the SELECT. But it is valid SQL assuming that product_id is unique in that table.
If you only want the product ids you don't need the products table:
select pc.product_id
from product_categories pc join
categories c
on pc.category_id = c.category_id
where is_private = 1
group by pc.product_id
having count(*) > 3;
you need every product witch .... so you should do group by on productID.also you can add other columns of products that you need in both 'select' and 'group by'
SELECT
products.product_id,products.name
FROM
products
INNER JOIN
product_categories
ON
products.product_id = product_categories.product_id
INNER JOIN
categories
ON
product_categories.category_id = categories.category_id
WHERE
categories.is_private = 1
GROUP BY
products.product_id,products.name
HAVING
COUNT (categories.category_id) > 3
To get all columns of products:
SELECT products.*
FROM products p
WHERE product_id in (SELECT pc.product_id
FROM product_categories pc
INNER JOIN categories c ON pc.category_id = c.category_id
WHERE c.is_private = 1
GROUP BY c.category_id
HAVING COUNT(c.category_id) > 3)
Check this out!
select * from products where product_id in
(select pc.product_id from product_categories inner join categories c on
pc.category_id=c.category_id where c.is_private=1
group by c.category_id having count(c.category_id)>3)

Is there a way I can get a total from a SQL Query?

Here is my query that I have:
SELECT CG.Id, CG.Name, CG.IsToggled, COUNT(*) AS TotalWordCount
FROM CategoryGroup AS CG
JOIN Category AS C ON CG.Id = C.GroupId
JOIN Phrase AS P ON C.Id = P.CategoryId
GROUP BY CG.Id
ORDER BY CG.Name
What I need to do is to get a total of the TotalWordCount column where CG.IsToggled = 1
Is this possible in the same query or is there a way I can do this with two queries. Two SQL statements would be okay if they do come up with the same value as all the TotalWordCount added together.
You can solve this with a window function
SELECT CG.Id, CG.Name, CG.IsToggled, COUNT(*) AS TotalWordCount,
COUNT(*) OVER (PARTITION BY CG.id) as TotalTotalWordCount
FROM CategoryGroup AS CG
JOIN Category AS C ON CG.Id = C.GroupId
JOIN Phrase AS P ON C.Id = P.CategoryId
GROUP BY CG.Id
ORDER BY CG.Name
Assuming this is actually Microsoft SQL Server (which means the original query posted would error out because it doesn't CG.Name in your GROUP BY).
What I need to do is to get a total of the TotalWordCount column where CG.IsToggled = 1
You cannot do a total of a total (aggregate of an aggregate) in the same query. But, you can do it in a subquery like this :
SELECT CG.Id, CG.Name, CG.IsToggled, SUM(c2.TotalWordCount) AS TotalWordCount
FROM CategoryGroup AS CG
INNER JOIN
(
SELECT Id, Name, COUNT(*) AS TotalWordCount
FROM CategoryGroup
WHERE IsToggled = 1
GROUP BY Id, Name
) AS c2 ON c2.Id = CG.Id AND c2.Name = CG.Name
JOIN Category AS C ON CG.Id = C.GroupId
JOIN Phrase AS P ON C.Id = P.CategoryId
GROUP BY CG.Id, CG.Name, CG.IsToggled
ORDER BY CG.Name
Note that: You might need to use LEFT JOIN to include those with IsToggled = 0 in the query as well:
SELECT CG.Id, CG.Name, CG.IsToggled, SUM(COALESCE(c2.TotalWordCount, 0)) AS TotalWordCount
FROM CategoryGroup AS CG
JOIN Category AS C ON CG.Id = C.GroupId
JOIN Phrase AS P ON C.Id = P.CategoryId
LEFT JOIN
(
SELECT Id, Name, COUNT(*) AS TotalWordCount
FROM CategoryGroup
WHERE IsToggled = 1
GROUP BY Id, Name
) AS c2 ON c2.Id = CG.Id AND c2.Name = CG.Name
GROUP BY CG.Id, CG.Name, CG.IsToggled
ORDER BY CG.Name

SQL Server stored procedure - Is it possible to return multiple results within a field of each row

I am wondering if it's possible to return multiple results within a column of each row or the best way to get multiple results within a column.
Example if you have a stored procedure that will return multiple students, but one of the columns within the student table is the photo id. A student can have several photos, but this stored procedure needs to return all three photo ids for each student.
SELECT
S.Name AS Name,
(SELECT S.Id FROM COURSES WHERE P.StudentId = S.Id) AS PhotoId
FROM
PHOTOS P
INNER JOIN
PHOTOS P ON P.StudentId = S.Id
The example above will not work, but I need to know what is the best way of doing it. As the maximum number of pictures is set to 3, I was thinking to create each photo id within a separate column such as the following.
SELECT
S.Name AS Name,
(SELECT TOP 1 S.Id FROM COURSES WHERE P.StudentId = S.Id) AS PhotoId1,
(SELECT TOP 2 S.Id FROM COURSES WHERE P.StudentId = S.Id) AS PhotoId2,
(SELECT TOP 3 S.Id FROM COURSES WHERE P.StudentId = S.Id) AS PhotoId3
FROM
PHOTOS P
INNER JOIN
PHOTOS P ON P.StudentId = S.Id
In this case only the top line would work. Instead of using TOP 2 and TOP 3 what is the equivalent of taking taking only the second and third result (same as in Linq Take(1) and Skip(1)).
Or is there a better way of doing it? Also I must make sure each student is only returned once even if they have multiple images
I am uncertain which way you are hoping to structure your data with photos in a row or photos in separate columns. However, Rows seem more natural to me.
Rows
SELECT ph.Name
, PhotoId = co.Id
FROM dbo.Photos (NOLOCK) ph
JOIN dbo.Courses (NOLOCK) co ON co.Id = ph.StudentId
Output:
Name | PhotId
Edward | 1234
Edward | 2345
Edward | 3456
Updated
Columns
;WITH CTE AS(
SELECT ph.Name
, PhotoId = co.Id
, RN = ROW_NUMBER()OVER(PARTITION BY ph.Name ORDER BY co.Id)
FROM dbo.Photos (NOLOCK) ph
JOIN dbo.Courses (NOLOCK) co ON co.Id = ph.StudentId
)
SELECT Name
, PhotoId1 = SUM(CASE WHEN RN = 1 THEN Id ELSE NULL END)
, PhotoId2 = SUM(CASE WHEN RN = 2 THEN Id ELSE NULL END)
, PhotoId3 = SUM(CASE WHEN RN = 3 THEN Id ELSE NULL END)
FROM CTE
GROUP BY Name
Try this using row_number to get 1, 2 and 3 photos
SELECT S.Name AS Name
,(select x.photo_id from (SELECT PHOTO_ID, row_number() over (order by photo_id) row_num from PHOTOS P where p.student_id = s.studenT_id) x where x.row_num = 1) AS PhotoId1
,(select x.photo_id from (SELECT PHOTO_ID, row_number() over (order by photo_id) row_num from PHOTOS P where p.student_id = s.studenT_id) x where x.row_num = 2) AS PhotoId2
,(select x.photo_id from (SELECT PHOTO_ID, row_number() over (order by photo_id) row_num from PHOTOS P where p.student_id = s.studenT_id) x where x.row_num = 3) AS PhotoId3
FROM STUDENTS S

Left outer join with only first row

I have a query something like
SELECT S.product_id, S.link, C.id AS category_id
FROM Products P
INNER JOIN SEO S ON S.product_id = P.id AND P.product_type = 1
LEFT OUTER JOIN Categories C ON c.product_id = P.id
WHERE P.active = 1
I works fine for me as long as each product has assigned to only one category. But if a product is assigned to many categories it returns all possible combinations.
Can I only select the first one and if a product don't have any category the link should still be returned with category_id = NULL
An easy way is to use outer apply, so as to have a correlated join, and make that a top 1 query. Thus you are able to access all columns of the category record in question. I'm adding a category name here as an example:
select s.product_id, s.link, c.id as category_id, c.name as category_name
from products p
inner join seo s on s.product_id = p.id
outer apply
(
select top 1 *
from categories cat
where cat.product_id = p.id
order by cat.id
) c
where p.active = 1
and p.product_type = 1;
You can use a GROUP BY to accomplish this along with an Aggregate function, most likely MIN or MAX.
Depending on which Category Id you prefer in your result you could select the minimum.
SELECT S.product_id, S.link, MIN(C.id) AS category_id
FROM Products P
INNER JOIN SEO S ON S.product_id = P.id AND P.product_type = 1
LEFT OUTER JOIN Categories C ON c.product_id = P.id
WHERE P.active = 1
GROUP BY S.product_id, S.link
Or the maximum.
SELECT S.product_id, S.link, MAX(C.id) AS category_id
FROM Products P
INNER JOIN SEO S ON S.product_id = P.id AND P.product_type = 1
LEFT OUTER JOIN Categories C ON c.product_id = P.id
WHERE P.active = 1
GROUP BY S.product_id, S.link
Alternate solution using subquery:
SELECT S.product_id, S.link,
(
SELECT C.id FROM Categories C WHERE C.product_id = P.id AND
ROW_NUMBER() OVER(ORDER BY /* your sort option goes here*/ ) = 1
) AS category_id
FROM Products P
INNER JOIN SEO S ON S.product_id = P.id AND P.product_type = 1
WHERE P.active = 1

I want to delete all products from a table(products) where a product is only represented in one category

Like the title says, i would like to delete all products in categoryID 102, but not if they are ALSO represented in any other categoryid.
Pseudo: Delete x where categoryid = 102, and x is not in any other category.
I gotta select from 3 tables: Product, Category, Product_Category_Mapping
This is what i have now:
select p.Name
from Product p, Category c, Product_Category_Mapping pcm
where c.Id = 102 and pcm.CategoryId = c.Id and pcm.ProductId = p.Id
I bet this is pretty simple. I might need a subselect?
This is an example of a set-within-a-sets query. A good way to handle this is with group by and having. Here is an example for two products:
select p.Name
from Product_Category_Mapping pcm join
Product p
on pcm.ProductId = p.Id
where pcm.id in (102, 103)
group by p.Name
having count(distinct pcm.id) = 2;
You don't specify which DBMS you're using, but this is pretty plain vanilla SQL:
SELECT p.Id AS ProductId, p.Name AS ProductName
FROM Product p INNER JOIN Product_Category_Mapping pcm
ON p.Id = pcm.ProductId
WHERE pcm.CategoryId = 102
AND NOT EXISTS ( SELECT 1 FROM Product_Category_Mapping pcm2
WHERE pcm.ProductId = p.Id
AND pcm2.CategoryId <> pcm.CategoryId )
Incidentally, joining to Category is unnecessary since the CategoryId is already present in the Product_Category_Mapping table. To delete, just use the above query with an IN:
DELETE FROM Product
WHERE Id IN (
SELECT p.Id AS ProductId
FROM Product p INNER JOIN Product_Category_Mapping pcm
ON p.Id = pcm.ProductId
WHERE pcm.CategoryId = 102
AND NOT EXISTS ( SELECT 1 FROM Product_Category_Mapping pcm2
WHERE pcm.ProductId = p.Id
AND pcm2.CategoryId <> pcm.CategoryId )
)
or you might do the following instead:
DELETE FROM Product p
WHERE EXISTS ( SELECT 1 FROM Product_Category_Mapping pcm
WHERE pcm.ProductId = p.Id
AND pcm.CategoryId = 102 )
AND NOT EXISTS ( SELECT 1 FROM Product_Category_Mapping pcm2
WHERE pcm2.ProductId = p.Id
AND pcm2.CategoryId <> 102 )