Inserção do SQL Server, se não existir

243

Quero inserir dados na minha tabela, mas inserir apenas dados que ainda não existem no meu banco de dados.

Aqui está o meu código:

ALTER PROCEDURE [dbo].[EmailsRecebidosInsert]
  (@_DE nvarchar(50),
   @_ASSUNTO nvarchar(50),
   @_DATA nvarchar(30) )
AS
BEGIN
   INSERT INTO EmailsRecebidos (De, Assunto, Data)
   VALUES (@_DE, @_ASSUNTO, @_DATA)
   WHERE NOT EXISTS ( SELECT * FROM EmailsRecebidos 
                   WHERE De = @_DE
                   AND Assunto = @_ASSUNTO
                   AND Data = @_DATA);
END

E o erro é:

Mensagem 156, Nível 15, Estado 1, Procedimento EmailsRecebidosInsert, Linha 11
Sintaxe incorreta perto da palavra-chave 'WHERE'.

Francisco Carvalho
fonte
10
Você não deve confiar apenas nessa verificação para garantir que não haja duplicatas, não é seguro para threads e receberá duplicatas quando uma condição de corrida for atendida. Se você realmente precisar de dados exclusivos, adicione uma restrição exclusiva à tabela e, em seguida, pegue o erro de violação de restrição exclusiva. Veja esta resposta
GarethD
1
Você pode usar a consulta MERGE ou, se não existir (instrução select), começar a inserir valores END
Abdul Hannan Ijaz
Depende do cenário, se você deve retransmitir ou não essa verificação. Se você estiver desenvolvendo um script de implantação que grava dados em uma tabela "estática", por exemplo, isso não é um problema.
AxelWass
você pode usar "se não existir (selecione * de ..." como este stackoverflow.com/a/43763687/2736742
A. Morel
2
@ GarethD: o que você quer dizer com "thread thread safe"? Pode não ser elegante, mas parece correto para mim. Uma única insertdeclaração é sempre uma única transação. Não é como se o SQL Server avaliasse a subconsulta primeiro e depois, em algum momento posterior, e sem manter um bloqueio, continue fazendo a inserção.
Ed Avis

Respostas:

322

em vez de abaixo do código

BEGIN
   INSERT INTO EmailsRecebidos (De, Assunto, Data)
   VALUES (@_DE, @_ASSUNTO, @_DATA)
   WHERE NOT EXISTS ( SELECT * FROM EmailsRecebidos 
                   WHERE De = @_DE
                   AND Assunto = @_ASSUNTO
                   AND Data = @_DATA);
END

substituir com

BEGIN
   IF NOT EXISTS (SELECT * FROM EmailsRecebidos 
                   WHERE De = @_DE
                   AND Assunto = @_ASSUNTO
                   AND Data = @_DATA)
   BEGIN
       INSERT INTO EmailsRecebidos (De, Assunto, Data)
       VALUES (@_DE, @_ASSUNTO, @_DATA)
   END
END

Atualizada : (obrigado a @Marc Durdin por apontar)

Observe que, sob carga alta, isso ainda falha algumas vezes, porque uma segunda conexão pode passar no teste SE NÃO EXISTE antes que a primeira conexão execute o INSERT, ou seja, uma condição de corrida. Consulte stackoverflow.com/a/3791506/1836776 para obter uma boa resposta sobre por que nem mesmo o encapsulamento em uma transação resolve isso.

Imran Ali Khan
fonte
20
Observe que, sob carga alta, isso ainda falha algumas vezes, porque uma segunda conexão pode passar no teste SE NÃO EXISTE antes que a primeira conexão execute o INSERT, ou seja, uma condição de corrida. Consulte Consulte stackoverflow.com/a/3791506/1836776 para obter uma boa resposta sobre por que nem mesmo o encapsulamento em uma transação resolve isso.
Marc Durdin
11
Selecione 1 EmailsRecebidos ONDE De = @_DE E Assunto = @_ASSUNTO e dados = @_DATA de usar 1 em vez de * seria mais eficiente
Reno
1
Coloque uma trava de gravação em volta da coisa toda e você não terá chance de duplicar.
Kevin Finkenbinder 31/03
10
@jazzcat select *, neste caso, não faz diferença alguma porque está sendo usado em uma EXISTScláusula. O SQL Server sempre o otimiza e o faz há séculos. Como sou muito velho, geralmente escrevo essas consultas como, EXISTS (SELECT 1 FROM...)mas não são mais necessárias.
Loudenvier 07/10
16
Por que esse tipo de pergunta simples gera mais dúvida do que certeza?
drowa
77

Para aqueles que procuram o caminho mais rápido , me deparei recentemente com esses benchmarks onde aparentemente usar "INSERT SELECT ... EXCEPT SELECT ..." acabou sendo o mais rápido para 50 milhões de registros ou mais.

Aqui está um exemplo de código do artigo (o terceiro bloco de código foi o mais rápido):

INSERT INTO #table1 (Id, guidd, TimeAdded, ExtraData)
SELECT Id, guidd, TimeAdded, ExtraData
FROM #table2
WHERE NOT EXISTS (Select Id, guidd From #table1 WHERE #table1.id = #table2.id)
-----------------------------------
MERGE #table1 as [Target]
USING  (select Id, guidd, TimeAdded, ExtraData from #table2) as [Source]
(id, guidd, TimeAdded, ExtraData)
    on [Target].id =[Source].id
WHEN NOT MATCHED THEN
    INSERT (id, guidd, TimeAdded, ExtraData)
    VALUES ([Source].id, [Source].guidd, [Source].TimeAdded, [Source].ExtraData);
------------------------------
INSERT INTO #table1 (id, guidd, TimeAdded, ExtraData)
SELECT id, guidd, TimeAdded, ExtraData from #table2
EXCEPT
SELECT id, guidd, TimeAdded, ExtraData from #table1
------------------------------
INSERT INTO #table1 (id, guidd, TimeAdded, ExtraData)
SELECT #table2.id, #table2.guidd, #table2.TimeAdded, #table2.ExtraData
FROM #table2
LEFT JOIN #table1 on #table1.id = #table2.id
WHERE #table1.id is null

fonte
6
Gosto de EXCEPT SELECT
Bryan
1
Primeira vez que usei EXCEPT. Simples e elegante.
jhowe
Mas EXCEPT pode não ser eficiente para operações em massa.
Aasish Kr. Sharma
EXCETO não é tão eficiente.
Biswa 3/08
1
@ Bisis: Não de acordo com esses benchmarks. O código está disponível no site. Sinta-se à vontade para executá-lo em seu sistema para ver como os resultados se comparam.
25

Eu usaria uma mesclagem:

create PROCEDURE [dbo].[EmailsRecebidosInsert]
  (@_DE nvarchar(50),
   @_ASSUNTO nvarchar(50),
   @_DATA nvarchar(30) )
AS
BEGIN
   with data as (select @_DE as de, @_ASSUNTO as assunto, @_DATA as data)
   merge EmailsRecebidos t
   using data s
      on s.de = t.de
     and s.assunte = t.assunto
     and s.data = t.data
    when not matched by target
    then insert (de, assunto, data) values (s.de, s.assunto, s.data);
END
Brett Schneider
fonte
estou indo com isso porque é mais chique
jokab 08/04
Eu adoraria usar a mesclagem ... mas não funciona para tabelas otimizadas para memória.
Don Sam
20

Experimente o código abaixo

ALTER PROCEDURE [dbo].[EmailsRecebidosInsert]
  (@_DE nvarchar(50),
   @_ASSUNTO nvarchar(50),
   @_DATA nvarchar(30) )
AS
BEGIN
   INSERT INTO EmailsRecebidos (De, Assunto, Data)
   select @_DE, @_ASSUNTO, @_DATA
   EXCEPT
   SELECT De, Assunto, Data from EmailsRecebidos
END
SaravanaC
fonte
11

O INSERTcomando não possui uma WHEREcláusula - você terá que escrevê-lo assim:

ALTER PROCEDURE [dbo].[EmailsRecebidosInsert]
  (@_DE nvarchar(50),
   @_ASSUNTO nvarchar(50),
   @_DATA nvarchar(30) )
AS
BEGIN
   IF NOT EXISTS (SELECT * FROM EmailsRecebidos 
                   WHERE De = @_DE
                   AND Assunto = @_ASSUNTO
                   AND Data = @_DATA)
   BEGIN
       INSERT INTO EmailsRecebidos (De, Assunto, Data)
       VALUES (@_DE, @_ASSUNTO, @_DATA)
   END
END
marc_s
fonte
1
Você precisa manipular erros para este procedimento, pois haverá casos em que uma inserção ocorrerá entre a verificação e a inserção.
Filip De Vos
@FilipDeVos: true - uma possibilidade, talvez não muito provável, mas ainda uma possibilidade. Bom ponto.
Marc
E se você agrupar os dois em uma transação? Isso bloquearia a possibilidade? (Não sou especialista em transações, então por favor, perdoe se isso é uma pergunta estúpida.)
David
1
Consulte stackoverflow.com/a/3791506/1836776 para obter uma boa resposta sobre por que uma transação não resolve isso, @David.
Marc Durdin
Na instrução IF: não há necessidade de usar BEGIN & END se o número de linhas de comando necessárias for apenas um, mesmo se você tiver usado mais de uma linha, para que você possa omitir aqui.
Wessam El Mahdy
11

Eu fiz a mesma coisa com o SQL Server 2012 e funcionou

Insert into #table1 With (ROWLOCK) (Id, studentId, name)
SELECT '18769', '2', 'Alex'
WHERE not exists (select * from #table1 where Id = '18769' and studentId = '2')
Hovhannes Babayan
fonte
4
Claro que funcionou, você está usando uma tabela temporária (ou seja, não precisa se preocupar com simultaneidade ao usar tabelas temporárias).
drowa
6

Dependendo da sua versão (2012?) Do SQL Server, além do IF EXISTS, você também pode usar o MERGE da seguinte forma:

ALTER PROCEDURE [dbo].[EmailsRecebidosInsert]
    ( @_DE nvarchar(50)
    , @_ASSUNTO nvarchar(50)
    , @_DATA nvarchar(30))
AS BEGIN
    MERGE [dbo].[EmailsRecebidos] [Target]
    USING (VALUES (@_DE, @_ASSUNTO, @_DATA)) [Source]([De], [Assunto], [Data])
         ON [Target].[De] = [Source].[De] AND [Target].[Assunto] = [Source].[Assunto] AND [Target].[Data] = [Source].[Data]
     WHEN NOT MATCHED THEN
        INSERT ([De], [Assunto], [Data])
        VALUES ([Source].[De], [Source].[Assunto], [Source].[Data]);
END
Don
fonte
2

SQL diferente, mesmo princípio. Inserir apenas se a cláusula em que não existe falhar

INSERT INTO FX_USDJPY
            (PriceDate, 
            PriceOpen, 
            PriceLow, 
            PriceHigh, 
            PriceClose, 
            TradingVolume, 
            TimeFrame)
    SELECT '2014-12-26 22:00',
           120.369000000000,
           118.864000000000,
           120.742000000000,
           120.494000000000,
           86513,
           'W'
    WHERE NOT EXISTS
        (SELECT 1
         FROM FX_USDJPY
         WHERE PriceDate = '2014-12-26 22:00'
           AND TimeFrame = 'W')
Malcolm Swaine
fonte
-1

Conforme explicado no código abaixo: Execute as consultas abaixo e verifique você mesmo.

CREATE TABLE `table_name` (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(255) NOT NULL,
  `address` varchar(255) NOT NULL,
  `tele` varchar(255) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB;

Inserir um registro:

INSERT INTO table_name (name, address, tele)
SELECT * FROM (SELECT 'Nazir', 'Kolkata', '033') AS tmp
WHERE NOT EXISTS (
    SELECT name FROM table_name WHERE name = 'Nazir'
) LIMIT 1;
Query OK, 1 row affected (0.00 sec)
Records: 1 Duplicates: 0 Warnings: 0

SELECT * FROM `table_name`;

+----+--------+-----------+------+
| id | name   | address   | tele |
+----+--------+-----------+------+
|  1 | Nazir  | Kolkata   | 033  |
+----+--------+-----------+------+

Agora, tente inserir o mesmo registro novamente:

INSERT INTO table_name (name, address, tele)
SELECT * FROM (SELECT 'Nazir', 'Kolkata', '033') AS tmp
WHERE NOT EXISTS (
    SELECT name FROM table_name WHERE name = 'Nazir'
) LIMIT 1;

Query OK, 0 rows affected (0.00 sec)
Records: 0  Duplicates: 0  Warnings: 0

+----+--------+-----------+------+
| id | name   | address   | tele |
+----+--------+-----------+------+
|  1 | Nazir  | Kolkata   | 033  |
+----+--------+-----------+------+

Insira um registro diferente:

INSERT INTO table_name (name, address, tele)
SELECT * FROM (SELECT 'Santosh', 'Kestopur', '044') AS tmp
WHERE NOT EXISTS (
    SELECT name FROM table_name WHERE name = 'Santosh'
) LIMIT 1;

Query OK, 1 row affected (0.00 sec)
Records: 1 Duplicates: 0 Warnings: 0

SELECT * FROM `table_name`;

+----+--------+-----------+------+
| id | name   | address   | tele |
+----+--------+-----------+------+
|  1 | Nazir  | Kolkata   | 033  |
|  2 | Santosh| Kestopur  | 044  |
+----+--------+-----------+------+
vadiraj jahagirdar
fonte
1
Não é para o MySQL e a pergunta é para o SQL Server?
Douglas Gaskell
Sim, é para o MySQL.
vadiraj Jahagirdar
-2

Você poderia usar o GOcomando Isso reiniciará a execução das instruções SQL após um erro. No meu caso, tenho algumas instruções 1000 INSERT, onde já existem alguns desses registros no banco de dados, simplesmente não sei quais. Descobri que, depois de processar alguns 100, a execução para com uma mensagem de erro que não pode, INSERTpois o registro já existe. Muito chato, mas colocando um GOresolvido isso. Pode não ser a solução mais rápida, mas a velocidade não era o meu problema.

GO
INSERT INTO mytable (C1,C2,C3) VALUES(1,2,3)
GO
INSERT INTO mytable (C1,C2,C3) VALUES(4,5,6)
 etc ...
mljm
fonte
GOé um separador de lotes? Não ajuda a impedir registros duplicados.
Dale K