The Problem
I'm trying to detect and react to changes in a table where each update is being recorded as a new row with some values being the same as the original, some changed (the ones I want to detect) and some NULL values (not considered changed).
For example, given the following table MyData, and assuming the OrderNumber is the common value,
ID OrderNumber CustomerName PartNumber Qty Price OrderDate
1 123 Acme Corp. WG301 4 15.02 2020-01-02
2 456 Base Inc. AL337 7 20.15 2020-02-03
3 123 NULL WG301b 5 19.57 2020-01-02
If I execute the query for OrderNumber = 123 I would like the following data returned:
Column OldValue NewValue
ID 1 3
PartNumber WG301 WG301b
Qty 4 5
Price 15.02 19.57
Or possibly a single row result with only the changes filled, like this (however, I would strongly prefer the former format):
ID OrderNumber CustomerName PartNumber Qty Price OrderDate
3 NULL NULL WG301b 5 19.57 NULL
My Solution
I have not had a chance to test this, but I was considering writing the query with the following approach (pseudo-code):
select
NewOrNull(last.ID, prev.ID) as ID,
NewOrNull(last.OrderNumber, prev.OrderNumber) as OrderNumber
NewOrNull(last.CustomerName, prev.CustomerName) as CustomerName,
...
from last row with OrderNumber = 123
join previous row where OrderNumber = 123
Where the function NewOrNull(lastVal, prevVal) returns NULL if the values are equal or lastVal value is NULL, otherwise the lastVal.
Why I'm Looking for an Answer
I'm afraid that the ugly join, the number of calls to the function, and the procedural approach may make this approach not scalable. Before I start down the rabbit hole, I was wondering...
The Question
...are there any other approaches I should try, or any best practices to solving this specific type of problem?
I came up with a solution for the second (less preferred) format:
The Data
Using the following data:
INSERT INTO MyData
([ID], [OrderNumber], [CustomerName], [PartNumber], [Qty], [Price], [OrderDate])
VALUES
(1, 123, 'Acme Corp.', 'WG301', '4', '15.02', '2020-01-02'),
(2, 456, 'Base Inc.', 'AL337', '7', '20.15', '2020-02-03'),
(3, 123, NULL, 'WG301b', '5', '19.57', '2020-01-02'),
(4, 123, 'ACME Corp.', 'WG301b', NULL, NULL, '2020-01-02'),
(6, 456, 'Base Inc.', NULL, '7', '20.15', '2020-02-05');
The Function
This function returns the updated value if it has changed, otherwise NULL:
CREATE FUNCTION dbo.NewOrNull
(
#newValue sql_variant,
#oldValue sql_variant
)
RETURNS sql_variant
AS
BEGIN
DECLARE #ret sql_variant
SELECT #ret = CASE
WHEN #newValue IS NULL THEN NULL
WHEN #oldValue IS NULL THEN #newValue
WHEN #newValue = #oldValue THEN NULL
ELSE #newValue
END
RETURN #ret
END;
The Query
This query returns the history of changes for the given order number:
select dbo.NewOrNull(new.ID, old.ID) as ID,
dbo.NewOrNull(new.OrderNumber, old.OrderNumber) as OrderNumber,
dbo.NewOrNull(new.CustomerName, old.CustomerName) as CustomerName,
dbo.NewOrNull(new.PartNumber, old.PartNumber) as PartNumber,
dbo.NewOrNull(new.Qty, old.Qty) as Qty,
dbo.NewOrNull(new.Price, old.Price) as Price,
dbo.NewOrNull(new.OrderDate, old.OrderDate) as OrderDate
from MyData new
left join MyData old
on old.ID = (
select top 1 ID
from MyData pre
where pre.OrderNumber = new.OrderNumber
and pre.ID < new.ID
order by pre.ID desc
)
where new.OrderNumber = 123
The Result
ID OrderNumber CustomerName PartNumber Qty Price OrderDate
1 123 Acme Corp. WG301 4 15.02 2020-01-02
3 (null) (null) WG301b 5 19.57 (null)
4 (null) ACME Corp. (null) (null) (null) (null)
The Fiddle
Here's the SQL Fiddle that shows the whole thing in action.
http://sqlfiddle.com/#!18/b720f/5/0
Related
I have the following record set output:
ID Name Pay_Type Paid_Amnt Interest_Amnt
1 John Smith Benefit 1075 0
1 John Smith Interest 1.23 0
2 Tom Ryder Benefit 1123 0
3 Mark Thompson Benefit 1211 0
3 Mark Thompson Interest 1.34 0
What I'd like is for values with the Pay_Type = Interest to be placed in the Interest column.
Desired output:
ID Name Pay_Type Pay_Type 2 Paid_Amnt Interest_Amnt
1 John Smith Benefit Interest 1075 1.23
2 Tom Ryder Benefit NULL 1123 0
3 Mark Thompson Benefit Interest 1211 1.34
I tried something like the following:
Select row_number()over(partition by id, case when pay_type = 'Interest' then interest_amnt = paid_amnt
when pay_type = 'Interest' then paid_amnt = 0 end) as new_interest
Does anyone know how to get the desired results?
Thank you
declare #t table(id int, pay_type varchar(25), name varchar(100), paid_amnt float, interest_amnt float)
insert into #t values(1, 'Benefit', 'John Smith', 1075, 0),
(1, 'Interest', 'John Smith',1.23, 0),
(2, 'Benefit', 'Tom Ryder', 1123, 0),
(3, 'Benefit', 'Mark Thompson', 1211, 0),
(4, 'Interest', 'Mark Thompson', 1.34, 0)
select * from #t
Just in case you can have more than 2 records per person, I believe this will give you what you want, it utilizes a couple of subqueries and group by,
subquery x groups your records so you get the interest sums and benefits sums in a row per user,
subquery y uses CASE expressions to place the summed amounts into their proper columns or zero in case of it being Benefit/Interest and adds the pay type columns of pay_type1 and pay_type2 with values of Benefit and Interest respectively,
outer query groups everything together into 1 row per user, and sums their interest and benefit columns respectively:
SELECT y.[id] AS [ID], y.[name] AS [Name],
y.[pay_type1] AS [Pay_Type], y.[Pay_Type2], SUM(y.[Paid_Amnt]) AS [Paid_Amnt],
SUM(y.[Interest_Amnt]) AS [Interest_Amnt]
FROM
(
SELECT id, name, 'Benefit' AS [pay_type1], 'Interest' AS [pay_type2],
CASE WHEN pay_type = 'Benefit' THEN x.Amount ELSE 0 END AS [Paid_Amnt],
CASE WHEN pay_type = 'Interest' THEN x.Amount ELSE 0 END AS [Interest_Amnt]
FROM
(
SELECT id, pay_type, name, SUM(paid_amnt) AS [Amount]
FROM table as t
GROUP BY id, pay_type, name
) AS x
) AS y
GROUP BY y.[id], y.[name], y.[pay_type1], y.[pay_type2]
I am trying to increment a project code when I do a SQL merge operation
I have two tables. One has lots of information including customer's and projects. I want to merge the customer name and project name from one table to the other. This article is perfect and showed me how to do what I needed to do
https://www.mssqltips.com/sqlservertip/1704/using-merge-in-sql-server-to-insert-update-and-delete-at-the-same-time/
However I need to maintain a project number that increments every time a record is added and left alone when you do an edit of customer or project name. If the project is deleted then we carry on from the next available number. I was trying to do it using row number over partition but it didn't give me the correct number of projects.
Using the articles example and to provide a visualisation I would need another column called Type with Food or Drink as the answer and get
Item Cost Code Type
Tea 10 1 Drink
Coffee 12 2 Drink
Muffin 11 1 Food
Biscuit 4 2 Food
I will go with the data from the example provided from the link and add a bit more data to be sure im covering all the cases so first lets start with these tables and fill them.
--Create a target table
Declare #Products TABLE
(
ProductID INT PRIMARY KEY,
ProductName VARCHAR(100),
ProductNumber int,
ProductType VARCHAR(100),
Rate MONEY
)
--Insert records into target table
INSERT INTO #Products
VALUES
(1, 'Tea', 1,'Drink',10.00),
(2, 'Coffee', 2,'Drink', 20.00),
(3, 'BiscuitX1', 1,'Food', 45.00) ,
(4, 'Muffin', 2,'Food', 30.00),
(5, 'BiscuitX2', 3,'Food', 40.00),
(6, 'BiscuitX3', 4,'Food', 45.00),
(7, 'Donut', 5, 'Food', 30.00),
(8, 'BiscuitX4', 6,'Food', 40.00),
(9, 'BiscuitX5', 7,'Food', 45.00)
--Create source table
Declare #UpdatedProducts TABLE
(
ProductID INT PRIMARY KEY,
ProductName VARCHAR(100),
ProductNumber int,
ProductType VARCHAR(100),
Rate MONEY
)
--Insert records into source table
INSERT INTO #UpdatedProducts
VALUES
(1, 'Tea', 0,'Drink', 10.00),
(2, 'Coffee', 0,'Drink', 25.00),
(4, 'Muffin', 0,'Food', 35.00),
(7, 'Donut', 0, 'Food', 30.00),
(10, 'Pizza', 0,'Food', 60.00),
(11, 'PizzaLarge', 0,'Food', 80.00)
You can see that I added the ProductNumber and ProductType.
for the #UpdatedProducts table Im assuming you dont have the product Number, if you do then you will do the direct merge with no problem, if you dont you would need to find it.
so lets first update ProductNumber in #UpdatedProducts
;with cte as (
select u.ProductID,u.ProductName,u.ProductType,u.Rate
,coalesce(p.ProductNumber,row_number() over (partition by u.ProductType order by u.ProductID)
+(select max(pp.ProductNumber) from #Products pp where pp.ProductType=u.ProductType)
-(select Count(*) from #UpdatedProducts uu
inner join #Products ppp on ppp.ProductID=uu.ProductID
where uu.ProductType=u.ProductType)) [ProductNumber]
from #UpdatedProducts u
left outer join #Products p on p.ProductID=u.ProductID
)
update a
set a.[ProductNumber]=cte.[ProductNumber]
From #UpdatedProducts a
inner join cte on cte.ProductID=a.ProductID
I did not find a way to put this in the merge directly.
The result of the #UpdatedProducts after the update will be as below:-
ProductID ProductName ProductNumber ProductType Rate
========= =========== ============= =========== ====
1 Tea 1 Drink 10.00
2 Coffee 2 Drink 25.00
4 Muffin 2 Food 35.00
7 Donut 5 Food 30.00
10 Pizza 8 Food 60.00
11 PizzaLarge 9 Food 80.00
So now we can do a direct merge, as below:-
--Synchronize the target table with refreshed data from source table
MERGE #Products AS TARGET
USING #UpdatedProducts AS SOURCE
ON (TARGET.ProductID = SOURCE.ProductID)
--When records are matched, update the records if there is any change
WHEN MATCHED AND TARGET.ProductName <> SOURCE.ProductName OR TARGET.Rate <> SOURCE.Rate
THEN UPDATE SET TARGET.ProductName = SOURCE.ProductName, TARGET.Rate = SOURCE.Rate ,TARGET.ProductNumber= TARGET.ProductNumber --left alone on edit
--When no records are matched, insert the incoming records from source table to target table
WHEN NOT MATCHED BY TARGET
THEN INSERT (ProductID, ProductName, Rate,ProductNumber,ProductType)
VALUES (SOURCE.ProductID, SOURCE.ProductName, SOURCE.Rate,SOURCE.ProductNumber,SOURCE.ProductType)-- increments every time a record is added
--When there is a row that exists in target and same record does not exist in source then delete this record target
WHEN NOT MATCHED BY SOURCE
THEN DELETE
--$action specifies a column of type nvarchar(10) in the OUTPUT clause that returns
--one of three values for each row: 'INSERT', 'UPDATE', or 'DELETE' according to the action that was performed on that row
OUTPUT $action,
DELETED.ProductID AS TargetProductID,
DELETED.ProductName AS TargetProductName,
DELETED.Rate AS TargetRate,
INSERTED.ProductID AS SourceProductID,
INSERTED.ProductName AS SourceProductName,
INSERTED.Rate AS SourceRate;
SELECT * FROM #Products
The result of #Products would be as below:-
ProductID ProductName ProductNumber ProductType Rate
========= =========== ============= =========== ====
1 Tea 1 Drink 10.00
2 Coffee 2 Drink 25.00
4 Muffin 2 Food 35.00
7 Donut 5 Food 30.00
10 Pizza 8 Food 60.00
11 PizzaLarge 9 Food 80.00
For the product numbers (1,3,4,6,7) were all skipped and the new food product Pizza took Product number 8 and continued to be Product 9 for the PrizzaLarge.
hope this helps.
I'm having some difficulty understanding the best approach to get the following result set.
I have a result set (thousands of rows) that I want to update from:
ID Question Answer
--- -------- --------
1 Business NULL
1 Job Other
1 Location UK
2 Business Legal
3 Location US
4 Location UK
To This:
ID Buisness Job Location
--- -------- --- --------
1 NULL Other UK
2 Legal NULL NULL
3 NULL NULL US
4 NULL NULL UK
I have been looking at SELF JOINS and PIVOT tables but wanted to understand the best method as I have not been able to achieve the desired output.
Thanks
Gary
If you want to use pivot, you can do it like this:
CREATE TABLE #Table1
([ID] int, [Question] varchar(8), [Answer] varchar(5))
;
INSERT INTO #Table1
([ID], [Question], [Answer])
VALUES
(1, 'Business', NULL),
(1, 'Job', 'Other'),
(1, 'Location', 'UK'),
(2, 'Business', 'Legal'),
(3, 'Location', 'US'),
(4, 'Location', 'UK')
;
select * from
(select * from #Table1) S
pivot (
max(Answer) for Question in (Business, Job, Location)
) P
select
id,
max(case when question='business' then answer end) 'business',
max(case when question='Job' then answer end) 'Job',
max(case when question='Location' then answer end) 'Location'
group by id
I need to copy some master-detail records, along the lines of:
INSERT INTO Order
(
SupplierId
,DateOrdered
)
SELECT
SupplierID
,DateOrdered
FROM Order
WHERE SupplierId = 10
DECLARE #OrderId int;
Select #OrderId = Scope_Identity;
INSERT INTO OrderItem
(
Quantity
,ProductCode
,Price
,FkOrderId
)
SELECT
Quantity
,ProductCode
,Price
,FkOrderId
FROM OrderItem
WHERE FkOrderId = #OrderId
This will not work. The reason is that there are multiple Orders for Supplier = 10. So what is the best way to iterate through each Order where Supplier = 10, Add the order, and then add the relevant child OrderItem BEFORE going onto the next Order Record where supplier=10. I think I am talking about batching, possibly cursors, but I am a newbie to T-SQL / Store Procedures.
I would appreciate advice on the above.
Thanks.
EDIT
Some more information which I hope will clarify by virtue of some sample data.
Original Order Table
Id SupplierId DateOrdered
1 10 01/01/2000
2 10 01/01/2000
Original OrderItem Table
Id Quantity ProductCode Price FkOrderId
1 20 X1 100 1
2 10 Y1 50 1
3 30 X1 100 2
4 20 Y1 50 2
Final Order Table
Id SupplierId DateOrdered
1 10 01/01/2000
2 10 01/01/2000
3 10 01/01/2000 (Clone of 1)
4 10 01/01/2000 (Clone of 2)
Final OrderItem Table
Id Quantity ProductCode Price FkOrderId
1 20 X1 100 1
2 10 Y1 50 1
3 30 X1 100 2
4 20 Y1 50 2
5 20 X1 100 3 (Clone of 1, linked to clone Order=3)
6 10 Y1 50 3 (Clone of 2, linked to clone Order=3)
7 30 X1 100 4 (Clone of 3, linked to clone Order=4)
8 20 Y1 50 5 (Clone of 4, linked to clone Order=4)
So I need some help with the code can do this cloning of Order and OrderItem to achieve the "final" table records.
It seems I need to do something like:
For each matching record in "Order"
Clone Order Record
Clone OrderItem Record where FkOrderId = OldOrderId
Next
This answers your question (no cursors either)
SQL Fiddle
MS SQL Server 2008 Schema Setup:
CREATE TABLE [Order]
(
Id Int Primary Key Identity,
SupplierId Int,
DateOrdered Date
)
SET IDENTITY_INSERT [Order] ON
INSERT INTO [Order] (Id, SupplierId, DateOrdered)
VALUES
(1, 10, '01/01/2000'),
(2, 10, '01/01/2000')
SET IDENTITY_INSERT [Order] OFF
CREATE TABLE [OrderItem]
(
ID INT Primary Key Identity,
Quantity Int,
ProductCode CHAR(2),
Price Int,
FKOrderId Int
)
SET IDENTITY_INSERT [OrderItem] ON
INSERT INTO [OrderItem] (Id, Quantity, ProductCode, Price, FKOrderId)
VALUES
(1, 20, 'X1', 100, 1),
(2, 10, 'Y1', 50, 1),
(3, 30, 'X1', 100, 2),
(4, 20, 'Y1', 50, 2)
SET IDENTITY_INSERT [OrderItem] OFF
Query 1:
DECLARE #NewEntries TABLE (ID Int, OldId Int);
MERGE INTO [Order]
USING [Order] AS cf
ON 1 = 0 -- Ensure never match - therefore an Insert
WHEN NOT MATCHED AND cf.SupplierId = 10 THEN
INSERT(SupplierId, DateOrdered) Values(cf.SupplierId, cf.DateOrdered)
Output inserted.Id, cf.Id INTO
#NewEntries(Id, OldId);
INSERT INTO [OrderItem]
(
Quantity
,ProductCode
,Price
,FkOrderId
)
SELECT
Quantity
,ProductCode
,Price
,NE.ID
FROM [OrderItem] OI
INNER JOIN #NewEntries NE
ON OI.FKOrderId = NE.OldId ;
SELECT *
FROM [OrderItem];
Results:
| ID | QUANTITY | PRODUCTCODE | PRICE | FKORDERID |
|----|----------|-------------|-------|-----------|
| 1 | 20 | X1 | 100 | 1 |
| 2 | 10 | Y1 | 50 | 1 |
| 3 | 30 | X1 | 100 | 2 |
| 4 | 20 | Y1 | 50 | 2 |
| 5 | 20 | X1 | 100 | 3 |
| 6 | 10 | Y1 | 50 | 3 |
| 7 | 30 | X1 | 100 | 4 |
| 8 | 20 | Y1 | 50 | 4 |
Add an additional column to the Order table called OriginalOrderId. Make it nullable, FK'd back to OrderId, and put an index on it. Use "INSERT INTO [Order]... SELECT ... OUTPUT INSERTED.* INTO #ClonedOrders From ...". Add an index on #ClonedOrders.OriginalOrderId. Then you can do "INSERT INTO OrderItem ... SELECT co.OrderId, ... FROM #ClonedOrders co INNER JOIN OrderItem oi ON oi.OrderId = co.OriginalOrderId". This will get you the functionality that you're looking for, along with the performance benefits of set based statements. It will also leave you evidence of the original source of the orders and a field that you can use to differentiate cloned orders from non-cloned orders.
in this case you have to use output clause.. let me give you one sample script that will help you to relate with your requirement
Declare #Order AS Table(id int identity(1,1),SupplierID INT)
DECLARE #outputOrder AS TABLE
(Orderid INT)
INSERT INTO #Order (SupplierID)
Output inserted.id into #outputOrder
Values (102),(202),(303)
select * from #outputOrder
next step for your case would be use newly generated orderid from outputorder table & join to get orderitems from input table
This will handle your first table.
PS: Supply your questions in this state and they will be answered faster.
IF OBJECT_ID('Orders') IS NOT NULL DROP TABLE Orders
IF OBJECT_ID('OrderItem') IS NOT NULL DROP TABLE OrderItem
IF OBJECT_ID('tempdb..#FinalOrders') IS NOT NULL DROP TABLE #FinalOrders
CREATE TABLE Orders (OrdersID INT, SupplierID INT, DateOrdered DATETIME)
CREATE TABLE OrderItem (OrderItemID INT, Quantity INT, FkOrderId INT)
INSERT INTO Orders VALUES (1,20,'01/01/2000'),(2,20,'01/01/2000')
INSERT INTO OrderItem VALUES
(1,20,1),
(2,10,1),
(3,30,2),
(4,20,2)
SELECT
a.OrderItemID,
b.SupplierID,
b.DateOrdered
INTO #FinalOrders
FROM OrderItem as a
INNER JOIN Orders as b
ON a.FkOrderId = b.OrdersID
SELECT * FROM #FinalOrders
This can be achieved with a cursor. But please note that cursors will pose significant performance drawbacks.
DECLARE #SupplierID AS INT
DECLARE #OrderId AS INT
DECLARE #DateOrdered AS DATE
DECLARE #OrderIdNew AS INT
Declare #Order AS Table(OrderId INT,SupplierID INT,DateOrdered Date)
INSERT INTO #Order
SELECT
ID
,SupplierID
,DateOrdered
FROM [Order]
WHERE SupplierId = 10
DECLARE CUR CURSOR FAST_FORWARD FOR
SELECT
OrderId
,SupplierID
,DateOrdered
FROM #Order
OPEN CUR
FETCH NEXT FROM CUR INTO #OrderId, #SupplierID, #DateOrdered
WHILE ##FETCH_STATUS = 0
BEGIN
INSERT INTO [Order]
(
SupplierId
,DateOrdered
)
VALUES
(#SupplierID,#DateOrdered)
Select #OrderIdNew=##IDENTITY
INSERT INTO [OrderItem]
([Quantity]
,[ProductCode]
,[Price]
,[FkOrderId])
SELECT [Quantity]
,[ProductCode]
,[Price]
,#OrderIdNew
FROM [OrderItem]
WHERE [FkOrderId]=#OrderId
FETCH NEXT FROM CUR INTO #OrderId, #SupplierID, #DateOrdered
END
CLOSE CUR;
DEALLOCATE CUR;
You could try doing and inner join between Order and OrderItems where the clause of the inner join is SupplierId = 10,
or just modify your where to achieve the same result.
Try doing something along the lines of:
INSERT INTO OrderItem
(
Quantity
,ProductCode
,Price
,FkOrderId
)
SELECT
Quantity
,ProductCode
,Price
,FkOrderId
FROM OrderItem
where FkOrderId in (Select Id FROM Order WHERE SupplierId = 10)
I have a temp table variable with a bunch of columns:
Declare #GearTemp table
(
ItemNumber varchar(20),
VendorNumber varchar(6),
ItemStatus varchar(20),
Style varchar(20),
ItemName varchar(100),
ItemDescription varchar(1000),
Color varchar(50),
[Size] varchar(50),
ItemCost decimal(9,4),
IsQuickShipFl bit,
IsEmbroiderable bit,
IsBackOrderable bit,
LoadDate smalldatetime
)
It gets filled with data from another table via an insert statement, and I want to take that data and update my Products table. If possible, I would like to do something like this:
Update Products blah blah blah all columns where itemnumbers match up
SELECT * FROM #GearTemp FT
WHERE EXISTS (SELECT P.ItemNumber FROM Products P WHERE FT.ItemNumber = P.ItemNumber)
Is that possible to do? If it's not, please point me in the right direction.
If I understand you correctly, you can use something like this:
UPDATE p SET X = gt.X, Y = gt.Y -- etc... (not sure whether your column names match up)
FROM Products p
INNER JOIN #GearTemp gt ON p.ItemNumber = gt.ItemNumber
Note that this will only work if, as you stated in the comments above, there is only ever one entry in #GearTemp for each ItemNumber.
Since you are working on SQL Server 2008 you can use the MERGE statement:
-- Target Table
DECLARE #tgt TABLE (OrdID INT, ItemID INT, Qty INT, Price MONEY);
INSERT INTO #tgt (OrdID, ItemID, Qty, Price)
SELECT 1, 100, 10, 10.00 UNION ALL
SELECT 1, 101, 10, 12.00
OrdID ItemID Qty Price
----------- ----------- ----------- ---------------------
1 100 10 10.00
1 101 10 12.00
-- Source Table
DECLARE #src TABLE (OrdID INT, ItemID INT, Qty INT, Price MONEY);
INSERT INTO #src (OrdID, ItemID, Qty, Price)
SELECT 1, 100, 12, 10.00 UNION ALL
SELECT 1, 102, 10, 12.00 UNION ALL
SELECT 1, 103, 5, 7.00
OrdID ItemID Qty Price
----------- ----------- ----------- ---------------------
1 100 12 13.00
1 102 10 12.00
1 103 5 7.00
MERGE #tgt AS t
USING #src AS s
ON t.OrdID = s.OrdID AND t.ItemID = s.ItemID
WHEN MATCHED THEN
UPDATE SET
t.Qty = s.Qty,
t.Price = s.Price;
Content of the target table after the MERGE operation:
OrdID ItemID Qty Price
----------- ----------- ----------- ---------------------
1 100 12 13.00