SQL - find parent in same column - sql

I cant express the problem correctly to find the answer online so i'm hoping someone could provide me with a link to a solution because i think this is a rather common task.
We have a hierachy of products and want to determine the parents.
All product names are in the same column and the logic is the following
ProductId ProductName
-----------------------------------------------------------------------------
1 ABC
2 ABCD
3 ABCD1
4 ABCD2
Result should be
ABCD1 & ABCD2 are children of ABCD and
ABCD is child of ABC
ProductId ProductName ParentName ParentId
------------------------------------------------------------------------------
1 ABC NULL NULL
2 ABCD ABC 1
3 ABCD1 ABCD 2
...

Hmmm. I think this does what you want:
select p.*, pp.ProductName as parentName, pp.ProductId as parentId
from products p outer apply
(select top (1) pp.*
from products pp
where p.ProductName like pp.ProductName + '%' and
p.ProductId <> pp.ProductId
order by len(pp.ProductName) desc
) pp;

No doubt that gordon's answer is best here, but still I gave it a go as well:
USE TEMPDB
CREATE TABLE #T (ProductID INT, ProductName VARCHAR (100))
INSERT INTO #T VALUES (1, 'ABC'), (2, 'ABCD'), (3, 'ABCD1'), (4, 'ABCD2')
WITH CTE AS
(
SELECT T.*,
T2.ProductID AS ParentID,
T2.ProductName AS ParentName
FROM #T AS T
CROSS JOIN #T AS T2
WHERE T.ProductName LIKE T2.ProductName + '%'
AND T.ProductID <> T2.ProductID
)
, CTE2 AS
(
SELECT TOP 1 T.*,
NULL AS ParentID,
NULL AS ParentName
FROM #T AS T
ORDER BY LEN (T.ProductName)
)
SELECT * FROM CTE UNION ALL SELECT * FROM CTE2 ORDER BY 1

If there's only 1 character difference.
Then you can LEFT JOIN to the ProductName & one wildcard character '_'
SELECT
p1.ProductId,
p1.ProductName,
p2.ProductName AS ParentName,
p2.ProductId AS ParentId
FROM Products p1
LEFT JOIN Products p2 ON p1.ProductName LIKE CONCAT(p2.ProductName,'_')
ORDER BY p1.ProductId;
Example snippet:
declare #Products table (
ProductId INT primary key identity(1,1),
ProductName varchar(30) not null,
unique (ProductName)
);
insert into #Products (ProductName) values
('ABC')
,('ABCD')
,('ABCD1')
,('ABCD2')
;
SELECT
p1.ProductId,
p1.ProductName,
p2.ProductName AS ParentName,
p2.ProductId AS ParentId
FROM #Products p1
LEFT JOIN #Products p2 ON p1.ProductName LIKE CONCAT(p2.ProductName,'_')
ORDER BY p1.ProductId;
Result:
ProductId ProductName ParentName ParentId
1 ABC NULL NULL
2 ABCD ABC 1
3 ABCD1 ABCD 2
4 ABCD2 ABCD 2
If it's possible that there's more than 1 character difference then:
SELECT TOP (1) WITH TIES
p1.ProductId,
p1.ProductName,
p2.ProductName AS ParentName,
p2.ProductId AS ParentId
FROM Products p1
LEFT JOIN Products p2 ON p1.ProductName LIKE CONCAT(p2.ProductName,'_%')
ORDER BY ROW_NUMBER() OVER (PARTITION BY p1.ProductId ORDER BY LEN(p2.ProductName) DESC);

Did you try using Case with the condition and represent each condition as a new column. You can refer to the syntax https://www.w3schools.com/sql/sql_case.asp

You can use Common Table Expression (CTE) to do the job.
with product_table (ProductId, ProductName) as
(
select 1 ProductId , 'ABC' ProductName union all
select 2 ProductId , 'ABCD' ProductName union all
select 3 ProductId , 'ABCD1' ProductName union all
select 4 ProductId , 'ABCD2' ProductName --union all
)
,product_result (ProductId, ProductName, ParentName, ParentId) as
(
select ProductId, ProductName, convert(varchar,null) ParentName, convert(int, null) ParentId
from product_table
where ProductName = 'ABC' --start with
union all
select d.ProductId, d.ProductName, convert(varchar,p.ProductName) ParentName, p.ProductId ParentId
from product_table d
, product_result p
where d.ProductName like p.ProductName+'_'
)
select *
from product_result
The first part product_table must be replaced by your own product table. It is used here to generate a tempory dataset.
Your final query will look like:
with product_result (ProductId, ProductName, ParentName, ParentId) as
(
select ProductId, ProductName, convert(varchar,null) ParentName, convert(int, null) ParentId
from <YOUR_PRODUCT_TABLE_GOES_HERE>
where ProductName = 'ABC' --start with
union all
select d.ProductId, d.ProductName, convert(varchar,p.ProductName) ParentName, p.ProductId ParentId
from <YOUR_PRODUCT_TABLE_GOES_HERE> d
, product_result p
where d.ProductName like p.ProductName+'_'
)
select *
from product_result
CTE is available since SQL2008. for more info WITH common_table_expression (Transact-SQL)

Related

Select same column from multiple tables with condition and LEFT JOIN

I have two tables with fiew similar columns :
One is for SellOrders
OrderID (PK)| id_seller| id_product| placement_date
And the other is for BuyOrders
OrderID (PK)| id_buyer| id_product| placement_date
With another table Customer concerning customers informations (Buyers and sellers)
id_customer(PK) | name_customer
And a Product table :
id(PK) | name_product
I want to write an SQL statement that will select the first placement_date when the a buy order or sell order was placed for the product and the correspending name_customer + the name_product.
I wrote a query that select the same logic but for only the selling side and I am looking to select the data for both buy and sell side :
SELECT p.name_product, s.placement_date, c.name_customer
FROM Product p
OUTER APPLY (SELECT TOP 1 placement_date, id_seller
FROM Selling
WHERE id_product = p.id
ORDER BY placement_date, OrderID ASC) s
LEFT JOIN Customer c
ON c.id_customer = s.id_seller
From what I have seen UNION SELECT seems to be the way to do this. I added UNION to the OUTER APPLY:
OUTER APPLY (SELECT TOP 1 placement_date, id_seller
FROM Selling
UNION
SELECT TOP 1 placement_date, id_buyer
WHERE id_product = p.id
ORDER BY placement_date, OrderID ASC) s
But I get stuck at the LEFT JOIN with the table Customer.
Any help ?
If I've got it right you want first of union of buy/sell orders
SELECT p.name_product, s.placement_date, c.name_customer
FROM Product p
OUTER APPLY (
SELECT TOP 1 placement_date, id_cust
FROM (
SELECT placement_date, OrderID, id_seller id_cust
FROM SellOrders
WHERE id_product = p.id
UNION
SELECT placement_date, OrderID, id_buyer
FROM BuyOrders
WHERE id_product = p.id
) t
ORDER BY placement_date, OrderID ASC
) s
LEFT JOIN Customer c
ON c.id_customer = s.id_cust

Recursive retrieve of child entities but with exact property match

I have the following tables:
Companies table:
CompanyId MotherCompanyId CompanyName
---------- ------------ ------------
1 NULL HpTopMother
2 1 HpTopDaughter1
3 2 HpTopDaughter2
4 3 HpTopDaughter3
CompanyCategories table:
CompanyCategoryId CompanyId Category
----------------- -------- -----------
1 1 Car
2 1 Lorry
3 2 Car
4 2 Lorry
5 2 Plane
6 3 Car
7 3 Lorry
8 4 Car
What I want to do is to display all the daughter companies of the head company (the Id will be passed as paramter), that have the exact match in the CompanyCategories table.
For example, in the case above only the head company, HpTopMother, and daughter company with Id 3, HpTopDaughter2, will be displayed
as both have the categories Car and Lorry.
HpTopDaughter1 will not be retrieved since it has the Plane category.
HpTopDaughter3 will not be retrieved since it does not have the Car category.
I have done the below to get all the daughters/grand-daughters of the head company:
DECLARE #companyId BIGINT
SET #companyId = 1;
WITH CTE AS
(
SELECT COM.CompanyId, COM.CompanyName
FROM Companies COM
WHERE COM.CompanyId = #companyId
UNION ALL
SELECT COM_CHILD.CompanyId, COM_CHILD.CompanyName
FROM Companies COM_CHILD JOIN cte c ON COM_CHILD.MotherCompanyId = c.CompanyId
INNER JOIN CompanyCategories CC ON CC.CompanyId = c.CompanyId
INNER JOIN CompanyCategories CC_CHILD on CC_CHILD.CompanyId = COR_CHILD.CompanyId and CC.Category = CC_CHILD.Category
)
SELECT CompanyId, CompanyName
FROM CTE
However, this is returning all the companies. Any idea of how I can achieve the listing of all the daughters/grand-daughters companies but only those having an exact category match?
Check if category sets are the same for the top level company and its descendants
DECLARE #companyId BIGINT
SET #companyId = 1;
WITH rcats as(
select null a, 1 b, category
from CompanyCategories CC
where CC.companyId = #companyId
), CTE AS
(
SELECT COM.CompanyId, COM.CompanyName
FROM Companies COM
WHERE COM.CompanyId = #companyId
UNION ALL
SELECT COM_CHILD.CompanyId, COM_CHILD.CompanyName
FROM Companies COM_CHILD
JOIN cte c ON COM_CHILD.MotherCompanyId = c.CompanyId
)
SELECT CompanyId, CompanyName
FROM CTE c
where not exists (
select 1
from
( select 1 a, null b, category
from CompanyCategories CC
where CC.CompanyId = c.CompanyId
union all
select a, b, category
from rcats
) t
group by category
having count(a) <> count(b)
)
db<>fiddle
Use CTE to search for parent-child chains and then filter out companies according to your condition using Not Exist and Except.
Declare #ID Int = 1;
With A As
( Select CompanyId, CompanyName, Row_Number() Over (Order by CompanyId) As Num
From Companies
Where CompanyId = #ID
Union All
Select Companies.CompanyId, Companies.CompanyName, A.Num
From Companies Inner Join A On (Companies.MotherCompanyId=A.CompanyId)
)
Select A.Num, A.CompanyName, String_Agg(C.Category,',') As Categories
From A Inner Join A As A_1 On (A.Num=A_1.Num)
Inner Join CompanyCategories As C On (A.CompanyId=C.CompanyId)
Where A.CompanyId<>A_1.CompanyId And
Not Exists (Select Category From CompanyCategories
Where A_1.CompanyId=CompanyCategories.CompanyId
Except
Select Category From CompanyCategories
Where A.CompanyId=CompanyCategories.CompanyId) And
Not Exists (Select Category From CompanyCategories
Where A.CompanyId=CompanyCategories.CompanyId
Except
Select Category From CompanyCategories
Where A_1.CompanyId=CompanyCategories.CompanyId)
Group by A.Num, A.CompanyName
Order by A.Num, A.CompanyName Desc
db<>fiddle
Result
Num
CompanyName
Categories
1
HpTopMother
Car,Lorry
1
HpTopDaughter2
Car,Lorry
to get companies with the same categories as the company associated with #ID, use the following query:
Declare #ID Int = 1;
With A As
( Select CompanyId, CompanyName, Row_Number() Over (Order by CompanyId) As Num
From Companies
Where CompanyId = #ID
Union All
Select Companies.CompanyId, Companies.CompanyName, A.Num
From Companies Inner Join A On (Companies.MotherCompanyId=A.CompanyId)
)
Select A.Num, A.CompanyName, String_Agg(C.Category,',') As Categories
From A Inner Join A As A_1 On (A.Num=A_1.Num)
Inner Join CompanyCategories As C On (A_1.CompanyId=C.CompanyId)
Where A_1.CompanyId = #ID
And Not Exists (Select Category From CompanyCategories
Where A_1.CompanyId=CompanyCategories.CompanyId
Except
Select Category From CompanyCategories
Where A.CompanyId=CompanyCategories.CompanyId)
And Not Exists (Select Category From CompanyCategories
Where A.CompanyId=CompanyCategories.CompanyId
Except
Select Category From CompanyCategories
Where A_1.CompanyId=CompanyCategories.CompanyId)
Group by A.Num, A.CompanyName
Order by A.Num, A.CompanyName Desc

sql cte distinct count

I want to add the total number of products of the brand to the total column.
http://sqlfiddle.com/#!18/2d00a/1
this my query;
WITH cte AS
(
SELECT id
FROM category
WHERE id = 1
UNION ALL
SELECT c.id
FROM category c, cte
WHERE c.parentid = cte.id
), cte2 AS
(
SELECT brandid, d.catid
FROM products d, cte
WHERE d.catid = cte.id
UNION ALL
SELECT u.brandid, COUNT(DISTINCT u.id)
FROM products u, cte
WHERE catid = cte.id
GROUP BY brandid
)
SELECT DISTINCT
brandid, logo, brand, id, id as total
FROM
brand, cte2
WHERE
id = cte2.brandid
ORDER BY
brand ASC
SELECT b.id,
b.logo,
b.brand,
count(p.id) total
FROM brand b
LEFT JOIN products p ON p.brandid = b.id
GROUP BY b.id, b.logo, b.brand
Assuming you want to filter products based on category 1 (where you include the subcategories as well)
WITH categories_and_subcategories AS (
SELECT id FROM category
WHERE id = 1
UNION ALL
SELECT c.id
FROM category c
INNER JOIN categories_and_subcategories cs
ON c.parentid = cs.id),
filtered_products AS (
SELECT p.id,
p.name,
p.catid,
p.brandid
FROM products p
INNER JOIN categories_and_subcategories c
ON p.catid = c.id
)
SELECT b.id,
b.logo,
b.brand,
count(p.id) total
FROM brand b
LEFT JOIN filtered_products p ON p.brandid = b.id
GROUP BY b.id, b.logo, b.brand
Here is the Query as per my Understanding to get the total products per brand. DBFIDDLE working code
CREATE TABLE brand (
[id] INT
,[brand] VARCHAR(13)
,[logo] VARCHAR(13)
);
INSERT INTO brand ([id], [brand], [logo])
VALUES ('1', 'samsung', 'logo.jpg');
CREATE TABLE products (
[id] INT
,[name] VARCHAR(13)
,[catid] INT
,[brandid] INT
);
INSERT INTO products ([id], [name], [catid], [brandid])
VALUES ('1', 'samsung tv', '2', '1')
,('1', 'samsung2 tv', '2', '1')
SELECT b.Id
,b.brand
,b.logo
,p.Id
,COUNT(1) [total]
FROM brand b
INNER JOIN products p ON b.Id = p.brandId
GROUP BY b.Id
,b.brand
,b.logo
,p.Id
Results:

Select Customers who purchased one specific Product

We have two tables:
Customers:
Products:
The goal is to select [Id] and [CustomerName] of Customers who purchased Milk AND did not purchase Bread. In the case the correct query should return a customer with Id 2 (Ann).
The query which I thought of (and which is obviously incorrect) is:
select CustomerName from dbo.Customers
where Id in
(
select CustomerId from dbo.Products
where ProductName = 'Milk' and ProductName != 'Bread'
)
It returns two customers: 1 (John) and 2 (Ann).
How to rewrite the query so it would return only customer with Id 2?
You can try the query below
SELECT CustomerName
FROM dbo.Customers c
WHERE EXISTS (
SELECT 1
FROM dbo.Products
WHERE CustomerId = c.Id
AND ProductName = 'Milk'
) AND NOT EXISTS (
SELECT 1
FROM dbo.Products
WHERE CustomerId = c.Id
AND ProductName = 'Bread'
)
You don't need to use two exists, just use where clause with not exists :
select c.*
from customer c
where ProductName = 'Milk' and
not exists (select 1 from Products p where p.CustomerId = c.id and p.ProductName = 'Bread');
I am inclined to use aggregation for this. Here is one method:
select c.customerId
from dbo.Products p
where p.productName in ('Milk', 'Bread')
group by c.customerId
having sum(case when p.productName = 'Milk' then 1 else 0 end) > 0 and
sum(case when p.productName = 'Bread' then 1 else 0 end) = 0 ;
You can add the join in to get the customer name, if you really need that.
Basically, this counts the number of rows for each customer that have 'Milk'. The > 0 says there is at least one. It then counts the number of rows that have 'Bread'. The = 0 says that there are none.
SELECT P.Id ,C.Customers
FROM Customers AS C , Product AS P
WHERE (C.Id = P.CustomerId)
AND (P.ProductName = 'Milk')
AND NOT EXISTS (
SELECT 1
FROM Products
WHERE CustomerId = C.Id
AND ProductName = 'Bread'
)
If normalization is not the case, efficiency isn't either. Here you go:
select CustomerName from dbo.Customers
where
Id in (select CustomerId from dbo.Products where ProductName = 'Milk')
and Id not in (select CustomerId from dbo.Products where ProductName = 'Bread')
Ah. Just saw it could be considered a duplicate of Eric's answer. Using exists may be a little faster indeed.

join within recursive with adjacency

I have something like this:
CREATE TABLE categories (
id varchar(250) PRIMARY KEY,
name varchar(250) NOT NULL,
parentid varchar(250)
);
CREATE TABLE products (
id varchar(250) PRIMARY KEY,
name varchar(250) NOT NULL,
price double precision,
category varchar(250) NOT NULL
);
INSERT INTO categories VALUES ('1', 'Rack', '');
INSERT INTO categories VALUES ('2', 'Women', '1');
INSERT INTO categories VALUES ('3', 'Shorts', '2');
INSERT INTO products VALUES ('1', 'Jean', 2.99, '3');
INSERT INTO products VALUES ('2', 'Inflatable Boat', 5.99, '1');
Now, if I wanted to see the total price of products for each category, I could do something like this:
SELECT
categories.name,
SUM(products.price) AS CATPRICE
FROM
categories,
products
WHERE products.category = categories.id
GROUP BY categories.name
;
Which produces output:
name | catprice
--------+----------
Rack | 5.99
Shorts | 2.99
(2 rows)
But notice that "Shorts" is an ancestor of "Rack". I want a query that will produce output like this:
name | catprice
--------+----------
Rack | 8.98
(1 row)
So that all product prices are added together under the root category. There are multiple root categories in the category table; only one has been shown for simplicity.
This is what I have thus far:
-- "nodes_cte" is the virtual table that is being created as the recursion continues
-- The contents of the ()s are the columns that are being built
WITH RECURSIVE nodes_cte(name, id, parentid, depth, path) AS (
-- Base case?
SELECT tn.name, tn.id, tn.parentid, 1::INT AS depth, tn.id::TEXT AS path FROM categories AS tn, products AS tn2
LEFT OUTER JOIN categories ON tn2.CATEGORY = categories.ID
WHERE tn.parentid IS NULL
UNION ALL
-- nth case
SELECT c.name, c.id, c.parentid, p.depth + 1 AS depth, (p.path || '->' || c.id::TEXT) FROM nodes_cte AS p, categories AS c, products AS c2
LEFT OUTER JOIN categories ON c2.CATEGORY = categories.ID
WHERE c.parentid = p.id
)
SELECT * FROM nodes_cte AS n ORDER BY n.id ASC;
I have no clue what I've done wrong. The above query returns zero results.
Your recursive query is off by a little. Give this a try:
EDIT -- To make this work with the SUM, use this:
WITH RECURSIVE nodes_cte(name, id, id2, parentid, price) AS (
-- Base case?
SELECT c.name,
c.id,
c.id id2,
c.parentid,
p.price
FROM categories c
LEFT JOIN products p on c.id = p.category
WHERE c.parentid = ''
UNION ALL
-- nth case
SELECT n.name,
n.id,
c.id id2,
c.parentid,
p.price
FROM nodes_cte n
JOIN categories c on n.id2 = c.parentid
LEFT JOIN products p on c.id = p.category
)
SELECT id, name, SUM(price) FROM nodes_cte GROUP BY id, name
And here is the Fiddle: http://sqlfiddle.com/#!1/7ac6d/19
Good luck.