Det för närvarande accepterade svaret verkar ok för ett enskilt konfliktmål, få konflikter, små tupler och inga triggers. Det undviker samtidighetsproblem 1 (se nedan) med brute force. Den enkla lösningen har sitt tilltal, biverkningarna kan vara mindre viktiga.
I alla andra fall, dock inte uppdatera identiska rader utan behov. Även om du inte ser någon skillnad på ytan finns det olika biverkningar :
-
Det kan utlösa triggers som inte bör avfyras.
-
Det skrivlåser "oskyldiga" rader, vilket kan medföra kostnader för samtidiga transaktioner.
-
Det kan få raden att verka som ny, fastän den är gammal (transaktionens tidsstämpel).
-
Det viktigaste , med PostgreSQL:s MVCC-modell skrivs en ny radversion för varje
UPDATE
, oavsett om raddata har ändrats. Detta medför en prestationsstraff för själva UPSERT, table bloat, index bloat, prestationsstraff för efterföljande operationer på bordet,VACUUM
kosta. En mindre effekt för få dubbletter, men massiv för mestadels duper.
Plus , ibland är det inte praktiskt eller ens möjligt att använda ON CONFLICT DO UPDATE
. Manualen:
För
ON CONFLICT DO UPDATE
, ettconflict_target
måste tillhandahållas.
En singel "konfliktmål" är inte möjligt om flera index / begränsningar är inblandade. Men här är en relaterad lösning för flera partiella index:
- UPSERT baserad på UNIK begränsning med NULL-värden
Tillbaka till ämnet kan du uppnå (nästan) samma sak utan tomma uppdateringar och biverkningar. Några av följande lösningar fungerar även med ON CONFLICT DO NOTHING
(inget "konfliktmål"), för att fånga alla möjliga konflikter som kan uppstå - som kanske är önskvärda eller inte.
Utan samtidig skrivbelastning
WITH input_rows(usr, contact, name) AS (
VALUES
(text 'foo1', text 'bar1', text 'bob1') -- type casts in first row
, ('foo2', 'bar2', 'bob2')
-- more?
)
, ins AS (
INSERT INTO chats (usr, contact, name)
SELECT * FROM input_rows
ON CONFLICT (usr, contact) DO NOTHING
RETURNING id --, usr, contact -- return more columns?
)
SELECT 'i' AS source -- 'i' for 'inserted'
, id --, usr, contact -- return more columns?
FROM ins
UNION ALL
SELECT 's' AS source -- 's' for 'selected'
, c.id --, usr, contact -- return more columns?
FROM input_rows
JOIN chats c USING (usr, contact); -- columns of unique index
source
kolumnen är ett valfritt tillägg för att visa hur detta fungerar. Du kan faktiskt behöva det för att se skillnaden mellan båda fallen (en annan fördel jämfört med tomma skrivningar).
De sista JOIN chats
fungerar eftersom nyligen infogade rader från en bifogad datamodifierande CTE ännu inte är synliga i den underliggande tabellen. (Alla delar av samma SQL-sats ser samma ögonblicksbilder av underliggande tabeller.)
Sedan VALUES
uttrycket är fristående (inte direkt kopplat till en INSERT
) Postgres kan inte härleda datatyper från målkolumnerna och du kan behöva lägga till explicita typcasts. Manualen:
När
VALUES
används iINSERT
, alla värden tvingas automatiskt till datatypen för motsvarande destinationskolumn. När det används i andra sammanhang kan det vara nödvändigt att ange rätt datatyp. Om alla poster är angivna bokstavliga konstanter, är det tillräckligt att tvinga den första för att bestämma den antagna typen för alla.
Frågan i sig (bortsett från biverkningarna) kan vara lite dyrare för få dupes, på grund av CTE:ns overhead och den extra SELECT
(vilket borde vara billigt eftersom det perfekta indexet finns där per definition - en unik begränsning implementeras med ett index).
Kan vara (mycket) snabbare för många dubbletter. Den effektiva kostnaden för ytterligare skrivningar beror på många faktorer.
Men det finns färre biverkningar och dolda kostnader hur som helst. Det är förmodligen billigare totalt sett.
Bifogade sekvenser är fortfarande avancerade, eftersom standardvärden fylls i före testa för konflikter.
Om CTE:
- Är sökfrågor av typen SELECT den enda typen som kan kapslas?
- Deduplicera SELECT-satser i relationsindelning
Med samtidig skrivbelastning
Förutsatt att standard READ COMMITTED
transaktionsisolering. Relaterat:
- Samtidiga transaktioner resulterar i tävlingstillstånd med unika begränsningar vid insättning
Den bästa strategin för att försvara sig mot rasförhållanden beror på exakta krav, antalet och storleken på rader i tabellen och i UPSERTs, antalet samtidiga transaktioner, sannolikheten för konflikter, tillgängliga resurser och andra faktorer ...
Samtidighetsproblem 1
Om en samtidig transaktion har skrivits till en rad som din transaktion nu försöker UPSERT, måste din transaktion vänta tills den andra slutförs.
Om den andra transaktionen slutar med ROLLBACK
(eller något fel, t.ex. automatisk ROLLBACK
), kan din transaktion fortsätta normalt. Mindre möjlig bieffekt:luckor i sekventiella nummer. Men inga saknade rader.
Om den andra transaktionen slutar normalt (implicit eller explicit COMMIT
), din INSERT
kommer att upptäcka en konflikt (den UNIQUE
index / constraint är absolut) och DO NOTHING
, därför inte heller returnera raden. (Kan inte heller låsa raden som visas i samtidighetsfråga 2 nedan, eftersom det inte är synligt .) SELECT
ser samma ögonblicksbild från början av frågan och kan inte heller returnera den ännu osynliga raden.
Alla sådana rader saknas i resultatuppsättningen (även om de finns i den underliggande tabellen)!
Detta kan vara ok som det är . Speciellt om du inte returnerar rader som i exemplet och är nöjd med att veta att raden finns där. Om det inte är tillräckligt bra finns det olika sätt att komma runt det.
Du kan kontrollera radantalet för utdata och upprepa påståendet om det inte stämmer överens med radantalet för input. Kan vara tillräckligt bra för det sällsynta fallet. Poängen är att starta en ny sökning (kan vara i samma transaktion), som sedan kommer att se de nyligen bekräftade raderna.
Eller leta efter saknade resultatrader inom samma fråga och skriv över de med brute force-tricket som visas i Alextonis svar.
WITH input_rows(usr, contact, name) AS ( ... ) -- see above
, ins AS (
INSERT INTO chats AS c (usr, contact, name)
SELECT * FROM input_rows
ON CONFLICT (usr, contact) DO NOTHING
RETURNING id, usr, contact -- we need unique columns for later join
)
, sel AS (
SELECT 'i'::"char" AS source -- 'i' for 'inserted'
, id, usr, contact
FROM ins
UNION ALL
SELECT 's'::"char" AS source -- 's' for 'selected'
, c.id, usr, contact
FROM input_rows
JOIN chats c USING (usr, contact)
)
, ups AS ( -- RARE corner case
INSERT INTO chats AS c (usr, contact, name) -- another UPSERT, not just UPDATE
SELECT i.*
FROM input_rows i
LEFT JOIN sel s USING (usr, contact) -- columns of unique index
WHERE s.usr IS NULL -- missing!
ON CONFLICT (usr, contact) DO UPDATE -- we've asked nicely the 1st time ...
SET name = c.name -- ... this time we overwrite with old value
-- SET name = EXCLUDED.name -- alternatively overwrite with *new* value
RETURNING 'u'::"char" AS source -- 'u' for updated
, id --, usr, contact -- return more columns?
)
SELECT source, id FROM sel
UNION ALL
TABLE ups;
Det är som frågan ovan, men vi lägger till ytterligare ett steg med CTE ups
, innan vi returnerar komplett resultatet satt. Den sista CTE kommer inte att göra någonting för det mesta. Endast om rader försvinner från det returnerade resultatet använder vi brute force.
Mer overhead, ännu. Ju fler konflikter med redan existerande rader, desto mer sannolikt kommer detta att överträffa det enkla tillvägagångssättet.
En bieffekt:den 2:a UPSERT skriver rader ur funktion, så den återinför möjligheten till dödläge (se nedan) om tre eller fler transaktioner som skrivs till samma rader överlappar varandra. Om det är ett problem behöver du en annan lösning - som att upprepa hela påståendet som nämnts ovan.
Samtidighetsproblem 2
Om samtidiga transaktioner kan skrivas till berörda kolumner med berörda rader, och du måste se till att raderna du hittade fortfarande finns där i ett senare skede i samma transaktion, kan du låsa befintliga rader billigt i CTE ins
(som annars skulle låsas upp) med:
...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE -- never executed, but still locks the row
...
Och lägg till en låssats till SELECT
likaså, som FOR UPDATE
.
Detta gör att konkurrerande skrivoperationer väntar till slutet av transaktionen, när alla lås släpps. Så var kortfattad.
Mer information och förklaring:
- Hur man inkluderar uteslutna rader i RETURNING from INSERT ... ON CONFLICT
- Är SELECT eller INSERT i en funktion utsatt för tävlingsförhållanden?
Dödläge?
Försvara dig mot dödläge genom att infoga rader i konsekvent ordning . Se:
- Deadlock med multi-rad INSERTs trots ON CONFLICT GÖR INGENTING
Datatyper och casts
Befintlig tabell som mall för datatyper ...
Explicit typ casts för den första raden med data i den fristående VALUES
uttryck kan vara obekvämt. Det finns vägar runt det. Du kan använda vilken befintlig relation som helst (tabell, vy, ...) som radmall. Måltabellen är det självklara valet för användningsfallet. Indata tvingas automatiskt till lämpliga typer, som i VALUES
sats av en INSERT
:
WITH input_rows AS (
(SELECT usr, contact, name FROM chats LIMIT 0) -- only copies column names and types
UNION ALL
VALUES
('foo1', 'bar1', 'bob1') -- no type casts here
, ('foo2', 'bar2', 'bob2')
)
...
Detta fungerar inte för vissa datatyper. Se:
- Casta NULL-typ vid uppdatering av flera rader
... och namn
Detta fungerar även för alla datatyper.
När du infogar i alla (ledande) kolumner i tabellen kan du utelämna kolumnnamn. Förutsatt att tabellen chats
i exemplet består endast av de 3 kolumner som används i UPSERT:
WITH input_rows AS (
SELECT * FROM (
VALUES
((NULL::chats).*) -- copies whole row definition
('foo1', 'bar1', 'bob1') -- no type casts needed
, ('foo2', 'bar2', 'bob2')
) sub
OFFSET 1
)
...
Bortsett från:använd inte reserverade ord som "user"
som identifierare. Det är ett laddat fotgevär. Använd juridiska, gemener, icke-citerade identifierare. Jag ersatte den med usr
.