Вставити, після повторного оновлення у PostgreSQL?


644

Кілька місяців тому я дізнався з відповіді на переповнення стека, як виконати кілька оновлень одночасно в MySQL, використовуючи наступний синтаксис:

INSERT INTO table (id, field, field2) VALUES (1, A, X), (2, B, Y), (3, C, Z)
ON DUPLICATE KEY UPDATE field=VALUES(Col1), field2=VALUES(Col2);

Зараз я перейшов на PostgreSQL і, мабуть, це не правильно. Це стосується всіх правильних таблиць, тому я припускаю, що мова йде про різні ключові слова, але я не знаю, де в документації PostgreSQL це висвітлено.

Для уточнення, я хочу вставити кілька речей, і якщо вони вже існують, оновити їх.


38
Кожен, хто знайде це питання, повинен прочитати статтю Депеса "Чому пустеля така складна?" . Це надзвичайно добре пояснює проблему та можливі рішення.
Крейг Рінгер

8
UPSERT буде доданий у Postgres 9.5: wiki.postgresql.org/wiki/…
приурочений

4
@tommed - це було зроблено: stackoverflow.com/a/34639631/4418
warren

Відповіді:


515

PostgreSQL з версії 9.5 має синтаксис UPSERT , з пунктом ON CONFLICT . із наступним синтаксисом (схожим на MySQL)

INSERT INTO the_table (id, column_1, column_2) 
VALUES (1, 'A', 'X'), (2, 'B', 'Y'), (3, 'C', 'Z')
ON CONFLICT (id) DO UPDATE 
  SET column_1 = excluded.column_1, 
      column_2 = excluded.column_2;

Пошук архівів групи електронних адрес postgresql для "upsert" призводить до пошуку прикладу того, що ви, можливо, хочете зробити, у посібнику :

Приклад 38-2. Винятки з UPDATE / INSERT

Цей приклад використовує обробку виключень для виконання UPDATE або INSERT, якщо це доречно:

CREATE TABLE db (a INT PRIMARY KEY, b TEXT);

CREATE FUNCTION merge_db(key INT, data TEXT) RETURNS VOID AS
$$
BEGIN
    LOOP
        -- first try to update the key
        -- note that "a" must be unique
        UPDATE db SET b = data WHERE a = key;
        IF found THEN
            RETURN;
        END IF;
        -- not there, so try to insert the key
        -- if someone else inserts the same key concurrently,
        -- we could get a unique-key failure
        BEGIN
            INSERT INTO db(a,b) VALUES (key, data);
            RETURN;
        EXCEPTION WHEN unique_violation THEN
            -- do nothing, and loop to try the UPDATE again
        END;
    END LOOP;
END;
$$
LANGUAGE plpgsql;

SELECT merge_db(1, 'david');
SELECT merge_db(1, 'dennis');

Можливо, є приклад того, як це зробити масово, використовуючи CTE 9.1 та вище, у списку розсилки хакерів :

WITH foos AS (SELECT (UNNEST(%foo[])).*)
updated as (UPDATE foo SET foo.a = foos.a ... RETURNING foo.id)
INSERT INTO foo SELECT foos.* FROM foos LEFT JOIN updated USING(id)
WHERE updated.id IS NULL;

Дивіться відповідь a_horse_with_no_name для більш чіткого прикладу.


7
Єдине, що мені не подобається в цьому, це те, що це було б набагато повільніше, тому що кожен прийом був би власним індивідуальним викликом у базу даних.
baash05

@ baash05 може бути спосіб зробити це оптом, дивіться мою оновлену відповідь.
Стівен Денн

2
Єдине, що я зробив би по-іншому, це використовувати ЗА 1..2 LOOP замість просто LOOP, так що, якщо порушується якесь інше унікальне обмеження, воно не буде крутитися нескінченно.
olamork

2
Про що excludedйдеться в першому рішенні тут?
ichbinallen

2
@ichbinallen в документах Указані пункти SET і WHERE у програмі ON CONFLICT DO UPDATE мають доступ до існуючого рядка, використовуючи ім'я таблиці (або псевдонім), і рядки, запропоновані для вставки, використовуючи спеціальну виключену таблицю . У цьому випадку спеціальна excludedтаблиця надає вам доступ до значень, які ви намагалися ВСТУПИТИ в першу чергу.
TMichel

429

Попередження: це не безпечно, якщо виконується з декількох сеансів одночасно (див. Попередження нижче).


Ще один розумний спосіб зробити "UPSERT" в postgresql - це зробити два послідовних операції UPDATE / INSERT, кожен з яких призначений для успіху або не має ефекту.

UPDATE table SET field='C', field2='Z' WHERE id=3;
INSERT INTO table (id, field, field2)
       SELECT 3, 'C', 'Z'
       WHERE NOT EXISTS (SELECT 1 FROM table WHERE id=3);

ОНОВЛЕННЯ вдасться, якщо рядок з "id = 3" вже існує, інакше це не матиме ефекту.

INSERT матиме успіх лише в тому випадку, якщо рядок з "id = 3" ще не існує.

Ви можете об'єднати ці два в єдиний рядок і запустити їх обома за допомогою одного оператора SQL, виконаного з вашої програми. Дуже рекомендується виконувати їх за допомогою однієї транзакції.

Це дуже добре працює, коли запускається ізольовано або на заблокованій таблиці, але це підлягає перегоновим умовам, які означають, що він може все-таки закінчуватися помилкою повторюваного ключа, якщо рядок вставлено одночасно, або може закінчитися, якщо рядок не вставляється, коли рядок видалено одночасно . SERIALIZABLEУгода по PostgreSQL 9.1 або вище буде обробляти його надійно за рахунок дуже високої інтенсивності відмов сериализации, тобто ви повинні будете повторити багато. Подивіться, чому наряд так складний , що детальніше обговорює цю справу.

Цей підхід також підлягає втраті оновлень у read committedвідриві, якщо програма не перевіряє підрахунок порушених рядків і не перевіряє, insertчи updateє ряд чи порушений рядок .


6
Коротка відповідь: якщо запис існує, INSERT нічого не робить. Довга відповідь: SELECT в INSERT поверне стільки результатів, скільки є відповідність пункту "where". Це максимум одне (якщо номер один не є результатом підбору), інакше дорівнює нулю. Таким чином, INSERT додасть або один, або нульовий рядки.
Пітер Бекер

3
частину "де" можна спростити за допомогою використання існує:... where not exists (select 1 from table where id = 3);
Endy Tjahjono

1
це має бути правильна відповідь .. з деякими незначними налаштуваннями, це можна було б використовувати для масового оновлення .. Гумм .. Цікаво, чи можна використовувати таблицю темп ..
baash05

1
@keaplogik, обмеження 9.1 є записом CTE (загальні вирази таблиці), який описаний в іншій відповіді. Синтаксис, який використовується у цій відповіді, є дуже базовим і довго підтримується.
бичачий

8
Попередження, це може бути уникнути втрачених оновлень поодиноко, read committedякщо ваша програма не перевіряє, insertчи updateнемає або нульовий рахунок рядків. Дивіться dba.stackexchange.com/q/78510/7788
Крейг Рінгер

227

З PostgreSQL 9.1 цього можна досягти, використовуючи записаний CTE ( загальний вираз таблиці ):

WITH new_values (id, field1, field2) as (
  values 
     (1, 'A', 'X'),
     (2, 'B', 'Y'),
     (3, 'C', 'Z')

),
upsert as
( 
    update mytable m 
        set field1 = nv.field1,
            field2 = nv.field2
    FROM new_values nv
    WHERE m.id = nv.id
    RETURNING m.*
)
INSERT INTO mytable (id, field1, field2)
SELECT id, field1, field2
FROM new_values
WHERE NOT EXISTS (SELECT 1 
                  FROM upsert up 
                  WHERE up.id = new_values.id)

Дивіться ці записи в блозі:


Зауважте, що це рішення не запобігає унікальному порушенню ключа, але воно не вразливе до втрачених оновлень.
Дивіться подальші дії Крейга Рінгера на сайті dba.stackexchange.com


1
@ FrançoisBeausoleil: шанс на стан гонки набагато менший, ніж при підході "спробувати / впоратися з виключенням"
a_horse_with_no_name

2
@a_horse_with_no_name Як ви точно означаєте, що шанс на умовах гонки значно менший? Коли я виконую цей запит одночасно з одними і тими ж записами, я отримую помилку "значення дублюючого ключа порушує унікальне обмеження" у 100% разів, поки запит не виявить, що запис було вставлено. Це повний приклад?
Jeroen van Dijk

4
@a_horse_with_no_name Ваше рішення, здається, працює в одночасних ситуаціях, коли ви обертаєте заяву про вставку із таким блокуванням: BEGIN WORK; СКАЧАТИ ТАБЛИЦЮ, яку можна змінити у розділі ЕКСКЛЮЗИВНИЙ РЕЖИМ; <UPSERT ТУТ>; РОБОЧИЙ РОБОТ;
Jeroen van Dijk

2
@JeroenvanDijk: спасибі Що я мав на увазі під "набагато меншими", це те, що якщо кілька транзакцій для цього (і здійснити зміну!) Проміжок часу між оновленням та вставкою менший, оскільки все є лише одним твердженням. Ви завжди можете генерувати порушення ПК за допомогою двох незалежних операторів INSERT. Якщо ви заблокуєте всю таблицю, ви ефективно серіалізуєте весь доступ до неї (чого б ви могли досягти і з рівнем ізоляції, що серіалізується).
a_horse_with_no_name

12
Це рішення підлягає втраті оновлень, якщо транзакція, що вставляється, відкочується назад; немає жодної перевірки, щоб підтвердити, чи були UPDATEпорушені будь-які рядки.
Крейг Рінгер

132

У PostgreSQL 9.5 і новіших ви можете використовувати INSERT ... ON CONFLICT UPDATE.

Дивіться документацію .

MySQL INSERT ... ON DUPLICATE KEY UPDATEможна безпосередньо перефразовувати на a ON CONFLICT UPDATE. Синтаксис не є стандартним SQL, вони обидва розширення для бази даних. Для цього MERGEне використовуються вагомі причини , а новий синтаксис створений не просто для розваги. (У синтаксисі MySQL також є проблеми, які означають, що він не був прийнятий безпосередньо).

наприклад задана установка:

CREATE TABLE tablename (a integer primary key, b integer, c integer);
INSERT INTO tablename (a, b, c) values (1, 2, 3);

запит MySQL:

INSERT INTO tablename (a,b,c) VALUES (1,2,3)
  ON DUPLICATE KEY UPDATE c=c+1;

стає:

INSERT INTO tablename (a, b, c) values (1, 2, 10)
ON CONFLICT (a) DO UPDATE SET c = tablename.c + 1;

Відмінності:

  • Ви повинні вказати ім'я стовпця (або унікальне ім'я обмеження), яке буде використано для перевірки унікальності. Ось щоON CONFLICT (columnname) DO

  • SETНеобхідно використовувати ключове слово , як якщо б це був звичайний UPDATEвислів

Він також має деякі приємні риси:

  • Ви можете мати WHEREпункт про своє UPDATE(дозволяючи ефективно перетворитись ON CONFLICT UPDATEна ON CONFLICT IGNOREпевні значення)

  • Запропоновані для вставки значення доступні у вигляді змінної рядків EXCLUDED, яка має ту саму структуру, що і цільова таблиця. Ви можете отримати вихідні значення в таблиці за допомогою імені таблиці. Так що в цьому випадку EXCLUDED.cбуде 10(тому що це ми намагалися вставити) і "table".cбуде, 3тому що це поточне значення в таблиці. Ви можете використовувати або в обох SETвиразах, і в WHEREпункті.

Довідку щодо запуску див. Як UPSERT (MERGE, INSERT ... ON DUPLICATE UPDATE) у PostgreSQL?


Я розглянув рішення 9.5 від PostgreSQL, як ви описали вище, тому що я переживав прогалини в полі автоматичного збільшення, перебуваючи під MySQL ON DUPLICATE KEY UPDATE. Я завантажив Postgres 9.5 і реалізував ваш код, але, як не дивно, ця ж проблема виникає і в Postgres: серійне поле первинного ключа не є послідовним (між вставками та оновленнями є прогалини). Будь-яка ідея, що тут відбувається? Це нормально? Будь-яка ідея, як уникнути такої поведінки? Дякую.
WM

@WM Це в значній мірі властиво виконувати операції. Ви повинні оцінити функцію, яка генерує послідовність, перш ніж намагатися вставити. Оскільки такі послідовності призначені для одночасного функціонування, вони звільнені від звичайної семантики транзакцій, але навіть якщо вони не були, генерація не викликається субтранзакцією і відкочується назад, вона завершується нормально і бере участь у решті операції. Тож це станеться навіть із реалізацією послідовностей "без змін". Єдиний спосіб, як БД зможе цього уникнути, - це затримати оцінку генерації послідовностей до моменту перевірки ключа.
Крейг Рінгер

1
@WM, яка б створила власні проблеми. В основному, ви застрягли. Але якщо ви покладаєтесь на те, що serial / auto_increment є безчисленним, у вас вже є помилки. Ви можете мати пробіли в послідовності через відкати, включаючи перехідні помилки - перезавантаження під навантаженням, помилки клієнта в середині транзакції, збої тощо. Ніколи не слід покладатися на SERIAL/ SEQUENCEабо AUTO_INCREMENTне мати прогалини. Якщо вам потрібні безлічі послідовності, вони складніші; зазвичай потрібно використовувати стільницький стіл. Google розповість вам більше. Але пам’ятайте, що безлічі послідовності заважають усім вставити одночасність.
Крейг Рінгер

@WM Якщо вам абсолютно потрібні послідовності без змін і доповнення, ви можете використовувати підхідний підхід на основі функцій, обговорений в посібнику, а також реалізацію послідовностей без змін, яка використовує таблицю лічильників. Оскільки BEGIN ... EXCEPTION ...запускається субтрасакція, яка повертається помилково, ваш приріст послідовності буде повернутий назад, якщо INSERTне вдалося.
Крейг Рінгер

Дуже дякую @Craig Ringer, що було досить інформативно. Я зрозумів, що я можу просто відмовитись від того, щоб мати цей автоматичний приріст первинного ключа. Я зробив складене первинне з 3-х полів, і для моєї конкретної поточної потреби насправді немає необхідності в безперервному автоматичному збільшенні поля. Дякую ще раз, надана вами інформація допоможе мені заощадити час у майбутньому, намагаючись запобігти природній та здоровій поведінці БД. Я зараз це краще розумію.
WM

17

Я шукав те саме, коли приїхав сюди, але відсутність загальної функції "upsert" трохи мене непокоїло, тому я подумав, що ви можете просто пройти оновлення та вставити sql як аргументи на цю функцію в посібнику

це виглядатиме так:

CREATE FUNCTION upsert (sql_update TEXT, sql_insert TEXT)
    RETURNS VOID
    LANGUAGE plpgsql
AS $$
BEGIN
    LOOP
        -- first try to update
        EXECUTE sql_update;
        -- check if the row is found
        IF FOUND THEN
            RETURN;
        END IF;
        -- not found so insert the row
        BEGIN
            EXECUTE sql_insert;
            RETURN;
            EXCEPTION WHEN unique_violation THEN
                -- do nothing and loop
        END;
    END LOOP;
END;
$$;

і, можливо, щоб зробити те, що ви спочатку хотіли зробити, створити "upsert", ви можете використовувати Tcl, щоб розділити sql_update і циклічити окремі оновлення, хіт передформації буде дуже малим див. http://archives.postgresql.org/pgsql- продуктивність / 2006-04 / msg00557.php

найвища вартість - це виконання запиту з вашого коду, на базі даних вартість виконання значно менша


3
Вам все одно доведеться запускати це в циклі повторного циклу, і він схильний до перегонів з одночасним, DELETEякщо ви не заблокуєте таблицю або не перебуваєте в SERIALIZABLEізоляції транзакцій на PostgreSQL 9.1 або вище.
Крейг Рінгер

13

Немає простої команди зробити це.

Найбільш правильний підхід - використовувати функцію, подібно до док .

Іншим рішенням (хоча це не так безпечно) є оновлення з поверненням, перевірити, які рядки були оновленнями, і вставити решту з них

Щось у напрямку:

update table
set column = x.column
from (values (1,'aa'),(2,'bb'),(3,'cc')) as x (id, column)
where table.id = x.id
returning id;

за умови, що id: 2 було повернуто:

insert into table (id, column) values (1, 'aa'), (3, 'cc');

Звичайно, вона рано чи пізно викупить (у паралельних умовах), оскільки тут чіткий стан перегонів, але зазвичай це спрацює.

Ось більш довга і всебічна стаття на цю тему .


1
Якщо ви використовуєте цю опцію, обов'язково перевірте, чи повертається ідентифікатор, навіть якщо оновлення нічого не робить. Я бачив, як бази даних оптимізують запити, відмінні від типу "Оновити панель столів для foo set = 4, де bar = 4".
thelem

10

Особисто я створив "правило", додане до заяви. Скажімо, у вас була таблиця "dns", яка щоденно записувала dns звернення на кожного клієнта:

CREATE TABLE dns (
    "time" timestamp without time zone NOT NULL,
    customer_id integer NOT NULL,
    hits integer
);

Ви хотіли мати можливість повторно вставити рядки з оновленими значеннями або створити їх, якщо вони ще не існували. Вказано на customer_id та час. Щось на зразок цього:

CREATE RULE replace_dns AS 
    ON INSERT TO dns 
    WHERE (EXISTS (SELECT 1 FROM dns WHERE ((dns."time" = new."time") 
            AND (dns.customer_id = new.customer_id)))) 
    DO INSTEAD UPDATE dns 
        SET hits = new.hits 
        WHERE ((dns."time" = new."time") AND (dns.customer_id = new.customer_id));

Оновлення: це може призвести до виходу з ладу, якщо відбуваються одночасні вставки, оскільки це генерує винятки унікальної поведінки. Однак нерозірвана транзакція триватиме і буде успішною, і вам просто потрібно повторити припинену транзакцію.

Однак якщо постійно відбувається багато вкладок, вам потрібно поставити блокування таблиці навколо операторів вставки: Блокування БЕЗКОШТОВНО ЕКСКЛЮЗИВНО запобіжить будь-яким операціям, які могли б вставити, видалити або оновити рядки в цільовій таблиці. Однак оновлення, які не оновлюють унікальний ключ, є безпечними, тому якщо ви не виконаєте жодної операції, використовуйте замість нього дорадчі блокування.

Також команда COPY не використовує ПРАВИЛА, тому якщо ви вставляєте за допомогою COPY, вам потрібно буде використовувати тригери.


9

Я використовую цю функцію злиття

CREATE OR REPLACE FUNCTION merge_tabla(key INT, data TEXT)
  RETURNS void AS
$BODY$
BEGIN
    IF EXISTS(SELECT a FROM tabla WHERE a = key)
        THEN
            UPDATE tabla SET b = data WHERE a = key;
        RETURN;
    ELSE
        INSERT INTO tabla(a,b) VALUES (key, data);
        RETURN;
    END IF;
END;
$BODY$
LANGUAGE plpgsql

1
Ефективніше просто зробити updateперший, а потім перевірити кількість оновлених рядків. (Див. Відповідь Ахмада)
a_horse_with_no_name

8

Я налаштовую функцію "upsert" вище, якщо ви хочете ВСТАВИТИ І ЗАМЕНИТИ:

`

 CREATE OR REPLACE FUNCTION upsert(sql_insert text, sql_update text)

 RETURNS void AS
 $BODY$
 BEGIN
    -- first try to insert and after to update. Note : insert has pk and update not...

    EXECUTE sql_insert;
    RETURN;
    EXCEPTION WHEN unique_violation THEN
    EXECUTE sql_update; 
    IF FOUND THEN 
        RETURN; 
    END IF;
 END;
 $BODY$
 LANGUAGE plpgsql VOLATILE
 COST 100;
 ALTER FUNCTION upsert(text, text)
 OWNER TO postgres;`

А після виконання виконайте щось подібне:

SELECT upsert($$INSERT INTO ...$$,$$UPDATE... $$)

Важливо поставити подвійну коду долара з комою, щоб уникнути помилок компілятора

  • перевірити швидкість ...

7

Схожа на найбільш вподобану відповідь, але працює трохи швидше:

WITH upsert AS (UPDATE spider_count SET tally=1 WHERE date='today' RETURNING *)
INSERT INTO spider_count (spider, tally) SELECT 'Googlebot', 1 WHERE NOT EXISTS (SELECT * FROM upsert)

(джерело: http://www.the-art-of-web.com/sql/upsert/ )


3
Це не вдасться, якщо запуститись одночасно протягом двох сеансів, оскільки жодне оновлення не побачить існуючий рядок, тому обидва оновлення матимуть нульових рядків, тому обидва запити видадуть вставку.
Крейг Рінгер

6

У мене є те саме питання щодо керування налаштуваннями облікового запису, як і пари значень імен. Критерії дизайну полягають у тому, що різні клієнти можуть мати різні набори налаштувань.

Моє рішення, подібне до JWP, полягає в масовому видаленні та заміні, генеруючи запис об'єднання у вашій програмі.

Це досить бронезахисна платформа, незалежна від платформи, і оскільки ніколи не буває більше 20 налаштувань на кожного клієнта, це лише 3 досить низькі навантаження на db-дзвінки - мабуть, найшвидший метод.

Альтернатива оновлення окремих рядків - перевірка винятків, а потім вставлення - або якась комбінація - це огидний код, повільний і часто порушується, оскільки (як було сказано вище) нестандартне оброблення винятків SQL змінюється з db на db - або навіть відпускається до випуску.

 #This is pseudo-code - within the application:
 BEGIN TRANSACTION - get transaction lock
 SELECT all current name value pairs where id = $id into a hash record
 create a merge record from the current and update record
  (set intersection where shared keys in new win, and empty values in new are deleted).
 DELETE all name value pairs where id = $id
 COPY/INSERT merged records 
 END TRANSACTION

Ласкаво просимо до SO. Приємного вступу! :-)
Дон Питання

1
Це більше схоже , REPLACE INTOніж INSERT INTO ... ON DUPLICATE KEY UPDATE, що може викликати проблеми , якщо ви використовуєте тригери. Ви закінчите видаляти та вставляти тригери / правила, а не оновлювати їх.
cHao

5

Відповідно до документації PostgreSQL INSERTзаяви , обробка ON DUPLICATE KEYсправи не підтримується. Ця частина синтаксису є власним розширенням MySQL.


@Lucian MERGE- це справді більше операція OLAP; див. stackoverflow.com/q/17267417/398670 для пояснення. Він не визначає семантику одночасності, і більшість людей, які використовують її для запуску, просто створюють помилки.
Крейг Рінгер

5
CREATE OR REPLACE FUNCTION save_user(_id integer, _name character varying)
  RETURNS boolean AS
$BODY$
BEGIN
    UPDATE users SET name = _name WHERE id = _id;
    IF FOUND THEN
        RETURN true;
    END IF;
    BEGIN
        INSERT INTO users (id, name) VALUES (_id, _name);
    EXCEPTION WHEN OTHERS THEN
            UPDATE users SET name = _name WHERE id = _id;
        END;
    RETURN TRUE;
END;

$BODY$
  LANGUAGE plpgsql VOLATILE STRICT

5

Для об'єднання невеликих наборів прекрасно використовувати вищевказану функцію. Однак, якщо ви об’єднуєте велику кількість даних, я б радив переглянути http://mbk.projects.postgresql.org

Нині найкраща практика, яку мені відомо, це:

  1. КОПУЙТЕ нові / оновлені дані в темп-таблицю (впевнений, або ви можете зробити ВСТАВИТИ, якщо вартість нормальна)
  2. Отримати блокування [необов’язково] (доцільно перевагу блокування таблиць, IMO)
  3. Злиття. (найцікавіша частина)

5

UPDATE поверне кількість модифікованих рядків. Якщо ви використовуєте JDBC (Java), ви можете перевірити це значення на 0 і, якщо жодні рядки не вплинули, натомість запустіть INSERT. Якщо ви використовуєте якусь іншу мову програмування, можливо, кількість модифікованих рядків все ж можна отримати, перегляньте документацію.

Це може бути не настільки елегантно, але у вас набагато простіший SQL, який більш тривіально використовувати в коді виклику. Інакше, якщо ви пишете десятирядковий скрипт в PL / PSQL, ви, мабуть, повинні мати тест одиничного чи іншого виду тільки для нього.


4

Редагувати: це не працює, як очікувалося. На відміну від прийнятої відповіді, це створює унікальні ключові порушення, коли два процеси повторно викликаютьupsert_foo одночасно.

Еврика! Я з'ясував спосіб зробити це в одному запиті: використовувати UPDATE ... RETURNINGдля перевірки, чи були порушені рядки:

CREATE TABLE foo (k INT PRIMARY KEY, v TEXT);

CREATE FUNCTION update_foo(k INT, v TEXT)
RETURNS SETOF INT AS $$
    UPDATE foo SET v = $2 WHERE k = $1 RETURNING $1
$$ LANGUAGE sql;

CREATE FUNCTION upsert_foo(k INT, v TEXT)
RETURNS VOID AS $$
    INSERT INTO foo
        SELECT $1, $2
        WHERE NOT EXISTS (SELECT update_foo($1, $2))
$$ LANGUAGE sql;

Це UPDATEпотрібно зробити в окремій процедурі, оскільки, на жаль, це синтаксична помилка:

... WHERE NOT EXISTS (UPDATE ...)

Тепер він працює за бажанням:

SELECT upsert_foo(1, 'hi');
SELECT upsert_foo(1, 'bye');
SELECT upsert_foo(3, 'hi');
SELECT upsert_foo(3, 'bye');

1
Ви можете об'єднати їх в одне твердження, якщо ви використовуєте CTE, що записується. Але, як і більшість рішень, розміщених тут, і це неправильно, і не вдасться при наявності одночасних оновлень.
Крейг Рінгер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.