Триггер подсчета ссылок в PostgreSQL

Дан запрос postgreSQL со следующими таблицами:

+--------------------+
|Foo                 |
+--------------------+
| id | value1 | refe |
+--------------------+

+--------------------+
|Bar                 |
+--------------------+
| id | value2 | refe |
+--------------------+

+-------------+
|Refe         |
+-------------+
| id | value3 |
+-------------+

Где столбцы refe являются внешними ключами для id из Refe. Теперь Refe хранит дополнительные данные, относящиеся как к Foo, так и к Bar (и, возможно, другим таблицам).

Я создал представление, в котором хранятся "активные" refe, другими словами: объединение полей refe Foo и Bar:

CREATE OR REPLACE VIEW liverefe AS 
        (SELECT refe FROM Foo)
        UNION (SELECT refe FROM Bar)

Теперь, когда кто-то удаляет строку из Foo или Bar, есть шанс, что refe станет мертвым (на нее не ссылается никакая другая строка Foo или Bar). В таком случае его следует удалить.

Можно ли объяснить, как реализовать такой триггер "подсчета ссылок"?


person Willem Van Onsem    schedule 07.08.2014    source источник
comment
Ради педантизма: это подсчет ссылок, а не GC. Сборка мусора была бы похожа на то, что делает VACUUM.   -  person cHao    schedule 07.08.2014
comment
Оба термина, на мой взгляд, несколько двусмысленны и частично совпадают. Если кто-то видит строки в базе данных как объекты, а внешние ключи являются ссылками. Это какая-то сборка мусора. Как я понял, подсчет ссылок — это просто стратегия для выполнения сборки мусора, другие методы, такие как Boehm, являются вариантами...   -  person Willem Van Onsem    schedule 07.08.2014
comment
Большая разница в том, что при сборке мусора сбор осуществляется отдельно, обычно партиями. Например, если вы хотите, чтобы демон запускался каждые X минут и удалял все строки без ссылок, это был бы сборщик мусора.   -  person cHao    schedule 07.08.2014
comment
Ну тоже нормально. Однако дело в том, что я не ожидаю, что таких операций будет много. А при удалении понятно, что можно собрать только удаляемый id. Это дает преимущество, поскольку известно, где искать (иначе нужно сначала вычислить все живые refe).   -  person Willem Van Onsem    schedule 07.08.2014
comment
Значит, строки-сироты могут какое-то время простоять, даже если на них ничего не ссылается, если они в конце концов будут удалены?   -  person cHao    schedule 07.08.2014
comment
я не знаю о триггере подсчета ссылок, но рассматривали ли вы триггер удаления на Foo и Bar? Эта триггерная функция может проверять любые оставшиеся ссылки. Либо так, либо вы можете поддерживать столбец «ref_count» в Refe и запускать его при достижении 0 (очевидно, все еще нужны триггеры вставки/удаления Foo и Bar для управления ref_count).   -  person Greg    schedule 07.08.2014
comment
@cHao: действительно... Это не имеет принципиального значения, но со временем (в разумные сроки) они должны исчезнуть.   -  person Willem Van Onsem    schedule 07.08.2014
comment
@Greg: ну, счет не должен рассчитываться явно. С того момента, как существует одна ссылка, строка не должна удаляться. Таким образом можно сделать проверку при удалении.   -  person Willem Van Onsem    schedule 07.08.2014


Ответы (2)



Если вы хотите, чтобы удаление произошло немедленно, вы можете объявить триггерную функцию следующим образом...

CREATE FUNCTION check_ref_count()
RETURNS trigger
LANGUAGE plpgsql
AS $$
  BEGIN
    DELETE FROM Refe WHERE id NOT IN (SELECT refe from liverefe);
    RETURN NULL;
  END;
$$;

А затем прикрепите его к рассматриваемым таблицам.

CREATE TRIGGER slay_orphans
AFTER DELETE ON Foo
FOR EACH STATEMENT
EXECUTE PROCEDURE check_ref_count();

CREATE TRIGGER slay_orphans
AFTER DELETE ON Bar
FOR EACH STATEMENT
EXECUTE PROCEDURE check_ref_count();

(В зависимости от того, как вы работаете с этими элементами, вам, возможно, придется говорить AFTER DELETE OR UPDATE, а не просто AFTER DELETE. Особенно если вы часто меняете refe. Если вы обычно устанавливаете его один раз и не трогаете, AFTER DELETE должно быть достаточно.)

Примечание: как только вы сделаете это, запись в Refe не сохранится при следующем удалении, если какая-либо запись в liverefe не имеет своего идентификатора. Даже если это то, что вы хотите, это означает, что если удаления являются обычным явлением, даже совершенно новые записи могут быть удалены прямо из-под вас.

Вы можете сделать вещи более строгими, проверяя только те Refe, на которые указывали удаленные строки. Просто добавьте id = old.refe к условиям удаления и сделайте триггеры FOR EACH ROW, а не FOR EACH STATEMENT. Это сделает так, что только ранее указанные записи будут кандидатами на удаление. (Но это также означает, что очистка не имеет обратной силы — текущие сироты не будут очищены, если что-то не укажет на них, а затем не укажет на что-то еще или не будет удалено. И если вы идете по этому пути, AFTER DELETE OR UPDATE необходимо — небрежность удалить триггерный дескриптор всего, приводит к утечке строк, если вы не очищаете всю таблицу.)

Или добавьте отметку времени к Refe и измените запрос, чтобы игнорировать записи, отметка времени которых меньше X секунд/минут/независимо от того, что было раньше.

Как бы вы ни решили эту проблему, вы, вероятно, также захотите рассмотреть возможность использования транзакций
, чтобы гарантировать, что запись не будет удалена прямо в тот момент, когда вы пытаетесь указать на нее. И убедитесь, что ваши внешние ключи указаны как ON DELETE RESTRICT (так что удаление записи в Refe не будет разрешено, если запись действительно не потеряна).


Если бы вы могли жить с удалением строк время от времени, а не сразу, это стало бы еще проще, поскольку вам не нужно создавать функцию и тому подобное.

Просто создайте скрипт, который выдает этот запрос:

DELETE FROM Refe WHERE id NOT IN (SELECT refe FROM liverefe)

и настройте это как задание cron (или запланированное задание в Windows).

person cHao    schedule 07.08.2014
comment
Ах, вы работали над этим, пока я писал. Я знаю это чувство. Я был быстрее, потому что я разместил полное решение раньше. Тем не менее, очень хороший и полный ответ. - person Erwin Brandstetter; 08.08.2014
comment
Хорошее решение, было трудно выбрать одно в качестве принятого решения :(. При попытке pgsql выдал ошибку RETURN 0, вероятно, следует заменить на RETURN NULL? - person Willem Van Onsem; 08.08.2014
comment
@CommuSoft: Возможно. Однако предполагается, что возвращаемое значение игнорируется для триггерных функций... - person cHao; 08.08.2014
comment
@cHao: да, но в соответствии со спецификациями PostgreSQL для триггера требуется оператор RETURN, который либо возвращает NULL, либо строку, соответствующую таблице, к которой применяется триггер. - person Willem Van Onsem; 08.08.2014
comment
@CommuSoft: Ах, ты прав. Только что еще раз просмотрел документы. :/ Отредактировано. - person cHao; 08.08.2014