Insertar, en actualización duplicada en PostgreSQL?

645

Hace varios meses aprendí de una respuesta en Stack Overflow cómo realizar múltiples actualizaciones a la vez en MySQL usando la siguiente sintaxis:

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

Ahora he cambiado a PostgreSQL y aparentemente esto no es correcto. Se refiere a todas las tablas correctas, así que supongo que se trata de diferentes palabras clave que se utilizan, pero no estoy seguro de en qué parte de la documentación de PostgreSQL se trata.

Para aclarar, quiero insertar varias cosas y, si ya existen, actualizarlas.

Teifion
fuente
38
Cualquiera que encuentre esta pregunta debería leer el artículo de Depesz "¿Por qué es tan complicado upsert?" . Explica el problema y las posibles soluciones extremadamente bien.
Craig Ringer
8
UPSERT se agregará en Postgres 9.5: wiki.postgresql.org/wiki/…
tommed
44
@tommed - se ha hecho: stackoverflow.com/a/34639631/4418
warren

Respuestas:

515

PostgreSQL desde la versión 9.5 tiene sintaxis UPSERT , con cláusula ON CONFLICT . con la siguiente sintaxis (similar a 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;

Buscar en los archivos del grupo de correo electrónico de postgresql "upsert" lleva a encontrar un ejemplo de hacer lo que posiblemente quiera hacer, en el manual :

Ejemplo 38-2. Excepciones con ACTUALIZAR / INSERTAR

Este ejemplo utiliza el manejo de excepciones para realizar ACTUALIZACIÓN o INSERTAR, según corresponda:

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

Posiblemente hay un ejemplo de cómo hacer esto de forma masiva, utilizando CTE en 9.1 y superior, en la lista de correo de los 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;

Vea la respuesta de a_horse_with_no_name para un ejemplo más claro.

Stephen Denne
fuente
77
Lo único que no me gusta de esto es que sería mucho más lento, porque cada upsert sería su propia llamada individual a la base de datos.
baash05
@ baash05 puede haber una manera de hacerlo en masa, vea mi respuesta actualizada.
Stephen Denne
2
Lo único que haría de manera diferente es usar FOR 1..2 LOOP en lugar de solo LOOP para que si se viola alguna otra restricción única, no gire indefinidamente.
Olamork
2
¿A qué se excludedrefiere aquí en la primera solución?
ichbinallen
2
@ichbinallen en los documentos Las cláusulas SET y WHERE en ON CONFLICT DO UPDATE tienen acceso a la fila existente utilizando el nombre de la tabla (o un alias), y a las filas propuestas para la inserción utilizando la tabla especial excluida . En este caso, la excludedtabla especial le da acceso a los valores que intentaba INSERTAR en primer lugar.
TMichel
429

Advertencia: esto no es seguro si se ejecuta desde varias sesiones al mismo tiempo (ver las advertencias a continuación).


Otra forma inteligente de hacer un "UPSERT" en postgresql es hacer dos declaraciones secuenciales de ACTUALIZACIÓN / INSERCIÓN que están diseñadas para tener éxito o no tienen efecto.

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

La ACTUALIZACIÓN tendrá éxito si ya existe una fila con "id = 3"; de lo contrario, no tiene ningún efecto.

INSERT solo tendrá éxito si la fila con "id = 3" aún no existe.

Puede combinar estos dos en una sola cadena y ejecutarlos con una sola instrucción SQL ejecutada desde su aplicación. Ejecutarlos juntos en una sola transacción es muy recomendable.

Esto funciona muy bien cuando se ejecuta de forma aislada o en una tabla bloqueada, pero está sujeto a condiciones de carrera que significan que aún podría fallar con un error de clave duplicada si se inserta una fila al mismo tiempo, o podría terminar sin una fila insertada cuando una fila se elimina al mismo tiempo . Una SERIALIZABLEtransacción en PostgreSQL 9.1 o superior lo manejará de manera confiable a costa de una tasa de falla de serialización muy alta, lo que significa que tendrá que volver a intentarlo mucho. Vea por qué es tan complicado upsert , que analiza este caso con más detalle.

Este enfoque también está sujeto a actualizaciones perdidas de forma read committedaislada, a menos que la aplicación verifique los recuentos de filas afectadas y verifique que ya sea una fila inserto la updateafectada .

bovino
fuente
66
Respuesta corta: si el registro existe, INSERT no hace nada. Respuesta larga: SELECT en INSERT devolverá tantos resultados como coincidencias con la cláusula where. Eso es a lo sumo uno (si el número uno no está en el resultado de la sub-selección), de lo contrario cero. El INSERTAR agregará así una o cero filas.
Peter Becker
3
la parte 'donde' se puede simplificar usando existe:... where not exists (select 1 from table where id = 3);
Endy Tjahjono
1
esta debería ser la respuesta correcta ... con algunos ajustes menores, podría usarse para hacer una actualización masiva ... Humm ... Me pregunto si podría usarse una tabla temporal ...
baash05
1
@keaplogik, esa limitación 9.1 es con CTE grabable (expresiones de tabla comunes) que se describe en otra de las respuestas. La sintaxis utilizada en esta respuesta es muy básica y ha sido admitida durante mucho tiempo.
bovino
8
Advertencia, esto está sujeto a la pérdida de actualizaciones en read committedel aislamiento a menos que su aplicación comprueba para asegurarse de que el inserto la updatetienen un recuento de filas que no sea cero. Ver dba.stackexchange.com/q/78510/7788
Craig Ringer el
227

Con PostgreSQL 9.1 esto se puede lograr utilizando un CTE grabable ( expresión de tabla común ):

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)

Ver estas entradas de blog:


Tenga en cuenta que esta solución no evita una violación de clave única, pero no es vulnerable a las actualizaciones perdidas.
Vea el seguimiento de Craig Ringer en dba.stackexchange.com

un caballo sin nombre
fuente
1
@ FrançoisBeausoleil: la posibilidad de una condición de carrera es mucho menor que con el enfoque "tratar / manejar excepción"
a_horse_with_no_name
2
@a_horse_with_no_name ¿Cómo quiere decir exactamente que la posibilidad en las condiciones de carrera es mucho menor? Cuando ejecuto esta consulta simultáneamente con los mismos registros, aparece el error "el valor de clave duplicada viola la restricción única" el 100% de las veces hasta que la consulta detecta que el registro se ha insertado. ¿Es este un ejemplo completo?
Jeroen van Dijk
44
@a_horse_with_no_name Su solución parece funcionar en situaciones concurrentes cuando ajusta la declaración upsert con el siguiente bloqueo: BEGIN WORK; MESA DE BLOQUEO mytable EN MODO EXCLUSIVO DE COMPARTIR FILA; <UPSERT AQUÍ>; COMPROMISO DE TRABAJO;
Jeroen van Dijk
2
@JeroenvanDijk: gracias. Lo que quise decir con "mucho más pequeño" es que si se realizan varias transacciones (¡y se compromete el cambio!), El intervalo de tiempo entre la actualización y la inserción es menor, ya que todo es solo una declaración. Siempre puede generar una violación de pk por dos declaraciones INSERT independientes. Si bloquea toda la tabla, serializa efectivamente todo el acceso a ella (algo que también podría lograr con el nivel de aislamiento serializable).
a_horse_with_no_name
12
Esta solución está sujeta a actualizaciones perdidas si la transacción de inserción retrocede; No hay verificación para hacer cumplir que las UPDATEfilas afectadas.
Craig Ringer
132

En PostgreSQL 9.5 y posteriores puedes usar INSERT ... ON CONFLICT UPDATE.

Ver la documentación .

Un MySQL INSERT ... ON DUPLICATE KEY UPDATEse puede reformular directamente a a ON CONFLICT UPDATE. Tampoco es la sintaxis estándar de SQL, ambas son extensiones específicas de la base de datos. Hay buenas razones MERGEpor las que no se usó para esto , no se creó una nueva sintaxis solo por diversión. (La sintaxis de MySQL también tiene problemas que significan que no se adoptó directamente).

por ejemplo, configuración dada:

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

la consulta MySQL:

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

se convierte en:

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

Diferencias:

  • Usted debe especificar el nombre de la columna (o el nombre de restricción única) a utilizar para el control de la unicidad. Eso esON CONFLICT (columnname) DO

  • Se SETdebe usar la palabra clave , como si fuera una UPDATEdeclaración normal

También tiene algunas características agradables:

  • Usted puede tener una WHEREcláusula en su UPDATE(lo que le permite girar con eficacia ON CONFLICT UPDATEen ON CONFLICT IGNOREciertos valores)

  • Los valores propuestos para la inserción están disponibles como la variable de fila EXCLUDED, que tiene la misma estructura que la tabla de destino. Puede obtener los valores originales en la tabla utilizando el nombre de la tabla. Entonces, en este caso EXCLUDED.cserá 10(porque eso es lo que intentamos insertar) y "table".cserá 3porque ese es el valor actual en la tabla. Puede usar uno o ambos en las SETexpresiones y la WHEREcláusula.

Para obtener más información sobre upsert, consulte ¿Cómo UPSERT (FUSIÓN, INSERCIÓN ... EN LA ACTUALIZACIÓN DUPLICADA) en PostgreSQL?

Craig Ringer
fuente
He examinado la solución 9.5 de PostgreSQL como describió anteriormente porque estaba experimentando lagunas en el campo de incremento automático mientras estaba bajo MySQL ON DUPLICATE KEY UPDATE. Descargué Postgres 9.5 e implementé su código, pero extrañamente ocurre el mismo problema en Postgres: el campo en serie de la clave primaria no es consecutivo (hay espacios entre las inserciones y las actualizaciones). ¿Alguna idea de lo que está pasando aquí? ¿Esto es normal? ¿Alguna idea de cómo evitar este comportamiento? Gracias.
WM
@WM Eso es bastante inherente a una operación upsert. Debe evaluar la función que genera la secuencia antes de intentar la inserción. Dado que tales secuencias están diseñadas para operar simultáneamente, están exentas de la semántica de las transacciones normales, pero incluso si no fueran así, la generación no se llama en una subtransacción y se revierte, se completa normalmente y se compromete con el resto de la operación. Entonces esto sucedería incluso con implementaciones de secuencia "sin espacios". La única forma en que el DB podría evitar esto sería retrasar la evaluación de la generación de la secuencia hasta después de la verificación de la clave.
Craig Ringer
1
@WM que crearía sus propios problemas. Básicamente, estás atrapado. Pero si confía en que serial / auto_increment no tenga espacios, ya tiene errores. Puede tener huecos de secuencia debido a reversiones que incluyen errores transitorios: reinicios bajo carga, errores del cliente a mitad de la transacción, bloqueos, etc. Nunca, nunca debe confiar en SERIAL/ SEQUENCEo AUTO_INCREMENTno tener huecos. Si necesita secuencias sin espacios, son más complejas; necesita usar una mesa de mostrador por lo general. Google te dirá más. Pero tenga en cuenta que las secuencias sin espacios evitan toda concurrencia de inserción.
Craig Ringer
@WM Si realmente requiere secuencias sin espacios y upsert, puede utilizar el enfoque de upsert basado en funciones que se describe en el manual junto con una implementación de secuencia sin espacios que utiliza una tabla de contador. Debido a que se BEGIN ... EXCEPTION ...ejecuta en una subtransacción que se revierte en caso de error, su incremento de secuencia se revierte si INSERTfalla.
Craig Ringer
Muchas gracias @Craig Ringer, eso fue bastante informativo. Me di cuenta de que simplemente puedo renunciar a tener esa clave primaria de incremento automático. Hice un primario compuesto de 3 campos y para mi necesidad actual particular, realmente no hay necesidad de un campo de incremento automático sin espacios. Gracias de nuevo, la información que proporcionó me ahorraría tiempo en el futuro tratando de prevenir un comportamiento de DB natural y saludable. Lo entiendo mejor ahora.
WM
17

Estaba buscando lo mismo cuando vine aquí, pero la falta de una función genérica "upsert" me molestó un poco, así que pensé que podría pasar la actualización e insertar sql como argumentos en esa función del manual.

eso se vería así:

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

y tal vez para hacer lo que inicialmente quería hacer, lote "upsert", podría usar Tcl para dividir sql_update y hacer un bucle de las actualizaciones individuales, el impacto de rendimiento será muy pequeño, consulte http://archives.postgresql.org/pgsql- rendimiento / 2006-04 / msg00557.php

el costo más alto es ejecutar la consulta desde su código, en el lado de la base de datos el costo de ejecución es mucho menor

Paul Scheltema
fuente
3
Todavía tiene que ejecutar esto en un bucle de reintento y es propenso a carreras concurrentes a DELETEmenos que bloquee la tabla o esté en SERIALIZABLEaislamiento de transacción en PostgreSQL 9.1 o superior.
Craig Ringer
13

No hay un comando simple para hacerlo.

El enfoque más correcto es usar la función, como la de los documentos .

Otra solución (aunque no es tan segura) es hacer una actualización con el retorno, verificar qué filas fueron actualizaciones e insertar el resto de ellas.

Algo en la línea 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;

suponiendo que se devolvió id: 2:

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

Por supuesto, se rescatará tarde o temprano (en un entorno concurrente), ya que aquí hay una clara condición de carrera, pero por lo general funcionará.

Aquí hay un artículo más extenso y completo sobre el tema .

Craig Ringer
fuente
1
Si usa esta opción, asegúrese de verificar que se devuelva la identificación incluso si la actualización no hace nada. He visto bases de datos que optimizan consultas como "Actualizar tabla foo set bar = 4 donde bar = 4".
thelem
10

Personalmente, he configurado una "regla" adjunta a la declaración de inserción. Supongamos que tenía una tabla de "dns" que registraba los hits de dns por cliente por tiempo:

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

Quería poder volver a insertar filas con valores actualizados o crearlas si aún no existían. Tecleado el customer_id y la hora. Algo como esto:

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

Actualización: Esto tiene el potencial de fallar si están ocurriendo inserciones simultáneas, ya que generará excepciones de violencia únicas. Sin embargo, la transacción no terminada continuará y tendrá éxito, y solo necesita repetir la transacción terminada.

Sin embargo, si hay toneladas de inserciones sucediendo todo el tiempo, querrá poner un bloqueo de tabla alrededor de las instrucciones de inserción: el bloqueo SHARE ROW EXCLUSIVE evitará cualquier operación que pueda insertar, eliminar o actualizar filas en su tabla de destino. Sin embargo, las actualizaciones que no actualizan la clave única son seguras, por lo que si no realiza ninguna operación, utilice bloqueos de aviso.

Además, el comando COPIA no usa REGLAS, por lo que si está insertando con COPIA, deberá usar desencadenantes.

Ch'marr
fuente
9

Yo uso esta función fusionar

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
fuente
1
Es más eficiente simplemente hacer lo updateprimero y luego verificar el número de filas actualizadas. (Ver la respuesta de Ahmad)
a_horse_with_no_name
8

Personalizo la función "upsert" anterior, si desea INSERTAR Y REEMPLAZAR:

``

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

Y después de ejecutar, haga algo como esto:

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

Es importante poner una coma doble en dólares para evitar errores de compilación

  • comprobar la velocidad ...
Felipe FMMobile
fuente
7

Similar a la respuesta que más me gustó, pero funciona un poco más 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)

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

alexkovelsky
fuente
3
Esto fallará si se ejecuta simultáneamente en dos sesiones, porque ninguna actualización verá una fila existente, por lo que ambas actualizaciones llegarán a cero filas, por lo que ambas consultas emitirán una inserción.
Craig Ringer
6

Tengo el mismo problema para administrar la configuración de la cuenta que los pares de nombre y valor. El criterio de diseño es que diferentes clientes podrían tener diferentes conjuntos de configuraciones.

Mi solución, similar a JWP es borrar y reemplazar en masa, generando el registro de fusión dentro de su aplicación.

Esto es bastante a prueba de balas, independiente de la plataforma y dado que nunca hay más de aproximadamente 20 configuraciones por cliente, esto es solo 3 llamadas db de carga bastante baja, probablemente el método más rápido.

La alternativa de actualizar filas individuales (buscar excepciones y luego insertarlas) o alguna combinación de ellas es un código horrible, lento y a menudo se rompe porque (como se mencionó anteriormente) el manejo de excepciones SQL no estándar cambia de db a db, o incluso de lanzamiento a lanzamiento.

 #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
fuente
Bienvenido a SO. Buena introducción! :-)
Don Question
1
Esto es más parecido REPLACE INTOa INSERT INTO ... ON DUPLICATE KEY UPDATE, lo que puede causar un problema si usa disparadores. Terminará ejecutando eliminar e insertar disparadores / reglas, en lugar de actualizar los.
cHao
5

De acuerdo con la documentación de PostgreSQL de la INSERTdeclaración , ON DUPLICATE KEYno se admite el manejo del caso. Esa parte de la sintaxis es una extensión propietaria de MySQL.

Christian Hang-Hicks
fuente
@Lucian MERGEtambién es realmente más una operación OLAP; consulte stackoverflow.com/q/17267417/398670 para obtener una explicación. No define la semántica de concurrencia y la mayoría de las personas que lo usan para upsert solo están creando errores.
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
fuente
5

Para fusionar conjuntos pequeños, usar la función anterior está bien. Sin embargo, si está fusionando grandes cantidades de datos, le sugiero que investigue http://mbk.projects.postgresql.org

La mejor práctica actual que conozco es:

  1. COPIE datos nuevos / actualizados en la tabla temporal (seguro, o puede hacer INSERTAR si el costo está bien)
  2. Adquirir bloqueo [opcional] (el aviso es preferible a los bloqueos de tabla, IMO)
  3. Unir. (la parte divertida)
jwp
fuente
5

ACTUALIZAR devolverá el número de filas modificadas. Si usa JDBC (Java), puede verificar este valor contra 0 y, si no se han visto afectadas las filas, active INSERT en su lugar. Si usa algún otro lenguaje de programación, quizás aún pueda obtener el número de filas modificadas, consulte la documentación.

Puede que esto no sea tan elegante, pero tiene un SQL mucho más simple que es más trivial de usar desde el código de llamada. De manera diferente, si escribe la secuencia de comandos de diez líneas en PL / PSQL, probablemente debería tener una prueba unitaria de uno u otro tipo solo por ella.

Audrius Meskauskas
fuente
4

Editar: esto no funciona como se esperaba. A diferencia de la respuesta aceptada, esto produce infracciones de clave únicas cuando dos procesos llaman repetidamenteupsert_foo simultáneamente.

Eureka! Descubrí una forma de hacerlo en una consulta: use UPDATE ... RETURNINGpara probar si alguna fila se vio afectada:

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;

Se UPDATEdebe hacer en un procedimiento separado porque, desafortunadamente, este es un error de sintaxis:

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

Ahora funciona como se desea:

SELECT upsert_foo(1, 'hi');
SELECT upsert_foo(1, 'bye');
SELECT upsert_foo(3, 'hi');
SELECT upsert_foo(3, 'bye');
Joey Adams
fuente
1
Puede combinarlos en una sola declaración si usa un CTE grabable. Pero como la mayoría de las soluciones publicadas aquí, esta es incorrecta y fallará en presencia de actualizaciones concurrentes.
Craig Ringer