Existem alguns cenários possíveis que são fáceis de resolver e um cenário pernicioso que não é.
Para um usuário que insere um valor, insere o mesmo valor algum tempo depois, um simples SELECT antes que o INSERT detecte o problema. Isso funciona para o caso em que um usuário envia um valor e, algum tempo depois, outro usuário envia o mesmo valor.
Se o usuário enviar uma lista de valores com duplicatas - por exemplo, {ABC, DEF, ABC} - em uma única chamada do código, o aplicativo poderá detectar e filtrar as duplicatas, talvez causando um erro. Você também precisará verificar se o banco de dados não contém nenhum valor exclusivo antes da inserção.
O cenário complicado é quando a gravação de um usuário está dentro do DBMS ao mesmo tempo que a gravação de outro usuário e ele está gravando o mesmo valor. Então você tem uma corrida uma condição entre eles. Como o DBMS é (provavelmente - você não diz qual deles está usando) um sistema multitarefa preemptivo, qualquer tarefa pode ser pausada a qualquer momento de sua execução. Isso significa que a tarefa do usuário1 pode verificar se não há linha existente, a tarefa do usuário2 pode verificar se não há linha existente, a tarefa do usuário1 pode inserir essa linha e a tarefa do usuário2 pode inserir essa linha. Em cada momento, as tarefas são felizes individualmente e estão fazendo a coisa certa. Globalmente, no entanto, ocorre um erro.
Normalmente, um DBMS lidaria com isso colocando um bloqueio no valor em questão. Nesse problema, você está criando uma nova linha para que ainda não haja nada a ser bloqueado. A resposta é um bloqueio de intervalo. Como sugere, isso bloqueia uma faixa de valores, existindo ou não no momento. Uma vez bloqueado, esse intervalo não pode ser acessado por outra tarefa até que o bloqueio seja liberado. Para obter bloqueios de intervalo, você deve especificar e nível de isolamento SERIALIZABLE . O fenômeno de outra tarefa se esgueirar seguidamente após a verificação da tarefa é conhecido como registros fantasmas .
Definir o nível de isolamento como Serializable em todo o aplicativo terá implicações. O rendimento será reduzido. Outras condições de corrida que funcionaram bem o suficiente no passado podem começar a mostrar erros agora. Eu sugeriria defini-lo na conexão que executa seu código de indução de duplicado e deixar o restante do aplicativo como está.
Uma alternativa baseada em código é verificar depois da gravação e não antes. O mesmo acontece com INSERT e, em seguida, conte o número de linhas que possuem esse valor de hash. Se houver duplicados, reverter a ação. Isso pode ter alguns resultados perversos. Diga que a tarefa 1 grava e depois a tarefa 2. Em seguida, a tarefa 1 verifica e encontra uma duplicata. Ele retrocede, embora tenha sido o primeiro. Da mesma forma, ambas as tarefas podem detectar a duplicação e a reversão. Mas pelo menos você terá uma mensagem para trabalhar, um mecanismo de nova tentativa e nenhuma nova duplicata. As reversões são desaprovadas, como usar exceções para controlar o fluxo do programa. Note bem que todoso trabalho na transação será revertido, não apenas a gravação de indução de duplicação. E você precisará ter transações explícitas que podem reduzir a simultaneidade. A verificação duplicada será terrivelmente lenta, a menos que você tenha um índice no hash. Se você o fizer, poderá torná-lo único!
Como você comentou, a solução real é um índice exclusivo. Parece-me que isso deve caber na sua janela de manutenção (apesar de você conhecer melhor o seu sistema). Digamos que o hash tenha oito bytes. Para cem milhões de linhas, são cerca de 1 GB. A experiência sugere que um pouco de hardware processaria essas muitas linhas em um ou dois minutos, no máximo. A verificação e a eliminação duplicadas serão adicionadas a isso, mas podem ser scripts com antecedência. Este é apenas um aparte, no entanto.