Muitas vezes encontramos a situação "Se não existe, insira". Blog de Dan Guzman tem uma excelente investigação sobre como tornar esse processo seguro.
Eu tenho uma tabela básica que simplesmente cataloga uma seqüência de caracteres para um número inteiro de um SEQUENCE
. Em um procedimento armazenado, preciso obter a chave inteira para o valor, se existir, ou INSERT
obter o valor resultante. Existe uma restrição de exclusividade nodbo.NameLookup.ItemName
coluna, portanto a integridade dos dados não corre risco, mas não quero encontrar as exceções.
Não é IDENTITY
assim que eu não consigo entender SCOPE_IDENTITY
e o valor pode estar NULL
em certos casos.
Na minha situação, eu só tenho que lidar com a INSERT
segurança na mesa, então estou tentando decidir se é uma prática melhor usar MERGE
dessa maneira:
SET NOCOUNT, XACT_ABORT ON;
DECLARE @vValueId INT
DECLARE @inserted AS TABLE (Id INT NOT NULL)
MERGE
dbo.NameLookup WITH (HOLDLOCK) AS f
USING
(SELECT @vName AS val WHERE @vName IS NOT NULL AND LEN(@vName) > 0) AS new_item
ON f.ItemName= new_item.val
WHEN MATCHED THEN
UPDATE SET @vValueId = f.Id
WHEN NOT MATCHED BY TARGET THEN
INSERT
(ItemName)
VALUES
(@vName)
OUTPUT inserted.Id AS Id INTO @inserted;
SELECT @vValueId = s.Id FROM @inserted AS s
Eu poderia fazer isso sem usar MERGE
apenas um condicional INSERT
seguido de um SELECT
Eu acho que essa segunda abordagem é mais clara para o leitor, mas não estou convencido de que seja uma "melhor" prática
SET NOCOUNT, XACT_ABORT ON;
INSERT INTO
dbo.NameLookup (ItemName)
SELECT
@vName
WHERE
NOT EXISTS (SELECT * FROM dbo.NameLookup AS t WHERE @vName IS NOT NULL AND LEN(@vName) > 0 AND t.ItemName = @vName)
DECLARE @vValueId int;
SELECT @vValueId = i.Id FROM dbo.NameLookup AS i WHERE i.ItemName = @vName
Ou talvez haja outra maneira melhor que eu não considerei
Eu pesquisei e referenciei outras perguntas. Este: /programming/5288283/sql-server-insert-if-not-exists-best-practice é o mais apropriado que eu poderia encontrar, mas não parece muito aplicável ao meu caso de uso. Outras questões para a IF NOT EXISTS() THEN
abordagem que não considero aceitáveis.
fonte
Respostas:
Como você está usando uma Sequência, pode usar a mesma função NEXT VALUE FOR - que você já possui em uma restrição padrão no
Id
campo Chave primária - para gerar um novoId
valor antecipadamente. Gerar o valor primeiro significa que você não precisa se preocupar em não terSCOPE_IDENTITY
, o que significa que você não precisa daOUTPUT
cláusula ou de fazer um adicionalSELECT
para obter o novo valor; você terá o valor antes de fazer o mesmoINSERT
e nem precisará mexer comSET IDENTITY INSERT ON / OFF
:-)Então, isso cuida de parte da situação geral. A outra parte é lidar com o problema de simultaneidade de dois processos, ao mesmo tempo, sem localizar uma linha existente para a mesma seqüência exata e prosseguir com o
INSERT
. A preocupação é evitar a violação de restrição exclusiva que ocorreria.Uma maneira de lidar com esses tipos de problemas de simultaneidade é forçar essa operação específica a ser encadeada única. A maneira de fazer isso é usando bloqueios de aplicativos (que funcionam entre sessões). Embora eficazes, eles podem ser um pouco pesados para uma situação como essa em que a frequência de colisões é provavelmente bastante baixa.
A outra maneira de lidar com as colisões é aceitar que elas às vezes ocorrem e lidar com elas, em vez de tentar evitá-las. Usando a
TRY...CATCH
construção, você pode efetivamente interceptar um erro específico (neste caso: "violação de restrição exclusiva", Msg 2601) e executar novamente oSELECT
para obter oId
valor, pois sabemos que ele existe agora por estar noCATCH
bloco com esse particular erro. Outros erros podem ser tratados da maneira típicaRAISERROR
/RETURN
ouTHROW
.Configuração de teste: sequência, tabela e índice exclusivo
Configuração de teste: procedimento armazenado
O teste
Pergunta do OP
MERGE
tem vários "problemas" (várias referências estão vinculadas na resposta do @ SqlZim, portanto, não é necessário duplicar essa informação aqui). E, como não há bloqueio adicional nessa abordagem (menos contenção), deve ser melhor em simultaneidade. Nesta abordagem, você nunca terá uma violação de restrição exclusiva, tudo sem nenhumaHOLDLOCK
, etc. É praticamente garantido que funcione.O raciocínio por trás dessa abordagem é:
CATCH
bloco em primeiro lugar será bem baixa. Faz mais sentido otimizar o código que será executado 99% do tempo, em vez do código que será executado 1% do tempo (a menos que não haja custo para otimizar ambos, mas esse não é o caso aqui).Comentário da resposta de @ SqlZim (ênfase adicionada)
Eu concordaria com esta primeira frase se ela fosse alterada para indicar "e _quando prudente". Só porque algo é tecnicamente possível, não significa que a situação (ou seja, caso de uso pretendido) seria beneficiada por ela.
O problema que vejo com essa abordagem é que ela bloqueia mais do que o que está sendo sugerido. É importante reler a documentação citada em "serializable", especificamente o seguinte (ênfase adicionada):
Agora, aqui está o comentário no código de exemplo:
A palavra operativa existe "alcance". O bloqueio que está sendo realizado não está apenas no valor
@vName
, mas com mais precisão, um intervalo que começa emo local onde esse novo valor deve ir (ou seja, entre os valores-chave existentes em ambos os lados de onde o novo valor se encaixa), mas não o valor em si. Ou seja, outros processos serão impedidos de inserir novos valores, dependendo dos valores que estão sendo pesquisados no momento. Se a pesquisa estiver sendo feita na parte superior do intervalo, a inserção de qualquer coisa que possa ocupar a mesma posição será bloqueada. Por exemplo, se os valores "a", "b" e "d" existirem, se um processo estiver executando o SELECT em "f", não será possível inserir os valores "g" ou mesmo "e" ( já que qualquer um desses virá imediatamente após "d"). Mas, a inserção de um valor de "c" será possível, pois não seria colocado no intervalo "reservado".O exemplo a seguir deve ilustrar esse comportamento:
(Na guia de consulta (ou seja, Sessão) nº 1)
(Na guia de consulta (ou seja, Sessão) nº 2)
Da mesma forma, se o valor "C" existir e o valor "A" estiver sendo selecionado (e, portanto, bloqueado), você poderá inserir um valor de "D", mas não um valor de "B":
(Na guia de consulta (ou seja, Sessão) nº 1)
(Na guia de consulta (ou seja, Sessão) nº 2)
Para ser justo, na minha abordagem sugerida, quando houver uma exceção, haverá 4 entradas no log de transações que não acontecerão nessa abordagem de "transação serializável". MAS, como eu disse acima, se a exceção ocorrer 1% (ou mesmo 5%) das vezes, isso será muito menos impactante do que o caso muito mais provável do SELECT inicial que bloqueia temporariamente as operações INSERT.
Outro problema, ainda que menor, com essa abordagem "transação serializável + cláusula OUTPUT" é que a
OUTPUT
cláusula (em seu uso atual) envia os dados de volta como um conjunto de resultados. Um conjunto de resultados requer mais sobrecarga (provavelmente nos dois lados: no SQL Server para gerenciar o cursor interno e na camada de aplicativo para gerenciar o objeto DataReader) do que um simplesOUTPUT
parâmetro . Dado que estamos lidando apenas com um único valor escalar e que a suposição é uma alta frequência de execuções, essa sobrecarga extra do conjunto de resultados provavelmente aumenta.Embora a
OUTPUT
cláusula possa ser usada de maneira a retornar umOUTPUT
parâmetro, isso exigiria etapas adicionais para criar uma tabela ou variável de tabela temporária e, em seguida, selecionar o valor dessa variável de tabela / tabela temporária noOUTPUT
parâmetroEsclarecimentos adicionais: Resposta à resposta de @ SqlZim (resposta atualizada) à minha Resposta à resposta de @ SqlZim (na resposta original) à minha declaração sobre concorrência e desempenho ;-)
Desculpe se esta parte é um pouquinho longa, mas neste momento estamos apenas nas nuances das duas abordagens.
Sim, admito que sou tendencioso, embora seja justo:
INSERT
falha ocorre devido a uma violação de restrição exclusiva. Eu não vi isso mencionado em nenhuma das outras respostas / postagens.Com relação à abordagem "JFDI" do @ gbn, o post "Ugly Pragmatism For The Win" de Michael J. Swart e o comentário de Aaron Bertrand no post de Michael (sobre seus testes mostrando quais cenários tiveram desempenho reduzido), e seu comentário sobre a "adaptação de Michael J" A adaptação de Stewart do procedimento Try Catch JFDI da @ gbn "afirmando:
Com relação à discussão do gbn / Michael / Aaron relacionada à abordagem "JFDI", seria incorreto equiparar minha sugestão à abordagem "JFDI" do gbn. Devido à natureza da operação "Obter ou Inserir", há uma necessidade explícita de fazer isso
SELECT
para obter oID
valor dos registros existentes. Esse SELECT atua comoIF EXISTS
verificação, o que torna essa abordagem mais igual à variação "CheckTryCatch" dos testes de Aaron. O código reescrito de Michael (e sua adaptação final da adaptação de Michael) também inclui um testeWHERE NOT EXISTS
para fazer a mesma verificação primeiro. Portanto, minha sugestão (junto com o código final de Michael e sua adaptação do código final) não chega aCATCH
esse ponto com tanta frequência. Só poderia ser situações em que duas sessões,ItemName
INSERT...SELECT
exatamente no mesmo momento, de modo que ambas as sessões recebam um "verdadeiro" paraWHERE NOT EXISTS
o exato momento e, portanto, ambas tentam fazerINSERT
exatamente no mesmo momento. Esse cenário muito específico acontece com muito menos frequência do que selecionar um existenteItemName
ou inserir um novoItemName
quando nenhum outro processo está tentando fazê-lo no mesmo momento .COM TODOS OS ACIMA EM MENTE: Por que prefiro minha abordagem?
Primeiro, vejamos qual bloqueio ocorre na abordagem "serializável". Como mencionado acima, o "intervalo" bloqueado depende dos valores da chave existentes em ambos os lados de onde o novo valor da chave se ajustaria. O início ou o final do intervalo também pode ser o início ou o final do índice, respectivamente, se não houver um valor de chave existente nessa direção. Suponha que temos o seguinte índice e chaves (
^
representa o início do índice e$
o final dele):Se a sessão 55 tentar inserir um valor-chave de:
A
, o intervalo # 1 (de^
aC
) está bloqueado: a sessão 56 não pode inserir um valor deB
, mesmo que único e válido (ainda). Mas sessão 56 pode inserir valores deD
,G
eM
.D
, o intervalo # 2 (deC
aF
) está bloqueado: a sessão 56 não pode inserir um valor deE
(ainda). Mas sessão 56 pode inserir valores deA
,G
eM
.M
, o intervalo # 4 (deJ
a$
) está bloqueado: a sessão 56 não pode inserir um valor deX
(ainda). Mas sessão 56 pode inserir valores deA
,D
eG
.À medida que mais valores-chave são adicionados, os intervalos entre os valores-chave se tornam mais estreitos, reduzindo assim a probabilidade / frequência de vários valores serem inseridos ao mesmo tempo, lutando pelo mesmo intervalo. É certo que este não é um problema grave e, felizmente, parece ser um problema que realmente diminui com o tempo.
O problema com minha abordagem foi descrito acima: só acontece quando duas sessões tentam inserir o mesmo valor de chave ao mesmo tempo. A esse respeito, resume-se a qual tem a maior probabilidade de acontecer: dois valores-chave diferentes, mas próximos, são tentados ao mesmo tempo ou o mesmo valor-chave é tentado ao mesmo tempo? Suponho que a resposta esteja na estrutura do aplicativo que faz as inserções, mas, em geral, eu diria que é mais provável que dois valores diferentes que compartilham o mesmo intervalo estejam sendo inseridos. Mas a única maneira de realmente saber seria testar os dois no sistema de OPs.
Em seguida, vamos considerar dois cenários e como cada abordagem os trata:
Todas as solicitações são de valores-chave exclusivos:
Nesse caso, o
CATCH
bloco na minha sugestão nunca é inserido, portanto, não há "problema" (ou seja, quatro entradas de log e o tempo necessário para fazer isso). Porém, na abordagem "serializável", mesmo com todas as pastilhas sendo únicas, sempre haverá algum potencial para bloquear outras pastilhas no mesmo intervalo (embora não por muito tempo).Alta frequência de solicitações para o mesmo valor da chave ao mesmo tempo:
Nesse caso - um grau muito baixo de exclusividade em termos de solicitações de entrada para valores-chave inexistentes - o
CATCH
bloco na minha sugestão será inserido regularmente. O efeito disso será que cada inserção com falha precisará reverter automaticamente e gravar as 4 entradas no log de transações, que é um pequeno desempenho atingido a cada vez. Mas a operação geral nunca deve falhar (pelo menos não devido a isso).(Houve um problema com a versão anterior da abordagem "atualizada" que permitia sofrer conflitos.
updlock
adicionada dica para resolver isso e ela não recebe mais conflitos.)MAS, na abordagem "serializável" (mesmo na versão otimizada e atualizada), a operação entra em conflito. Por quê? Porque oserializable
comportamento impede apenasINSERT
operações no intervalo que foi lido e, portanto, bloqueado; isso não impedeSELECT
operações nesse intervalo.A
serializable
abordagem, neste caso, parece não ter sobrecarga adicional e pode ter um desempenho um pouco melhor do que estou sugerindo.Como ocorre com muitas / a maioria das discussões sobre desempenho, devido à existência de muitos fatores que podem afetar o resultado, a única maneira de realmente ter uma noção de como algo será executado é testá-lo no ambiente de destino onde será executado. Nesse ponto, não será uma questão de opinião :).
fonte
Resposta atualizada
Resposta a @srutzky
Concordo e, pelas mesmas razões, uso parâmetros de saída quando prudentes . Foi meu erro não usar um parâmetro de saída na minha resposta inicial, eu estava sendo preguiçoso.
Aqui está um procedimento revisado usando um parâmetro de saída, otimizações adicionais, juntamente com o
next value for
que @srutzky explica em sua resposta :nota de atualização : Incluir
updlock
com o select irá capturar os bloqueios adequados nesse cenário. Obrigado a @srutzky, que apontou que isso pode causar conflitos ao usar apenasserializable
noselect
.Nota: Pode não ser o caso, mas se for possível, o procedimento será chamado com um valor para
@vValueId
, includeset @vValueId = null;
afterset xact_abort on;
, caso contrário, poderá ser removido.Sobre os exemplos de comportamento de bloqueio do intervalo de chaves de @ srutzky:
O @srutzky usa apenas um valor em sua tabela e bloqueia a tecla "next" / "infinito" nos testes para ilustrar o bloqueio do intervalo de teclas. Embora seus testes ilustrem o que acontece nessas situações, acredito que a maneira como as informações são apresentadas pode levar a falsas suposições sobre a quantidade de bloqueios que se poderia esperar ao usar
serializable
no cenário, conforme apresentado na pergunta original.Mesmo que eu perceba um viés (talvez falsamente) na maneira como ele apresenta sua explicação e exemplos de bloqueio de intervalo de teclas, eles ainda estão corretos.
Após mais pesquisas, encontrei um artigo de blog particularmente pertinente de 2011 por Michael J. Swart: Mythbusting: soluções simultâneas de atualização / inserção . Nele, ele testa vários métodos para precisão e simultaneidade. Método 4: Maior isolamento + bloqueios de ajuste fino é baseado no padrão de inserção ou atualização de Sam Saffron para o SQL Server , e o único método no teste original para atender às suas expectativas (unidas posteriormente por
merge with (holdlock)
).Em fevereiro de 2016, Michael J. Swart postou Ugg Pragmatism For The Win . Nesse post, ele aborda alguns ajustes adicionais que ele fez nos procedimentos de upsert do Saffron para reduzir o bloqueio (que eu incluí no procedimento acima).
Depois de fazer essas alterações, Michael não ficou satisfeito com o fato de seu procedimento começar a parecer mais complicado e consultou um colega chamado Chris. Chris leu todos os posts originais do Mythbusters, todos os comentários e perguntou sobre o padrão TRY CATCH JFDI da @ gbn . Esse padrão é semelhante à resposta de @ srutzky e é a solução que Michael acabou usando nessa instância.
Michael J Swart:
Na minha opinião, ambas as soluções são viáveis. Embora eu ainda prefira aumentar o nível de isolamento e ajustar os bloqueios, a resposta de @ srutzky também é válida e pode ou não ter melhor desempenho em sua situação específica.
Talvez no futuro eu também chegue à mesma conclusão que Michael J. Swart, mas ainda não estou lá.
Não é minha preferência, mas eis a minha adaptação da adaptação de Michael J. Stewart do procedimento Try Catch JFDI da @ gbn :
Se você estiver inserindo novos valores com mais frequência do que selecionando valores existentes, isso pode ter um desempenho melhor do que a versão do @ srutzky . Caso contrário, eu preferiria a versão do @ srutzky a esta.
Os comentários de Aaron Bertrand no post de Michael J Swart vinculam-se a testes relevantes que ele fez e levaram a essa troca. Trecho da seção de comentários sobre o pragmatismo feio para a vitória :
e a resposta de:
Novos links:
Resposta original
Eu ainda prefiro a abordagem upsert Sam Saffron vs usar
merge
, especialmente quando se lida com uma única linha.Eu adaptaria esse método upsert a esta situação como esta:
Eu seria consistente com sua nomeação e, como
serializable
é o mesmoholdlock
, escolha uma e seja consistente em seu uso. Eu costumo usarserializable
porque é o mesmo nome usado ao especificarset transaction isolation level serializable
.Ao usar
serializable
ouholdlock
um bloqueio de intervalo é obtido com base no valor@vName
que faz com que outras operações esperem se elas selecionarem ou inserirem valoresdbo.NameLookup
que incluam o valor nawhere
cláusula.Para que o bloqueio de intervalo funcione corretamente, é necessário haver um índice na
ItemName
coluna que se aplica ao usarmerge
também.Aqui está como seria o procedimento, principalmente seguindo os documentos técnicos de Erland Sommarskog para o tratamento de erros , usando
throw
. Sethrow
não é assim que você está gerando seus erros, altere-o para ser consistente com o restante de seus procedimentos:Para resumir o que está acontecendo no procedimento acima:
set nocount on; set xact_abort on;
como você sempre faz , então se a nossa entrada for variávelis null
ou vazia,select id = cast(null as int)
como resultado. Se não for nulo ou vazio, obtenha aId
variável for enquanto mantém esse ponto , caso não esteja lá. SeId
houver, envie-o. Se não estiver lá, insira-o e envie o novoId
.Enquanto isso, outras chamadas para esse procedimento que tentam encontrar o ID para o mesmo valor aguardam até a primeira transação ser concluída e, em seguida, selecionam e retornam. Outras chamadas para este procedimento ou outras instruções que procuram outros valores continuarão porque este não está no caminho.
Embora eu concorde com @srutzky que você pode lidar com colisões e engolir as exceções para esse tipo de problema, eu pessoalmente prefiro tentar adaptar uma solução para evitar fazer isso sempre que possível. Nesse caso, não acho que o uso dos bloqueios
serializable
seja uma abordagem pesada, e eu estaria confiante de que lidaria bem com alta simultaneidade.A citação da documentação do servidor sql nas dicas de tabela
serializable
/holdlock
:Citação da documentação do servidor sql no nível de isolamento da transação
serializable
Links relacionados à solução acima:
Padrão de inserção ou atualização para Sql Server - Sam Saffron
Documentação sobre dicas de tabela serializáveis e outras - MSDN
Tratamento de erros e transações no SQL Server, parte um - Jumpstart Tratamento de erros - Erland Sommarskog
O conselho de Erland Sommarskog sobre @@ rowcount (que não segui neste caso).
MERGE
tem um histórico irregular e parece ser necessário bisbilhotar para garantir que o código esteja se comportando como você deseja, sob toda essa sintaxe.merge
Artigos relevantes :Um interessante MERGE Bug - Paul White
Condição de corrida UPSERT com mesclagem - sqlteam
Tenha cuidado com a instrução MERGE do SQL Server - Aaron Bertrand
Posso otimizar esta declaração de mesclagem - Aaron Bertrand
Se você estiver usando visualizações indexadas e MERGE, leia isto! - Aaron Bertrand
Um último link, Kendra Little fez uma comparação aproximada de
merge
vsinsert with left join
, com a ressalva em que ela diz: "Não fiz testes de carga completos nisso", mas ainda é uma boa leitura.fonte