sql >> Database >  >> RDS >> Sqlserver

Atomic UPSERT in SQL Server 2005

INSERT INTO <table>
SELECT <natural keys>, <other stuff...>
FROM <table>
WHERE NOT EXISTS
   -- race condition risk here?
   ( SELECT 1 FROM <table> WHERE <natural keys> )

UPDATE ...
WHERE <natural keys>
  • er is een raceconditie in de eerste INSERT. De sleutel bestaat mogelijk niet tijdens de innerlijke query SELECT, maar bestaat wel op INSERT-tijd, wat resulteert in schending van de sleutel.
  • er is een race-conditie tussen de INSERT en UPDATE. De sleutel kan bestaan ​​wanneer deze is ingecheckt in de innerlijke query van de INSERT, maar is verdwenen tegen de tijd dat UPDATE wordt uitgevoerd.

Voor de tweede racevoorwaarde zou men kunnen stellen dat de sleutel toch zou zijn verwijderd door de gelijktijdige thread, dus het is niet echt een verloren update.

De optimale oplossing is meestal om het meest waarschijnlijke geval te proberen en de fout af te handelen als deze mislukt (uiteraard binnen een transactie):

  • als de sleutel waarschijnlijk ontbreekt, voeg dan altijd eerst in. Behandel de unieke beperkingsschending, fallback om te updaten.
  • als de sleutel waarschijnlijk aanwezig is, update dan altijd eerst. Invoegen als er geen rij is gevonden. Mogelijke schending van unieke beperkingen afhandelen, terugvallen op update.

Naast correctheid is dit patroon ook optimaal voor snelheid:het is efficiënter om te proberen de uitzondering in te voegen en af ​​te handelen dan om spurious lockups te doen. Lockups betekenen logische paginalezingen (wat kan betekenen dat fysieke pagina's worden gelezen), en IO (zelfs logisch) is duurder dan SEH.

Bijwerken @Peter

Waarom is geen enkele uitspraak 'atomair'? Laten we zeggen dat we een triviale tabel hebben:

create table Test (id int primary key);

Als ik nu deze enkele instructie van twee threads, in een lus, zou uitvoeren, zou het 'atomair' zijn, zoals je zegt, er kan een no-race-conditie bestaan:

  insert into Test (id)
    select top (1) id
    from Numbers n
    where not exists (select id from Test where id = n.id); 

Maar binnen een paar seconden vindt er een schending van de primaire sleutel plaats:

Msg 2627, niveau 14, staat 1, regel 4
Overtreding van PRIMARY KEY-beperking 'PK__Test__24927208'. Kan geen dubbele sleutel invoegen in object 'dbo.Test'.

Waarom is dat? Je hebt gelijk dat SQL-queryplan het 'juiste' zal doen op DELETE ... FROM ... JOIN , op WITH cte AS (SELECT...FROM ) DELETE FROM cte en in veel andere gevallen. Maar er is een cruciaal verschil in deze gevallen:de 'subquery' verwijst naar het doel van een update of verwijderen operatie. Voor dergelijke gevallen zal het queryplan inderdaad een geschikte vergrendeling gebruiken, in feite is dit gedrag van cruciaal belang in bepaalde gevallen, zoals bij het implementeren van wachtrijen Tabellen gebruiken als wachtrijen.

Maar in de oorspronkelijke vraag, evenals in mijn voorbeeld, wordt de subquery door de query-optimizer gezien als een subquery in een query, niet als een speciale 'scan for update'-query die speciale vergrendelingsbeveiliging nodig heeft. Het resultaat is dat de uitvoering van de subquery-lookup door een gelijktijdige waarnemer kan worden waargenomen als een afzonderlijke operatie , waardoor het 'atomaire' gedrag van de verklaring wordt verbroken. Tenzij er speciale voorzorgsmaatregelen worden genomen, kunnen meerdere threads proberen dezelfde waarde in te voegen, beide ervan overtuigd dat ze het hebben gecontroleerd en de waarde bestaat nog niet. Slechts één kan slagen, de andere zal de PK-overtreding raken. QED.



  1. U weet dat u dat wilt:migreren van Oracle naar MariaDB

  2. Afstemmen van invoer/uitvoer (I/O) bewerkingen voor PostgreSQL

  3. Gebruik COLUMNPROPERTY() om kolom- of parameterinformatie in SQL Server te retourneren

  4. Hoe XMLTYPE in VARCHAR in ORACLE te converteren?