Qual é mais rápido: NOT IN ou NOT EXISTS?

p037_a_minus_bQual a melhor forma de obter relação dos produtos que não foram vendidos nos últimos 7 dias, de modo a avaliar estratégias de promoção desses produtos?

Ou seja, como saber quais elementos estão no conjunto A mas não estão no conjunto B.

1. Introdução

p037_sumarioBancos de dados relacionais estão baseados na teoria dos conjuntos e na lógica de predicados. Para construir códigos SQL considero fundamental entender esses pilares pois fica mais simples de programar se nos atermos que estamos tratando de conjuntos.

Algo que com frequência se necessita é saber quais valores estão em uma tabela mas não estão em outra tabela. Por exemplo, tendo-se uma tabela de produtos à venda e outra de vendas efetuadas, como saber quais produtos não foram vendidos na última semana? Se ficarmos atento à teoria dos conjuntos, percebemos que nada mais é do que uma operação de diferença entre os conjuntos A e B:  A – B. Ou, graficamente,

p037_a_minus_b

Em suma, saber quais elementos existem na tabela A mas não existem na tabela B. Na linguagem SQL o operador de diferença é o MINUS, sendo que o código SQL para obter a diferença entre as tabelas A e B é

SELECT Nome_Produto from A
  MINUS
SELECT Nome_Produto from B;

Antes de prosseguir, uma observação. Desenvolver códigos T-SQL exige tempo e dedicação para estudos e testes, afora conhecimento técnico. Sempre que utilizo código T-SQL de outra pessoa como base eu informo o nome do autor e/ou o endereço web da página onde foi publicado, de modo a respeitar o esforço de programação de quem o desenvolveu. Do contrário tem-se o plágio, que além de ser uma violação dos direitos autorais é também uma demonstração de falta de ética profissional. Nesse ponto faço minhas as palavras de Edvaldo Castro em seu artigo “PLÁGIO – Sério mesmo?”. O plágio, tão condenado por toda instituição séria, acontece quando omite-se o crédito ao verdadeiro autor.

Prosseguindo, para o desenvolvimento deste artigo serão utilizadas tabelas de produtos, vendas e produtos vendidos em cada venda realizada. Os códigos T-SQL de criação e carga dessas tabelas estão no anexo deste artigo (na versão em PDF). Para facilitar o acompanhamento deste artigo este é o conteúdo de cada uma das tabelas:

PRODUTO
  COD_PRODUTO: código do produto
  DENO_PRODUTO: denominação do produto
  PREÇO_UNIT_PRODUTO: preço de cada unidade do produto

VENDA
  COD_VENDA: sequência de venda efetuada
  DATA_VENDA: data em que a venda foi efetuada
  TOTAL_VENDA:  valor total da venda

ITEM_VENDA
  COD_VENDA: chave estrangeira com a sequência de venda efetuada
  SEQ_VENDA: numeração sequencial do item, por venda
  COD_PRODUTO: código do produto
  QTD_ITENS: quantidade de unidades vendidas do produto
  PREÇO_UNIT_VENDA: preço de cada unidade do produto no momento da venda
  TOTAL_ITEM: valor da venda desse item

Este artigo está em revisão e atualização. Caso observe algo a ser melhorado ou corrigido à medida que o ler, por favor entre em contato.

2. Formas de implementar a diferença entre dois conjuntos

Para encontrar os produtos que não foram vendidos na última semana primeiro é necessário definir o que é “última semana”: os 7 dias anteriores, terminando em ontem. A operação diferença no SQL Server é implementada através do operador EXCEPT.

Inicialmente vamos obter quais foram os produtos vendidos nos últimos 7 dias:

-- código #2.1 v2
-- Autor: José Diz 
-- Publicado em: Porto SQL - https://portosql.wordpress.com

-- define período de 7 dias
declare @Hoje datetime2(2), @7Dias datetime2(2);
set @Hoje= cast (cast (sysdatetime() as date)
                 as datetime2(2));
set @7Dias= cast (dateadd (day, -7, @Hoje) as datetime2(2));
  
SELECT distinct IV.COD_PRODUTO
 from dbo.ITEM_VENDA as IV
      inner join dbo.VENDA as V on V.COD_VENDA = IV.COD_VENDA
 where V.DATA_VENDA >= @7Dias
       and V.DATA_VENDA < @Hoje;

Observe que foi utilizado DISTINCT para eliminar linhas repetidas. Afinal, ao longo de uma semana um mesmo produto pode ser vendido várias vezes e precisamos de somente uma linha por produto vendido ao longo da semana anterior.

As variáveis @Hoje e @7Dias foram declaradas com o mesmo tipo de dados da coluna DATA_VENDA, da tabela VENDA, de modo a evitar conversão automática de tipos de dados (implicit conversion) ou outras transformações. Sobre riscos com as conversões automáticas sugiro a leitura do artigo “Os perigos da conversão automática de tipos de dados”.

2.1. EXCEPT

EXCEPT é um operador relacional que implementa a operação de diferença entre conjuntos. Para o contexto deste artigo a estrutura da consulta SQL é

SELECT Nome_Produto from A
  EXCEPT
SELECT Nome_Produto from B;

Ele retorna linhas não repetidas que façam parte do resultado da primeira consulta mas que não existam no resultado da segunda consulta. Atento que existem regras para utilizar o EXCEPT:

  • a quantidade e tipo de dados das colunas devem ser os mesmos nas consultas SQL;
  • o tipo de dados coluna a coluna deve ser igual ou compatível.

O código #2.1 pode ser encapsulado em uma CTE e temos o seguinte código SQL para obter a relação de produtos vendidos na semana anterior:

-- código #2.2 v2
-- Autor: José Diz 
-- Publicado em: Porto SQL - https://portosql.wordpress.com

-- define período de 7 dias
declare @Hoje datetime2(2), @7Dias datetime2(2);
set @Hoje= cast (cast (sysdatetime() as date)
                 as datetime2(2));
set @7Dias= cast (dateadd (day, -7, @Hoje) as datetime2(2));

--
with VENDAS_7DIAS as (
SELECT distinct IV.COD_PRODUTO
  from dbo.ITEM_VENDA as IV
       inner join dbo.VENDA as V on V.COD_VENDA = IV.COD_VENDA
  where V.DATA_VENDA >= @7Dias
        and V.DATA_VENDA < @Hoje
)
SELECT COD_PRODUTO from dbo.PRODUTO
  EXCEPT
SELECT COD_PRODUTO from VENDAS_7DIAS;

O uso de CTE torna o código modular, facilitando a programação e a posterior manutenção. Sobre esse assunto recomendo a leitura do artigo “Programação modular com expressões de tabela (CTE)”.

2.2. NOT IN

Além do operador EXCEPT, há outras formas de obter o mesmo resultado sendo que duas delas fazem parte do título deste artigo e neste item vamos tratar de NOT IN.

IN é utilizado para verificar se determinado valor está contido em um conjunto de valores. Seria como o operador ∈ (“pertence a”), na matemática. Por sua vez, NOT IN é o mesmo que ∉ (“não pertence a”).

Para o contexto deste artigo a estrutura da consulta SQL é

SELECT Nome_Produto from A where Nome_Produto
  not in
(SELECT Nome_Produto from B);

Retornando às nossas tabelas, podemos então escrever o seguinte código SQL para obter quais produtos não foram vendidos nos últimos 7 dias:

-- código #2.3 v2
-- Autor: José Diz 
-- Publicado em: Porto SQL - https://portosql.wordpress.com

-- define período de 7 dias
declare @Hoje datetime2(2), @7Dias datetime2(2);
set @Hoje= cast (cast (sysdatetime() as date)
                 as datetime2(2));
set @7Dias= cast (dateadd (day, -7, @Hoje) as datetime2(2));
  
--
with VENDAS_7DIAS as (
SELECT distinct IV.COD_PRODUTO
  from dbo.ITEM_VENDA as IV
       inner join dbo.VENDA as V on V.COD_VENDA = IV.COD_VENDA
  where V.DATA_VENDA >= @7Dias
        and V.DATA_VENDA < @Hoje
)
SELECT P.COD_PRODUTO 
  from dbo.PRODUTO as P
  where P.COD_PRODUTO 
     not in 
(SELECT COD_PRODUTO from VENDAS_7DIAS);

Novamente utilizamos a CTE VENDAS_7DIAS pois ela facilita a programação do código SQL, tornando-o modular, enxuto e de mais fácil entendimento e manutenção.

O resultado do código #2.3 é o mesmo do código #2.2. E nem poderia ser diferente, pois senão teríamos algum erro na programação.

2.3. NOT EXISTS

Se no item anterior foi explicado o NOT IN, agora vamos para a outra opção descrita no título: NOT EXISTS.

EXISTS analisa o resultado de uma subconsulta e retorna verdadeiro (true) se a subconsulta retorna ao menos uma linha e falso (false) caso contrário. Internamente a função acrescenta TOP (1) no código SQL da subconsulta, de modo a agilizar o processamento; afinal, somente é necessário saber se há ou não linha retornada.

Para o contexto deste artigo a estrutura da consulta SQL fica assim:

SELECT Nome_Produto from A
  where not exists 
(SELECT * from from B where B.Nome_Produto = A.Nome_Produto);

Temos implementação de A – B mas utilizando agora NOT EXISTS.

Retornando às nossas tabelas, podemos então escrever o seguinte código SQL para obter quais produtos não foram vendidos nos últimos 7 dias:

-- código #2.4 v2
-- Autor: José Diz 
-- Publicado em: Porto SQL - https://portosql.wordpress.com

-- define período de 7 dias
declare @Hoje datetime2(2), @7Dias datetime2(2);
set @Hoje= cast (cast (sysdatetime() as date)
                 as datetime2(2));
set @7Dias= cast (dateadd (day, -7, @Hoje) as datetime2(2));

--
with VENDAS_7DIAS as (
SELECT distinct IV.COD_PRODUTO
  from dbo.ITEM_VENDA as IV
       inner join dbo.VENDA as V on V.COD_VENDA = IV.COD_VENDA
  where V.DATA_VENDA >= @7Dias
        and V.DATA_VENDA < @Hoje
)
SELECT P.COD_PRODUTO 
  from dbo.PRODUTO as P
  where not exists 
        (SELECT * from VENDAS_7DIAS as V7
          where V7.COD_PRODUTO = P.COD_PRODUTO);

Novamente utilizamos a CTE VENDAS_7DIAS pois ela facilita a programação do código SQL, tornando-o modular, enxuto e de mais fácil entendimento e manutenção.

O resultado do código #2.4 é o mesmo dos códigos #2.2 e #2.3. E nem poderia ser diferente, pois senão teríamos algum erro na programação.

2.4. LEFT OUTER JOIN

Mas além do NOT IN e do NOT EXISTS há ainda outras formas de se obter a diferença entre dois conjuntos e neste item trataremos do uso de LEFT JOIN para simular a operação.

LEFT OUTER JOIN é a junção aberta entre duas tabelas em que todas as linhas da tabela à esquerda são listadas e, havendo correspondência com linhas da tabela à direita, o conteúdo destas são listadas conjuntamente.

De forma geral, a estrutura da consulta SQL fica

SELECT Nome_Produto from A
  left join B 
       on B.Nome_Produto = A.Nome_Produto
  where B.Nome_Produto is null;

Temos simulação de A – B, utilizando agora LEFT OUTER JOIN.

Retornando às nossas tabelas, podemos então escrever o seguinte código SQL para obter quais produtos não foram vendidos nos últimos 7 dias:

-- código #2.5 v2
-- Autor: José Diz 
-- Publicado em: Porto SQL - https://portosql.wordpress.com

-- define período de 7 dias
declare @Hoje datetime2(2), @7Dias datetime2(2);
set @Hoje= cast (cast (sysdatetime() as date)
                 as datetime2(2));
set @7Dias= cast (dateadd (day, -7, @Hoje) as datetime2(2));

--
with VENDAS_7DIAS as (
SELECT distinct IV.COD_PRODUTO
  from dbo.ITEM_VENDA as IV
       inner join dbo.VENDA as V on V.COD_VENDA = IV.COD_VENDA
  where V.DATA_VENDA >= @7Dias
        and V.DATA_VENDA < @Hoje
)
SELECT P.COD_PRODUTO 
  from dbo.PRODUTO as P
       left join VENDAS_7DIAS as V7
                 on V7.COD_PRODUTO = P.COD_PRODUTO
  where V7.COD_PRODUTO is null;

Novamente utilizamos a CTE VENDAS_7DIAS pois ela facilita a programação do código SQL, tornando-o modular, enxuto e de mais fácil entendimento e manutenção.

2.5. OUTER APPLY

E para finalizar as opções de implementação do operador diferença de dois conjuntos temos o OUTER APPLY. De forma geral, a estrutura da consulta SQL fica

SELECT A.Nome_Produto 
  from A
       outer apply (SELECT T.Nome_Produto 
                      from T 
                      where T.Nome_Produto = A.Nome_Produto) as B
  where B.Nome_Produto is null;

Novamente temos A – B, mas utilizando agora OUTER APPLY.

Retornando às nossas tabelas, podemos então escrever o seguinte código SQL para obter quais produtos não foram vendidos nos últimos 7 dias:

-- código #2.6 v2
-- Autor: José Diz 
-- Publicado em: Porto SQL - https://portosql.wordpress.com

-- define período de 7 dias
declare @Hoje datetime2(2), @7Dias datetime2(2);
set @Hoje= cast (cast (sysdatetime() as date)
                 as datetime2(2));
set @7Dias= cast (dateadd (day, -7, @Hoje) as datetime2(2));
  
--
with VENDAS_7DIAS as (
SELECT distinct IV.COD_PRODUTO
  from dbo.ITEM_VENDA as IV
       inner join dbo.VENDA as V on V.COD_VENDA = IV.COD_VENDA
  where V.DATA_VENDA >= @7Dias
        and V.DATA_VENDA < @Hoje
)
SELECT P.COD_PRODUTO 
  from dbo.PRODUTO as P
       outer apply (SELECT T.COD_PRODUTO
                      from VENDAS_7DIAS as T
                      where T.COD_PRODUTO = P.COD_PRODUTO) as V
  where V.COD_PRODUTO is null;

Novamente utilizamos a CTE VENDAS_7DIAS pois ela facilita a programação do código SQL, tornando-o modular, enxuto e de mais fácil entendimento e manutenção.

3. Análise de casos

No capítulo anterior foram apresentadas 5 formas de se implementar o operador diferença entre 2 conjuntos e agora vamos comparar a performance das soluções apresentadas. O primeiro passo é gerar o plano de execução sem métricas de cada uma das soluções. Caso você não tenha conhecimento ou experiência na geração e análise de planos de execução, sugiro a leitura do artigo “O Plano Perfeito” antes de prosseguir na leitura deste artigo.

Caso você tenha interesse em acompanhar os testes deste artigo, os códigos T-SQL de criação e carga dessas tabelas estão no anexo. Os testes foram efetuados em instância com SQL Server versão 2017, edição Developer, com o banco de dados configurado com nível de compatibilidade 140. Sobre o impacto deste ajuste recomendo a leitura do artigo “Como melhorar a performance do banco de dados ao ajustar o nível de compatibilidade”.

3.1. Cenário 1

3.1.1. Primeira rodada de testes

Diagramas. Os diagramas de cada um dos planos de execução nesta primeira rodada de testes foram os seguintes:
                      clique sobre a imagem para vê-la ampliada

EXCEPT  e  NOT EXISTS

p037_codigo #2.2 - plano 1

NOT IN

p037_codigo #2.3 - plano 1

LEFT OUTER JOIN

p037_codigo #2.5 - plano 1

OUTER APPLY

p037_codigo #2.6 - plano 1

Em todos os diagramas percebe-se que a junção entre as tabelas VENDA e ITEM_VENDA foi através do operador MERGE JOIN e com leitura sequencial completa pelos índices das tabelas. Com relação à junção entre a tabela PRODUTO e o resultado da CTE VENDAS_7DIAS foi também com o operador MERGE JOIN, exceto no caso de NOT IN, que foi através do operador HASH MATCH. As diferenças entre estes operadores de junção estão descritas na página JOINS.

Quadro comparativo. Abaixo o quadro de medidas coletadas em valores disponíveis nos planos de execução com métricas:

p037_quadro_1

Os tempos de CPU e decorrido variaram muito durantes as repetições dos testes, motivo pelo qual eles não devem ser considerados (por enquanto).

Analisando os dados percebe-se que toda a tabela VENDA é lida mas para retornar menos de 1% das linhas lidas, por causa do filtro pela coluna DATA_VENDA. Se for acrescentado índice pela coluna DATA_VENDA pode-se obter melhoria na leitura da tabela VENDA.

CREATE nonclustered INDEX I2_VENDA 
  on dbo.VENDA (DATA_VENDA) include (COD_VENDA);

Atenção. A respeito de criação de índices é preciso ficar atento ao ganhaperde: ganha-se na redução de tempo de execução das consultas SQL que venham a fazer uso do novo índice mas perde-se no aumento do espaço físico utilizado pelo índice nonclustered além da necessidade do database engine manter o índice atualizado.

3.1.2. Segunda rodada de testes

O passo seguinte é descobrir porque na junção entre as tabelas VENDA e ITEM_VENDA a leitura na tabela ITEM_VENDA está sequencial em toda a tabela. Afinal, como a tabela ITEM_VENDA possui a coluna COD_VENDA como primeira coluna na definição da chave primária, então esperava uma junção do tipo NESTED LOOP e não MERGE JOIN. Mas há como forçar este comportamento ao alterar o trecho

  from dbo.ITEM_VENDA as IV
       inner join dbo.VENDA as V on V.COD_VENDA = IV.COD_VENDA

para

from dbo.VENDA as V
       inner loop join dbo.ITEM_VENDA as IV on IV.COD_VENDA = V.COD_VENDA

Observe que foi necessário alterar a ordem em que as duas tabelas foram definidas na cláusula FROM para somente após acrescentar a dica de junção LOOP. No caso de junções entre tabelas do tipo INNER JOIN tanto faz a ordem em que as tabelas são declaradas mas neste caso, pelo fato de acrescentar dica de tabela, é necessário declarar as tabelas na ordem.

Quadro comparativo 2. Após essas modificações reexecuto os códigos SQL das 5 formas de implementação e o quadro com as medidas coletadas passa a ter os seguintes valores:

p037_quadro_2

De imediato percebe-se que a quantidade de “total lidas” nas tabelas VENDA e ITEM_VENDA teve enorme redução, principalmente na tabela ITEM_VENDA. Além disso, o tempo de processamento também teve ótima redução. Entretanto, para o número de páginas lidas da tabela ITEM_VENDA ocorreu aumento, o que significa que algumas páginas foram lidas mais de uma vez. Da primeira vez a página foi lida fisicamente mas nas vezes seguintes a leitura provavelmente foi direta na memória, neste caso de massa de dados com poucas linhas na tabela ITEM_VENDA (cerca de 120 mil).

Mas os novos resultados aumentaram ainda mais as dúvidas:

  • qual o motivo de que ao acrescentar a dica de junção LOOP a performance geral tenha melhorado?
  • ganhou-se na redução de leitura e também de tempo mas e os demais recursos, houve aumento em algum deles?
  • porque o “compilador SQL” (optimizer query) não está gerando plano de execução eficiente como o ajustado manualmente?

😕

3.1.3. Terceira rodada de testes

Será que os planos de execução gerados são os ideais? Há como melhorá-los?

Separo então o código da CTE VENDAS_7DIAS e faço vários testes com PARAMETERIZATION (simple e forced), etc etc até que apelo para a opção RECOMPILE e obtenho algumas melhorias.

...
option (recompile)

A explicação dessa melhoria tem relação direta com as variáveis @Hoje e @7Dias pois o usual é a construção de um plano de execução genérico mas quando se acrescenta a opção RECOMPILE o plano de execução é gerado especificamente para os valores de @Hoje e @7Dias no momento da execução.

Diagramas.  Os diagramas dos planos de execução gerados nesta terceira rodada de testes são os seguintes:
                      clique sobre a imagem para vê-la ampliada

EXCEPT  e  NOT EXISTS

p037_codigo #2.2 - plano 3

NOT IN

p037_codigo #2.3 - plano 1

LEFT OUTER JOIN

p037_codigo #2.5 - plano 1

OUTER APPLY

p037_codigo #2.6 - plano 3

O que se observa nos diagramas anteriores é que há semelhanças entre os planos de execução de LEFT OUTER JOIN e OUTER APPLY.

Quadro comparativo. Após as alterações nos códigos SQL, a execução de cada uma das formas retornou os seguintes valores:

p037_quadro_3

Os resultados de tempo de processamento foram ainda melhores, embora nele não esteja contabilizado o tempo de compilação do código SQL.

3.2. Cenário 2

Sabemos que para um mesmo código SQL podem ser gerados vários planos de execução, dependendo principalmente das características dos dados. As 3 primeiras rodadas foram realizadas com tabela de vendas com cerca de 30 mil linhas e tabela de itens vendidos com cerca de 120 mil linhas; o que ocorre se utilizarmos uma massa de dados mais volumosa?

Foi então criada nova massa de dados, com cerca de 100 mil linhas na tabela de vendas, com datas dos 6 últimos meses, e cerca de 600 mil linhas na tabela de itens vendidos.

3.2.1. Quarta rodada de testes

Após a criação da nova massa de dados, como ficam os planos de execução e a performance de cada forma de implementação?

O resultado da execução dos códigos #2.2 a #2.6 é – novamente – surpreendente (pelo menos para mim). Os diagramas dos planos de execução são os seguintes:
                      clique sobre a imagem para vê-la ampliada

EXCEPT  e  NOT IN  NOT EXISTS

p037_codigo #2.2 - plano 4

LEFT OUTER JOIN

p037_codigo #2.5 - plano 4

OUTER APPLY

p037_codigo #2.6 - plano 4

As 3 formas de implementação iniciais geraram planos de execução semelhantes e com tempos elevados de processamento e decorrido. Já as 2 últimas, LEFT OUTER JOIN e OUTER APPLY, geraram planos de execução diferentes e com reduzido tempo de processamento, comparando-se com as 3 primeiras..

O quadro comparativo da quarta rodada de testes é o seguinte:

p037_quadro_4

É interessante novamente observar como que um mesmo código SQL e um mesmo banco de dados pode gerar planos de execução dependendo dos dados armazenados, dentre outros fatores.

3.2.1. Quinta rodada de testes

E se novamente acrescentarmos a opção RECOMPILE nos códigos SQL, qual seria o resultado com a nova massa de dados?

Os diagramas dos planos de execução são os seguintes:
                      clique sobre a imagem para vê-la ampliada

EXCEPT  e  NOT IN  NOT EXISTS

p037_codigo #2.2 - plano 5

LEFT OUTER JOIN

p037_codigo #2.5 - plano 5

OUTER APPLY

p037_codigo #2.6 - plano 5

E o quadro comparativo é o seguinte:

p037_quadro_5

Uau! Como uma simples modificação no código SQL alterou completamente os planos de execução gerados para as formas de implementação EXCEPT, NOT IN e NOT EXISTS!

Mas observe que toda a tabela ITEM_VENDA foi lida nas 5 formas, quando esperava que fossem lidas somente as linhas referentes às 3.808 vendas do período. E se tivermos 10 milhões de linhas na tabela ITEM_VENDA, todas elas serão lidas?

Com relação aos resultados destas quinta rodada de testes, uma forma de reduzir a quantidade de “linhas lidas” na tabela ITEM_VENDA pode ser o mesmo recurso que foi utilizado na segunda rodada: acrescentar a dica LOOP na junção entre as tabelas. É fato que o número de páginas lidas aumentará.

4. Finalizando o artigo…

Ponto 1. Embora no título foram mencionadas as construções NOT IN e NOT EXISTS, o objetivo do artigo foi o de demonstrar algumas formas de implementar código SQL para obter quais valores estão em uma tabela mas não estão em outra tabela e comparar a performance dessas formas. Como exemplo foram utilizadas tabelas de produtos à venda e de vendas efetuadas e com a seguinte pergunta: “Como saber quais produtos não foram vendidos na última semana?”.

No capítulo 2 foram descritas 5 formas de implementar a diferença entre dois conjuntos, sendo que as que percebo de uso mais frequente são NOT IN e NOT EXISTS; algum motivo especial para isto?

A seguir, no capítulo 3 foram realizados vários testes com as 5 formas de implementação e disponibilizados os diagramas dos planos de execução bem como quadro comparativo com algumas métricas. Ao longo dos testes, o uso da opção para recompilar o código SQL da consulta a cada execução, utilizada nas terceira e quinta  rodadas de testes, fez com que planos de execução fossem gerados especificamente para os valores de @7Dias e @Hoje, otimizando assim a consulta SQL. Algumas surpresas surgiram nos resultados da quarta rodada; quem leu este artigo com atenção sabe a que me refiro.

Nos testes foram coletadas algumas métricas mas seria necessário analisar a fundo cada plano de execução, esmiuçando as informações de cada operador. No artigo “O Plano Perfeito”, há algumas sugestões de itens a analisar e um deles é um dos critérios utilizado pelo query optimizer para gerar o plano de execução são as estatísticas, incluindo a cardinalidade das colunas envolvidas. Assim, comparar as estatísticas de número estimado de linhas e número real de linhas pode fornecer informações sobre a acurácia das estatísticas e da qualidade do plano de execução. Não foi possível um exame completo de cada plano de execução, pois senão este artigo teria dezenas de páginas.

Ponto 2. Ao analisar outros artigos sobre o assunto percebe-se que em alguns foram utilizados casos em que a situação é otimizada para se obter determinada conclusão. Por exemplo, no artigo “Should I use NOT IN, OUTER APPLY, LEFT OUTER JOIN, EXCEPT, or NOT EXISTS?”, de Aaron Bertrand, ele afirma logo no início do artigo que “Instead of NOT IN, use a correlated NOT EXISTS for this query pattern. Always. Ou seja, segundo ele NOT EXISTS sempre é mais rápido. Entretanto, ele cita “for this query pattern“. Que padrão de consulta? Consulta direta entre somente duas tabelas e que possuam índices que permitam a junção direta entre as tabelas. Ora, aí é mamão com açúcar, ?

Já no artigo “Consider using [NOT] EXISTS instead of [NOT] IN with a subquery”, de Phil Streiff, consta que “It used to be that the EXISTS logical operator was faster than IN, when comparing data sets using a subquery. (…) However, the query optimizer now treats EXISTS and IN the same way, whenever it can, so you’re unlikely to see any significant performance differences”. O sublinhado foi acrescentado por mim, para dar destaque ao segundo parágrafo.

Ponto 3. Não me esqueci do NULL e o impacto em construções com NOT IN. Oportunamente o assunto será acrescentado neste artigo.

Ponto final. Este artigo serviu para me ensinar que não há como definir qual é mais rápido – NOT EXISTS ou NOT IN – sem conhecer o cenário e que cada caso é um caso e também que a cada momento o caso se altera


Gostou deste artigo? Então clica no botão CURTIR aí embaixo.


Este artigo faz parte da série “Qual é mais rápido?”.


Este artigo está em revisão e atualização. Caso tenha observado algo a ser melhorado ou corrigido, por favor entre em contato.

PDF
Este artigo também está disponível em arquivo no formato PDF, o que possibilita tanto visualizar online quanto obter cópia (download) para leitura offline e até mesmo impressão. Clique no texto abaixo para obter o artigo completo.

Qual é mais rápido: NOT IN ou NOT EXISTS?

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google

Você está comentando utilizando sua conta Google. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s