Het momenteel geaccepteerde antwoord lijkt ok voor een enkel conflictdoel, weinig conflicten, kleine tuples en geen triggers. Het vermijdt gelijktijdigheidsprobleem 1 (zie hieronder) met brute kracht. De eenvoudige oplossing heeft zijn aantrekkingskracht, de bijwerkingen zijn misschien minder belangrijk.
Doe echter in alle andere gevallen niet update identieke rijen zonder noodzaak. Zelfs als u aan de oppervlakte geen verschil ziet, zijn er verschillende bijwerkingen :
-
Het kan triggers afvuren die niet zouden moeten worden geactiveerd.
-
Het vergrendelt "onschuldige" rijen, wat mogelijk kosten met zich meebrengt voor gelijktijdige transacties.
-
Hierdoor kan de rij als nieuw lijken, hoewel hij oud is (tijdstempel van de transactie).
-
Het belangrijkste , met het MVCC-model van PostgreSQL wordt voor elke
UPDATE
een nieuwe rijversie geschreven , ongeacht of de rijgegevens zijn gewijzigd. Dit brengt een prestatiestraf met zich mee voor de UPSERT zelf, tafelbloat, indexbloat, prestatiestraf voor daaropvolgende bewerkingen op de tafel,VACUUM
kosten. Een klein effect voor een paar duplicaten, maar enorm voor meestal dupes.
Plus , soms is het niet praktisch of zelfs niet mogelijk om ON CONFLICT DO UPDATE
te gebruiken . De handleiding:
Voor
ON CONFLICT DO UPDATE
, eenconflict_target
moet worden verstrekt.
Een enkele "conflictdoel" is niet mogelijk als er meerdere indexen / beperkingen bij betrokken zijn. Maar hier is een gerelateerde oplossing voor meerdere gedeeltelijke indexen:
- UPSERT gebaseerd op UNIEKE beperking met NULL-waarden
Terug op het onderwerp, je kunt (bijna) hetzelfde bereiken zonder lege updates en bijwerkingen. Sommige van de volgende oplossingen werken ook met ON CONFLICT DO NOTHING
(geen "conflictdoel"), om allen te vangen mogelijke conflicten die zich kunnen voordoen - al dan niet wenselijk.
Zonder gelijktijdige schrijfbelasting
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
De source
kolom is een optionele toevoeging om te laten zien hoe dit werkt. Je hebt het misschien nodig om het verschil tussen beide gevallen te zien (nog een voordeel ten opzichte van lege schrijfacties).
De laatste JOIN chats
werkt omdat nieuw ingevoegde rijen van een bijgevoegde CTE voor het wijzigen van gegevens nog niet zichtbaar zijn in de onderliggende tabel. (Alle delen van dezelfde SQL-instructie zien dezelfde snapshots van onderliggende tabellen.)
Sinds de VALUES
uitdrukking is vrijstaand (niet direct gekoppeld aan een INSERT
) Postgres kan geen gegevenstypen afleiden uit de doelkolommen en het kan zijn dat u expliciete typecasts moet toevoegen. De handleiding:
Wanneer
VALUES
wordt gebruikt inINSERT
, worden de waarden allemaal automatisch gedwongen naar het gegevenstype van de corresponderende bestemmingskolom. Wanneer het in andere contexten wordt gebruikt, kan het nodig zijn om het juiste gegevenstype op te geven. Als de items allemaal geciteerde letterlijke constanten zijn, is het dwingen van de eerste voldoende om het veronderstelde type voor iedereen te bepalen.
De zoekopdracht zelf (de bijwerkingen niet meegerekend) kan een beetje duurder zijn voor weinigen dupes, vanwege de overhead van de CTE en de extra SELECT
(wat goedkoop zou moeten zijn aangezien de perfecte index er per definitie is - een unieke beperking wordt geïmplementeerd met een index).
Kan (veel) sneller zijn voor velen duplicaten. De effectieve kosten van extra schrijfbewerkingen hangen van veel factoren af.
Maar er zijn minder bijwerkingen en verborgen kosten in elk geval. Het is over het algemeen waarschijnlijk goedkoper.
Bijgevoegde reeksen zijn nog steeds geavanceerd, omdat standaardwaarden voor . worden ingevuld testen op conflicten.
Over CTE's:
- Zijn zoekopdrachten van het SELECT-type het enige type dat kan worden genest?
- Dedupliceer SELECT-instructies in relationele verdeling
Met gelijktijdige schrijfbelasting
Uitgaande van standaard READ COMMITTED
transactie isolatie. Gerelateerd:
- Gelijktijdige transacties resulteren in raceconditie met unieke beperking op insert
De beste strategie om je te verdedigen tegen race-omstandigheden hangt af van de exacte vereisten, het aantal en de grootte van de rijen in de tabel en in de UPSERT's, het aantal gelijktijdige transacties, de kans op conflicten, beschikbare middelen en andere factoren ...
Gelijktijdigheidsprobleem 1
Als een gelijktijdige transactie naar een rij is geschreven die uw transactie nu probeert te UPSERT, moet uw transactie wachten tot de andere is voltooid.
Als de andere transactie eindigt met ROLLBACK
(of een fout, d.w.z. automatische ROLLBACK
), kan uw transactie normaal verlopen. Kleine mogelijke bijwerking:hiaten in opeenvolgende nummers. Maar geen ontbrekende rijen.
Als de andere transactie normaal eindigt (impliciete of expliciete COMMIT
), uw INSERT
zal een conflict detecteren (de UNIQUE
index / beperking is absoluut) en DO NOTHING
, dus ook de rij niet retourneren. (Kan de rij ook niet vergrendelen zoals aangetoond in gelijktijdigheidsprobleem 2 hieronder, omdat het niet zichtbaar is .) De SELECT
ziet dezelfde momentopname vanaf het begin van de zoekopdracht en kan de nog onzichtbare rij ook niet retourneren.
Zulke rijen ontbreken in de resultatenset (ook al bestaan ze in de onderliggende tabel)!
Dit kan goed zijn zoals het is . Vooral als u geen rijen retourneert zoals in het voorbeeld en tevreden bent wetende dat de rij er is. Als dat niet goed genoeg is, zijn er verschillende manieren om dit te omzeilen.
U kunt het aantal rijen van de uitvoer controleren en de instructie herhalen als het niet overeenkomt met het aantal rijen van de invoer. Misschien goed genoeg voor het zeldzame geval. Het punt is om een nieuwe zoekopdracht te starten (kan in dezelfde transactie zijn), die dan de nieuw vastgelegde rijen zal zien.
Of controleer op ontbrekende resultaatrijen binnen dezelfde zoekopdracht en overschrijven degenen met de brute kracht truc gedemonstreerd in Alextoni's antwoord.
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;
Het is zoals de bovenstaande vraag, maar we voegen nog een stap toe met de CTE ups
, voordat we de complete . retourneren resultaat set. Die laatste CTE zal meestal niets doen. Alleen als er rijen ontbreken in het geretourneerde resultaat, gebruiken we brute kracht.
Nog meer overhead. Hoe meer conflicten met reeds bestaande rijen, hoe groter de kans dat dit beter presteert dan de eenvoudige aanpak.
Een neveneffect:de 2e UPSERT schrijft rijen in de verkeerde volgorde, dus het introduceert opnieuw de mogelijkheid van deadlocks (zie hieronder) als drie of meer transacties die naar dezelfde rijen schrijven, overlappen elkaar. Als dat een probleem is, heb je een andere oplossing nodig - zoals het herhalen van de hele verklaring zoals hierboven vermeld.
Gelijktijdigheidsprobleem 2
Als gelijktijdige transacties naar betrokken kolommen van betrokken rijen kunnen schrijven en u ervoor moet zorgen dat de gevonden rijen er in een later stadium in dezelfde transactie nog steeds zijn, kunt u bestaande rijen vergrendelen goedkoop in de CTE ins
(die anders ontgrendeld zou zijn) met:
...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE -- never executed, but still locks the row
...
En voeg een vergrendelingsclausule toe aan de SELECT
ook, zoals FOR UPDATE
.
Dit zorgt ervoor dat concurrerende schrijfbewerkingen wachten tot het einde van de transactie, wanneer alle vergrendelingen worden vrijgegeven. Dus wees kort.
Meer details en uitleg:
- Uitgesloten rijen opnemen in RETURNING from INSERT ... ON CONFLICT
- Is SELECT of INSERT in een functie die vatbaar is voor race-omstandigheden?
Immers?
Verdedigen tegen impasses door rijen in te voegen in consistente volgorde . Zie:
- Deadlock met INSERT's met meerdere rijen ondanks ON CONFLICT NIETS DOEN
Gegevenstypen en casts
Bestaande tabel als sjabloon voor gegevenstypen ...
Expliciete typecasts voor de eerste rij gegevens in de vrijstaande VALUES
uitdrukking kan ongemakkelijk zijn. Er zijn manieren omheen. U kunt elke bestaande relatie (tabel, weergave, ...) als rijsjabloon gebruiken. De doeltabel is de voor de hand liggende keuze voor de use case. Invoergegevens worden automatisch naar de juiste typen gedwongen, zoals in de VALUES
clausule van een 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')
)
...
Dit werkt niet voor sommige gegevenstypen. Zie:
- NULL-type casten bij het bijwerken van meerdere rijen
... en namen
Dit werkt ook voor alle gegevenstypen.
Bij het invoegen in alle (hoofd)kolommen van de tabel kunt u kolomnamen weglaten. Uitgaande van tabel chats
bestaat in het voorbeeld alleen uit de 3 kolommen die in de UPSERT worden gebruikt:
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
)
...
Terzijde:gebruik geen gereserveerde woorden zoals "user"
als identificatie. Dat is een geladen voetpistool. Gebruik legale, kleine letters, niet-geciteerde identifiers. Ik heb het vervangen door usr
.