sql >> Database teknologi >  >> RDS >> PostgreSQL

Hvordan bruger man RETURNING med ON CONFLICT i PostgreSQL?

Det aktuelt accepterede svar virker ok for et enkelt konfliktmål, få konflikter, små tupler og ingen triggere. Det undgår sammenfaldsproblem 1 (se nedenfor) med brute force. Den simple løsning har sin appel, bivirkningerne kan være mindre vigtige.

I alle andre tilfælde skal du dog ikke opdatere identiske rækker uden behov. Selvom du ikke ser nogen forskel på overfladen, er der forskellige bivirkninger :

  • Det kan udløse triggere, som ikke bør udløses.

  • Det skrivelåser "uskyldige" rækker, hvilket muligvis medfører omkostninger til samtidige transaktioner.

  • Det kan få rækken til at virke ny, selvom den er gammel (transaktionens tidsstempel).

  • Vigtigst af alt , med PostgreSQL's MVCC-model skrives en ny rækkeversion for hver UPDATE , uanset om rækkedataene er ændret. Dette medfører en præstationsstraf for selve UPSERT, table bloat, index bloat, performance penalty for efterfølgende operationer på bordet, VACUUM koste. En mindre effekt for få dubletter, men massiv for det meste duper.

Plus , nogle gange er det ikke praktisk eller endda muligt at bruge ON CONFLICT DO UPDATE . Manualen:

For ON CONFLICT DO UPDATE , et conflict_target skal leveres.

En enkelt "konfliktmål" er ikke muligt, hvis flere indekser / begrænsninger er involveret. Men her er en relateret løsning til flere partielle indekser:

  • UPSERT baseret på UNIQUE begrænsning med NULL-værdier

Tilbage til emnet kan du opnå (næsten) det samme uden tomme opdateringer og bivirkninger. Nogle af de følgende løsninger fungerer også med ON CONFLICT DO NOTHING (ingen "konfliktmål"), for at fange alle mulige konflikter, der måtte opstå - som måske eller måske ikke er ønskværdige.

Uden samtidig skrivebelastning

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 kolonne er en valgfri tilføjelse for at demonstrere, hvordan dette fungerer. Du kan faktisk få brug for det for at se forskel på begge tilfælde (en anden fordel i forhold til tomme skrivninger).

De sidste JOIN chats fungerer, fordi nyindsatte rækker fra en vedhæftet datamodificerende CTE endnu ikke er synlige i den underliggende tabel. (Alle dele af den samme SQL-sætning ser de samme øjebliksbilleder af underliggende tabeller.)

Siden VALUES udtryk er fritstående (ikke direkte knyttet til en INSERT). ) Postgres kan ikke udlede datatyper fra målkolonnerne, og du skal muligvis tilføje eksplicitte typecasts. Manualen:

Når VALUES bruges i INSERT , bliver værdierne alle automatisk tvunget til datatypen for den tilsvarende destinationskolonne. Når det bruges i andre sammenhænge, ​​kan det være nødvendigt at angive den korrekte datatype. Hvis indtastningerne alle er anførte bogstavelige konstanter, er det tilstrækkeligt at tvinge den første til at bestemme den antagne type for alle.

Selve forespørgslen (bortset fra bivirkningerne) kan være en smule dyrere for dupes, på grund af overhead af CTE og den ekstra SELECT (hvilket burde være billigt, da det perfekte indeks er der per definition - en unik begrænsning er implementeret med et indeks).

Kan være (meget) hurtigere for mange dubletter. De effektive omkostninger ved yderligere skrivninger afhænger af mange faktorer.

Men der er færre bivirkninger og skjulte omkostninger i hvert fald. Det er højst sandsynligt billigere generelt.

Vedhæftede sekvenser er stadig avancerede, da standardværdier er udfyldt før test for konflikter.

Om CTE'er:

  • Er SELECT-type-forespørgsler den eneste type, der kan indlejres?
  • Dedupliker SELECT-sætninger i relationel division

Med samtidig skrivebelastning

Forudsat standard READ COMMITTED transaktionsisolering. Relateret:

  • Samtidige transaktioner resulterer i løbstilstand med unik begrænsning ved indsættelse

Den bedste strategi til at forsvare sig mod raceforhold afhænger af nøjagtige krav, antallet og størrelsen af ​​rækker i tabellen og i UPSERT'erne, antallet af samtidige transaktioner, sandsynligheden for konflikter, tilgængelige ressourcer og andre faktorer ...

Samtidighedsproblem 1

Hvis en samtidig transaktion har skrevet til en række, som din transaktion nu forsøger at UPSERT, skal din transaktion vente på, at den anden er færdig.

Hvis den anden transaktion ender med ROLLBACK (eller enhver fejl, dvs. automatisk ROLLBACK ), kan din transaktion fortsætte normalt. Mindre mulig bivirkning:huller i fortløbende numre. Men ingen manglende rækker.

Hvis den anden transaktion ender normalt (implicit eller eksplicit COMMIT ), din INSERT vil opdage en konflikt (den UNIQUE indeks / begrænsning er absolut) og DO NOTHING , derfor heller ikke returnere rækken. (Kan heller ikke låse rækken som vist i sammenfaldsproblem 2 nedenfor, da det ikke er synligt .) SELECT ser det samme øjebliksbillede fra starten af ​​forespørgslen og kan heller ikke returnere den endnu usynlige række.

Alle sådanne rækker mangler i resultatsættet (selvom de findes i den underliggende tabel)!

Dette kan være ok, som det er . Især hvis du ikke returnerer rækker som i eksemplet og er tilfreds med at vide, at rækken er der. Hvis det ikke er godt nok, er der forskellige måder at undgå det på.

Du kan kontrollere rækkeantallet af output og gentage sætningen, hvis det ikke stemmer overens med rækkeantallet af input. Kan være god nok til det sjældne tilfælde. Pointen er at starte en ny forespørgsel (kan være i den samme transaktion), som så vil se de nyligt forpligtede rækker.

Eller tjek for manglende resultatrækker indenfor den samme forespørgsel og overskriv dem med brute force-tricket, der er demonstreret 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 er ligesom forespørgslen ovenfor, men vi tilføjer endnu et trin med CTE ups , før vi returnerer den komplette resultatsæt. Den sidste CTE vil ikke gøre noget det meste af tiden. Kun hvis rækker mangler i det returnerede resultat, bruger vi brute force.

Mere overhead, endnu. Jo flere konflikter med allerede eksisterende rækker, jo mere sandsynligt vil dette overgå den simple tilgang.

En bivirkning:2. UPSERT skriver rækker ude af rækkefølge, så den genindfører muligheden for dødvande (se nedenfor), hvis tre eller flere transaktioner, der skrives til de samme rækker, overlapper hinanden. Hvis det er et problem, har du brug for en anden løsning - som at gentage hele udsagnet som nævnt ovenfor.

Samtidighedsproblem 2

Hvis samtidige transaktioner kan skrive til involverede kolonner af berørte rækker, og du skal sikre dig, at de rækker, du fandt, stadig er der på et senere tidspunkt i den samme transaktion, kan du låse eksisterende rækker billigt i CTE ins (som ellers ville blive låst op) med:

...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE  -- never executed, but still locks the row
...

Og tilføj en låsesætning til SELECT også som FOR UPDATE .

Dette får konkurrerende skriveoperationer til at vente til slutningen af ​​transaktionen, når alle låse er frigivet. Så vær kort.

Flere detaljer og forklaring:

  • Sådan inkluderes ekskluderede rækker i RETURNING from INSERT ... ON CONFLICT
  • Er SELECT eller INSERT i en funktion, der er tilbøjelig til løbsforhold?

Deadlocks?

Forsvar mod deadlocks ved at indsætte rækker i konsistent rækkefølge . Se:

  • Deadlock med multi-row INSERTs trods ON CONFLICT GØR INTET

Datatyper og casts

Eksisterende tabel som skabelon for datatyper ...

Eksplicit type casts for den første række af data i den fritstående VALUES udtryk kan være ubelejligt. Der er måder uden om det. Du kan bruge enhver eksisterende relation (tabel, visning, ...) som rækkeskabelon. Måltabellen er det oplagte valg til use casen. Inputdata tvinges automatisk til passende typer, som i VALUES klausul af 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')
   )
   ...

Dette virker ikke for nogle datatyper. Se:

  • Caster NULL-typen ved opdatering af flere rækker

... og navne

Dette virker også for alle datatyper.

Mens du indsætter i alle (førende) kolonner i tabellen, kan du udelade kolonnenavne. Forudsat tabel chats i eksemplet kun består af de 3 kolonner, der bruges 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
   )
   ...

Til side:Brug ikke reserverede ord som "user" som identifikator. Det er et ladt fodgevær. Brug lovlige, små bogstaver, id'er uden anførselstegn. Jeg erstattede den med usr .



  1. Hvordan kan SQL Workload Analysis hjælpe dig?

  2. SQL Server 2016:Opret en lagret procedure

  3. Skal jeg slette eller deaktivere en række i en relationsdatabase?

  4. Escapende kontroltegn i Oracle XDB