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>
- der er en løbstilstand i den første INSERT. Nøglen eksisterer muligvis ikke under den indre forespørgsel SELECT, men eksisterer på INSERT tidspunktet, hvilket resulterer i nøgleovertrædelse.
- der er en race tilstand mellem INSERT og UPDATE. Nøglen kan eksistere, når den er markeret i den indre forespørgsel i INSERT, men den er væk, når UPDATE kører.
For den anden race-tilstand kan man argumentere for, at nøglen alligevel ville være blevet slettet af den samtidige tråd, så det er egentlig ikke en tabt opdatering.
Den optimale løsning er normalt at prøve det mest sandsynlige tilfælde og håndtere fejlen, hvis den mislykkes (naturligvis inde i en transaktion):
- hvis nøglen sandsynligvis mangler, skal du altid indsætte den først. Håndter den unikke overtrædelse af begrænsninger, tilbagevenden til opdatering.
- hvis nøglen sandsynligvis er til stede, skal du altid opdatere først. Indsæt, hvis ingen række blev fundet. Håndter mulig unik overtrædelse af begrænsninger, fallback til opdatering.
Udover korrekthed er dette mønster også optimalt for hastighed:er mere effektivt at forsøge at indsætte og håndtere undtagelsen end at lave falske låsninger. Lockups betyder logisk sidelæsning (hvilket kan betyde fysisk sidelæsning), og IO (selv logisk) er dyrere end SEH.
Opdater @Peter
Hvorfor er et enkelt udsagn ikke 'atomart'? Lad os sige, at vi har en triviel tabel:
create table Test (id int primary key);
Hvis jeg nu ville køre denne enkelte erklæring fra to tråde i en løkke, ville den være "atomisk", som du siger, en tilstand uden race kan eksistere:
insert into Test (id)
select top (1) id
from Numbers n
where not exists (select id from Test where id = n.id);
Men på kun et par sekunder sker der en primærnøglebrud:
Msg 2627, Level 14, State 1, Line 4
Overtrædelse af PRIMÆR NØGLE-begrænsning 'PK__Test__24927208'. Kan ikke indsætte dubletnøgle i objektet 'dbo.Test'.
Hvorfor det? Du har ret i, at SQL-forespørgselsplanen vil gøre det 'rigtige' på DELETE ... FROM ... JOIN
, på WITH cte AS (SELECT...FROM ) DELETE FROM cte
og i mange andre tilfælde. Men der er en afgørende forskel i disse tilfælde:'underforespørgslen' henviser til målet af en opdatering eller slet operation. I sådanne tilfælde vil forespørgselsplanen faktisk bruge en passende lås, faktisk er denne adfærd kritisk i visse tilfælde, f.eks. når du implementerer køer ved at bruge tabeller som køer.
Men i det oprindelige spørgsmål, såvel som i mit eksempel, ses underforespørgslen af forespørgselsoptimeringsværktøjet bare som en underforespørgsel i en forespørgsel, ikke som en speciel forespørgsel af typen "scan for opdatering", der har brug for speciel låsebeskyttelse. Resultatet er, at udførelsen af underforespørgselsopslaget kan observeres som en særskilt operation af en samtidig observatør , hvilket bryder udsagnets 'atomare' adfærd. Medmindre der tages særlige forholdsregler, kan flere tråde forsøge at indsætte den samme værdi, både overbevist om, at de havde tjekket, og værdien ikke allerede eksisterer. Kun én kan lykkes, den anden vil ramme PK-overtrædelsen. QED.