SQL Server checking inventory stock on insert - sql-server

I'm creating an inventory for a restaurant using Microsoft SQL Server 2014.
This is diagram from 5 tables
I want to write a trigger for whenever I insert new order into orderDetail table, the table from inventory will auto update the outStock as orderDetail.quantity and the stockLeft based on previous stockLeft within the table that matched the product_ID.
I'm very new to SQL Server. Any opinion/suggestion would be very helpful to me. Thanks!!
Here are my table database from
inventory and orderDetail
This is what I've tried so far
USE [BuffaloWildWingsDB]
GO
CREATE TRIGGER update_stock
ON orderDetail
AFTER INSERT
AS
BEGIN
DECLARE #stock INT,#productID INT, #prevDate date, #prevStock INT;
SET #stock = (SELECT quantity FROM orderDetail)
SET #productID = (SELECT product_ID FROM orderDetail)
SET #prevDate = (SELECT [date] FROM inventory WHERE inventory.product_ID = #productID AND [date] < GETDATE())
SET #prevStock = (SELECT stockLeft FROM inventory WHERE [date] = #prevDate)
INSERT INTO inventory (product_ID, outStock, stockLeft) VALUES (#productID,#stock,#prevStock-#stock);
END
I can run this query fine but whenever I update orderDetail with
INSERT INTO orderDetail(order_ID,product_ID, quantity) values (10101, 1013, 2)
GO
I got this error
Msg 512, Level 16, State 1, Procedure update_stock, Line 14 Subquery
returned more than 1 value. This is not permitted when the subquery
follows =, !=, <, <= , >, >= or when the subquery is used as an
expression. The statement has been terminated.
Any idea?

Your trigger will only work for separate inserts only. If you will insert several rows at once it will fail. Also you are selecting from base table instead of INSERTED table. Here is what your trigger should look like:
insert into inventory (product_id, outStock, stockLeft)
select i.product_id, i.quantity, isnull(oa.stockLeft, 0) - i.quantity
from inserted i
outer apply(select top 1 stockLeft
from inventory
where product_id = i.product_id and [date] < GETDATE()
order by [date] desc)oa

Related

Sql Server Trigger error: SqlException: Subquery returned more than 1 value.

I have a trigger that inserts into another table based on one of INSERTED's values.
CREATE TRIGGER
[dbo].[triggerTest]
ON
[dbo].[Original]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON
If (SELECT city FROM INSERTED) = 'LA'
INSERT INTO Hotel
SELECT price, quality FROM Inserted AS I
ELSE IF (SELECT city FROM INSERTED) = 'NY'
INSERT INTO Inn
SELECT price, quality FROM Inserted AS I
END
I found that when I insert multiple rows into Original table, I get this error:
SQL Server Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >=
What's the problem with above trigger?
Your INSERTED table has more rows returned, So, you need top (1) with order by clause, but i would exists instead :
CREATE TRIGGER [dbo].[triggerTest]
ON [dbo].[Original]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON
IF EXISTS (SELECT 1 FROM INSERTED WHERE city = 'LA')
INSERT INTO Hotel (price, quality)
SELECT price, quality
FROM Inserted AS I
IF EXISTS (SELECT 1 FROM INSERTED WHERE city = 'NY')
INSERT INTO Inn (price, quality)
SELECT price, quality
FROM Inserted AS I
END
Note :
Always qualify column name explicitly while doing INSERT operation.

SQL Server set based processing for orders and details

I have been trying to get acclimated to set based processing with SQL Server. Below is a simplified version of cursor processing for this task. It involves creating an order from items in a shopping cart. The order is created, line items are added to the order details table, the total is accumulated and eventually updated on the order table. Can anyone suggest how to do this with a set based approach instead of a cursor?
One other question is that in most cases the cursor will process at most 10 or 12 line items at a time. Is that enough of a reason to not have to consider the set based approach?
declare getCart2 cursor for
select MemberID,ProductID,Quantity,Price
from Carts
where MemberID = #MemberID
open getCart2
fetch next from getCart2 into #MemberID,#ProductID,#Quantity,#Price
Insert into Orders
(MemberID,TotalAmount0
Values
(#MemberID, 0.00)
set #OrderID = ##Identity
while ##FETCH_STATUS = 0 Begin
Insert into OrderDetails
(OrderID,ProductID,Quantity)
Values
(#OderID,#ProductID,#Quantity)
set #TotalAmout = #TotalAmount + (#Quantity * #Price)
set #PrevMemberID = #MemberID
fetch next from getCart2 into #MemberID,#ProductID,#Quantity,#Price
End
close getCart2
deallocate getCart2
Update Orders
Set TotalAmount = #TotalAmount
Where OrderID = #OrderID
Thanks for your help.
Here goes an approach:
In this case I am creating a temporary table variable that will store the order id's on it.
Then, it performs the insertions on the Order table and after that, in the OrderDetails.
Finally, it computes the TotalAmount and updates on the Orders table.
Although you don't have it in your code (and in mine as well), but I recommend you to use this code inside a transaction.
Hope it helps you improve your performance.
USE [tempdb];
GO
SET NOCOUNT ON;
IF OBJECT_ID(N'dbo.Carts', N'U') IS NOT NULL DROP TABLE [dbo].[Carts];
IF OBJECT_ID(N'dbo.Orders', N'U') IS NOT NULL DROP TABLE [dbo].[Orders];
IF OBJECT_ID(N'dbo.OrderDetails', N'U') IS NOT NULL DROP TABLE [dbo].[OrderDetails];
GO
-- Creates the tables like you have
CREATE TABLE [dbo].[Carts] (MemberID INT, ProductID INT, Quantity INT, Price DECIMAL(10, 2));
CREATE TABLE [dbo].[Orders] (OrderID INT IDENTITY(1, 1), MemberID INT, TotalAmount DECIMAL(10, 2));
CREATE TABLE [dbo].[OrderDetails] (OrderID INT, ProductID INT, Quantity INT);
-- Inserts dummy data
INSERT INTO [dbo].[Carts] VALUES (1001, 80, 5, 25.00);
INSERT INTO [dbo].[Carts] VALUES (1002, 120, 2, 12.90);
INSERT INTO [dbo].[Carts] VALUES (1010, 70, 3, 12.00)
INSERT INTO [dbo].[Carts] VALUES (1034, 176, 5, 45.00);
-- Temporary table that stores the inserted Order ID's
DECLARE #OrdersToProcess TABLE (OrderID INT, MemberID INT);
-- Inserts all Orders
INSERT INTO Orders (MemberID, TotalAmount)
OUTPUT inserted.OrderID, inserted.MemberID INTO #OrdersToProcess
SELECT MemberID, 0
FROM [dbo].[Carts]
-- Inserts order details
INSERT INTO OrderDetails (OrderID, ProductID, Quantity)
SELECT OrderID, ProductID, Quantity
FROM [dbo].[Carts] C
INNER JOIN #OrdersToProcess O ON C.MemberID = O.MemberID;
-- Updates order totals
UPDATE [dbo].[Orders]
SET TotalAmount = T.Total FROM
(
SELECT OrderID, SUM(Quantity * Price) AS [Total]
FROM [dbo].[Carts] C
INNER JOIN #OrdersToProcess O ON C.MemberID = O.MemberID
GROUP BY OrderID
) T
WHERE [dbo].[Orders].OrderID = T.OrderID
SELECT * FROM [dbo].[Orders];
SELECT * FROM [dbo].[OrderDetails];
Results:
As I understand your problem, this store procedure should be called when a particular member presses the check-out button, so it should create a single order with all the items in the cart of that member.
You can use something like this:
INSERT INTO Orders (MemberID, TotalAmount)
VALUES (#MemberID, 0)
SET #OrderID=SCOPE_IDENTITY()
INSERT INTO OrderDetails (OrderID, ProductID, Quantity)
SELECT OrderID, ProductID, Quantity
FROM [dbo].[Carts] C
WHERE C.MemberID=#MemberID
UPDATE dbo.Orders SET TotalAmount=(
SELECT SUM(c.Quantity*c.Price)
FROM dbo.Carts c
WHERE c.MemberID=#MemberID
) WHERE OrderID=#OrderID
It's true that this reads the Carts table twice, but with a proper index (on the MemberID column) that should be fast enough.

SQL Server - Subquery returned more than 1 value

I have read some posts here about it but I do not understand at all.
In my case I have a table, let's say 'Parent' with two columns ID, DELETED.
ID is int and DELETED is a bit column.
I have another table, let's say 'Child' with columns ID, ID_PARENT, NAME, DELETED as Parent Table.
ID and ID_PARENT are int
NAME is nvarchar
DELETED is a bit
I have created a trigger FOR UPDATE on table Parent which updates Child table:
UPDATE
[dbo].[Child]
SET
[DELETED] = I.[DELETED]
FROM
INSERTED I INNER JOIN [dbo].[Child] AS c
ON I.ID = c.ID_PARENT
WHERE
c.[NAME] IS NOT NULL;
Keep in mind that one parent may have many childs, and one child only one parent.
So when I do below query:
UPDATE [dbo].[Parent]
SET DELETED = 1
WHERE ID IN (100, 200);
Of course, this will affect to many rows. I get below error:
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
Though you have already resolved your question. In comment section you have mentioned that you have triggered created on child. But for other community users here is complete scenario creation with solution.
CREATE TABLE TempParent
(
ID INT,
DELETED BIT
)
GO
CREATE TABLE TempChild
(
ID INT,
ID_PARENT int,
NAME varchar(100),
DELETED BIT
)
INSERT INTO TempParent
SELECT
100,0 UNION ALL
SELECT
200,0 UNION ALL
SELECT
300,0 UNION ALL
SELECT
400,0
GO
INSERT INTO TempChild
SELECT
100,100,'Rahul',0 UNION ALL
SELECT
200,200,'Rajesh',0 UNION ALL
SELECT
300,300,'Anil',0 UNION ALL
SELECT
400,400,NULL,0
GO
CREATE TRIGGER dbo.tr_TempParent
ON dbo.TempParent
FOR UPDATE
AS
BEGIN
SET NOCOUNT ON;
UPDATE
[dbo].TempChild
SET
[DELETED] = I.[DELETED]
FROM
INSERTED I INNER JOIN [dbo].TempChild AS c
ON I.ID = c.ID_PARENT
WHERE
c.[NAME] IS NOT NULL;
END
GO
SELECT * FROM TempParent
SELECT * FROM TempChild
UPDATE [dbo].TempParent
SET DELETED = 1
WHERE ID IN (100, 200);
DROP TABLE TempParent
DROP TABLE TempChild

How to make a Trigger on Master-Detail

I Have the following scenario:
CREATE TABLE dbo.Orders
(
OrderID int IDENTITY (1,1) NOT NULL
, OrderVersion int DEFAULT(1)
, Customer varchar(30)
, ScheduleDate date
, PaymentOption int
);
CREATE TABLE dbo.OrdersItems
(
OrderItemsID int IDENTITY (1,1) NOT NULL
, OrderID int
, Product varchar(100)
, Qty int
, value decimal(18,2)
);
CREATE TABLE dbo.logOrders
(
OrderID int NOT NULL
, OrderVersion int DEFAULT(1)
, Customer varchar(30)
, ScheduleDate date
, PaymentOption int
);
CREATE TABLE dbo.logOrdersItems
(
OrderItemsID int NOT NULL
, OrderID int
, Product varchar(100)
, Qty int
, value decimal(18,2)
);
-- Insert values into the table.
INSERT INTO dbo.Orders (Customer , ScheduleDate, PaymentOption)
VALUES ('John', 2016-09-01, 1);
INSERT INTO dbo.OrdersItems( OrderId, Product, Qty, Value)
VALUES (1, 'Foo', 20, 35.658),
(1, 'Bla', 50, 100)
(1, 'XYZ', 10, 3589)
First Statement
UPDATE Orders set ScheduleDate = 2016-10-05 WHERE OrderId = 1
Second Statement
Delete From OrdersItems WHERE OrderItemsID = 2
UPDATE OrdersItems set Qty = 5 WHERE OrderItemsID = 1
Third Statement
Update Orders set PaymentOption = 2 WHERE OrderId = 1
Update OrdersItems set Value = 1050 WHERE OrderItemsID = 3
I am trying to figure out how to make a trigger that after each one of the Statements Sample above Insert on the log Tables the data before the changing. And setting the OrderVersion to OrderVersion + 1 on table Orders.
So on the log Tables I will have all versions after the later one.
Is it possible to make a single trigger to monitor both tables and execute getting the original data before the UPDATE, DELETE , INSERT statement to get the original data and INSERT on the logTables ?
Here comes a sample to explain better what result I want.
This is the Initial Data on table Orders and OrdersItems
If I make an Update on Orders ( any column ) or Make an Update,Insert,Delete on OrdersItems I need to Insert on respectively logTables the data on the image.
And with this I'll have on logOrders and logItems the original data and on the Orders and Items the altered data.
I Hope I could explain better what I mean.
You will need two triggers. The trigger for the Orders table handles Orders table update/delete. The trigger for the OrdersItems table does the same for OrdersItems. The triggers look like this:
For the Orders table:
CREATE TRIGGER dbo.Orders_trigger
ON dbo.Orders
AFTER DELETE,UPDATE
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO dbo.logOrders
SELECT * FROM DELETED;
INSERT INTO dbo.logOrdersItems
SELECT oi.* FROM OrdersItems oi
WHERE oi.OrderID IN (SELECT OrderId FROM DELETED);
END
GO
For OrdersItems:
CREATE TRIGGER dbo.OrdersItems_trigger
ON dbo.OrdersItems
AFTER DELETE,UPDATE
AS
BEGIN
SET NOCOUNT ON;
--Inerst the changed/deleted OrdersItems into the log
INSERT INTO dbo.logOrdersItems
SELECT * FROM DELETED
--Inserts the unchanged sibling OrdersItems records into the log
INSERT INTO dbo.logOrdersItems
SELECT oi.* FROM OrdersItems oi
WHERE oi.OrderId IN (SELECT DISTINCT OrderId FROM DELETED)
AND oi.OrderItemsID NOT IN (SELECT DISTINCT OrderItemsID FROM DELETED);
INSERT INTO dbo.logOrders
SELECT o.* FROM Orders o
WHERE o.OrderID IN (SELECT DISTINCT OrderId FROM DELETED);
END
GO
The Orders Trigger is fairly straightforward. Use the virtual DELETED table to insert the original version of the records into the log. Then join to the child OrdersItems records and insert them into the log as well. The way this is written, it will work even if you update or delete multiple Order records at a time.
The OrdersItems Trigger is a bit more complicated. You need to log the pre-chage version of the OrdersItems and Orders Records. But you also want (I think) to log the unchanged "sibling" OrdersItems records as well so that you have a complete picture of the records.
I know this is just your sample data, but you will want to add some kind of a timestamp to the records in the log tables. Otherwise you just end up with a bunch of duplicate rows and you cannot tell which is which. At the beginning of the trigger you can create a variable to hold the update datetime and then append that to your INSERT statement for the logs.
DECLARE #UpdateDateTime DATETIME;
SET #UpdateDateTime = GETUTCDATE();

SQL Server UPDATE with WHERE spanning over 2 tables

I have a SQL Server database and I need to manually do an update query. There for no solutions using any programming language can be used.(stored procedures can be used)
I have 4 tables affected (/used) in the query.
[Orders]
[StatusHistoryForOrder]
[StatusHistory]
[Statuses]
I need to update the field [Orders].[OrderStatusID] which is a foreign key to [Statuses]. (So actually changing the state of the order. The table [StatusHistoryForOrder] is a linking table to [StatusHistory] and only contains 2 colums.
[StatusHistoryForOrder].[OrderId]
[StatusHistoryForOrder].[OrderStatusHistoryid]
Don't say that this is not logically cause I already know that. The company who designed the database is a complete retarded company but the database is now too large to set things straight and there is neither the time or money to do it.
The [StatusHistory] table has multiple columns:
[StatusHistory].[OrderStatusHistoryId]
[StatusHistory].[OrderStatusId]
[StatusHistory].[Date]
[StatusHistory].[Message]
The [StatusHistory].[OrderStatusId] is also a foreign key to [Statuses].
In the update query I need to update the status of the order to status 16. But only on rows that now have status 1 and are older then 60 days. I know I can check the date by using the function
DATEDIFF(DD,[StatusHistory].[Date],GETDATE()) > 60
But how to implement this query if the date field is not in the orders. And to set the new [StatusHistory] a new row has to be made for that table and the [StatusHistoryForOrder] table also needs a new row and the ID of that row needs to be set in the [Orders] table row.
Does anyone know how to do this? I am fairly new to SQL Server (or SQL for that matter) and I have absolutly no clue where to begin.
Conclusion:
I need a stored procedure that first checks every row in [Orders] if the [StatusHistory].[Date] (which is linked to the order using foreign keys) of that order is older that 60. If it is older then a new StatusHistory row must be inserted with the current date and status 16. Then in [StatusHistoryForOrder] a new row must be inserted with the new ID of the statusHistory been set in [StatusHistoryForOrder].[OrderStatusHistoryid] and the order id set in [StatusHistoryForOrder].[OrderId]. And last but not least: The [Orders].[OrderStatusID] also needs to be set to 16.
A select query to select the date and status of the order:
SELECT TOP (100) PERCENT
dbo.Orders.OrderID,
dbo.Statuses.Description AS Status,
dbo.StatusHistory.Date
FROM
dbo.Orders
INNER JOIN
dbo.Statuses
ON
dbo.Orders.OrderStatusID = dbo.Statuses.StatusId
INNER JOIN
dbo.StatusHistoryForOrder
ON
dbo.Orders.OrderID = dbo.StatusHistoryForOrder.OrderId
INNER JOIN
dbo.StatusHistory
ON
dbo.StatusHistoryForOrder.OrderStatusHistoryid = dbo.StatusHistory.OrderStatusHistoryId
WHERE
(dbo.Statuses.StatusId = 1)
AND
(DATEDIFF(DD, dbo.StatusHistory.Date, GETDATE()) > 60)
UPDATE
For #marc_s:
Can anyone help me with that?
Try this CTE (Common Table Expression) to find all those orders - does it work, are the results plausible? (this doesn't update anything just yet - just SELECTing for now):
USE (your database name here)
GO
DECLARE #OrdersToUpdate TABLE (OrderID INT, StatusHistoryID INT, StatusDate DATETIME)
;WITH RelevantOrders AS
(
SELECT
o.OrderId, sh.Date
FROM dbo.Orders o
INNER JOIN dbo.StatusHistoryForOrder ho ON ho.OrderId = o.OrderId
INNER JOIN dbo.StatusHistory sh ON ho.OrderStatusHistoryid = sh.OrderStatusHistoryid
WHERE
sh.Date <= DATEADD(D, -60, GETDATE()) -- older than 60 days back from today
AND o.OrderStatusID = 1 -- status = 1
)
INSERT INTO #OrdersToUpdate(OrderID, StatusDate)
SELECT OrderID, [Date]
FROM RelevantOrders
BEGIN TRANSACTION
BEGIN TRY
DECLARE #OrderIDToInsert INT, -- OrderID to process
#InsertedStatusHistoryID INT -- new ID of the inserted row in StatusHistory
-- grab the first OrderID that needs to be processed
SELECT TOP 1 #OrderIDToInsert = OrderID
FROM #OrdersToUpdate
WHERE StatusHistoryID IS NULL
ORDER BY OrderID
-- as long as there are still more OrderID to be processed ....
WHILE #OrderIDToInsert IS NOT NULL
BEGIN
PRINT 'Now inserting new StatusHistory entry for OrderID = ' + CAST(#OrderIDToInsert AS VARCHAR(10))
INSERT INTO dbo.StatusHistory(OrderStatusID, [Date], [Message])
VALUES(16, GETDATE(), 'Bulk Insert/Update operation') -- enter here whatever you want to store
SELECT #InsertedStatusHistoryID = SCOPE_IDENTITY(); -- grab newly inserted ID
PRINT 'New StatusHistory entry inserted with ID = ' + CAST(#InsertedStatusHistoryID AS VARCHAR(10))
UPDATE #OrdersToUpdate
SET StatusHistoryID = #InsertedStatusHistoryID
WHERE OrderID = #OrderIDToInsert
-- safety - reset #OrderIDToInsert to NULL so that we'll know when we're done
SET #OrderIDToInsert = NULL
-- read next OrderID to be processed
SELECT TOP 1 #OrderIDToInsert = OrderID
FROM #OrdersToUpdate
WHERE StatusHistoryID IS NULL
ORDER BY OrderID
END
-- insert into the StatusHistoryForOrder table
INSERT INTO dbo.StatusHistoryForOrder(OrderID, OrderStatusHistoryID)
SELECT OrderID, StatusHistoryID
FROM #OrdersToUpdate
-- update your Orders to status ID = 16
UPDATE dbo.Orders
SET OrderStatusID = 16
FROM #OrdersToUpdate upd
WHERE dbo.Orders.OrderID = upd.OrderID
COMMIT TRANSACTION
END TRY
BEGIN CATCH
SELECT
ERROR_NUMBER() AS ErrorNumber,
ERROR_SEVERITY() AS ErrorSeverity,
ERROR_STATE() AS ErrorState,
ERROR_PROCEDURE() AS ErrorProcedure,
ERROR_LINE() AS ErrorLine,
ERROR_MESSAGE() AS ErrorMessage
ROLLBACK TRANSACTION
END CATCH
This CTE basically joins your Orders table to the StatusHistory table (via the intermediate link table) and selects the values you're interested in (hopefully!).
This particular problem seems solvable with set operations only.
DECLARE #Orders TABLE (ID int, rownum int IDENTITY);
DECLARE #StatusHistory TABLE (ID int, rownum int IDENTITY);
/* get the list of orders with expired statuses */
INSERT INTO #Orders (ID)
SELECT o.OrderID
FROM Orders o
INNER JOIN StatusHistoryForOrder shfo ON o.OrderID = shfo.OrderId
INNER JOIN StatusHistory sh ON shfo.OrderStatusHistoryid = sh.OrderStatusHistoryId
GROUP BY o.OrderID
HAVING DATEDIFF(DD, MAX(sh.Date), GETDATE()) > 60
/* add so many new rows to StatusHistory and remember the new IDs */
INSERT INTO StatusHistory (OrderStatusId, Date, Message)
OUTPUT inserted.OrderStatusHistoryId INTO #StatusHistory (ID)
SELECT
16,
GETDATE(),
'Auto-inserted as the previous status has expired'
FROM #Orders
/* join the two temp lists together and add rows to StatusHistoryForOrder */
INSERT INTO StatusHistoryForOrder (OrderId, OrderStatusHistoryid)
SELECT o.ID, sh.ID
FROM #Orders o
INNER JOIN #StatusHistory sh ON o.rownum = sh.rownum
/* finally update the statuses in Orders */
UPDATE Orders
SET OrderStatusID = 16
FROM #Orders o
WHERE Orders.OrderID = o.ID
This should be the body of a single transaction, of course.

Resources