Вставить, при повторном обновлении в PostgreSQL?

645

Несколько месяцев назад я узнал из ответа на Stack Overflow, как выполнять несколько обновлений одновременно в 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 это рассматривается.

Чтобы уточнить, я хочу вставить несколько вещей, и если они уже существуют, чтобы обновить их.

Teifion
источник
38
Любой, кто найдет этот вопрос, должен прочитать статью Депеша «Почему так тяжело расстраиваться , Это объясняет проблему и возможные решения очень хорошо.
Крейг Рингер,
8
UPSERT будет добавлен в Postgres 9.5: wiki.postgresql.org/wiki/...
tommed
4
@tommed - это было сделано: stackoverflow.com/a/34639631/4418
Уоррен

Ответы:

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
Единственное, что я бы сделал по-другому, - это использовать FOR 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' может быть упрощена с помощью существования:... 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

a_horse_with_no_name
источник
1
@ FrançoisBeausoleil: вероятность состояния гонки намного меньше, чем при использовании подхода «попробуй / обработай исключение»
a_horse_with_no_name
2
@a_horse_with_no_name Как именно вы имеете в виду, что шанс в условиях гонки намного меньше? Когда я выполняю этот запрос одновременно с одними и теми же записями, я получаю сообщение об ошибке «значение ключа-дубликата нарушает уникальное ограничение» в 100% случаев, пока запрос не обнаружит, что запись была вставлена. Это полный пример?
Йерун ван Дейк
4
@a_horse_with_no_name Похоже, что ваше решение работает в параллельных ситуациях, когда вы переносите оператор upsert со следующей блокировкой: BEGIN WORK; LOCK TABLE mytable в эксклюзивном режиме SHARE ROW; <СУПЕР ЗДЕСЬ>; КОМИТЕТНАЯ РАБОТА;
Йерун ван Дейк
2
@JeroenvanDijk: спасибо. Что я имел в виду под «гораздо меньшим», так это то, что если несколько транзакций для этого (и зафиксировать изменение!), Промежуток времени между обновлением и вставкой будет меньше, поскольку все это всего лишь один оператор. Вы всегда можете сгенерировать нарушение pk с помощью двух независимых операторов INSERT. Если вы заблокируете всю таблицу, вы фактически сериализуете весь доступ к ней (чего вы могли бы достичь с помощью сериализуемого уровня изоляции).
a_horse_with_no_name
12
Это решение может потерять обновления, если транзакция вставки откатывается; нет проверки, чтобы убедиться, что UPDATEзатронуты какие-либо строки.
Крейг Рингер
132

В PostgreSQL 9.5 и новее вы можете использовать INSERT ... ON CONFLICT UPDATE.

Смотрите документацию .

MySQL INSERT ... ON DUPLICATE KEY UPDATEможет быть непосредственно перефразирован в 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 смотрите Как сделать UPSERT (MERGE, INSERT ... ON DUPLICATE UPDATE) в PostgreSQL?

Крейг Рингер
источник
Я рассмотрел решение PostgreSQL 9.5, как вы описали выше, потому что я испытывал пробелы в поле автоинкремента в то время как под MySQL ON DUPLICATE KEY UPDATE. Я скачал Postgres 9.5 и внедрил ваш код, но, как ни странно, та же проблема возникает в Postgres: поле последовательного ключа первичного ключа не является последовательным (между вставками и обновлениями есть пробелы). Есть идеи, что здесь происходит? Это нормально? Есть идеи, как избежать такого поведения? Спасибо.
WM
@WM Это в значительной степени присуще операции upsert. Вы должны оценить функцию, которая генерирует последовательность, прежде чем пытаться вставить. Поскольку такие последовательности предназначены для одновременной работы, они освобождены от обычной семантики транзакции, но даже если они не были генерацией, она не вызывается в субтранзакции и откатывается, она завершается нормально и фиксируется с остальной частью операции. Так что это может произойти даже с «последовательными» реализациями последовательностей. Единственный способ, которым БД могла бы избежать этого, - отложить оценку генерации последовательности до проверки ключа.
Крейг Рингер,
1
@WM, которая создаст свои собственные проблемы. По сути, вы застряли. Но если вы полагаетесь на то, что serial / auto_increment не содержит пробелов, у вас уже есть ошибки. Вы можете иметь пропуски последовательности из-за откатов, включая временные ошибки - перезагрузки под нагрузкой, ошибки клиента во время транзакции, сбои и т. Д. Вы никогда не должны полагаться на SERIAL/ SEQUENCEили AUTO_INCREMENTне иметь пропусков. Если вам нужны последовательности без промежутков, они более сложны; вам обычно нужно использовать таблицу счетчиков. Google скажет вам больше. Но имейте в виду, что последовательности без пробелов предотвращают параллелизм всех вставок.
Крейг Рингер,
@WM Если вам абсолютно необходимы последовательности без пропусков и upsert, вы можете использовать подход upsert на основе функций, описанный в руководстве, вместе с реализацией последовательностей без пропусков, которая использует таблицу счетчиков. Поскольку 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 и зацикливания отдельных обновлений, попадание preformance будет очень маленьким, см. 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 bar = 4, где bar = 4».
Тель
10

Лично я установил «правило», прикрепленное к оператору вставки. Скажем, у вас есть таблица «dns», в которой записано количество посещений DNS для каждого клиента в отдельности:

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

Вы хотели иметь возможность повторно вставлять строки с обновленными значениями или создавать их, если они еще не существовали. Введите идентификатор клиента и время. Что-то вроде этого:

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

Обновление: это может привести к сбою при одновременных вставках, так как это приведет к исключениям unique_violation. Однако незавершенная транзакция будет продолжена и завершится успешно, и вам просто нужно повторить прерванную транзакцию.

Тем не менее, если все время происходит множество операций вставки, вам нужно установить блокировку таблицы вокруг операторов вставки: блокировка SHARE ROW EXCLUSIVE предотвратит любые операции, которые могут вставлять, удалять или обновлять строки в вашей целевой таблице. Однако обновления, которые не обновляют уникальный ключ, безопасны, поэтому, если вы не выполняете никаких действий, используйте вместо этого рекомендательные блокировки.

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

Ch'marr
источник
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
Mise
источник
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... $$)

Важно ставить двойную запятую, чтобы избежать ошибок компилятора

  • проверь скорость ...
Фелипе FMMobile
источник
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/ )

alexkovelsky
источник
3
Это не удастся, если запускаться одновременно в двух сеансах, потому что ни одно обновление не увидит существующую строку, поэтому оба обновления попадут в ноль строк, поэтому оба запроса будут выполнять вставку.
Крейг Рингер
6

У меня та же проблема для управления настройками учетной записи, что и для пар имя-значение. Критерии дизайна таковы, что разные клиенты могут иметь разные наборы настроек.

Мое решение, аналогичное JWP, заключается в массовом удалении и замене, генерируя запись слияния в вашем приложении.

Это довольно пуленепробиваемый, независимый от платформы, и поскольку на клиента не может быть более 20 настроек, это всего лишь 3 вызова с достаточно низкой нагрузкой - вероятно, самый быстрый метод.

Альтернативой обновления отдельных строк - проверки исключений, а затем вставки - или какой-либо комбинации является отвратительный код, медленный и часто ломающийся, потому что (как упомянуто выше) нестандартная обработка исключений 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
Бенно
источник
Добро пожаловать в ТАК. Хорошее введение! :-)
Дон Вопрос
1
Это больше похоже , REPLACE INTOчем INSERT INTO ... ON DUPLICATE KEY UPDATE, что может вызвать проблемы , если вы используете триггеры. В конечном итоге вы будете запускать удаление и вставлять триггеры / правила, а не обновлять их.
Чао
5

Согласно документации INSERTоператора PostgreSQL , обработка ON DUPLICATE KEYдела не поддерживается. Эта часть синтаксиса является проприетарным расширением MySQL.

Кристиан Ханг-Хикс
источник
@Lucian MERGE- это действительно больше операция OLAP; см. stackoverflow.com/q/17267417/398670 для объяснения. Он не определяет семантику параллелизма, и большинство людей, которые используют его для upsert, просто создают ошибки.
Крейг Рингер,
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
Ahmad
источник
5

Для объединения небольших наборов хорошо использовать вышеуказанную функцию. Однако, если вы объединяете большие объемы данных, я бы посоветовал заглянуть в http://mbk.projects.postgresql.org

В настоящее время я знаю о наилучшей практике:

  1. КОПИРОВАНИЕ новых / обновленных данных во временную таблицу (конечно, или вы можете сделать INSERT, если стоимость в порядке)
  2. Получить блокировку [необязательно] (рекомендуется использовать для блокировки таблиц, IMO)
  3. Merge. (забавная часть)
СПР
источник
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. Но, как и большинство решений, опубликованных здесь, это неверное решение и оно не будет работать при наличии одновременных обновлений.
Крейг Рингер,