Postgresql - чистый способ вставить записи, если они не существуют, обновить, если они есть - PullRequest
4 голосов
/ 18 сентября 2011

Вот моя ситуация. У меня есть таблица с кучей URL-адресов и сканировать даты, связанные с ними. Когда моя программа обрабатывает URL, я хочу ВСТАВИТЬ новую строку с датой сканирования. Если URL уже существует, я хотите обновить дату сканирования до текущей даты и времени. С MS SQL или Oracle Я бы, наверное, использовал для этого команду MERGE. С MySQL я бы вероятно, используйте синтаксис ON DUPLICATE KEY UPDATE.

Я мог бы сделать несколько запросов в моей программе, которые могут или не могут быть Поток безопасно. Я мог бы написать функцию SQL, которая имеет различные IF ... ELSE логика. Тем не менее, ради того, чтобы попробовать функции Postgres я никогда не использовал раньше, я думаю о создании правила вставки - как то так:

CREATE RULE Pages_Upsert AS ON INSERT TO Pages
  WHERE EXISTS (SELECT 1 from Pages P where NEW.Url = P.Url)
  DO INSTEAD
     UPDATE Pages SET LastCrawled = NOW(), Html = NEW.Html WHERE Url = NEW.Url;

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

Есть ли другие недостатки этой идеи или, может быть, ваша идея? отстой, вы должны сделать это / это / вместо этого "комментарий? Я на PG 9.0, если это имеет значение.

ОБНОВЛЕНИЕ : План запроса, поскольку кто-то хотел его:)

"Insert  (cost=2.79..2.81 rows=1 width=0)"
"  InitPlan 1 (returns $0)"
"    ->  Seq Scan on pages p  (cost=0.00..2.79 rows=1 width=0)"
"          Filter: ('http://www.foo.com'::text = lower((url)::text))"
"  ->  Result  (cost=0.00..0.01 rows=1 width=0)"
"        One-Time Filter: ($0 IS NOT TRUE)"
""
"Update  (cost=2.79..5.46 rows=1 width=111)"
"  InitPlan 1 (returns $0)"
"    ->  Seq Scan on pages p  (cost=0.00..2.79 rows=1 width=0)"
"          Filter: ('http://www.foo.com'::text = lower((url)::text))"
"  ->  Result  (cost=0.00..2.67 rows=1 width=111)"
"        One-Time Filter: $0"
"        ->  Seq Scan on pages  (cost=0.00..2.66 rows=1 width=111)"
"              Filter: ((url)::text = 'http://www.foo.com'::text)"

Ответы [ 5 ]

4 голосов
/ 19 сентября 2011

Хорошо, мне удалось создать тестовый сценарий.В результате часть обновления всегда выполняется, даже при новой вставке.COPY, кажется, обходит систему правил.[Для ясности я поместил это в отдельный ответ]

DROP TABLE pages CASCADE;
CREATE TABLE pages
    ( url VARCHAR NOT NULL  PRIMARY KEY
    , html VARCHAR
    , last TIMESTAMP
    );

INSERT INTO pages(url,html,last) VALUES ('www.example.com://page1' , 'meuk1' , '2001-09-18 23:30:00'::timestamp );

CREATE RULE Pages_Upsert AS ON INSERT TO pages
  WHERE EXISTS (SELECT 1 from pages P where NEW.url = P.url)
     DO INSTEAD (
     UPDATE pages SET html=new.html , last = NOW() WHERE url = NEW.url
    );

INSERT INTO pages(url,html,last) VALUES ('www.example.com://page2' , 'meuk2' , '2002-09-18 23:30:00':: timestamp );
INSERT INTO pages(url,html,last) VALUES ('www.example.com://page3' , 'meuk3' , '2003-09-18 23:30:00':: timestamp );

INSERT INTO pages(url,html,last) SELECT pp.url || '/added'::text, pp.html || '.html'::text , pp.last + interval '20 years' FROM pages pp;

COPY pages(url,html,last) FROM STDIN;
www.example.com://pageX     stdin   2000-09-18 23:30:00
\.

SELECT * FROM pages;

Результат:

              url              |    html    |            last            
-------------------------------+------------+----------------------------
 www.example.com://page1       | meuk1      | 2001-09-18 23:30:00
 www.example.com://page2       | meuk2      | 2011-09-18 23:48:30.775373
 www.example.com://page3       | meuk3      | 2011-09-18 23:48:30.783758
 www.example.com://page1/added | meuk1.html | 2011-09-18 23:48:30.792097
 www.example.com://page2/added | meuk2.html | 2011-09-18 23:48:30.792097
 www.example.com://page3/added | meuk3.html | 2011-09-18 23:48:30.792097
 www.example.com://pageX       | stdin      | 2000-09-18 23:30:00
 (7 rows)

ОБНОВЛЕНИЕ: Просто чтобы доказать, что это можно сделать:

INSERT INTO pages(url,html,last) VALUES ('www.example.com://page1' , 'meuk1' , '2001-09-18 23:30:00'::timestamp );
CREATE VIEW vpages AS (SELECT * from pages);

CREATE RULE Pages_Upsert AS ON INSERT TO vpages
  DO INSTEAD (
     UPDATE pages p0
     SET html=NEW.html , last = NOW() WHERE p0.url = NEW.url
    ;
     INSERT INTO pages (url,html,last)
    SELECT NEW.url, NEW.html, NEW.last
        WHERE NOT EXISTS ( SELECT * FROM pages p1 WHERE p1.url = NEW.url)
    );

CREATE RULE Pages_Indate AS ON UPDATE TO vpages
  DO INSTEAD (
     INSERT INTO pages (url,html,last)
    SELECT NEW.url, NEW.html, NEW.last
        WHERE NOT EXISTS ( SELECT * FROM pages p1 WHERE p1.url = OLD.url)
        ;
     UPDATE pages p0
     SET html=NEW.html , last = NEW.last WHERE p0.url = NEW.url
        ;
    );

INSERT INTO vpages(url,html,last) VALUES ('www.example.com://page2' , 'meuk2' , '2002-09-18 23:30:00':: timestamp );
INSERT INTO vpages(url,html,last) VALUES ('www.example.com://page3' , 'meuk3' , '2003-09-18 23:30:00':: timestamp );

INSERT INTO vpages(url,html,last) SELECT pp.url || '/added'::text, pp.html || '.html'::text , pp.last + interval '20 years' FROM vpages pp;
UPDATE vpages SET last = last + interval '-10 years' WHERE url = 'www.example.com://page1' ;

-- Copy does NOT work on views
-- COPY vpages(url,html,last) FROM STDIN;
-- www.example.com://pageX    stdin    2000-09-18 23:30:00
-- \.

SELECT * FROM vpages;

Результат:

INSERT 0 1
INSERT 0 1
INSERT 0 3
UPDATE 1
              url              |    html    |        last         
-------------------------------+------------+---------------------
 www.example.com://page2       | meuk2      | 2002-09-18 23:30:00
 www.example.com://page3       | meuk3      | 2003-09-18 23:30:00
 www.example.com://page1/added | meuk1.html | 2021-09-18 23:30:00
 www.example.com://page2/added | meuk2.html | 2022-09-18 23:30:00
 www.example.com://page3/added | meuk3.html | 2023-09-18 23:30:00
 www.example.com://page1       | meuk1      | 1991-09-18 23:30:00
(6 rows)

Представление необходимо для предотвращения перехода системы переписывания в рекурсию.Построение правила DELETE оставлено читателю в качестве упражнения.

2 голосов
/ 18 сентября 2011

Несколько хороших замечаний от того, кто должен это знать или быть очень рядом с кем-то подобным; -)

Для чего нужны правила PostgreSQL?

Короткая история:

  • Хорошо ли работают правила с SERIAL и BIGSERIAL?
  • Хорошо ли работают правила с предложениями RETURNING INSERT и UPDATE?
  • Хорошо ли работают правила с такими вещами, как random()?

Все это сводится к тому, что система правил не управляется строкойно трансформирует ваши высказывания так, как вы никогда не представляете.

Сделайте себе и своей команде одолжение и прекратите использовать роли для подобных вещей.

Редактировать : Ваша проблемахорошо обсуждается в сообществе PostgreSQL.Ключевые слова для поиска: MERGE, UPSERT.

1 голос
/ 18 сентября 2011

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

Сначала я попробую проверить URL в вашей программе, а затем выбрать, вставить или обновить. Если бы это оказалось слишком медленным, я бы использовал функцию. Если вы назовете его как insert_or_update_url, вы автоматически получите некоторую документацию бесплатно. Правило переписывания требует от вас неявных знаний, и я обычно стараюсь этого избегать.

С положительной стороны: если кто-то копирует данные, но забывает правила и функции, ваше решение может молча сломаться (но это может зависеть от других ограничений), но отсутствующая функция закричит. Не поймите меня неправильно, я думаю, что ваше решение очень креативное и умное. Просто слишком мало на мой вкус.

0 голосов
/ 19 сентября 2011

Существует пример реализации upsert / слияние с использованием простой функции в документации Postgres.

Никогда не используйте правила & mdash; они злые.

0 голосов
/ 18 сентября 2011

Вы не можете ссылаться на другие таблицы, кроме старых и новых в квалификации правила. Вместо этого вы должны сделать это в теле правила . Это все потому, что правило - это просто способ информировать систему перезаписи о том, какие преобразования она должна и не должна выполнять. Правила не являются триггерами, они выполняются для каждой строки, но они дают организатору запросов хороший массаж и просят его переписать план. Из документов:

Что такое квалификация правила? Это ограничение, которое указывает, когда действия правила должны быть выполнены, а когда нет. Эта квалификация может ссылаться только на псевдоотношения NEW и / или OLD, которые в основном представляют отношение, которое было дано как объект (но с особым значением).

...