Domanda

Diversi mesi fa ho imparato da una risposta su Stack Overflow come eseguire più aggiornamenti contemporaneamente in MySQL utilizzando la seguente sintassi:

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

Ora sono passato a PostgreSQL e apparentemente questo non è corretto.Si riferisce a tutte le tabelle corrette, quindi presumo che sia una questione di parole chiave diverse utilizzate, ma non sono sicuro di dove sia trattato questo argomento nella documentazione di PostgreSQL.

Per fare chiarezza voglio inserire diverse cose e se già esistono aggiornarle.

È stato utile?

Soluzione

PostgreSQL a partire dalla versione 9.5 ha UPSERT sintassi, con < strong> clausola ON CONFLICT . con la seguente sintassi (simile 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;

Ricerca negli archivi del gruppo e-mail di PostgreSQL per "upsert" porta a trovare un esempio di fare quello che forse vuole fare, nel manuale :

  

Esempio 38-2. Eccezioni con UPDATE / INSERT

     

Questo esempio utilizza la gestione delle eccezioni per eseguire un'UPDATE o INSERT, a seconda dei casi:

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

C'è forse un esempio di come fare questo in massa, utilizzando CTE in 9.1 e sopra, nel noreferrer hacker mailing list :

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;

di a_horse_with_no_name risposta per un esempio più chiaro.

Altri suggerimenti

Attenzione:. Questo non è sicuro se eseguito da più sessioni allo stesso tempo (vedi avvertimenti sotto)


Un altro modo intelligente per fare un "UPSERT" in PostgreSQL è di fare due dichiarazioni UPDATE / INSERT sequenziali che sono ciascuno progettato per avere successo o non avere effetto.

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

L'UPDATE avrà successo se una riga con "id = 3" esiste già, altrimenti non ha alcun effetto.

L'INSERT avrà successo solo se riga con "id = 3" non esiste già.

È possibile combinare questi due in un'unica stringa e sia eseguito con una singola istruzione SQL eseguire dalla vostra applicazione. eseguendoli insieme in una singola transazione è altamente raccomandato.

Questo funziona molto bene quando eseguito isolatamente o su un tavolo bloccato, ma è soggetto a condizioni che significa che potrebbe ancora con errore di chiave duplicata se una riga viene inserita contemporaneamente, o potrebbe essere terminato con nessuna riga inserita quando una riga correre viene eliminato contemporaneamente. Una transazione SERIALIZABLE su PostgreSQL 9.1 o superiore in grado di gestire in modo affidabile a costo di un alto tasso di fallimento serializzazione, il che significa che dovrete riprovare un sacco. Vedere perché è così complicato upsert , che discute questo caso in modo più dettagliato.

soggetta ad aggiornamenti persi in isolamento read committed meno che l'applicazione controlla i conteggi delle righe colpite e verifica che sia il insert o update colpito una fila .

Con PostgreSQL 9.1 questo può essere realizzato utilizzando un CTE scrivibile ( espressione di tabella comune ) :

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)

Vedere queste voci di blog:


Si noti che questa soluzione fa non evitare che una violazione di chiave unica, ma non è vulnerabile agli aggiornamenti persi.
Vedere la follow-up da Craig Ringer su dba.stackexchange.com

In PostgreSQL 9.5 e successivi è possibile utilizzare INSERT ... ON CONFLICT UPDATE.

la documentazione .

Un INSERT ... ON DUPLICATE KEY UPDATE MySQL può essere direttamente riformulato per un ON CONFLICT UPDATE. Né è la sintassi SQL standard, sono entrambe le estensioni specifiche del database. Ci sono buone ragioni MERGE non è stato utilizzato per questo , una nuova sintassi non è stato creato solo per divertimento. (Sintassi di MySQL ha anche problemi che significa che non è stata adottata direttamente).

es. data di installazione:

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

la query MySQL:

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

diventa:

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

Differenze:

  • deve specificare il nome della colonna (o il nome vincolo unico) da utilizzare per il controllo di unicità. Questo è il ON CONFLICT (columnname) DO

  • Il SET parola chiave deve essere utilizzato, come se questa fosse una dichiarazione UPDATE normale

Ha alcune caratteristiche troppo:

  • Si può avere una clausola WHERE sul tuo UPDATE (che ti permette di trasformare in modo efficace ON CONFLICT UPDATE in ON CONFLICT IGNORE per certi valori)

  • Il proposto-per-inserimento valori sono disponibili come EXCLUDED righe variabile, che ha la stessa struttura della tabella di destinazione. È possibile ottenere i valori originali nella tabella utilizzando il nome della tabella. Quindi, in questo caso EXCLUDED.c sarà 10 (perché questo è quello che abbiamo cercato di inserire) e "table".c sarà 3 perché questo è il valore corrente nella tabella. È possibile utilizzare uno o entrambi nella clausola espressioni SET e WHERE.

Per fondo su upsert vedi Come upsert (Merge, INSERT ... ON DUPLICATE UPDATE) in PostgreSQL?

Stavo cercando la stessa cosa quando sono arrivato qui, ma la mancanza di una funzione generica "upsert" mi ha infastidito un po' quindi ho pensato che potevi semplicemente passare l'aggiornamento e inserire sql come argomenti su quella funzione dal manuale

sarebbe simile a questo:

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 forse per fare quello che volevi fare inizialmente, batch "upsert", potresti usare Tcl per dividere sql_update e ripetere in loop i singoli aggiornamenti, l'impatto sulle prestazioni sarà molto piccolo vedi http://archives.postgresql.org/pgsql-performance/2006-04/msg00557.php

il costo più elevato è l'esecuzione della query dal codice, dal lato del database il costo di esecuzione è molto inferiore

Non esiste un comando semplice per farlo.

L'approccio più corretto è quello di utilizzare la funzione, come quella da docs .

Un'altra soluzione (anche se non così sicuro) è di fare l'aggiornamento con rinvio, verificare quali righe erano aggiornamenti, e inserire il resto di loro

Qualcosa sulla falsariga di:

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 assumendo: 2 è stato restituito:

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

Naturalmente sarà bail out prima o poi (in ambiente concorrente), in quanto non v'è chiaro race condition nel qui, ma di solito funziona.

Ecco un più a lungo e articolo più completo sul argomento .

Personalmente, ho creato una "regola" allegata alla dichiarazione dell'inserto. Diciamo che ha avuto un tavolo "DNS" che ha registrato DNS colpi al cliente su una base per-tempo:

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

Si voleva essere in grado di reinserire le righe con i valori aggiornati, o crearli se non esistono già. Calettato sulla customer_id e il tempo. Qualcosa di simile a questo:

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

Aggiornamento: Questo ha il potenziale di fallire se inserti simultanei stanno accadendo, in quanto genererà eccezioni unique_violation. Tuttavia, l'operazione non terminato continuerà e avere successo, e basta essere necessario ripetere l'operazione terminata.

Tuttavia, se ci sono tonnellate di inserti che accadono tutto il tempo, si vuole mettere un blocco di tabella intorno alle istruzioni INSERT: SHARE ROW bloccaggio ESCLUSIVO impedirà qualsiasi operazione che potrebbe inserire, eliminare o aggiornare righe nella tabella di destinazione. Tuttavia, gli aggiornamenti che non aggiornano la chiave univoca sono sicuri, quindi se nessuna operazione farà questo, utilizzare i blocchi di consulenza, invece.

Inoltre, il comando COPY non utilizzare le regole, quindi se si sta inserendo con la copia, è necessario utilizzare i trigger, invece.

I personalizzato "upsert" funzione di cui sopra, se si desidera inserire E SOSTITUIRE:

`

 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 dopo l'esecuzione, fare qualcosa di simile:

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

E 'importante mettere a doppio dollaro-virgola per evitare gli errori del compilatore

  • verificare la velocità ...

Simile a risposta più voluto, ma funziona leggermente più veloce:

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

Ho lo stesso problema per la gestione delle impostazioni dell'account come coppie nome-valore. I criteri di progettazione è che diversi clienti potrebbero avere diverse impostazioni set.

La mia soluzione, simile a JWP è quello di cancellare massa e sostituire, generando il record di unione all'interno dell'applicazione.

Questo è abbastanza a prova di proiettile, indipendente dalla piattaforma e dal momento che non ci sono mai più di circa 20 impostazioni per cliente, questo è solo 3 chiamate carico db piuttosto bassi -. Probabilmente il metodo più veloce

L'alternativa di aggiornamento singole righe - controllo eccezioni quindi inserendo - o una combinazione di è codice orribile, lento e spesso si rompe a causa (come detto sopra) trattamento non standard eccezioni SQL cambiando da db a db - o anche rilasciare per rilasciare .

 #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

Secondo la PostgreSQL documentazione della dichiarazione INSERT , la manipolazione il caso ON DUPLICATE KEY non è supportato. Quella parte della sintassi è un'estensione proprietaria di MySQL.

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

Io uso questa funzione di unione

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

Per fondere piccoli insiemi, utilizzando la funzione sopra è soddisfacente. Tuttavia, se si uniscono grandi quantità di dati, suggerirei di guardare in http: //mbk.projects.postgresql .org

La migliore prassi corrente che io sappia è:

  1. Copia dati nuovi / aggiornati in tabella temporanea (sicuro, o si può fare INSERT se il costo è ok)
  2. Acquisire Blocco [opzionale] (advisory è preferibile blocchi di tabella, IMO)
  3. Unisci. (La parte più divertente)

UPDATE restituirà il numero di righe modificate. Se si utilizza JDBC (Java), è possibile quindi controllare questo valore contro 0 e, se non le righe sono state colpite, il fuoco INSERT invece. Se si utilizza un altro linguaggio di programmazione, forse il numero delle righe modificate ancora può essere ottenuto, la documentazione di controllo.

Questo potrebbe non essere così elegante, ma si dispone di SQL molto più semplice che è più banale per utilizzare dal codice chiamante. Diversamente, se si scrive la sceneggiatura di dieci linea in PL / PSQL, probabilmente dovrebbe avere una prova di unità di uno o un altro tipo solo per da solo.

Modifica Questo non funziona come previsto. A differenza della risposta accettata, questo produce violazioni delle chiavi uniche quando due processi più volte chiamano upsert_foo contemporaneamente.

Eureka! Ho trovato un modo per farlo in una query: utilizzare UPDATE ... RETURNING per verificare se sono state colpite tutte le righe:

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;

Il UPDATE deve essere fatto in una procedura a parte perché, purtroppo, questo è un errore di sintassi:

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

Ora funziona, se lo desideri:

SELECT upsert_foo(1, 'hi');
SELECT upsert_foo(1, 'bye');
SELECT upsert_foo(3, 'hi');
SELECT upsert_foo(3, 'bye');
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top