Maneira linguística de implementar o UPSERT no PostgreSQL

40

Eu li sobre diferentes UPSERTimplementações no PostgreSQL, mas todas essas soluções são relativamente antigas ou relativamente exóticas (usando CTE gravável , por exemplo).

E eu não sou um especialista em psql para descobrir imediatamente, se essas soluções são antigas porque são bem recomendadas ou são (bom, quase todas são) apenas exemplos de brinquedos que não são apropriados ao uso da produção.

Qual é a maneira mais segura de encadear a implementação do UPSERT no PostgreSQL?

shabunc
fonte

Respostas:

23

O PostgreSQL agora possui o UPSERT .


O método preferido de acordo com uma pergunta StackOverflow semelhante é atualmente o seguinte:

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
        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');
Leigh Riffel
fonte
7
Eu prefiro usar uma CTE gravável: stackoverflow.com/a/8702291/330315
a_horse_with_no_name
Qual é a vantagem de um CTE gravável versus uma função?
François Beausoleil
11
@ François, por um lado, velocidade. Usando um CTE, você acessa o banco de dados uma vez. Fazendo dessa maneira, você pode bater duas ou mais vezes. Além disso, o otimizador não pode otimizar os procedimentos pl / pgsql com a mesma eficiência que o código SQL puro.
precisa
11
@ François Por outro lado, simultaneidade. Como o exemplo acima possui várias instruções SQL, você deve se preocupar com as condições de corrida (a razão do loop klugey). Uma única instrução SQL será atômica. Veja este link
Adam Mackler 23/11
11
@ FrançoisBeausoleil veja aqui e aqui o porquê. Basicamente, sem um loop de repetição, você precisa serializar ou tem a possibilidade de falhas devido à condição de corrida inerente.
7114 Jack Douglas
27

ATUALIZAÇÃO (20-08-2015):

Agora existe uma implementação oficial para lidar com upserts através do uso de ON CONFLICT DO UPDATE(documentação oficial). No momento da redação deste artigo, esse recurso atualmente reside no PostgreSQL 9.5 Alpha 2, que está disponível para download aqui: Diretórios de origem do Postgres .

Aqui está um exemplo, assumindo que item_idé sua Chave Primária:

INSERT INTO my_table
    (item_id, price)
VALUES
    (123456, 10.99)
ON
    CONFLICT (item_id)
DO UPDATE SET
    price = EXCLUDED.price

Mensagem original ...

Aqui está uma implementação que cheguei ao desejar obter visibilidade sobre a ocorrência de uma inserção ou atualização.

A definição de upsert_dataé consolidar os valores em um único recurso, em vez de precisar especificar o preço e o item_id duas vezes: uma vez para a atualização e outra para a inserção.

WITH upsert_data AS (
    SELECT
    '19.99'::numeric(10,2) AS price,
    'abcdefg'::character varying AS item_id
),
update_outcome AS (
    UPDATE pricing_tbl
    SET price = upsert_data.price
    FROM upsert_data
    WHERE pricing_tbl.item_id = upsert_data.item_id
    RETURNING 'update'::text AS action, item_id
),
insert_outcome AS (
    INSERT INTO
        pricing_tbl
    (price, item_id)
    SELECT
        upsert_data.price AS price,
        upsert_data.item_id AS item_id
    FROM upsert_data
    WHERE NOT EXISTS (SELECT item_id FROM update_outcome LIMIT 1)
    RETURNING 'insert'::text AS action, item_id
)
SELECT * FROM update_outcome UNION ALL SELECT * FROM insert_outcome

Se você não gosta do uso de upsert_data, aqui está uma implementação alternativa:

WITH update_outcome AS (
    UPDATE pricing_tbl
    SET price = '19.99'
    WHERE pricing_tbl.item_id = 'abcdefg'
    RETURNING 'update'::text AS action, item_id
),
insert_outcome AS (
    INSERT INTO
        pricing_tbl
    (price, item_id)
    SELECT
        '19.99' AS price,
        'abcdefg' AS item_id
    WHERE NOT EXISTS (SELECT item_id FROM update_outcome LIMIT 1)
    RETURNING 'insert'::text AS action, item_id
)
SELECT * FROM update_outcome UNION ALL SELECT * FROM insert_outcome
Joshua Burns
fonte
Como funciona?
jb.
11
@jb. não tão bem quanto eu gostaria. Você verá penalidades significativas de desempenho em comparação à execução de inserções retas. No entanto, para lotes menores (digamos 1000 ou menos), este exemplo deve ter um bom desempenho.
Joshua Burns,
0

Isso informará se a inserção ou atualização ocorreu:

with "update_items" as (
  -- Update statement here
  update items set price = 3499, name = 'Uncle Bob'
  where id = 1 returning *
)
-- Insert statement here
insert into items (price, name)
-- But make sure you put your values like so
select 3499, 'Uncle Bob'
where not exists ( select * from "update_items" );

Se a atualização ocorrer, você receberá uma inserção 0, caso contrário, insira 1 ou um erro.

John Fawcett
fonte