Inserir, em atualização duplicada no PostgreSQL?

645

Vários meses atrás, aprendi com uma resposta no Stack Overflow como executar várias atualizações de uma vez no MySQL usando a seguinte sintaxe:

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

Agora mudei para o PostgreSQL e, aparentemente, isso não está correto. Está se referindo a todas as tabelas corretas, portanto, presumo que sejam usadas palavras-chave diferentes, mas não tenho certeza de onde isso está coberto na documentação do PostgreSQL.

Para esclarecer, quero inserir várias coisas e, se elas já existirem, atualizá-las.

Teifion
fonte
38
Quem encontrar essa pergunta deve ler o artigo de Depesz "Por que o upsert é tão complicado?" . Explica o problema e as possíveis soluções extremamente bem.
Craig Ringer
8
O UPSERT será adicionado no Postgres 9.5: wiki.postgresql.org/wiki/…
tommed
4
@tommed - isso já foi feito: stackoverflow.com/a/34639631/4418
Warren

Respostas:

515

O PostgreSQL desde a versão 9.5 possui sintaxe UPSERT , com a cláusula ON CONFLICT . com a seguinte sintaxe (semelhante ao 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;

A busca por "upsert" nos arquivos do grupo de e-mail do postgresql leva a encontrar um exemplo de como você possivelmente quer fazer, no manual :

Exemplo 38-2 Exceções com UPDATE / INSERT

Este exemplo usa manipulação de exceção para executar UPDATE ou INSERT, conforme apropriado:

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

Existe possivelmente um exemplo de como fazer isso em massa, usando CTEs na 9.1 e acima, na lista de e-mails dos hackers :

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;

Veja a resposta de a_horse_with_no_name para um exemplo mais claro.

Stephen Denne
fonte
7
A única coisa que eu não gosto disso é que seria muito mais lento, porque cada upsert seria sua própria chamada individual no banco de dados.
precisa saber é o seguinte
@ baash05 pode haver uma maneira de fazer isso em massa, veja minha resposta atualizada.
Stephen Denne
2
A única coisa que eu faria de diferente é usar FOR 1..2 LOOP em vez de apenas LOOP, para que, se alguma outra restrição exclusiva for violada, ela não gire indefinidamente.
olamork
2
A que se excludedrefere a primeira solução aqui?
precisa saber é o seguinte
2
@ichbinallen nos documentos As cláusulas SET e WHERE em ON CONFLICT DO UPDATE têm acesso à linha existente usando o nome da tabela (ou um alias) e às linhas propostas para inserção usando a tabela excluída especial . Nesse caso, a excludedtabela especial fornece acesso aos valores que você estava tentando inserir em primeiro lugar.
TMichel
429

Aviso: isso não é seguro se executado a partir de várias sessões ao mesmo tempo (veja advertências abaixo).


Outra maneira inteligente de executar um "UPSERT" no postgresql é executar duas instruções UPDATE / INSERT sequenciais, cada uma projetada para ter sucesso ou não ter efeito.

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

O UPDATE será bem-sucedido se uma linha com "id = 3" já existir, caso contrário não terá efeito.

O INSERT terá êxito apenas se a linha com "id = 3" ainda não existir.

Você pode combinar esses dois em uma única sequência e executá-los com uma única instrução SQL executada no seu aplicativo. Executá-los juntos em uma única transação é altamente recomendado.

Isso funciona muito bem quando executado isoladamente ou em uma tabela bloqueada, mas está sujeito a condições de corrida, o que significa que ainda poderá falhar com erro de chave duplicada se uma linha for inserida simultaneamente ou pode terminar sem nenhuma linha inserida quando uma linha for excluída simultaneamente. . Uma SERIALIZABLEtransação no PostgreSQL 9.1 ou superior irá lidar com isso de maneira confiável ao custo de uma taxa de falha de serialização muito alta, o que significa que você precisará tentar muito. Vejo por que o upsert é tão complicado , que discute esse caso com mais detalhes.

Esta abordagem também é sujeito a atualizações perdidas em read committedisolamento, a menos que a aplicação verifica a contagem de linhas afetadas e verifica que tanto o insertou o updateafetou uma linha .

bovino
fonte
6
Resposta curta: se o registro existe, o INSERT não faz nada. Resposta longa: o SELECT no INSERT retornará tantos resultados quanto houver correspondências da cláusula where. Isso é no máximo um (se o número um não estiver no resultado da sub-seleção), caso contrário, zero. O INSERT adicionará, assim, uma ou zero linhas.
Peter Becker
3
a parte 'where' pode ser simplificada usando existe:... where not exists (select 1 from table where id = 3);
Endy Tjahjono 21/10/11
1
esta deve ser a resposta certa .. com alguns pequenos ajustes, poderia ser usado para fazer uma atualização em massa .. Humm .. Eu me pergunto se uma tabela temporária poderia ser usado ..
baash05
1
@keaplogik, essa limitação 9.1 é com CTE gravável (expressões comuns de tabela) que é descrita em outra das respostas. A sintaxe usada nesta resposta é muito básica e há muito é suportada.
bovine
8
Aviso, isso está sujeito a atualizações perdidas read committedisoladamente, a menos que seu aplicativo verifique se o número de linhas é diferente de zero insertou updatenão. Veja dba.stackexchange.com/q/78510/7788
Craig Ringer
227

Com o PostgreSQL 9.1, isso pode ser alcançado usando um CTE gravável ( expressão de tabela comum ):

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)

Veja estas entradas do blog:


Observe que esta solução não evita uma violação exclusiva da chave, mas não é vulnerável a atualizações perdidas.
Veja o acompanhamento de Craig Ringer em dba.stackexchange.com

um cavalo sem nome
fonte
1
@ FrançoisBeausoleil: a chance de uma condição de corrida é muito menor do que com a abordagem "exceção de tentativa / tratamento"
a_horse_with_no_name
2
@a_horse_with_no_name Como você quer dizer exatamente que a chance nas condições da corrida é muito menor? Quando executo essa consulta simultaneamente com os mesmos registros, recebo o erro "o valor da chave duplicada viola a restrição exclusiva" 100% das vezes até que a consulta detecte que o registro foi inserido. Este é um exemplo completo?
Jeroen van Dijk
4
@a_horse_with_no_name Sua solução parece funcionar em situações simultâneas quando você quebra a instrução upsert com o seguinte bloqueio: BEGIN WORK; TABELA DE BLOQUEIO mytable NO MODO EXCLUSIVO DA SHARE ROW; <UPSERT HERE>; COMPROMETE O TRABALHO;
Jeroen van Dijk
2
@JeroenvanDijk: obrigado. O que eu quis dizer com "muito menor" é que, se várias transações para isso (e confirmar a alteração!), O intervalo de tempo entre a atualização e a inserção é menor, pois tudo é apenas uma única declaração. Você sempre pode gerar uma violação de pk por duas instruções INSERT independentes. Se você bloquear a tabela inteira, serializa efetivamente todo o acesso a ela (algo que também poderia ser alcançado com o nível de isolamento serializável).
a_horse_with_no_name 26/03
12
Esta solução está sujeita a atualizações perdidas se a transação de inserção reverter; não há verificação para garantir que as UPDATElinhas afetadas sejam afetadas.
Craig Ringer
132

No PostgreSQL 9.5 e mais recente, você pode usar INSERT ... ON CONFLICT UPDATE.

Veja a documentação .

Um MySQL INSERT ... ON DUPLICATE KEY UPDATEpode ser reformulado diretamente para a ON CONFLICT UPDATE. A sintaxe padrão do SQL também não é, ambas são extensões específicas do banco de dados. Existem boas razões para MERGEnão ter sido usado para isso , uma nova sintaxe não foi criada apenas por diversão. (A sintaxe do MySQL também tem problemas que significam que não foi adotado diretamente).

por exemplo, dada configuração:

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

a consulta do MySQL:

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

torna-se:

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

Diferenças:

  • Você deve especificar o nome da coluna (ou nome exclusivo da restrição) a ser usado para a verificação de exclusividade. Essa é aON CONFLICT (columnname) DO

  • A palavra-chave SETdeve ser usada, como se fosse uma UPDATEdeclaração normal

Também possui alguns recursos interessantes:

  • Você pode ter uma WHEREcláusula UPDATE(permitindo efetivamente transformar-se ON CONFLICT UPDATEem ON CONFLICT IGNOREcertos valores)

  • Os valores propostos para inserção estão disponíveis como a variável de linha EXCLUDED, que possui a mesma estrutura da tabela de destino. Você pode obter os valores originais na tabela usando o nome da tabela. Então, neste caso EXCLUDED.c, será 10(porque foi o que tentamos inserir) e "table".cserá 3porque esse é o valor atual na tabela. Você pode usar uma ou ambas as SETexpressões e WHEREcláusula.

Para obter mais informações sobre o upsert, consulte Como UPSERT (MERGE, INSERT ... ON DUPLICATE UPDATE) no PostgreSQL?

Craig Ringer
fonte
Eu examinei a solução 9.5 do PostgreSQL como você descreveu acima, porque eu estava enfrentando falhas no campo de incremento automático enquanto estava no MySQL ON DUPLICATE KEY UPDATE. Fiz o download do Postgres 9.5 e implementei seu código, mas estranhamente ocorre o mesmo problema no Postgres: o campo serial da chave primária não é consecutivo (existem lacunas entre as inserções e atualizações). Alguma idéia do que está acontecendo aqui? Isso é normal? Alguma idéia de como evitar esse comportamento? Obrigado.
WM
@WM Isso é praticamente inerente a uma operação de upsert. Você deve avaliar a função que gera a sequência antes de tentar a inserção. Como essas seqüências são projetadas para operar simultaneamente, elas estão isentas da semântica normal de transações, mas mesmo que não fossem a geração não é chamada em uma subtransação e revertida, ela é concluída normalmente e confirmada com o restante da operação. Portanto, isso aconteceria mesmo com implementações de sequência "sem intervalos". A única maneira de o DB evitar isso seria atrasar a avaliação da geração de sequência até depois da verificação da chave.
Craig Ringer
1
@WM, o que criaria seus próprios problemas. Basicamente, você está preso. Mas se você está confiando em que o serial / auto_increment esteja sem intervalos, você já tem bugs. Você pode ter lacunas seqüência devido a reversões incluindo erros transitórios - reinicializações sob carga, erros de clientes meados de transação, falhas, etc. Você nunca deve, nunca confiar em SERIAL/ SEQUENCEou AUTO_INCREMENTnão ter lacunas. Se você precisa de sequências sem intervalos, elas são mais complexas; você precisa usar uma mesa de contador normalmente. O Google lhe dirá mais. Mas esteja ciente de que seqüências sem intervalos impedem toda a simultaneidade da inserção.
Craig Ringer
@WM Se você realmente precisar de sequências sem intervalos e upsert, poderá usar a abordagem de upsert baseada em funções discutida no manual, juntamente com uma implementação de sequência sem intervalos que use uma tabela de contador. Como as BEGIN ... EXCEPTION ...execuções em uma subtransação são revertidas por erro, seu incremento de sequência seria revertido se INSERTfalhasse.
Craig Ringer
Muito obrigado ao @Craig Ringer, isso foi bastante informativo. Percebi que posso simplesmente desistir de ter essa chave primária de incremento automático. Fiz um primário composto de 3 campos e, para minha necessidade atual específica, não há realmente a necessidade de um campo de incremento automático sem intervalos. Obrigado novamente, as informações que você forneceu me poupariam tempo no futuro tentando impedir um comportamento natural e saudável do banco de dados. Eu entendo melhor agora.
WM
17

Eu estava procurando a mesma coisa quando cheguei aqui, mas a falta de uma função genérica "upsert" me incomodou um pouco, então pensei que você poderia simplesmente passar a atualização e inserir sql como argumentos nessa função do manual

que ficaria assim:

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

e, talvez, para fazer o que você inicialmente queria, em lotes "upsert", você poderia usar o Tcl para dividir o sql_update e fazer o loop das atualizações individuais, o resultado da pré-execução será muito pequeno, consulte http://archives.postgresql.org/pgsql- performance / 2006-04 / msg00557.php

o custo mais alto é a execução da consulta a partir do seu código, no lado do banco de dados o custo de execução é muito menor

Paul Scheltema
fonte
3
Você ainda precisa executar isso em um loop de repetição e é propenso a corridas simultâneas, a DELETEmenos que você bloqueie a tabela ou esteja em SERIALIZABLEisolamento de transação no PostgreSQL 9.1 ou superior.
Craig Ringer
13

Não há um comando simples para fazê-lo.

A abordagem mais correta é usar a função, como a dos documentos .

Outra solução (embora não tão segura) é atualizar com o retorno, verificar quais linhas foram atualizadas e inserir o restante delas.

Algo ao longo das linhas de:

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;

assumindo id: 2 foi retornado:

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

É claro que ele será resgatado mais cedo ou mais tarde (em ambiente concorrente), pois há uma clara condição de corrida aqui, mas geralmente funcionará.

Aqui está um artigo mais longo e abrangente sobre o assunto .

Craig Ringer
fonte
1
Se estiver usando esta opção, verifique se o ID é retornado, mesmo que a atualização não faça nada. Já vi bancos de dados otimizando consultas ausentes como "Atualizar tabela foo set bar = 4, onde bar = 4".
Thelem
10

Pessoalmente, configurei uma "regra" anexada à instrução insert. Digamos que você tenha uma tabela "dns" que registre hits de DNS por cliente em uma base de tempo:

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

Você queria poder reinserir linhas com valores atualizados ou criá-las se elas já não existissem. Introduziu o customer_id e a hora. Algo assim:

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

Atualização: isso pode falhar se inserções simultâneas estiverem acontecendo, pois gerará exceções de violação única. No entanto, a transação não finalizada continuará e terá êxito, e você só precisará repetir a transação finalizada.

No entanto, se houver muitas inserções acontecendo o tempo todo, você desejará colocar um bloqueio de tabela em torno das instruções de inserção: O bloqueio SHARE ROW EXCLUSIVE impedirá qualquer operação que possa inserir, excluir ou atualizar linhas na tabela de destino. No entanto, as atualizações que não atualizam a chave exclusiva são seguras; portanto, se nenhuma operação fizer isso, use bloqueios de aviso.

Além disso, o comando COPY não usa REGRAS, portanto, se você estiver inserindo com COPY, precisará usar gatilhos.

Ch'marr
fonte
9

Eu uso essa função mesclar

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
fonte
1
É mais eficiente simplesmente fazer o updateprimeiro e depois verificar o número de linhas atualizadas. (Veja a resposta de Ahmad)
a_horse_with_no_name
8

Customizei a função "upsert" acima, se você deseja INSERIR E SUBSTITUIR:

`

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

E depois de executar, faça algo como isto:

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

É importante colocar uma vírgula dupla para evitar erros do compilador

  • verifique a velocidade ...
Felipe FMMobile
fonte
7

Semelhante à resposta mais curtida, mas funciona um pouco mais rápido:

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)

(fonte: http://www.the-art-of-web.com/sql/upsert/ )

alexkovelsky
fonte
3
Isso falhará se for executado simultaneamente em duas sessões, porque nenhuma atualização verá uma linha existente, portanto as duas atualizações atingirão zero linhas; portanto, ambas as consultas emitirão uma inserção.
Craig Ringer
6

Tenho o mesmo problema para gerenciar as configurações da conta que os pares de valor e nome. O critério de design é que clientes diferentes possam ter conjuntos de configurações diferentes.

Minha solução, semelhante ao JWP, é apagar e substituir em massa, gerando o registro de mesclagem no seu aplicativo.

Isso é bastante à prova de balas, independente de plataforma e, como nunca há mais de 20 configurações por cliente, são apenas três chamadas db de carga bastante baixa - provavelmente o método mais rápido.

A alternativa de atualizar linhas individuais - verificar se há exceções e depois inserir - ou alguma combinação de código hediondo, é lento e frequentemente quebra porque (como mencionado acima) o tratamento de exceção SQL fora do padrão, alterando de db para db - ou mesmo de liberação para liberação.

 #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
benno
fonte
Bem-vindo ao SO. Boa introdução! :-)
Don question
1
É mais do REPLACE INTOque isso INSERT INTO ... ON DUPLICATE KEY UPDATE, o que pode causar um problema se você usar gatilhos. Você acabará executando excluir e inserir gatilhos / regras, em vez de atualizar.
cHao 15/05
5

De acordo com a documentação da INSERTdeclaração do PostgreSQL , o tratamento do ON DUPLICATE KEYcaso não é suportado. Essa parte da sintaxe é uma extensão proprietária do MySQL.

Christian Hang-Hicks
fonte
@Lucian MERGEtambém é realmente mais uma operação OLAP; consulte stackoverflow.com/q/17267417/398670 para obter explicação. Ele não define a semântica de simultaneidade e a maioria das pessoas que a usa para upsert está apenas criando bugs.
Craig Ringer
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
fonte
5

Para mesclar conjuntos pequenos, usar a função acima é adequado. No entanto, se você estiver mesclando grandes quantidades de dados, sugiro consultar http://mbk.projects.postgresql.org

A melhor prática atual que eu conheço é:

  1. COPIE os dados novos / atualizados na tabela temporária (com certeza, ou você pode fazer INSERT se o custo estiver bom)
  2. Adquirir bloqueio [opcional] (o aconselhamento é preferível aos bloqueios de tabela, IMO)
  3. Mesclar. (a parte divertida)
jwp
fonte
5

UPDATE retornará o número de linhas modificadas. Se você usar JDBC (Java), poderá verificar esse valor em relação a 0 e, se nenhuma linha tiver sido afetada, acionar INSERT. Se você usar alguma outra linguagem de programação, talvez o número de linhas modificadas ainda possa ser obtido, consulte a documentação.

Isso pode não ser tão elegante, mas você tem um SQL muito mais simples que é mais trivial para usar no código de chamada. Diferentemente, se você escrever o script de dez linhas no PL / PSQL, provavelmente deverá ter um teste de unidade de um ou outro tipo apenas para ele.

Audrius Meskauskas
fonte
4

Editar: Isso não funciona conforme o esperado. Diferentemente da resposta aceita, isso gera violações de chave exclusivas quando dois processos chamam repetidamenteupsert_foo simultaneamente.

Eureka! Eu descobri uma maneira de fazer isso em uma consulta: use UPDATE ... RETURNINGpara testar se alguma linha foi afetada:

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;

O UPDATEque deve ser feito em um procedimento separado, porque, infelizmente, este é um erro de sintaxe:

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

Agora funciona como desejado:

SELECT upsert_foo(1, 'hi');
SELECT upsert_foo(1, 'bye');
SELECT upsert_foo(3, 'hi');
SELECT upsert_foo(3, 'bye');
Joey Adams
fonte
1
Você pode combiná-los em uma instrução se você usar um CTE gravável. Mas, como a maioria das soluções postadas aqui, esta está errada e falhará na presença de atualizações simultâneas.
Craig Ringer