Archiving data from Table1 to Table2 upon condition | Sql Server 2017 Express - sql-server

I have a database with two tables:
Table Student with the following
columns:
StudentID int identity,
StudentFN,
StudentLN,
Active bit,
EnrollmentDate
Table ArchivedStudent with the following columns:
ArvchivedStudentID int identity,
StudentID int,
StudentFN,
StudentLN,
WithdrawalDate getdate(),
ReasonDropped
In the long run, I'd like to schedule automatic updates for the table AcrchivedStudent and move the data from columns StudentID, StudentFN and StudentLN from table Student to table ArchnivedStudent when column Active changes from 1 (true) to 0 (false).
Here's my start up script that is not working:
update [as]
set [as].StudentID = s.StudentID,
[as].StudentFN = s.StudentFN,
[as].StudentLN = s.StudentLN
from ArchivedStudent [as]
inner join Student s
on [as].StudentID = s.StudentID
where s.Active = 0
go
The issue is that it does not return any results.
Once I'll be able to update table ArchivedStudent, I'd like to delete data of the students whose Active status changed to 0 in the Student table.

Your question still isn't very clear on the process. For example, do you want to allow the student to be deactivated for a certain period of time before they are moved to the archive table or do you want the student to be immediately moved to the archived table once the student is deactivated?
If the latter, this is much easier:
INSERT INTO ArchivedStudent (StudentId, StudentFn, StudentLn, WithdrawalDate)
SELECT S.StudentId, S.StudentFn, S.StudentLn, GETDATE()
FROM Student S
WHERE StudentId = ?
DELETE FROM Student WHERE StudentId = ?
If the former, then that is more challenging and we will require more detail.
Update 1:
To set the Withdrawal date based off a calculated value, use the following:
INSERT INTO ArchivedStudent (StudentId, StudentFn, StudentLn, WithdrawalDate)
SELECT S.StudentId, S.StudentFn, S.StudentLn, CAST(DATEADD(D,14,GETDATE()) AS DATE)
FROM Student S
WHERE StudentId = ?
Note 1: In DATEADD(), use a positive value for future dates and use a negative value for past dates. You can remove the DATE CAST if you need the actual time in addition to the date.
Note 2: The DELETE script posted in the original answer still stands.

You need a trigger to do it :
CREATE TRIGGER ArchiveStudent ON Student
FOR UPDATE
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO ArchivedStudent (StudentID, StudentFN, StudentLN)
SELECT
StudentID
, StudentFN
, StudentLN
FROM
Student
WHERE
Active = 0
DELETE FROM Student
WHERE
Active = 0
END
However, your approach it's simple, and risky at the same time. For instance, if someone made a student inactive by mistake, then the trigger will immediately insert that student into archive table then deleted. Surely you can retrieve it by many ways such as deleted, inserted tables or even get the max id of archive table, but why you put yourself in this situation in the first place?. This is one of many general issues could be experienced by the current approach. A better approach is to actually add more versioning or historian methods for the tables, and make the archives run either from SQL Job or a store procedure on a fixed dates rather than triggers. this would give you a scheduled and controlled data archiving.
You can even add a historian columns which will store the value of active column and the date of the change. Then, use trigger or store procedure to do it for you (or even a computed column with a generic scalar function that will be reused on multiple tables). for instance, if the student is inactive for 5 business days, then archive it and delete it from the table.

You could use a TRIGGER AFTER UPDATE on the Student table.
This trigger would:
- react only on UPDATE,
- transfer Student to ArchiveStudent, when Active is set to 0
- and set WithdrawalDate to 2 weeks from today.
Trigger:
CREATE TRIGGER [Student_Changed]
ON Student
AFTER UPDATE
AS
BEGIN
-- Archive
INSERT INTO ArchiveStudent
(StudentID, StudentFN, StudentLN, WithdrawalDate)
SELECT
DELETED.StudentID
,DELETED.[StudentFN]
,DELETED.[StudentLN]
,DATEADD(day, 14, GETDATE()) -- 2 Weeks from today
FROM DELETED
WHERE DELETED.Active = 1
-- Delete archived
DELETE FROM Student
WHERE StudentID = (SELECT DELETED.[StudentID] FROM DELETED)
AND Active = 0
END;
DEMO:
You can take a look at the SQL Fiddle solution here.

There are a number of solutions here that all appear partially correct, but with some issues. Your initial update of your archive table will not insert into the archive table, only update an existing row. And since you are trying to join between the live table and the archive table, you will get no results - well, no updates since an update statement doesn't produce "results" as such anyway.
So as other have said you would use two statements - one an insert statement and one a delete. I would tend to be on the careful side and make sure that I a) dont get duplicates in my archive table and b) dont delete from live before I am sure its made it into the archive. So the two statements would be:
insert archivestudent(...fieldlist...)
select * from student
where active=0
and not exists(select * from archivestudent where archivestudent.studentid=student.studentid)
delete student
where active=0
and exists(select * from archivestudent where archivedstudent.studentid=student.studentid)
You can then run this code whenever you wish, schedule it as a job to run each night, whatever makes sense in your app.
If, on the other hand you want to immediately run then a trigger is the way to go. Be aware though that triggers are set-based operations, meaning that the trigger runs once for all rows affected by an update. This means that the solution proposed by #Milan will fail if the triggering update affects more than one row, because the clause WHERE StudentID = (SELECT DELETED.[StudentID] FROM DELETED) will return more one value. An example might be update student set active=0 where enrolmentdate<'2017-01-01'
You should always join to the internal tables exposed inside a trigger, in this case the DELETED table
delete student
from deleted
join student on student.studentid=deleted.studentid
where active=0
I'd still be tempted to add the where exists/not exists clauses inside the trigger as well just to make it more error-proof.

You need two queries:
Insert
Insert into archivedstudent (studentid, student, studentln) select studentid, studentfn, studentln from student where active=0 and studentid not in (select studentid from archivedstudent);
And the delete
Delete from student where studentid in (select studentid from archivedstudent);

What you need is a trigger.SQL Server Trigger After Update for a Specific Value However you should be careful with triggers on large amounts of data, they can hurt performance.

Related

How to write query/trigger that satisfies following scenario?

Solution required in SQL Server.
Suppose there are 2 tables
TICKET table with following columns:
item_id - PK
ticket_cost
TICKET_PAST with columns:
price - FK
ticket_new_cost
The question is to write a trigger such that whenever a price in the TICKET table is inserted, updated or deleted a new row should be generated by trigger?
Using an after update trigger, and accessing the new value from inserted and the old value from deleted:
create trigger dbo.item_price_update_trigger
on dbo.item
after update as
begin;
set nocount on;
insert into item_hist (item_id, price, new_price)
select i.item_id, d.price, i.price
from inserted i
inner join deleted d
on i.item_id = d.item_id
end;
go
rextester demo: http://rextester.com/PBB85814
This trigger answers your question, but would most likely be just one of three triggers for a complete auditing/history solution. You may want a trigger for after insert to store the initial insert of an item, and an after delete trigger to record the final price of an item that was deleted.
You would also probably want to include the dates of when these actions occurred, so you could query what the effective price was of an item at a given time.
For a somewhat automated option of creating audit/history tables and related triggers, this article and the related scripts may be helpful: Quick And Easy Audit Tables - Dave Britten
Reference:
Use the inserted and deleted Tables
create trigger (Transact-SQL)
DML Triggers

Writing a SQL Server trigger - error

I have three tables:
Bus_Driver (drNo,drName,salary,StationNo,experience)
Station (StationNo,address,district,Salary_Commission)
Cleaner (Cleaner_No, Cname, StationNo)
The question is to write a trigger. It states that if a bus driver's salary is increased by 20% more than the original salary then 0.05% of the increased value will be transferred to his Station as Salary commission.
I managed to write the trigger halfway, but got stuck when I have to transfer the amount to the other table.
My code is:
CREATE TRIGGER tr_1
ON Bus_Driver
AFTER INSERT
AS
BEGIN
DECLARE #salary MONEY
SET #Salary = 0
SELECT #Salary= salary
FROM Inserted
WHERE #Salary > (120 / 100 * #Salary)
Can anyone help me how to write the next steps please
The trigger you wrote is wrong.
First, it's a trigger for insert, while the question states that the salary is raised, meaning it should be a trigger for update.
Second, your trigger assumes only a single row will be in the inserted table. However, this assumption is wrong. Triggers in SQL server are fired per statement, not per row, meaning that the inserted (and deleted) tables might contain zero, one, or many rows.
A solution for this question will be to write a trigger for update, that will, in turn, update the station table. Something like this:
CREATE TRIGGER tr_Bus_Driver_Update ON Bus_Driver
FOR UPDATE
AS
UPDATE s
SET Salary_Commission = Salary_Commission -
(0.0005 * D.Salary) + -- Remove old salary of the driver(s) from salary_commition.
(0.0005 * I.Salary) -- add new salary of the driver(s) to salary_commition
FROM Station s
INNER JOIN Inserted I ON s.StationNo = I.StationNo
INNER JOIN Deleted D ON I.drNo = D.drNo -- assuming drNo is unique in Bus_Driver table
WHERE I.Salary >= D.Salady * 1.2 -- You might need to cast to a floating point data type if the Salary is an integer data type
Note you might need to cast the salary to a floating point data type if the Salary is an integer data type whenever it's used in this trigger.
You need to stop and start again from scratch.
First of all, you need to catch the AFTER UPDATE event - not the insert - since you want to do something when the salary is updated (an existing value is replaced with a higher one).
Secondly, the trigger will be called once per UPDATE statement and if that UPDATE affects more than one row, the Deleted and Inserted pseudo tables will contain multiple rows of data - so your SELECT #Salary = salary FROM Inserted statement is doomed - it will fetch one arbitrary row and ignore all others that might be also affected.
In an UPDATE case, Inserted will have the new values (after the update), while Deleted has the old values (before the update) - so the difference between these two pseudo tables can be used to figure out if the salary increase was more than 20%:
CREATE TRIGGER trBusDriverSalaryIncrease
ON dbo.Bus_Driver
AFTER UPDATE
AS
BEGIN
-- declare a table variable to hold all revelant values
DECLARE #RelevantIncreases TABLE (drNo INT, StationNo INT, SalaryIncrease DECIMAL(18,2))
-- find those bus drivers who have had a more than 20% increase in their salary
INSERT INTO #relevantIncreases (drNo, StationNo, SalaryIncrease)
SELECT
i.drNo, i.StationNo, -- Driver and Station No
(i.Salary - d.Salary) -- Salary increase in absolute numbers
FROM
Deleted d
INNER JOIN
Inserted i ON d.drNo = i.drNo
WHERE
-- Salary increased by more than 20%
i.Salary > 1.2 * d.Salary
-- now that we have all the relevant bus drivers and their salary increase
-- insert this into the Station.Salary_Commission column
UPDATE s
SET Salary_Commission = s.Salary_Commission + ri.SalaryIncrease * 0.0005
FROM dbo.Station s
INNER JOIN #RelevantIncreases ri ON ri.StationNo = s.StationNo
END

How to store existing column value in new table using trigger

I have two tables
Customer
CustomerUpdate
Structure of both tables are like this
Customer table's structure
CustomerName | CustomerId
CustomerUpdate table's structure
NewCustomerName | NewCustomerId | OldCustomerName
I have few values inserted in the Customer table. Whenever I should update the data in this table I want that the existing as well as new data should be triggered into new table CustomerUpdate.
For this I created a trigger but this is only pulling the updated data, it's not pulling the existing data..
CREATE TRIGGER trgAfterUpdate
ON [dbo].Customer
FOR UPDATE
AS
SET NOCOUNT ON
declare #NewCustomerName nchar(20);
declare #NewCustomerId nchar(20);
declare #OldCustomerName nchar(20);
declare #audit_action varchar(100);
select #NewCustomerName = i.CustomerName from inserted i;
select #NewCustomerId = i.CustomerId from inserted i;
select #OldCustomerName = c.CustomerName
from Customer c
where CustomerId = #NewCustomerId;
if update(CustomerName)
set #audit_action='Updated Record -- After Update Trigger.';
if update(CustomerId)
set #audit_action='Updated Record -- After Update Trigger.';
insert into CustomerUpdate(NewCustomerName, NewCustomerId, OldCustomername)
values(#NewCustomerName, #NewCustomerId, #OldCustomerName);
PRINT 'AFTER UPDATE Trigger fired.'
GO
Please help me out
First, selecting from the table being modified when an update trigger is executing will get the new value. These are AFTER triggers (rather than INSTEAD triggers) and therefore the update has already happened by the time the trigger fires (although it can be rolled back). If you need the old value, you should select from the DELETED pseudo-table.
Second, as pointed out by #marc_s in comments, your trigger has the hidden assumption that only one row is affected by each update. This may very well be a valid assumption for your environment, if your application only ever updates one row at a time, but in the general case, every trigger should be ready to handle the case where many rows are affected by a single update. Writing your triggers to handle multiple rows is good practice.
Third, all of your sequentially executing code is pretty much unnecessary. The old value and the new value can be retrieved and inserted all at once:
CREATE TRIGGER trgAfterUpdate
ON [dbo].Customer
FOR UPDATE
AS
BEGIN
SET NOCOUNT ON
insert into CustomerUpdate(NewCustomerName, NewCustomerId, OldCustomername)
-- case 1: ID unchanged
SELECT I.CustomerName, I.CustomerID, D.CustomerName
FROM Inserted I
JOIN Deleted D on I.CustomerID=D.CustomerID
UNION ALL
-- case 2: ID changed, Name unchanged
SELECT I.CustomerName, I.CustomerID, D.CustomerName
FROM Inserted I
JOIN Deleted D on I.CustomerName=D.CustomerName
WHERE I.CustomerID<>D.CustomerID
UNION ALL
--case 3: ID changed, Name changed
SELECT I.CustomerName, I.CustomerID, D.CustomerName
FROM Inserted I
LEFT JOIN Deleted D on I.CustomerID=D.CustomerID OR I.CustomerName=D.CustomerName
WHERE D.CustomerID IS NULL;
END

Logging deletion of rows in table sql server

I have been searching for a way to log the deletion of rows from a table.
Tried this Log record changes in SQL server in an audit table but it didn't help me.
I have a song list database, the log table has the columns: Title / Artist / Year / Position / SentinDate .
There is a list with songs from the years 1999 to 2014, and every year has 2000 songs (top2000 is what it is called in The Netherlands).
Basically what the log table should look like once a certain Year has been deleted:
I need a basic way trigger-log when someone deletes a certain year from the list of 1999-2014.
I hope to have informed enough for you to understand, if not I will try to explain in more detail.
A trigger rejects or accepts each data modification transaction as a whole.
Using a correlated subquery in a trigger can force the trigger to examine the modified rows one by one.
Examples
A. Use an AFTER INSERT trigger
The following example assumes the existence of a table called newsale in the pubs database. This the CREATE statement for newsale:
CREATE TABLE newsale
(stor_id char(4),
ord_num varchar(20),
date datetime,
qty smallint,
payterms varchar(12),
title_id tid)
If you want to examine each of the records you are trying to insert, the trigger conditionalinsert analyzes the insert row by row, and then deletes the rows that do not have a title_id in titles.
CREATE TRIGGER conditionalinsert
ON sales
AFTER INSERT AS
IF
(SELECT COUNT(*) FROM titles, inserted
WHERE titles.title_id = inserted.title_id) <> ##ROWCOUNT
BEGIN
DELETE sales FROM sales, inserted
WHERE sales.title_id = inserted.title_id AND
inserted.title_id NOT IN
(SELECT title_id
FROM titles)
PRINT 'Only sales records with matching title_ids added.'
END
When unacceptable titles have been inserted, the transaction is not rolled back; instead, the trigger deletes the unwanted rows. This ability to delete rows that have been inserted relies on the order in which processing occurs when triggers are fired. First, rows are inserted into the sales table and the inserted table, and then the trigger fires.
Simply create an INSTEAD OF DELETE trigger ! In that trigger, you have a "virtual" table called deletedwhich contains all records which are to be deleted.
So in the trigger, you can just insert all records contained in deleted to your log table, and then you delete the records from our table. (this will then be a DELETE statement with a join to the deleted table)

T-SQL Insert or update

I have a question regarding performance of SQL Server.
Suppose I have a table persons with the following columns: id, name, surname.
Now, I want to insert a new row in this table. The rule is the following:
If id is not present in the table, then insert the row.
If id is present, then update.
I have two solutions here:
First:
update persons
set id=#p_id, name=#p_name, surname=#p_surname
where id=#p_id
if ##ROWCOUNT = 0
insert into persons(id, name, surname)
values (#p_id, #p_name, #p_surname)
Second:
if exists (select id from persons where id = #p_id)
update persons
set id=#p_id, name=#p_name, surname=#p_surname
where id=#p_id
else
insert into persons(id, name, surname)
values (#p_id, #p_name, #p_surname)
What is a better approach? It seems like in the second choice, to update a row, it has to be searched two times, whereas in the first option - just once. Are there any other solutions to the problem? I am using MS SQL 2000.
Both work fine, but I usually use option 2 (pre-mssql 2008) since it reads a bit more clearly. I wouldn't stress about the performance here either...If it becomes an issue, you can use NOLOCK in the exists clause. Though before you start using NOLOCK everywhere, make sure you've covered all your bases (indexes and big picture architecture stuff). If you know you will be updating every item more than once, then it might pay to consider option 1.
Option 3 is to not use destructive updates. It takes more work, but basically you insert a new row every time the data changes (never update or delete from the table) and have a view that selects all the most recent rows. It's useful if you want the table to contain a history of all its previous states, but it can also be overkill.
Option 1 seems good. However, if you're on SQL Server 2008, you could also use MERGE, which may perform good for such UPSERT tasks.
Note that you may want to use an explicit transaction and the XACT_ABORT option for such tasks, so that the transaction consistency remains in the case of a problem or concurrent change.
I tend to use option 1. If there is record in a table, you save one search. If there isn't, you don't loose anything. Moreover, in the second option you may run into funny locking and deadlocking issues related to locks incompatibility.
There's some more info on my blog:
http://sqlblogcasts.com/blogs/piotr_rodak/archive/2010/01/04/updlock-holdlock-and-deadlocks.aspx
You could just use ##RowCount to see if the update did anything. Something like:
UPDATE MyTable
SET SomeData = 'Some Data' WHERE ID = 1
IF ##ROWCOUNT = 0
BEGIN
INSERT MyTable
SELECT 1, 'Some Data'
END
Aiming to be a little more DRY, I avoid writing out the values list twice.
begin tran
insert into persons (id)
select #p_id from persons
where not exists (select * from persons where id = #p_id)
update persons
set name=#p_name, surname=#p_surname
where id = #p_id
commit
Columns name and surname have to be nullable.
The transaction means no other user will ever see the "blank" record.
Edit: cleanup

Resources