Хранимые агрегаты - Триггеры и процедуры

Запросы, планы, оптимизация запросов, ...

Модераторы: kdv, CyberMax

Ответить
Query
Сообщения: 19
Зарегистрирован: 18 мар 2005, 13:31

Хранимые агрегаты - Триггеры и процедуры

Сообщение Query » 02 мар 2006, 12:36

Всем привет!

Давно меня мучает один неразрешенный вопрос, связанный с обеспечением бизнес-логики приложения. Прошу извинение за большой пост, но для наглядности привожу пример.
Есть две таблицы - справочник товаров и движение товаров:

Код: Выделить всё

CREATE TABLE GOODS  - Справочник товаров
ID   INTEGER         - код товара
RES  NUMERIC(18,4)   - остаток товара

CREATE TABLE DETAL  -  Движение товара:
GDS   INTEGER        - товар
MODE  SMALLINT       - признак (-1 расход, 1 приход)
CNT   NUMERIC(18,4)  - количество
Если было движение, то остаток товара в зависмости от признака должен или увеличится или уменьшится. Это можно реализовать через триггеры или через триггеры-процедуры.

Код: Выделить всё


Первый вариант - Только триггеры:

CREATE TRIGGER DETAL_AI FOR DETAL
ACTIVE AFTER INSERT POSITION 0
AS
BEGIN
    UPDATE GOODS
    SET RES = RES + NEW.CNT * NEW.MODE
    WHERE ID = NEW.GDS;
END

CREATE TRIGGER DETAL_AU FOR DETAL
ACTIVE AFTER UPDATE POSITION 0
AS
BEGIN
    UPDATE GOODS
    SET RES = RES - OLD.CNT * OLD.MODE
    WHERE ID = OLD.GDS;

    UPDATE GOODS
    SET RES = RES + NEW.CNT * NEW.MODE
    WHERE ID = NEW.GDS;
END

CREATE TRIGGER DETAL_AD FOR DETAL
ACTIVE AFTER DELETE POSITION 0
AS
BEGIN
    UPDATE GOODS
    SET RES = RES - OLD.CNT * OLD.MODE
    WHERE ID = OLD.GDS;
END

Код: Выделить всё

Второй вариант - триггеры с процедурой:

CREATE PROCEDURE SET_GDS_RES(
    PGDS INTEGER,
    PCNT NUMERIC(18,4))
AS
BEGIN
    UPDATE GOODS
    SET RES = RES + PCNT
    WHERE ID = PGDS;
END

CREATE TRIGGER DETAL_AI FOR DETAL
ACTIVE AFTER INSERT POSITION 0
AS
BEGIN
    EXECUTE PROCEDURE SET_GDS_RES(NEW.GDS, NEW.CNT * NEW.MODE);
END

CREATE TRIGGER DETAL_AU FOR DETAL
ACTIVE AFTER UPDATE POSITION 0
AS
BEGIN
    EXECUTE PROCEDURE SET_GDS_RES(OLD.GDS, 0-OLD.CNT * OLD.MODE);
    EXECUTE PROCEDURE SET_GDS_RES(NEW.GDS, NEW.CNT * NEW.MODE);
END

CREATE TRIGGER DETAL_AD FOR DETAL
ACTIVE AFTER DELETE POSITION 0
AS
BEGIN
    EXECUTE PROCEDURE SET_GDS_RES(OLD.GDS, 0-OLD.CNT * OLD.MODE);
END
Оба способа работают хорошо, но замечено, что первый способ (только на триггерах) со временем по некоторым позициям товара начинает неправильно считать. Второй способ никогда не давал сбоев. Эта закономерность наблюдается достаточно долго на разных базах, сетях, машинах, клиентах, организациях, монопольных режимах и многопользовательских и пр. - всегда сначала ставлю первый вариант бизнес-логики(все хочу докопаться до истины), и как только через пару недель проверочной процедурой нахожу разногласия в остатках, перевожу на второй вариант - после этого годами все работате без сбоев. Стоит перевести на первый вариант, опять через некоторое время обнаруживаю несхождения. Может это известные грабли или я что-то не так делаю? Также интересно, кто как обеспечивает логику таких вот "хранимых агрегатов".

kdv
Forum Admin
Сообщения: 6595
Зарегистрирован: 25 окт 2004, 18:07

Сообщение kdv » 02 мар 2006, 12:57

такого не может быть, потому что не может быть никогда. либо твой триггер работает в другой транзакции, и при этом по ошибке в нем ты гасишь exception, либо, что более вероятно, "битый" BLR в триггерах или триггеры с blr в первом диалекте, не знаю, какой у тебя диалект в БД.

у тебя все равно так или иначе все обрабатывается триггерами. процедуру ты вызваешь внутри них или напрямую оператор sql - не имеет значения.

Query
Сообщения: 19
Зарегистрирован: 18 мар 2005, 13:31

Сообщение Query » 02 мар 2006, 13:05

Диалект 3, всегда стараюсь использовать последний стабильный релиз, сечас везде стоит 1.5.2, на 1.5.3. пока не перешел. Я сам понимаю, что не должно быть. Но вот только что позвонили, проблема та же - сейчас поеду и поменяю все на процедуры - больше ничего трогать не буду. Погоняю так пару месяцев, потом опять переведу на триггеры и псомотрю, что будет. О результатах постараюсь сообщить.

Merlin
Динозавр IB/FB
Сообщения: 1502
Зарегистрирован: 27 окт 2004, 11:44

Сообщение Merlin » 02 мар 2006, 13:12

Любопытно было бы взглянуть на реальный текст какого-нибудь из триггеров, на который есть подозрения, а то апдейт-экзампл то слишком простой. Ну и на тело реальной заменяющей процедуры вкупе с вызывающим триггером-заменителем. Без приглаживания и редактирования. Могут быть некоторые нюансы. Кстати, мне не нравится, что код товара в записи о движении у тебя может меняться, но это долгий разговор, к вопросу прямого отношения не имеющий.

Query
Сообщения: 19
Зарегистрирован: 18 мар 2005, 13:31

Сообщение Query » 02 мар 2006, 13:36

Merlin писал(а):Любопытно было бы взглянуть на реальный текст какого-нибудь из триггеров, на который есть подозрения
Есть у меня одна контора, 25 рабочих мест, база 1.5 гига, сервер классик - сделано на процедурах. Сегодня вечерком поменяю все на триггеры и если со временем замечу расхождения, то тут же представлю тескты триггеров и процедур. Надо будеет только подождать, но на этой базе, я помню, это достоточно быстро происходило.
Merlin писал(а):Кстати, мне не нравится, что код товара в записи о движении у тебя может меняться, но это долгий разговор
А как же менять товар в накладной? Удалять и по новой создавать?

Merlin
Динозавр IB/FB
Сообщения: 1502
Зарегистрирован: 27 окт 2004, 11:44

Сообщение Merlin » 02 мар 2006, 13:51

Query писал(а): Сегодня вечерком поменяю все на триггеры и если со временем замечу расхождения, то тут же представлю тескты триггеров и процедур.
Валяй. Там могут быть чисто синтаксические вариации при замене контестные переменные - параметры и обратно, незаметно для глаза автора ликвидирующие его ошибки при обслуживании нуллов или использования if (exists select ...) или ещё какой фигни, прямо не имеющей отношения к вопросу триггер или процедура.
Query писал(а):
Merlin писал(а):Кстати, мне не нравится, что код товара в записи о движении у тебя может меняться, но это долгий разговор
А как же менять товар в накладной? Удалять и по новой создавать?
В завершённой накладной вообще ничего менять не принято, принято выпускать корректирующий акт. Это для внутреннего реального учёта, чтоб сотрудники вагонами не коммуниздили. А если речь об бамажках отдела по борьбе с налогами (бухгалтэрия), то там да, всякие чюдеса случаются. Но там и не ведут хранимых агрегатов на триггерах.

Query
Сообщения: 19
Зарегистрирован: 18 мар 2005, 13:31

Сообщение Query » 14 мар 2006, 11:30

Query писал(а):
Merlin писал(а):Любопытно было бы взглянуть на реальный текст какого-нибудь из триггеров, на который есть подозрения
Есть у меня одна контора, 25 рабочих мест, база 1.5 гига, сервер классик - сделано на процедурах.
Что-то побоялся править тамошние многоэтажные триггера - работает, да и ладно. Лучше тренироваться на кошках:) Появился у меня недавно новый клиент - простая складская программа для Черного Плаща, т.е. для чеписта. Специально сделал логику на триггерах. Не прошло и пару недель появились первые расхождения. Сегодня поеду и переделаю на процедуры. Уверен, что с процедурами все будет ОК - уже десятый раз проделываю сию манипуляцию. Я сам понимаю, что так не должгно быть и хочу понять где криворучю:) Ведь несколько лет уже такая фигня:(

Сервер используется FB1.5.2SS, работает сервисом под WinXPsp1, гвардеец демобилизован - не используется, база прописана на localhost. Программа работает в однопользовательском режиме, сети нет. Компоненты FIBPlus6.4, используются механизм раздельных транзакций - на запись и чтение. Вырезка из метаданных:

Код: Выделить всё

Старая логика, которая ведет к расхождениям:

CREATE TRIGGER DTL_AI0 FOR DTL
ACTIVE AFTER INSERT POSITION 0
AS
BEGIN
  IF(NEW.FON = 1 AND NEW.VMODE <> 0)THEN
    UPDATE OBJ
    SET VCNT = VCNT + NEW.VCNT * NEW.VMODE
    WHERE KID = NEW.ROBJ;

  UPDATE DOC
  SET VSUM = VSUM + NEW.VSUM
  WHERE KID = NEW.RDOC;
END

CREATE TRIGGER DTL_AU0 FOR DTL
ACTIVE AFTER UPDATE POSITION 0
AS
begin
  IF(OLD.FON = 1 AND OLD.VMODE <> 0)THEN
    UPDATE OBJ
    SET VCNT = VCNT - OLD.VCNT * OLD.VMODE
    WHERE KID = OLD.ROBJ;

  IF(NEW.FON = 1 AND NEW.VMODE <> 0)THEN
    UPDATE OBJ
    SET VCNT = VCNT + NEW.VCNT * NEW.VMODE
    WHERE KID = NEW.ROBJ;

  UPDATE DOC
  SET VSUM = VSUM - OLD.VSUM
  WHERE KID = OLD.RDOC;

  UPDATE DOC
  SET VSUM = VSUM + NEW.VSUM
  WHERE KID = NEW.RDOC;
end

CREATE TRIGGER DTL_AD0 FOR DTL
ACTIVE AFTER DELETE POSITION 0
AS
begin
  IF(OLD.FON = 1 AND OLD.VMODE <> 0)THEN
    UPDATE OBJ
    SET VCNT = VCNT - OLD.VCNT * OLD.VMODE
    WHERE KID = OLD.ROBJ;

  UPDATE DOC
  SET VSUM = VSUM - OLD.VSUM
  WHERE KID = OLD.RDOC;
end

Код: Выделить всё

Новая логика, которая ОК

CREATE PROCEDURE SET_GDS_CNT(
    PGDS INTEGER,
    PCNT NUMERIC(18,4))
AS
BEGIN
  UPDATE OBJ
  SET VCNT = VCNT + :PCNT
  WHERE KID = :PGDS;
END

CREATE TRIGGER DTL_AI0 FOR DTL
ACTIVE AFTER INSERT POSITION 0
AS
BEGIN
  IF(NEW.FON = 1 AND NEW.VMODE <> 0)THEN
    EXECUTE PROCEDURE SET_GDS_CNT(NEW.ROBJ, NEW.VCNT * NEW.VMODE);

  UPDATE DOC
  SET VSUM = VSUM + NEW.VSUM
  WHERE KID = NEW.RDOC;
END

CREATE TRIGGER DTL_AU0 FOR DTL
ACTIVE AFTER UPDATE POSITION 0
AS
begin
  IF(OLD.FON = 1 AND OLD.VMODE <> 0)THEN
    EXECUTE PROCEDURE SET_GDS_CNT(OLD.ROBJ, 0 - OLD.VCNT * OLD.VMODE);

  IF(NEW.FON = 1 AND NEW.VMODE <> 0)THEN
    EXECUTE PROCEDURE SET_GDS_CNT(NEW.ROBJ, NEW.VCNT * NEW.VMODE);

  UPDATE DOC
  SET VSUM = VSUM - OLD.VSUM
  WHERE KID = OLD.RDOC;

  UPDATE DOC
  SET VSUM = VSUM + NEW.VSUM
  WHERE KID = NEW.RDOC;
end

CREATE TRIGGER DTL_AD0 FOR DTL
ACTIVE AFTER DELETE POSITION 0
AS
begin
  IF(OLD.FON = 1 AND OLD.VMODE <> 0)THEN
    EXECUTE PROCEDURE SET_GDS_CNT(OLD.ROBJ, 0 - OLD.VCNT * OLD.VMODE);

  UPDATE DOC
  SET VSUM = VSUM - OLD.VSUM
  WHERE KID = OLD.RDOC;
end
Код привел как есть - без исправлений. Это триггера детальной части накладной. Здесь FON - статус документа(проведен - не проведен), VMODE - режим(приход, расход, перемещение 1,-1,0). Так же в триггерах поддерживается еще один агрегат - сумма накладной. По ней косяки значительно реже, но пару раз было. Остальные метаданные не привожу, т.к. там интересующие поля не трогаются - только проверка на NULL. Если что, могу представить базу вместе с программой.

Merlin
Динозавр IB/FB
Сообщения: 1502
Зарегистрирован: 27 окт 2004, 11:44

Сообщение Merlin » 14 мар 2006, 15:35

Явного криминала не вижу. Приведу несколько непричёсанных мыслей, возникавших во время просмотра.

1. Спрашивать о том, являются ли типы FON, VMODE, KID и ROBJ Integer, пожалуй, неполиткорректно, но мысль таки упомяну ;)

2. Являются ли типы VCNT в таблицах DTL и OBJ Numeric (18,4), как и параметр в процедуре?

3. Если с типами всё ОК, то разница только в том, что в одном случае умножение делается внутри SQL-оператора, в другом - в PSQL. Об арифметике третьего диалекта я могу только рассуждать по-ламерски, то есть теоретицки. Но что я знаю точно, в сложной системе всё может быть. Действие одно, а код может быть разным. Или обращение к нему. Если собака рылась здесь, то вычисление выражения в переменную и использование её в апдейте по идее должно дать тот же эффект, что и замещение его процедурой.

4. Между 1.5.2 и 1.5.3 dimitr что-то ковырял в арифметическом блоке. Точно знаю про арифметику со строками в первом диалекте, хоть в WhatsNew не углядел. При этом шипел что-то по-змейному в адрес предшествующих кодописателей и насчёт третьего, но я внимания не обратил.

5. Когда придёшь в гости к базе, сделай-ка сначала gfix -v -f (можно и -n до кучи из осторожности) и глянь в лог, не скажет ли оно чего насчёт битых страниц в индексе на OBJ. B/R индексы чинит молча, так что если база периодически освежается, то битый индекс в предыдущей инкарнации запросто может быть хорошо скрытой причиной таких фокусов.

Query
Сообщения: 19
Зарегистрирован: 18 мар 2005, 13:31

Сообщение Query » 14 мар 2006, 19:01

Merlin писал(а): 1. Спрашивать о том, являются ли типы FON, VMODE, KID и ROBJ Integer, пожалуй, неполиткорректно, но мысль таки упомяну ;)
FON, VMODE - smallint, KID, ROBJ - integer
Merlin писал(а): 2. Являются ли типы VCNT в таблицах DTL и OBJ Numeric (18,4), как и параметр в процедуре?
Иногда в таблицах использую Numeric(18,2), но в процедуре всегда стараюсь использовать Numeric (18,4).
Merlin писал(а): ... то вычисление выражения в переменную и использование её в апдейте по идее должно дать тот же эффект, что и замещение его процедурой.
Т.е. делать так:

Код: Выделить всё

DECLARE VARIABLE ACNT NUMERIC(18,4);

    ACNT = NEW.VCNT * NEW.VMODE;

    UPDATE OBJ
    SET VCNT = VCNT + :ACNT
    WHERE KID = NEW.ROBJ; 
Интересная мысль, попробую. Если сработает, то надо мне все триггеры на этот момент прошерстить, на всякий случай.
Merlin писал(а): 4. Между 1.5.2 и 1.5.3 dimitr что-то ковырял в арифметическом блоке.
Кого-нибудь из Черных Плащей пересажу на 1.5.3, понаблюдаю.
Merlin писал(а): 5. Когда придёшь в гости к базе, сделай-ка сначала gfix -v -f (можно и -n до кучи из осторожности) и глянь в лог, не скажет ли оно чего насчёт битых страниц в индексе на OBJ.
И за это спасибо - про индексы я даже не догадывался.

Merlin
Динозавр IB/FB
Сообщения: 1502
Зарегистрирован: 27 окт 2004, 11:44

Сообщение Merlin » 14 мар 2006, 19:37

Query писал(а):
Merlin писал(а): 2. Являются ли типы VCNT в таблицах DTL и OBJ Numeric (18,4), как и параметр в процедуре?
Иногда в таблицах использую Numeric(18,2), но в процедуре всегда стараюсь использовать Numeric (18,4).
Дык, голубчик. Если в таблицах 18,2 то у тебя просто выражение на полях в триггере считается с одной точностью, а через процедуру с другой...

Query
Сообщения: 19
Зарегистрирован: 18 мар 2005, 13:31

Сообщение Query » 14 мар 2006, 22:17

Merlin писал(а): Дык, голубчик. Если в таблицах 18,2 то у тебя просто выражение на полях в триггере считается с одной точностью, а через процедуру с другой...
Сейчас посмотрел - именно VCNT везде проходит как 18,4. Про 18,2 я упомянул, что иногда его использую и бывает смешение этих двух форматов, но не в данном случае. И даже если бы это влияло, то легко было бы воспроизвести ситуацию, что не удается. Это также подтверждается тем фактом, что весового товара почти нет, всё целочисленное и расхождения идут на целые числа, а не на дробные.
И потом, неужели в операциях сложения это критично, если 18,2 привести к 18,4 и сложить сдругим числом? Понятно было бы, если наоборот, приводим 18,4 к 18,2 - теряем точность. Но здесь же ничего не теряем. Или я не прав?

Query
Сообщения: 19
Зарегистрирован: 18 мар 2005, 13:31

Сообщение Query » 16 май 2006, 00:17

Разобрался в чем дело - криво мною написанное клиентское приложение, которое в отдельной транзакции втихаря изменяла данные.

Посыпаю голову пеплом.

Ответить