В моей базе данных University
есть таблицы Student
, Takes
и Course
:
Student
(идентификатор, имя, название отдела, общее количество кредитов)Takes
(StudentId, CourseId,sectionId, семестр, год, оценка)Course
(Идентификатор курса, Должность, Название отдела, Кредиты)Я хочу написать триггер для автоматического обновления TotalCredits
студента, который получает приемлемую оценку (когда преподаватель обновляет оценку за курс, пройденный студентом).
Я написал этот триггер и выполнил его:
CREATE TRIGGER [dbo].[credits_earned]
ON [dbo].[Takes]
AFTER UPDATE
AS
BEGIN
IF UPDATE(grade)
BEGIN
DECLARE @new_grade varchar(2)
SELECT @new_grade = I.grade
FROM inserted I
DECLARE @old_grade varchar(2)
SELECT @old_grade = T.grade
FROM Takes T
IF @new_grade<>'F' AND @new_grade IS NOT NULL
AND (@old_grade = 'F' OR @old_grade IS NULL)
BEGIN
UPDATE Student
SET tot_cred = tot_cred +
(SELECT credits
FROM Course C
WHERE C.course_id = (SELECT I.course_id
FROM inserted I))
WHERE Student.id = (SELECT I.id FROM inserted I)
END
END
END
И команды завершились успешно.
Но когда я выполняю процедуру обновления оценки курса студента (первая оценка в таблице дублей равна нулю), триггер не влияет на TotalCredits
этого студента.
Буду признателен за любой ответ, который вы напишете.
Вероятно, вашим триггером должен быть просто просмотр.
Этот тип триггера — очень плохая идея, и его следует избегать, если это вообще возможно, поскольку он может привести к проблемам. Если логика обнаружения изменений когда-либо ошибается, вы получаете неверные данные. Если в вашем триггере возникнет ошибка, вы получите рассинхронизированные данные. В идеале вы всегда должны вычислять эту информацию, когда она вам нужна, например. с представлением, а не хранить вычисленное значение.
Однако, если вам нужно использовать триггер, при написании логики триггера это все равно SQL, и, следовательно, он все равно должен быть основан на наборах, а не на процедурах (поскольку SQL Server оптимизирован для операций на основе наборов).
Теперь я думаю, что следующее работает. (Я делаю предположение о том, что уникально идентифицирует Take
, чтобы соединить Inserted
с Deleted
(именно так вы обнаруживаете изменения).
И тогда нам нужно предварительно агрегировать, потому что ваша агрегация добавила бы баллы для всех Takes
независимо от того, изменилась ли оценка.
Также обратите внимание, что я склонен инвертировать флажок UPDATE
, чтобы было ясно, что это вариант досрочного выхода.
IF NOT UPDATE(Grade) RETURN;
UPDATE s SET
tot_cred = tot_cred + c.Credits
FROM Student s
JOIN (
SELECT i.StudentId, SUM(c.Credits) Credits
FROM Inserted i
JOIN Deleted d ON d.StudentId = i.StudentId
AND d.CourseId = i.CourseId
AND d.SectionId = i.SectionId
AND d.Semester = i.Semester
AND d.Year = i.Year
JOIN Course c ON c.CourseId = i.CourseId
WHERE i.Grade <> 'F' AND i.Grade IS NOT NULL
AND (d.Grade = 'F' OR d.Grade IS NULL)
GROUP BY i.StudentId
) c ON c.Student = s.Id;
Более того, если ваш триггер обновляет инкрементное значение, это более рискованный подход, потому что, если итоговое значение когда-либо окажется неверным, оно так и останется неверным. Обычно лучше каждый раз рассчитывать общую сумму с нуля, например:
UPDATE s SET
tot_cred = (
-- Whatever logic gives the accurate credits at a point in time
SELECT SUM(c.Credits)
FROM Takes t
JOIN Courses c on c.CourseId = t.CourseId
WHERE t.StudentId = s.Id
AND t.Grade <> 'F'
)
FROM Student s
WHERE s.id IN (
SELECT i.StudentId
FROM Inserted i
JOIN Deleted d ON d.StudentId = i.StudentId
AND d.CourseId = i.CourseId
AND d.SectionId = i.SectionId
AND d.Semester = i.Semester
AND d.Year = i.Year
WHERE i.Grade <> 'F' AND i.Grade IS NOT NULL
AND (d.Grade = 'F' OR d.Grade IS NULL)
)
Это, вероятно, можно еще упростить следующим образом - что может обновить случайного студента, где ничего не изменилось - но это небольшая цена за простоту и скорость:
UPDATE s SET
tot_cred = (
-- Whatever logic gives the accurate credits at a point in time
SELECT SUM(c.Credits)
FROM Takes t
JOIN Courses c on c.CourseId = t.CourseId
WHERE t.StudentId = s.Id
AND t.Grade <> 'F'
)
FROM Student s
WHERE s.id IN (
SELECT i.StudentId
FROM Inserted i
WHERE i.Grade <> 'F'
)
ИМХО, ваша заметка должна быть реальным ответом... Я имею в виду что-то вроде: «Вам действительно следует стараться избегать хранения данных, которые можно легко вычислить, потому что ...., но если вам действительно нужно, вот как вам следует написать свой триггер" - но эй, это действительно зависит от тебя. В любом случае, вы получили мой голос.
Слышу вас, нам всем когда-то приходилось идти по пути плохих решений :-)
У вас есть фатальный недостаток в вашем триггере: вы не учитываете несколько (или ноль) строк в таблице inserted
. Вам также необходимо учитывать вставки и удаления. А функция UPDATE
не сообщает вам, действительно ли столбец изменился, только если он присутствовал в операторе.
Более того, если вы запросите реальную таблицу, вы получите новые результаты. Вам нужно воспользоваться deleted
таблицей.
Следующий триггер будет учитывать несколько строк, а также вставки, обновления и удаления. Мы объединяем inserted
и deleted
, беря 1 и -1 соответственно, а затем прибавляя кредиты.
CREATE OR ALTER TRIGGER [dbo].[credits_earned]
ON [dbo].[Takes]
AFTER INSERT, UPDATE, DELETE
AS
SET NOCOUNT ON;
IF NOT EXISTS (SELECT 1 FROM inserted) AND NOT EXISTS (SELECT 1 FROM deleted)
RETURN;
UPDATE Student
SET tot_cred += i.diff
FROM Student s
JOIN (
SELECT
i.student_id,
SUM(i.diff * c.credits) AS diff
FROM (
SELECT i.student_id, i.course_id, IIF(i.grade = 'F', 1, 0) AS diff
FROM inserted
UNION ALL
SELECT d.student_id, d.course_id, IIF(i.grade = 'F', -1, 0)
FROM deleted d
) i
JOIN Course c ON c.course_id = i.course_id
GROUP BY
i.student_id
) i ON i.student_id = s.id
WHERE i.diff <> 0;
Сказав все это, я все равно не рекомендовал бы это. Это можно реализовать гораздо эффективнее, используя Индексированное представление.
CREATE OR ALTER VIEW dbo.Student_TotalCredit
WITH SCHEMABINDING
AS
SELECT
s.id,
COUNT_BIG(*) AS count, -- must have a count
SUM(ISNULL(IIF(t.grade = 'F', c.credits, 0), 0)) AS total_credit
FROM dbo.Student s
JOIN dbo.Takes t ON t.student_id = s.id
JOIN Course c ON c.course_id = t.course_id
GROUP BY
s.id;
CREATE UNIQUE CLUSTERED INDEX IX ON dbo.Student_TotalCredit (id);
Хорошая работа, как всегда :)
Ваш триггер фатально неисправен; предполагается, что
UPDATE
может влиять только на одну строку; Это просто не соответствует действительности. От вредных привычек: злоупотребление триггерами и «Тихая ошибка», которую я нахожу в большинстве триггеров