He estado operando bajo la suposición de que una sola declaración en SQL Server es consistente
Esa suposición es incorrecta. Las dos transacciones siguientes tienen una semántica de bloqueo idéntica:
STATEMENT
BEGIN TRAN; STATEMENT; COMMIT
No hay diferencia en absoluto. Las declaraciones individuales y las confirmaciones automáticas no cambian nada.
Por lo tanto, fusionar toda la lógica en una declaración no ayuda (si lo hace, fue por accidente porque el plan cambió).
Arreglemos el problema en cuestión. SERIALIZABLE
arreglará la inconsistencia que está viendo porque garantiza que sus transacciones se comporten como si se ejecutaran con un solo subproceso. De manera equivalente, se comportan como si se ejecutaran instantáneamente.
Obtendrá interbloqueos. Si está de acuerdo con un bucle de reintento, ya ha terminado.
Si desea invertir más tiempo, aplique sugerencias de bloqueo para forzar el acceso exclusivo a los datos relevantes:
UPDATE Gifts -- U-locked anyway
SET GivenAway = 1
WHERE GiftID = (
SELECT TOP 1 GiftID
FROM Gifts WITH (UPDLOCK, HOLDLOCK) --this normally just S-locks.
WHERE g2.GivenAway = 0
AND (SELECT COUNT(*) FROM Gifts g2 WITH (UPDLOCK, HOLDLOCK) WHERE g2.GivenAway = 1) < 5
ORDER BY g2.GiftValue DESC
)
Ahora verá una simultaneidad reducida. Eso podría estar totalmente bien dependiendo de tu carga.
La naturaleza misma de su problema hace que sea difícil lograr la concurrencia. Si necesita una solución para eso, necesitaríamos aplicar técnicas más invasivas.
Puede simplificar un poco la ACTUALIZACIÓN:
WITH g AS (
SELECT TOP 1 Gifts.*
FROM Gifts
WHERE g2.GivenAway = 0
AND (SELECT COUNT(*) FROM Gifts g2 WITH (UPDLOCK, HOLDLOCK) WHERE g2.GivenAway = 1) < 5
ORDER BY g2.GiftValue DESC
)
UPDATE g -- U-locked anyway
SET GivenAway = 1
Esto elimina una unión innecesaria.