I have two table Stocks and Purchases
CREATE TABLE Stocks
(
id int PRIMARY KEY IDENTITY(1,1),
itemId int NOT NULL,
qty int NOT NULL,
status NVarChar(50) NOT NULL,
)
CREATE TABLE Purchases
(
id int PRIMARY KEY IDENTITY(1,1),
itemId int NOT NULL,
suppId int NOT NULL,
qty int NOT NULL,
Date NVarChar(50) NOT NULL,
status NVarChar(50) NOT NULL,
)
Here is what I want do:
ON Inserting Multiple Records into Purchases table, I want Create a Trigger that Iterates over each Record Inserted and Check itemId FROM the Stocks Table
UPDATE TheQuantity(qty in this case) of each itemId found AND INSERT into the Stocks Table if itemId isn't found
I have tried several ways, in fact I have been struggling with it since yesterday.
this Post was the closest one I have seen but could figure out what is going on, seems like it's only Updating already existing records
Here is what I have tried so far, I meanthe closest I came
CREATE trigger [dbo].[trPurchaseInsert] on [dbo].[Purchases] FOR
INSERT
AS
DECLARE #id int;
DECLARE #itemId int;
DECLARE #status NVarChar(50) = 'available';
SELECT #itemId=i.ItemId FROM INSERTED i
IF EXISTS(SELECT * FROM Stocks WHERE itemId=#itemId)
BEGIN
SET NOCOUNT ON;
DECLARE #itemIdUpdate int;
DECLARE #qty int;
SELECT #qty=[quantity], #itemIdUpdate=[itemId] FROM INSERTED i
UPDATE Stocks SET qty=qty+#qty WHERE itemId=#itemIdUpdate
END
ELSE
BEGIN
SET NOCOUNT ON;
INSERT INTO Stocks SELECT [itemId], [id], [quantity], #status
FROM INSERTED i
END
this works fine in a single Insert but doesn't work when multiple records are inserted into the Purchase Table at once
The above Trigger Updates the first itemId only and doesn't update the rest or even insert new ones if one itemid is found
The Goal here is to Update in stock items if itemid is found and Insert if itemId isn't found
For Further Details see this SQL Fiddle. It contains Tables & what I have tried with commented details
I have seen several comments advising to use set base operations with joins but couldn't figure a direction
How can I get it to work?
Your trigger is fatally flawed. It does not take into account multiple rows being inserted, it also doesn't deal with updates and deletes.
Instead you should use this:
Note how inserted and deleted are both used. deleted.qty is subtracted.
The whole thing is grouped up by itemId and then the difference is applied to Stocks.
Since you also want to insert, you need to use MERGE. Alternatively, you can have separate joined UPDATE and INSERT...WHERE NOT EXISTS statements.
CREATE OR ALTER TRIGGER dbo.trPurchaseInsert
ON dbo.Purchases
AFTER INSERT, UPDATE, DELETE
AS
SET NOCOUNT ON;
MERGE Stocks s
USING (
SELECT
diff.ItemId,
qty = SUM(diff.qty)
FROM (
SELECT
i.ItemId,
i.qty
FROM inserted i
UNION ALL
SELECT
d.ItemId,
-d.qty
FROM deleted d
) diff
GROUP BY
diff.ItemId
) diff
ON diff.ItemId = s.ItemId
WHEN NOT MATCHED THEN
INSERT (itemId, qty, status)
VALUES (diff.itemId, diff.qty, 'available')
WHEN MATCHED THEN
UPDATE SET
qty += diff.qty;
I must say, I would not implement this with triggers at all.
It's best not to denormalize such data into another table. If you need to query it, you can just do it on the fly from the Purchase table. Use a view if you want, and even index it.
CREATE VIEW dbo.vTotalStock
WITH SCHEMABINDING -- an indexed view must be schema-bound
AS
SELECT
p.ItemId,
qty = SUM(p.qty),
count = COUNT_BIG(*) -- must always include COUNT_BIG if aggregating and indexing
FROM dbo.Purchases p
GROUP BY
p.ItemId;
CREATE UNIQUE CLUSTERED INDEX UX ON vTotalStock (ItemId)
db<>fiddle
Related
I have a staging table titled [Staging]. The data from this table needs to be inserted into two separate tables. Half of the columns go to the first table (we'll call it [Table1]) and the other half go to a second table (we'll call it [Table2])
Both of these tables have a column titled "ChainID". In [Table1] the ChainID is an identity column. In [Table2] it's not.
The ChainID is the one column that links these two tables together for when we need to query this data.
I currently have it set up to where it will do the insert into [Table1] which then generates the new ChainIds. I can use "OUTPUT INSERTED.ChainID" to get the ChainId's that were generated but my problem is tying this back to the original staging table in order to grab the rest of the data for the second table.
DECLARE #Staging TABLE
(
[RowID] [int],
[ChainID] [varchar](50) ,
[LoanNo] [varchar](50) ,
[AssignmentFrom] [varchar](4000),
[AssignmentTo] [varchar](4000),
[CustodianUID] [nvarchar](100) null,
[DocCode] [nvarchar](100) null
)
INSERT
#Staging
SELECT
RowID,
ChainID,
LoanNo,
AssignmentFrom,
AssignmentTo,
CustodianUID,
DocCode
FROM
[MDR_CSV].[dbo].[TblCollateralAssignmentChainImport]
WHERE
UploadID = 1
This is where we do the insert into the first table which generates the new chainIds that will be needed to merge into Table2.
INSERT INTO
Table1
SELECT
LoanNo,
AssignmentFrom,
AssignmentTo,
CustodianUID
FROM
#Assignments AS MDRCA
WHERE
MDRCA.ChainID IS NULL
Now I need to insert the data from the DocCode field into Table2. I can get the list of newly generated ChainIds by doing something such as
OUTPUT INSERTED.ChainID
But that doesn't help being able to tie the newly generated chainId's back to the corresponding data rows from the Staging table in order to do the insert into Table2.
I ended up figuring out a solution. I used a curser to go through each row of data from the staging table one by one. That allowed me to do the insert into the first table (with only the pertinent columns from the staging table) along with doing an OUTPUT.INSTERTED ChainID and stored that newly generated ChainID into a table variable. I then assign that ChainID value to a regular variable by doing
#ChainID = (SELECT TOP 1 * FROM #tableVariable)
I could then use this ChainID to insert into the second table along with the rest of the data from the staging table that pertained to the same row of data (which was possible due to the curser)
Then before the curser loops I drop the table variable and recreate it back at the top of the loop so that the SELECT TOP 1 works correctly each time.
Not sure if it's the best or more efficient way to go about this but at least it worked!
Here's an example showing how I got it to work:
TblCollateralAssignmentChainImport is the Staging Table
temp_tblCollateralAssignment is Table1
temp_CustodianData is Table2
DECLARE #RowID int, #ChainID int;
DECLARE import_cur CURSOR FOR
SELECT RowID
FROM [MDR_CSV].[dbo].[TblCollateralAssignmentChainImport]
WHERE ChainId IS NULL
order by RowID;
OPEN import_cur
FETCH NEXT FROM import_cur
INTO #RowID
WHILE ##FETCH_STATUS = 0
BEGIN
DECLARE #NewlyCreatedChainId table (nChainId int);
INSERT INTO temp_tblCollateralAssignment
OUTPUT INSERTED.ChainID INTO #NewlyCreatedChainId
SELECT LoanNo, AssignmentFrom, AssignmentTo
FROM TblCollateralAssignmentChainImport
WHERE RowId = #RowID
SET #ChainID = (SELECT TOP 1 nChainId FROM #NewlyCreatedChainId)
INSERT INTO temp_CustodianData (ChainID, LoanNo, CustodianUID, DocCode)
SELECT #ChainID, import.LoanNo, import.CustodianUID, import.DocCode
FROM TblCollateralAssignmentChainImport AS import
WHERE RowId = #RowID
DELETE FROM #NewlyCreatedChainId
FETCH NEXT FROM import_cur
INTO #RowID
END
CLOSE import_cur;
DEALLOCATE import_cur;
I've a table type named product
CREATE TYPE [dbo].[udttProduct] AS TABLE(
[ProductID] [bigint] NOT NULL,
[ProductDescription] [varchar](500) NOT NULL,
[ProductCode] [varchar](50) NOT NULL)
And the stored procedure
CREATE PROCEDURE [dbo].[uspCreateOrUpdateProduct]
#ProductParam udttProduct READONLY,
AS
BEGIN
SET NOCOUNT ON;
MERGE Product AS [Target]
USING #ProductParam AS [Source]
ON
[Target].ProductCode = [Source].ProductCode
WHEN MATCHED THEN
UPDATE
SET
[Target].ProductDescription = [Source].ProductDescription
-- i would like to assign the updated ID back to #ProductParam so i can insert to log
WHEN NOT MATCHED BY TARGET THEN
INSERT
(
ProductDescription
, ProductCode
)
VALUES
(
[Source].[ProductDescription]
, [Source].[ProductCode]
);
-- i would like to assign the auto generated ID back to #ProductParam so i can insert to log
-- after insert / update, insert to log
INSERT INTO [dbo].[ProductLog]
(
ProductId, -- so i can insert id to here
ProductDescription,
ProductCode
)
SELECT
ProductID,
ProductDescription,
ProductCode
FROM
#ProductParam
SET NOCOUNT OFF;
END
GO
When performing merging, i would like to retrieve the updated / new id back to the #ProductParam, so that i can insert the record to log with ProductID.
I've other stored procedure is using merge and would like to do the same to get the id and insert into log, but those stored procedure involve large amount of data and the transaction is like 1 second for 10000+ records.
I get the id using temp table, but i'm just wondering if merge could do this.
If this is not the good way please advise me. thanks.
PS: My productID key is auto generated during insert.
You can use the OUTPUT clause of the MERGE statement to insert directly to your log table, and not worry about modifying your table valued parameter (which you can't do anyway because it is readonly):
MERGE Product AS [Target]
USING #ProductParam AS [Source]
ON [Target].ProductCode = [Source].ProductCode
WHEN MATCHED THEN
UPDATE SET [Target].ProductDescription = [Source].ProductDescription
WHEN NOT MATCHED BY TARGET THEN
INSERT (ProductDescription, ProductCode)
VALUES ([Source].[ProductDescription], [Source].[ProductCode])
OUTPUT inserted.ProductID, inserted.ProductDescription, inserted.ProductCode
INTO dbo.ProductLog (ProductID, ProductDescription, ProductCode);
The issue you may have is that if ProductLog.ProductID has a foreign key reference to Product.ProductID (which it should) then this technique won't work, you would need to stage the results into a new table, then do the insert:
DECLARE #tmpProductLog dbo.udttProduct;
MERGE Product AS [Target]
USING #ProductParam AS [Source]
ON [Target].ProductCode = [Source].ProductCode
WHEN MATCHED THEN
UPDATE SET [Target].ProductDescription = [Source].ProductDescription
WHEN NOT MATCHED BY TARGET THEN
INSERT (ProductDescription, ProductCode)
VALUES ([Source].[ProductDescription], [Source].[ProductCode])
OUTPUT inserted.ProductID, inserted.ProductDescription, inserted.ProductCode
INTO tmpProductLog (ProductID, ProductDescription, ProductCode);
INSERT dbo.ProductLog (ProductID, ProductDescription, ProductCode)
SELECT ProductID, ProductDescription, ProductCode
FROM #tmpProductLog;
I'm writing a DML trigger when change (update or Insert) happens in one table (Master table), I want to write the whole row into another table (Delta table).
The Master and Delta tables contains the same column with same datatype, except that Delta table contains an additional column called 'change_type', which should say either 'INSERT' OR 'MODIFY', depending on which trigger is updating the delta table.
The difficulty I'm having is I want to use the inserted table to update the Delta table row but its giving me errors.
CREATE TRIGGER [dbo].[TR_Update]
ON [dbo].[People_Master]
AFTER Update
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
Declare #RowCount int
Declare #ID int
Declare #Email nvarchar(50)
Declare #ct nvarchar(10)
select #ID = ID from inserted
Select #RowCount=COUNT(*) from People_Delta where People_Delta.ID = #ID and People_Delta.change_type = 'Modify';
if(#RowCount = 0)
Begin
Insert into People_Delta (ID,Email,uac,Department,FirstName,change_type)
values (iserted.ID,inserted.Email,inserted.uac,inserted.Department,inserted.Firstname'Modify');
END
END
GO
My table has 5 columns.
ID (primary key)
Email
Firstname
uac
Department
You are missing a , in your INSERT statement.
And because the number of columns you have specified does not match with the number of values you are inserting, you get an error.
inserted.Firstname , 'Modify'
Insert into People_Delta (ID,Email,uac,Department,FirstName,change_type)
values (iserted.ID,inserted.Email,inserted.uac,inserted.Department,inserted.Firstname,'Modify');
I am trying to create a simple to insert trigger that gets the count from a table and adds it to another like this
CREATE TABLE [poll-count](
id VARCHAR(100),
altid BIGINT,
option_order BIGINT,
uip VARCHAR(50),
[uid] VARCHAR(100),
[order] BIGINT
PRIMARY KEY NONCLUSTERED([order]),
FOREIGN KEY ([order]) references ord ([order]
)
GO
CREATE TRIGGER [get-poll-count]
ON [poll-count]
FOR INSERT
AS
BEGIN
DECLARE #count INT
SET #count = (SELECT COUNT (*) FROM [poll-count] WHERE option_order = i.option_order)
UPDATE [poll-options] SET [total] = #count WHERE [order] = i.option_order
END
GO
when i ever i try to run this i get this error:
The multi-part identifier "i.option_order" could not be bound
what is the problem?
thanks
Your trigger currently assumes that there will always be one-row inserts. Have you tried your trigger with anything like this?
INSERT dbo.[poll-options](option_order --, ...)
VALUES(1 --, ...),
(2 --, ...);
Also, you say that SQL Server "cannot access inserted table" - yet your statement says this. Where do you reference inserted (even if this were a valid subquery structure)?
SET #count = (SELECT COUNT (*) FROM [poll-count]
WHERE option_order = i.option_order)
-----------------------^ "i" <> "inserted"
Here is a trigger that properly references inserted and also properly handles multi-row inserts:
CREATE TRIGGER dbo.pollupdate
ON dbo.[poll-options]
FOR INSERT
AS
BEGIN
SET NOCOUNT ON;
;WITH x AS
(
SELECT option_order, c = COUNT(*)
FROM dbo.[poll-options] AS p
WHERE EXISTS
(
SELECT 1 FROM inserted
WHERE option_order = p.option_order
)
GROUP BY option_order
)
UPDATE p SET total = x.c
FROM dbo.[poll-options] AS p
INNER JOIN x
ON p.option_order = x.option_order;
END
GO
However, why do you want to store this data on every row? You can always derive the count at runtime, know that it is perfectly up to date, and avoid the need for a trigger altogether. If it's about the performance aspect of deriving the count at runtime, a much easier way to implement this write-ahead optimization for about the same maintenance cost during DML is to create an indexed view:
CREATE VIEW dbo.[poll-options-count]
WITH SCHEMABINDING
AS
SELECT option_order, c = COUNT_BIG(*)
FROM dbo.[poll-options]
GROUP BY option_order;
GO
CREATE UNIQUE CLUSTERED INDEX oo ON dbo.[poll-options-count](option_order);
GO
Now the index is maintained for you and you can derive very quick counts for any given (or all) option_order values. You'll have test, of course, whether the improvement in query time is worth the increased maintenance (though you are already paying that price with the trigger, except that it can affect many more rows in any given insert, so...).
As a final suggestion, don't use special characters like - in object names. It just forces you to always wrap it in [square brackets] and that's no fun for anyone.
I've been researching but I just can't seem to get it right.
I have the following tables:
create table school_tb
(idSchool int identity primary key,
nameSchool varchar(100),
schoolPopulation int
)
create table career_tb
(idCareer int identity primary key,
nameCareer varchar(100),
carrerPopulation int,
numberClasses int,
idSchool int foreign key references school_tb(idSchool)
)
to find out the populatuon in the first table I have to SUM() population from the careers in the same school.
I need to create a trigger that will update the column population in table school_tb when I update population in career_tb. please help me.
I had something like this, but I can't get it to work.
--create trigger updatePopulation
--on career_tb
--for update as
--if UPDATE(carrerPopulation)
--update school_tb set schoolPopulation =(SELECT add(carrerPopulation)
-- from career_tb
-- where idSchool=(SELECT idSchool
-- from career_tb
-- where idCareer=#idCareer)
-- )
--go
I appreciate any help given. thanks
This should help you out. Please see the comments inside the body of a trigger.
create trigger updatePopulation
on career_tb
-- to update sum even if carreer gets deleted or inserted
after insert, update, delete
as
-- to avoid trigger messing up rows affected
set nocount on
if UPDATE(carrerPopulation)
begin
-- update sum by difference between previous and current state of one record in career
update school_tb
set schoolPopulation = schoolPopulation + difference
from school_tb
-- derived table sums all the careers changed in one go
inner join
(
-- sum all values from careers by school
select idSchool, sum (carrerPopulation) difference
from
(
-- change sign of previous values
select deleted.idSchool, -deleted.carrerPopulation carrerPopulation
from deleted
union all
-- + current values
select inserted.idSchool, inserted.carrerPopulation
from inserted
) a
group by idSchool
-- Skip update in case of no change
having sum (carrerPopulation) <> 0
) a
on school_tb.idSchool = a.idSchool
end
CREATE TRIGGER name ON career_tb
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
SET NOCOUNT ON;
MERGE school_tb T
USING
(
SELECT idSchool, SUM(carrerPopulation) res
FROM
(
SELECT idSchool, carrerPopulation
FROM INSERTED
UNION ALL
SELECT idSchool, -carrerPopulation
FROM DELETED
) t
GROUP BY idSchool
) S
ON T.idSchool = S.idSchool
WHEN MATCHED THEN UPDATE SET
schoolPopulation = T.schoolPopulation +S.res
;
END