Postgresql — чистый способ вставки записей, если они не существуют, обновить, если они есть

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

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

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)"

person Mike Christensen    schedule 18.09.2011    source источник
comment
Пожалуйста, добавьте полученный план запроса (для действия INSERT в целевой таблице) в сообщение.   -  person wildplasser    schedule 18.09.2011
comment
Кстати, на самом деле есть ИНДЕКС на нижнем (url) на страницах - я думаю, что он выполняет последовательное сканирование, потому что в таблице на данный момент только 4 строки, и планировщик считает, что быстрее просто посмотреть на 4 строки, чем нажать показатель. Просто предположение.   -  person Mike Christensen    schedule 19.09.2011
comment
Спасибо. У меня примерно так же. Часть обновления всегда имеет место. Если вы действительно выполняете INSERT INTO pages(...) VALUES (...), значение для lastcrawled перезаписывается функцией now(), даже если ключ отсутствовал до вставки.   -  person wildplasser    schedule 19.09.2011


Ответы (5)


Хорошо, мне удалось создать тестовый пример. В результате часть обновления выполняется всегда, даже при новой вставке. 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 предоставляется читателю в качестве упражнения.

person wildplasser    schedule 18.09.2011
comment
Много хороших отзывов в этой ветке, но отметьте этот как ответ, потому что вы вложили в него много работы. - person Mike Christensen; 20.09.2011
comment
Спасибо. Я недавно исследовал материал, так что я получил большую часть этого удобно. Вывод: тестируйте. Протестируйте все крайние случаи. SELECT ... from tab INSERT в tab() может стать настоящим препятствием, если вы соблазните переписчика использовать неправильные записи в таблице диапазонов и/или объединить подзапросы или предложения. EXPLAIN — ваш друг. - person wildplasser; 20.09.2011

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

Для чего нужны ПРАВИЛА PostgreSQL?

Короткий рассказ:

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

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

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

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

person A.H.    schedule 18.09.2011

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

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

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

person musiKk    schedule 18.09.2011
comment
Да вроде согласен. Выполнение проверки, чтобы увидеть, существует ли строка первой, будет слишком медленной и небезопасной для транзакций, если одновременно запущено много экземпляров моей программы. Функция PG может исправить это и работать быстрее. Схема таблицы запрещает существование одного и того же URL-адреса дважды, поэтому копирование данных не является проблемой, плюс эти данные носят временный характер, поэтому не очень важны. Возможно, на днях у PG появится возможность UPSERT, и это не будет проблемой. - person Mike Christensen; 18.09.2011

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

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

person Tometzky    schedule 18.09.2011
comment
Я согласен с тем, что не используйте правила как эмпирическое правило. Но они не злы сами по себе; они просто хитрые. Трудно получить право. Дьявол кроется в деталях. Тем не менее, это хороший способ научиться понимать планы запросов ;-] - person wildplasser; 19.09.2011
comment
@wildplasser - Они зло на уровне GOTO. Если вы хоть как-то сделаете их правильно, 95% других разработчиков этого не сделают. Почти все, что реализовано с помощью правила, может быть реализовано намного понятнее и проще для понимания и переносимости с использованием, например, триггера или просто функции. ЗЛО я говорю. - person Tometzky; 19.09.2011
comment
Сделайте это 99%. Проблема в том, что большинство разработчиков думают об обработке строк за раз, что хорошо сочетается с триггерами. Рассмотрение правил как триггеров на стероидах является результатом этой сломанной парадигмы. Чтобы расширить вашу метафору GOTO: квалификация правила в OP сравнима с void main() в программе на C. Это просто неправильно, даже если это работает в каком-то конкретном случае. Тот факт, что меня проголосовали за то, что я упомянул об этом, подтверждает ваше правило 95%. Я считаю так. - person wildplasser; 19.09.2011

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

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

person wildplasser    schedule 18.09.2011
comment
ваша квалификация WHERE EXISTS (SELECT 1 from Pages P, где NEW.Url = P.Url) недействительна. Ты пробовал это? - person wildplasser; 18.09.2011
comment
Я только что проверил это. Это принято и, кажется, работает. Прости. Но это не должно работать, согласно документации. Может случиться так, что, хотя это работает, он будет генерировать неправильные планы, когда столкнется с более сложными запросами (такими как вставка в .. выбор из ... или самосоединение) - person wildplasser; 19.09.2011
comment
Я думаю, что общее мнение таково: да, это сработает, по крайней мере, в моем конкретном случае, но это хакерство, вы играете с огнем, это трудно читать, а правила плохие, ммм, ладно. Это был хороший повод поиграть с функция PG, которую я раньше не использовал, но я не думаю, что это правильное решение в долгосрочной перспективе. - person Mike Christensen; 19.09.2011