sql >> Database >  >> RDS >> PostgreSQL

Hoe gebruik ik RETURNING met ON CONFLICT in PostgreSQL?

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 , een conflict_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 in INSERT , 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 .



  1. 3 manieren om het aantal systeemtabellen in een SQL Server-database te tellen

  2. Verschillen tussen INDEX, PRIMAIRE, UNIEKE, FULLTEXT in MySQL?

  3. Point-in-Time Recovery van MySQL- en MariaDB-gegevens uitvoeren met ClusterControl

  4. ExecuteNonQuery retourneert -1 bij gebruik van sql COUNT ondanks de querystring