Updating Product Order is not working - sql

I have Products table with (Name, ParentID, Order) columns. I have a insert statement which keep the inserted child products. After the insert I need to update the order,
I have the following SQL,
UPDATE Products
SET [Order] = (SELECT ISNULL(MAX([Order]), 0) + 1 FROM Products WHERE ParentID = CP.ParentID)
FROM Products P
INNER JOIN #InsertedChildProduct CP ON (CP.ID = P.ID)
The problem is that I am updating the order of products that are just inserted, but [Order] is not working. If I have,
Products
--------
ParentID Order
----------------
1 1
1 2
and let say I have inserted 2 child products then the table should be,
Products
--------
ParentID Order
----------------
1 1
1 2
1 3
1 4
But I am seeing,
Products
--------
ParentID Order
----------------
1 1
1 2
1 3
1 3

You can try this, taken from the answers on here:
declare #MaxNumber int
set #MaxNumber = 0
UPDATE Products
SET [Order] = #MaxNumber, #MaxNumber = (SELECT ISNULL(MAX([Order]), 0)
FROM Products
WHERE ParentID = CP.ParentID) + 1
FROM Products P
INNER JOIN #InsertedChildProduct CP ON (CP.ID = P.ID)

Try like this instead, you need to gt the MAX() first in inner query
UPDATE P
SET [Order] = X.newval
FROM Products P
JOIN
(
SELECT ID, (ISNULL(MAX([Order]), 0) + 1) as newval
FROM Products P
JOIN #InsertedChildProduct ip
on P.ParentID = ip.ParentID
group by ID
) X
ON X.ID = P.ID

Thought about this a bunch of different ways and can't see how this would work without imposing order on both datasets, even if arbitrary. Here's one way to do it (Fiddle - make sure to build the schema first, then run the code): http://www.sqlfiddle.com/#!3/d34df/3)
WITH cteRN_c
AS
(
SELECT ROW_NUMBER() OVER (PARTITION BY ID ORDER BY ID) AS RN_c,
ID
FROM #InsertedChildProduct
),
cteRN_p
AS
(
SELECT ROW_NUMBER() OVER (PARTITION BY ParentID ORDER BY [Order]) AS RN_p,
ParentID,
[Order]
FROM Products
WHERE [Order] IS NULL
)
UPDATE p
SET [Order] = (SELECT ISNULL(MAX([ORDER]), 0) FROM Products WHERE ParentID = p.ParentID) + c.RN_c
FROM cteRN_p p INNER JOIN cteRN_c c
ON p.ParentID = c.ID AND
p.RN_p = c.RN_c;
We impose order by adding arbitrary row numbers to both the temp table set and the parent set, via ROW_NUMBER in CTEs. From that point, it's just a matter of joining the CTEs on the correct datapoints, and running the updates against the parent CTE. Granted, it's arbitrary which child will get numbered in which order, but at least it will happen.
Edit: Forgot the ISNULL in the MAX portion of the query - in case no children yet. Fiddle updated as well.

Related

Fetch the data based on min(sort_order)

I have a table Product which have many Images.
Product
id name
1 Sofa
2 Bed
Images
id product_id image_url sort_order
1 1 "url5" 521
2 1 "url1" 200
3 1 "url1" 100
4 2 "url1" 1
5 2 "url2" 2
I want to fetch images where sort_order have minimum value for 100 products like below:
id product_id image_url sort_order
3 1 "url1" 100
4 2 "url1" 1
I know I need to use min(sort_order) for images but don't find the correct syntax.
I am trying the below. but no luck
select i.*
from images i
join product p on p.id = i.product_id
where p.id in (1, 2, ....)
and i.sort_order = min(sort_order)
Any help to build the correct query?
Number your images per product and keep the rows numbered 1.
select id, product_id, image_url, sort_order
from
(
select
i.*,
row_number() over (partition by product_id order by sort_order) as rn
from images i
) numbered
where rn = 1;
An alternative is to query the table twice:
select *
from images
where (product_id, sort_order) in
(
select product_id, min(sort_order)
from images
group by product_id
);
why you need to use min ?? correct me if i am wrong but you said you need images where sort_order = 1, so you just need to change your where and add limit.
WHERE i.sort_order = 1 LIMIT 100
This is pretty bad code, but maybe it'll give you an idea of how you can do this.
select p.id,p.name,*
From product p
inner join (
select i.product_id, min(sort_order) as minSortOrder
from images i
group by i.product_id
) i on i.product_id = p.id
inner join images ii on ii.product_id = p.id and ii.sort_order = i.minSortOrder
https://dbfiddle.uk/?rdbms=sqlserver_2019&fiddle=ccae44d199f9c9ac0edf4b75dd1b973b
edit: another way to do this is to create a row_number ordered by Sort_Order, so the Row_number = 1 will always be the lowest Sort_order, then we wrap that query in an outer query to apply the where clause
SELECT *
FROM (
SELECT p.id
,p.name
,ii.image_url
,ii.sort_order
,row_number() OVER (
PARTITION BY ii.product_id ORDER BY sort_order
) AS RowNumber
FROM product p
INNER JOIN images ii ON ii.product_id = p.id
) s
WHERE s.rownumber = 1
https://dbfiddle.uk/?rdbms=sqlserver_2019&fiddle=ca31c7650bc87174110bf9aa09b230d9

SQL Server: Query for products with matching tags

I have been pondering over this for the past few hours but I cannot find a solution.
I have a products in a table, tags in another table and a product/tag link table.
Now I want to retrieve all products which have the same tags as a certain product.
Here are the tables (simplified):
PRODUCT:
id varchar(36) (primary key)
Name varchar(50)
TAG:
id varchar(36) (primary key)
Name varchar(50)
PRODUCTTAG:
id varchar(36) (primary key)
ProductID varchar(36)
TagID varchar(36)
I find quite a few answers here on Stackoverflow talking about returning full and partial matches. However I am looking for a query which only gives full matches.
Example:
Product A has tags 1, 2, 3
Product B has tags 1, 2
Product C has tags 1, 2, 3
Product D has tags 1, 2, 3, 4
If I query for product A, only product C should be found - as it is the only one having exactly the same tags.
Is this even possible?
Yes, yes, try this way:
with aa as (
select count(*) count
from [PRODUCTTAG]
where ProductID = '19A947C0-6A0F-4A6F-9675-48FBE30A877D'
), bb as
(
select ProductID, count(*) count
from [PRODUCTTAG]
group by ProductID
)
select distinct b.ProductID
from [dbo].[PRODUCTTAG] a join
[dbo].[PRODUCTTAG] b on a.TagID = b.TagID cross join
aa join
bb on aa.count = bb.count and b.ProductID = bb.ProductID
where a.ProductID = '19A947C0-6A0F-4A6F-9675-48FBE30A877D'
declare #PRODUCTTAG table(id int identity(1,1),ProductID int,TagID int)
insert into #PRODUCTTAG VALUES
(1,1),(1,2),(1,3)
,(2,1),(2,2)
,(3,1),(3,2),(3,3)
,(4,1),(4,2),(4,3),(4,4)
;With CTE as
(
select ProductID,count(*)smallCount
FROM #PRODUCTTAG
group by ProductID
)
,CTE1 as
(
select smallCount, count(smallCount)BigCount
from cte
group by smallCount
)
,CTE2 as
(
select * from cTE c
where exists(
select smallCount from cte1 c1
where BigCount>1 and c1.smallCount=c.smallCount
)
)
select * from cte2
--depending upon the output expected join this with #PRODUCTTAG,#Product,#Tag
--like this
--select * from #PRODUCTTAG PT
--where exists(
--select * from cte2 c2 where pt.productid=c2.productid
--)
Or Tell what is final output look like ?
This is a case where I find it simpler to combine all the tags into a single string and compare the strings. But, that is painful in SQL Server until 2016.
So, there is a set based solution:
with pt as (
select pt.*, count(*) over (partition by productid) as cnt
from producttag pt
)
select pt.productid
from pt join
pt pt2
on pt.cnt = pt2.cnt and
pt.productid <> pt2.productid and
pt.tagid = pt2.tagid
where pt2.productid = #x
group by pt.productid, pt.cnt
having count(*) = pt.cnt;
This matches every product to your given product based on the tags. The having clause then ensures that the number of matching tags is the same for the two products. Because the join only considers matching tags, all the tags are the same.

Combine rows from Mulitple tables into single table

I have one parent table Products with multiple child tables -Hoses,Steeltubes,ElectricCables,FiberOptics.
ProductId -Primary key field in Product table
ProductId- ForeignKey field in Hoses,Steeltubes,ElectricCables,FiberOptics.
Product table has 1 to many relationship with Child tables
I want to combine result of all tables .
For eg - Product P1 has PK field ProductId which is used in all child tables as FK.
If Hoses table has 4 record with ProductId 50 and Steeltubes table has 2 records with ProductId 50 when I perform left join then left join is doing cartesian product of records showing 8 record as result But it should be 4 records .
;with HOSESTEELCTE
as
(
select '' as ModeType, '' as FiberOpticQty , '' as NumberFibers, '' as FiberLength, '' as CableType , '' as Conductorsize , '' as Voltage,'' as ElecticCableLength , s.TubeMaterial , s.TubeQty, s.TubeID , s.WallThickness , s.DWP ,s.Length as SteelLength , h.HoseSeries, h.HoseLength ,h.ProductId
from Hoses h
left join
(
--'' as HoseSeries,'' as HoseLength ,
select TubeMaterial , TubeQty, TubeID , WallThickness , DWP , Length,ProductId from SteelTubes
) s on (s.ProductId = h.ProductId)
) select * from HOSESTEELCTE
Assuming there are no relationships between child tables and you simply want a list of all child entities which make up a product you could generate a cte which has a number of rows which are equal to the largest number of entries across all the child tables for a product. In the example below I have used a dates table to simplify the example.
so for this data
create table products(pid int);
insert into products values
(1),(2);
create table hoses (pid int,descr varchar(2));
insert into hoses values (1,'h1'),(1,'h2'),(1,'h3'),(1,'h4');
create table steeltubes (pid int,descr varchar(2));
insert into steeltubes values (1,'t1'),(1,'t2');
create table electriccables(pid int,descr varchar(2));
truncate table electriccables
insert into electriccables values (1,'e1'),(1,'e2'),(1,'e3'),(2,'e1');
this cte
;with cte as
(select row_number() over(partition by p.pid order by datekey) rn, p.pid
from dimdate, products p
where datekey < 20050105)
select * from cte
create a cartesian join (one of the rare ocassions where an implicit join helps) pid to rn
result
rn pid
-------------------- -----------
1 1
2 1
3 1
4 1
1 2
2 2
3 2
4 2
And if we add the child tables
;with cte as
(select row_number() over(partition by p.pid order by datekey) rn, p.pid
from dimdate, products p
where datekey < 20050106)
select c.pid,h.descr hoses,s.descr steeltubes,e.descr electriccables from cte c
left join (select h.*, row_number() over(order by h.pid) rn from hoses h) h on h.rn = c.rn and h.pid = c.pid
left join (select s.*, row_number() over(order by s.pid) rn from steeltubes s) s on s.rn = c.rn and s.pid = c.pid
left join (select e.*, row_number() over(order by e.pid) rn from electriccables e) e on e.rn = c.rn and e.pid = c.pid
where h.rn is not null or s.rn is not null or e.rn is not null
order by c.pid,c.rn
we get this
pid hoses steeltubes electriccables
----------- ----- ---------- --------------
1 h1 t1 e1
1 h2 t2 e2
1 h3 NULL e3
1 h4 NULL NULL
2 NULL NULL e1
In fact, the result having 8 rows can be expected to be the result, since your four records are joined with the first record in the other table and then your four records are joined with the second record of the other table, making it 4 + 4 = 8.
The very fact that you expect 4 records to be in the result instead of 8 shows that you want to use some kind of grouping. You can group your inner query issued for SteelTubes by ProductId, but then you will need to use aggregate functions for the other columns. Since you have only explained the structure of the desired output, but not the semantics, I am not able with my current knowledge about your problem to determine what aggregations you need.
Once you find out the answer for the first table, you will be able to easily add the other tables into the selection as well, but in case of large data you might get some scaling problems, so you might want to have a table where you store these groups, maintain it when something changes and use it for these selections.

SQL Server - only join if condition is met

I have three tables (at least, something similar) with the following relationships:
Item table:
ID | Val
---------+---------
1 | 12
2 | 5
3 | 22
Group table:
ID | Parent | Range
---------+---------+---------
1 | NULL | [10-30]
2 | 1 | [20-25]
3 | NULL | [0-15]
GroupToItem table:
GroupID | ItemID
---------+---------
1 | 1
1 | 3
And now I want to add rows to the GroupToItem table for Groups 2 and 3, using the same query (since some other conditions not shown here are more complicated). I want to restrict the items through which I search if the new group has a parent, but to look through all items if there is not.
At the moment I am using an IF/ELSE on two statements that are almost exactly the same, but for the addition of another JOIN row when a parent exists. Is it possible to do a join to reduce the number of items to look at, only if a restriction is possible?
My two queries as they stand are given below:
DECLARE #GroupID INT = 2;...
INSERT INTO GroupToItem(GroupID, ItemID)
SELECT g.ID,
i.ID,
FROM Group g
JOIN Item i ON i.Val IN g.Range
JOIN GroupToItem gti ON g.Parent = gti.GroupID AND i.ID = gti.ItemID
WHERE g.ID = #GroupID
-
DECLARE #GroupID INT = 3;...
INSERT INTO GroupToItem(GroupID, ItemID)
SELECT g.ID,
i.ID,
FROM Group g
JOIN Item i ON i.Val IN g.Range
WHERE g.ID = #GroupID
So essentially I only want to do the second JOIN if the given group has a parent. Is this possible in a single query? It is important that the number of items that are compared against the range is as small as possible, since for me this is an intensive operation.
EDIT: This seems to have solved it in this test setup, similar to what was suggested by Denis Valeev. I'll accept if I can get it to work with my live data. I've been having some weird issues - potentially more questions coming up.
SELECT g.Id,
i.Id
FROM Group g
JOIN Item i ON (i.Val > g.Start AND i.Val < g.End)
WHERE g.Id = 2
AND (
(g.ParentId IS NULL)
OR
(EXISTS(SELECT 1 FROM GroupToItem gti WHERE g.ParentId = gti.GroupId AND i.Id = gti.ItemId))
)
SQL Fiddle
Try this:
INSERT INTO GroupToItem(GroupID, ItemID)
SELECT g.ID,
i.ID,
FROM Group g
JOIN Item i ON i.Val IN g.Range
WHERE g.ID = #GroupID
and (g.ID in (3) or exists (select top 1 1 from GroupToItem gti where g.Parent = gti.GroupID AND i.ID = gti.ItemID))
If a Range column is a varchar datatype, you can try something like this:
INSERT INTO GROUPTOITEM (GROUPID, ITEMID)
SELECT A.ID, B.ID
FROM GROUP AS A
LEFT JOIN ITEM AS B
ON B.VAL BETWEEN CAST(SUBSTRING(SUBSTRING(A.RANGE,1,CHARINDEX('-',A.RANGE,1)-1),2,10) AS INT)
AND CAST(REPLACE(SUBSTRING(A.RANGE,CHARINDEX('-',A.RANGE,1)+1,10),']','') AS INT)

How do you get the last record generated in a recursive CTE?

In the code below I am using a recursive CTE(Common Table Expression) in SQL Server 2005 to try and find the top level parent of a basic hierarchical structure. The rule of this hierarchy is that every CustID has a ParentID and if the CustID has no parent then the ParentID = CustID and it is the highest level.
DECLARE #LookupID int
--Our test value
SET #LookupID = 1
WITH cteLevelOne (ParentID, CustID) AS
(
SELECT a.ParentID, a.CustID
FROM tblCustomer AS a
WHERE a.CustID = #LookupID
UNION ALL
SELECT a.ParentID, a.CustID
FROM tblCustomer AS a
INNER JOIN cteLevelOne AS c ON a.CustID = c.ParentID
WHERE c.CustID <> a.CustomerID
)
So if tblCustomer looks like this:
ParentID CustID
5 5
1 8
5 4
4 1
The result I get from the code above is:
ParentID CustID
4 1
5 4
5 5
What I want is just the last row of that result:
ParentID CustID
5 5
How do I just return the last record generated in the CTE (which would be highest level CustID)?
Also note that there are multiple unrelated CustID hierarchies in this table so I can't just do a SELECT * FROM tblCustomer WHERE ParentID = CustID. I can't order by ParentID or CustID because the ID number is not related to where it is in the hierarchy.
If you just want want the highest recursion depth couldn't you do something like this?Then, when you actually query the CTE just look for the row with max(Depth)? Like so:
DECLARE #LookupID int
--Our test value
SET #LookupID = 1;
WITH cteLevelOne (ParentID, CustID, Depth) AS
(
SELECT a.ParentID, a.CustID, 1
FROM tblCustomer AS a
WHERE a.CustID = #LookupID
UNION ALL
SELECT a.ParentID, a.CustID, c.Depth + 1
FROM tblCustomer AS a
INNER JOIN cteLevelOne AS c ON a.CustID = c.ParentID
WHERE c.CustID <> a.CustID
)
select * from CTELevelone where Depth = (select max(Depth) from CTELevelone)
or, adapting what trevor suggests, this could be used with the same CTE:
select top 1 * from CTELevelone order by Depth desc
I don't think CustomerID was necessarily what you wanted to order by in the case you described, but I wasn't perfectly clear on the question either.
I'm not certain I fully understand the problem, but just to hack & slash at it you could try:
SELECT TOP 1 FROM cteLevelOne ORDER BY CustID DESC
That assumes that the CustID is also in order as in the example, and not something like a GUID.
First the cte will not be finished if any of the parent child are same. As it is a recursive CTE it has to be terminated. Having Parent and cust id same , the loop will not end.
Msg 530, Level 16, State 1, Line 15
The statement terminated. The maximum recursion 100 has been exhausted before statement completion.