Você pode usar LOCKs para tornar as coisas SERIALIZÁVEIS, mas isso reduz a simultaneidade. Por que não tentar primeiro a condição comum ("principalmente inserir ou principalmente selecionar"), seguida pelo manuseio seguro da ação "corretiva"? Ou seja, o padrão "JFDI"...
Principalmente INSERTs esperados (estacionamento de bola 70-80%+):
Basta tentar inserir. Se falhar, a linha já foi criada. Não há necessidade de se preocupar com a simultaneidade porque o TRY/CATCH lida com duplicatas para você.
BEGIN TRY
INSERT Table VALUES (@Value)
SELECT @id = SCOPE_IDENTITY()
END TRY
BEGIN CATCH
IF ERROR_NUMBER() <> 2627
RAISERROR etc
ELSE -- only error was a dupe insert so must already have a row to select
SELECT @id = RowID FROM Table WHERE RowValue = @VALUE
END CATCH
Principalmente SELECTs:
Semelhante, mas tente obter os dados primeiro. Sem dados =INSERT necessário. Novamente, se 2 chamadas simultâneas tentarem INSERT porque ambas encontraram a linha sem os identificadores TRY/CATCH.
BEGIN TRY
SELECT @id = RowID FROM Table WHERE RowValue = @VALUE
IF @@ROWCOUNT = 0
BEGIN
INSERT Table VALUES (@Value)
SELECT @id = SCOPE_IDENTITY()
END
END TRY
BEGIN CATCH
IF ERROR_NUMBER() <> 2627
RAISERROR etc
ELSE
SELECT @id = RowID FROM Table WHERE RowValue = @VALUE
END CATCH
O segundo parece se repetir, mas é altamente simultâneo. Os bloqueios alcançariam o mesmo, mas às custas da simultaneidade ...
Editar:
Por que não para usar MERGE...
Se você usar a cláusula OUTPUT ela retornará apenas o que estiver atualizado. Portanto, você precisa de um UPDATE fictício para gerar a tabela INSERTED para a cláusula OUTPUT. Se você tiver que fazer atualizações fictícias com muitas chamadas (como implícito no OP), há muitas gravações de log apenas para poder usar MERGE.