SQL Server trigger to update another table's column - sql

I have two SQL Server tables called Table A and Table B. I have an application which inserts one row into Table A and three rows into Table B at the same time. As you can see in the screenshot below, we can link these inserted records based on their ID column in Table A and TransID column in Table B.
During the data insert on table B, if any rows out of 3 inserted rows contain a value called Printed in the Printed column, I want to update my Table A's relevant record's PrintStatus column to Printed as well.
How do I write a SQL Server trigger for this?

Well the best solution is to do this in your code(app) but if there is no way,
you can write a Trigger After Insert for Table B like the trigger example below:
CREATE TRIGGER [dbo].[YourTrigger] ON [dbo].[TableB]
AFTER INSERT
AS
DECLARE #id INT
BEGIN
SET NOCOUNT ON;
SET #id = (SELECT DISTINCT ID FROM Inserted)
IF EXISTS (SELECT * FROM Inserted WHERE Printed='Printed')
UPDATE TableA
SET PrintStatus='Printed'
WHERE ID = #id
END
May this help you

It could be correct for your problem : (not sure at 100%)
CREATE TRIGGER TriggerTableB
ON TableB
AFTER INSERT
AS
UPDATE TableA AS A
SET PrintStatus = 'Printed'
WHERE A.TranID = inserted.ID
AND 'Printed' = (SELECT MAX(I.Printed)
FROM inserted AS I)

I would recommend querying for the information:
select a.*,
(case when exists (select 1
from b
where b.id = a.tranid and b.printed = 'Printed'
)
then 'Printed'
end) as printstatus
from a;
This is simpler than writing a query and you can wrap this in a view.
From a performance perspective, an index on b(id, printed) should make this pretty fast -- and not slow down inserts.
A trigger can be quite complicated, if you want to take inserts, updates, and deletes into account. I prefer to avoid such complication, if possible.

Related

Insert data from Table A to Table B, then Update Table A with Table B ID

I currently have two tables like the following:
Table A
TableAId
TableAPrivateField
CommonField1
CommonField2
CommonField..
TableBGeneratedId
1
datadatadata
datadatadata2
datadatadata3
d...
NULL
2
datadatadata5
datadatadata6
datadatadata7
d...
NULL
...
Table B
TableBId
CommonField1
CommonField2
CommonField..
...
What i want to do is insert into TableB some record fetched from TableA, and then update the column [TableBGenerateId] of TableA with the corresponding new Id from the inserted record in TableB.
I tried with declaring a Table Value Parameter and then use it with the OUTPUT clause, but i can't find a way to relate back to the original TableAId of the row that acted as the source for the insert
something like that:
DECLARE #InsertedTableB TABLE (
TableBId INT PRIMARY KEY
);
INSERT INTO TableB
OUTPUT inserted.TableBId INTO #InsertedTableB
SELECT CommonField1, CommonField2,..
FROM TableA
WHERE TableAPrivateField = 'MyCondition';
WITH NumberedTableA AS(
SELECT TableAId, ROW_NUMBER() OVER(ORDER BY TableAId) AS RowNum
FROM TableA
WHERE TableAPrivateField = 'MyCondition'
),
NumberedInsert AS(
SELECT TableBId, ROW_NUMBER() OVER(ORDER BY TableBId) AS RowNum
FROM #InsertedTableB
)
UPDATE TableA
SET GeneratedTableBId = NumberedInsert.TableBId
FROM TableA
JOIN NumberedTableA ON Table.TableAId = NumberedTableA.TableAId
JOIN NumberedInsert ON NumberedTableA.RowNum = NumberedTable.RowNum
My problem is that even thought the query works i have no guaranties that the order of the fetched records will be the same, so i would risk linking back the wrong Ids. I tried to figure out some different solutions, but the closest one i found was to temporarily add a column to TableB containing TableAId and then perform the update, but i disliked it because this operation needs to be executed frequently and it would be too performance demanding. Adding the column permanently also isn't an acceptable solution sadly.
Anyone has any suggestion on how solve this?
If you use MERGE rather than INSERT (but still only ever insert with the MERGE by using a condition that will never be met e.g. 1=0), you can capture both the ID from TableA, and the new ID from tableB in the OUTPUT clause and insert this to your table variable. Then use this table variable to update tableA:
DECLARE #InsertedTableB TABLE (TableBId INT PRIMARY KEY, TableAId INT NOT NULL);
MERGE INTO dbo.TableB AS b
USING
( SELECT TableAId, CommonField1, CommonField2
FROM dbo.TableA
WHERE TableAPrivateField = 'MyCondition'
) AS a
ON 1 = 0 -- <<<< Always false so will never match and only ever insert
WHEN NOT MATCHED THEN
INSERT (CommonField1, CommonField2)
VALUES (a.CommonField1, a.CommonField2)
OUTPUT inserted.TableBId, a.TableAId INTO #InsertedTableB (TableBId, TableAId);
UPDATE a
SET GeneratedTableBId = b.TableBId
FROM dbo.TableA AS a
INNER JOIN #InsertedTableB AS b
ON b.TableAId = a.TableAId;
Example on db<>fiddle
Whenever I post any answer that in anyway condones the use of MERGE it is met with at least one comment highlighting all of the bugs with it, so to pre-empt that: There are a lot of issues with using MERGE in SQL Server - I do not believe that any of those risks will apply in this scenario if you are (a) forcing an insert and (b) using a table as the target. So while I will always avoid MERGE where I can by using multiple statements, this is one scenario where I don't avoid it because I don't think there is a cleaner solution available without using MERGE. It is anecdotal, but I have used this method for years and have never once encountered an issue.

Conditional column in SQL Server based in another column

I'm looking for a solution to have a version column on my table based on another column.
I have a column "document No" in my table. Every time I insert a new row with the same document no, I would like to increase the column version.
I know I can it by the back-end. But, it means I have first to read the table and then insert. My idea is to optimize the performance and leave it with SQL Server.
Is It possible?
pk DocNo Version
---------------------
1 ABC 0
2 CBD 0
3 ABC 1
4 FGH 0
5 ABC 2
Assuming that you can parameterize your query (as in a stored procedure), AND your primary key is set to IDENTITY, you can use something along the lines of:
INSERT INTO TableA (DocNo, Version)
(SELECT TOP 1 'XYZ',ISNULL(MAX(Version)+1,0)
FROM TableA WHERE DocNo = 'XYZ')
I used 'XYZ' where you would place your parameter like:
INSERT INTO TableA (DocNo, Version)
(SELECT TOP 1 #DocNo,ISNULL(MAX(Version)+1,0)
FROM TableA WHERE DocNo = #DocNo)
Stored Procedure Solution
CREATE PROCEDURE tableUpsert(#DocNo varchar(100))
AS
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRAN
IF EXISTS(SELECT * FROM dbo.YourTable WITH (UPDLOCK) WHERE DocNo = #DocNo)
UPDATE dbo.YourTable
SET Version = Version + 1
WHERE DocNo = #DocNo;
ELSE
INSERT dbo.YourTable(DocNo, Version)
VALUES(#DocNo, 1);
COMMIT
Code is pretty self-explanatory. If the record exists, you update by incrementing your VersionNumber column and if it doesn't, then insert a new record with default VersionNumber of 1. Note the use of UPDLOCK to ensure that only your specific process is currently updating the record.
You can use insert trigger. In the trigger, update the Version by getting last version of same DocNo and increment by 1.
update t
set Version = isnull(v.Version, 0) + 1
from inserted i
inner join mytable t on i.pk = t.pk
outer apply
(
select Version = max(Version)
from mytable x
where x.DocNo = i.DocNo
) v
Your version number is implicit in your data. Use the PK to determine it via
SELECT DocNo, ROW_NUMBER() over (PARTITION BY DocNo ORDER BY pk) as version order by DocNo when you retrieve the data (or put that in a view)
Relying on IDENTITY may give you gaps
Relying on MAX(x)+1 may not always work depending on your concurrency model.
Locking the table/column will introduce concurrency issues (which may be unimportant or trivial in your case).

Comparing datetime value of columns from different tables

I have my sql query like this
if not exists(select RowId from dbo.Cache where StringSearched = #FirstName and colName = 'FirstName')
begin
--some code here
end
The purpose of above if statement is not to execute the piece of code inside of it if value of StringSearched is already present in Cache table which means it has been looked up before and so no need to make calculations again. The code inside of if statement if executed returns row number of rows from Table Band those are then inserted into Cache table to continue maintaining the cache. anyway .I need the records to be picked from Cache only if ModifiedAt column of Cache table is latest than ModifiedAt column of rows of Table B.
Note: I understand that I may need to use a subquery in where clause but in where clause itself, I need to check ModifiedAt column of Table B only for RowId's returned by Outer select query .
How can I proceed without making it much complex ?
You can use the subquery in the current query along with the Where clause.You didn't specified what are the columns to know for figure out which rows to get value so I assumed your tableB also has StringSearched and colName to get max(ModifiedAt) for that string vlaue.
IF NOT EXISTS (SELECT * from dbo.Cache as c WHERE StringSearched = #FirstName
AND colName = 'FirstName'
AND ModifiedAt > (Select MAX(ModifiedAt) FROM tableB as tabB WHERE tabB.RowID = c.RowID ))
BEGIN
--your query
END

SQL pivoted table is read-only and cells can't be edited?

If I create a VIEW using this pivot table query, it isn't editable. The cells are read-only and give me the SQL2005 error: "No row was updated. The data in row 2 was not committed. Update or insert of view or function 'VIEWNAME' failed because it contains a derived or constant field."
Any ideas on how this could be solved OR is a pivot like this just never editable?
SELECT n_id,
MAX(CASE field WHEN 'fId' THEN c_metadata_value ELSE ' ' END) AS fId,
MAX(CASE field WHEN 'sID' THEN c_metadata_value ELSE ' ' END) AS sID,
MAX(CASE field WHEN 'NUMBER' THEN c_metadata_value ELSE ' ' END) AS NUMBER
FROM metadata
GROUP BY n_id
Assuming you have a unique constraint on n_id, field which means that at most one row can match you can (in theory at least) use an INSTEAD OF trigger.
This would be easier with MERGE (but that is not available until SQL Server 2008) as you need to cover UPDATES of existing data, INSERTS (Where a NULL value is set to a NON NULL one) and DELETES where a NON NULL value is set to NULL.
One thing you would need to consider here is how to cope with UPDATES that set all of the columns in a row to NULL I did this during testing the code below and was quite confused for a minute or two until I realised that this had deleted all the rows in the base table for an n_id (which meant the operation was not reversible via another UPDATE statement). This issue could be avoided by having the VIEW definition OUTER JOIN onto what ever table n_id is the PK of.
An example of the type of thing is below. You would also need to consider potential race conditions in the INSERT/DELETE code indicated and whether you need some additional locking hints in there.
CREATE TRIGGER trig
ON pivoted
INSTEAD OF UPDATE
AS
BEGIN
SET nocount ON;
DECLARE #unpivoted TABLE (
n_id INT,
field VARCHAR(10),
c_metadata_value VARCHAR(10))
INSERT INTO #unpivoted
SELECT *
FROM inserted UNPIVOT (data FOR col IN (fid, sid, NUMBER) ) AS unpvt
WHERE data IS NOT NULL
UPDATE m
SET m.c_metadata_value = u.c_metadata_value
FROM metadata m
JOIN #unpivoted u
ON u.n_id = m.n_id
AND u.c_metadata_value = m.field;
/*You need to consider race conditions below*/
DELETE FROM metadata
WHERE NOT EXISTS(SELECT *
FROM #unpivoted u
WHERE metadata.n_id = u.n_id
AND u.field = metadata.field)
INSERT INTO metadata
SELECT u.n_id,
u.field,
u.c_metadata_value
FROM #unpivoted u
WHERE NOT EXISTS (SELECT *
FROM metadata m
WHERE m.n_id = u.n_id
AND u.field = m.field)
END
You'll have to create trigger on view, because direct update is not possible:
CREATE TRIGGER TrMyViewUpdate on MyView
INSTEAD OF UPDATE
AS
BEGIN
SET NOCOUNT ON;
UPDATE MyTable
SET ...
FROM INSERTED...
END

T-SQL cursor and update

I use a cursor to iterate through quite a big table. For each row I check if value from one column exists in other.
If the value exists, I would like to increase value column in that other table.
If not, I would like to insert there new row with value set to 1.
I check "if exists" by:
IF (SELECT COUNT(*) FROM otherTabe WHERE... > 1)
BEGIN
...
END
ELSE
BEGIN
...
END
I don't know how to get that row which was found and update value. I don't want to make another select.
How can I do this efficiently?
I assume that the method of checking described above isn't good for this case.
Depending on the size of your data and the actual condition, you have two basic approaches:
1) use MERGE
MERGE TOP (...) INTO table1
USING table2 ON table1.column = table2.column
WHEN MATCHED
THEN UPDATE SET table1.counter += 1
WHEN NOT MATCHED SOURCE
THEN INSERT (...) VALUES (...);
the TOP is needed because when you're doing a huge update like this (you mention the table is 'big', big is relative, but lets assume truly big, +100MM rows) you have to batch the updates, otherwise you'll overwhelm the transaction log with one single gigantic transaction.
2) use a cursor, as you are trying. Your original question can be easily solved, simply always update and then check the count of rows updated:
UPDATE table
SET column += 1
WHERE ...;
IF ##ROW_COUNT = 0
BEGIN
-- no match, insert new value
INSERT INTO (...) VALUES (...);
END
Note that this approach is dangerous though because of race conditions: there is nothing to prevent another thread from inserting the value concurrently, so you may end up with either duplicates or a constraint violation error (preferably the latter...).
This is just psuedo code because I have no idea of your table structure but I think you will understand... basically Update the columns you want then Insert the columns you need. A Cursor operation sounds unnecessary.
Update OtherTable
Set ColumnToIncrease = ColumnToIncrease + 1
FROM CurrentTable Where ColumnToCheckValue is not null
Insert Into OtherTable (ColumnToIncrease, Field1, Field2,...)
SELECT
1,
?
?
FROM CurrentTable Where ColumnToCheckValue is not null
Without a sample, I think this is the best I can do. Bottom line: you don't need a cursor. UPDATE where a match exists (INNER JOIN) and INSERT where one does not.
UPDATE otherTable
SET IncrementingColumn = IncrementingColumn + 1
FROM thisTable INNER JOIN otherTable ON thisTable.ID = otherTable.ID
INSERT INTO otherTable
(
ID
, IncrementingColumn
)
SELECT ID, 1
FROM thisTable
WHERE NOT EXISTS (SELECT *
FROM otherTable
WHERE thisTable.ID = otherTable.ID)
I think you'd be better off using a view for this -- then it's always up to date, no risk of mistakenly double/triple/etc counting:
CREATE VIEW vw_value_count AS
SELECT st.value,
COUNT(*) AS numValue
FROM SOME_TABLE st
GROUP BY st.value
But if you still want to use the INSERT/UPDATE approach:
IF EXISTS(SELECT NULL
FROM SOMETABLE WHERE ... > 1)
BEGIN
UPDATE TABLE
SET count = count + 1
WHERE value = #value
END
ELSE
BEGIN
INSERT INTO TABLE
(value, count)
VALUES
(#value, 1)
END
What about Update statement with inner join to perform +1, and Insert selected rows that do not exist in the first table.
Provide the tables schema and the columns you want to check and update so I can help.
Regards.