Comparando Semi Joins
Publicado; 18/05/2013 Arquivado em: SQL Optimizer, SQL T-SQL Mitos 1 comentárioExistem alguns mitos envolvendo o que conceitualmente é conhecido como semi join, e, infelizmente, tais mitos acabam por prejudicar a escrita de consultas, causando problemas de performance. Mas, o que são semi joins? São joins que retornam linhas de uma tabela A baseado na existência de linhas correlacionadas em uma tabela B. Se a consulta retorna apenas atributos (campos) da tabela da esquerda, então o join é chamado de Left Semi Join; se retorna apenas atributos da tabela da direita, então é chamado de Right Semi Join. Um semi join pode ser produzido usando INNER JOINS, EXISTS, IN e também com o INTERSECT.
Antes de entrarmos em mais detalhes, listemos os mitos comuns envolvendo semi joins:
· JOIN versus EXISTS
· IN versus EXISTS
Será que realmente existe ganho de performance ao adotar, por exemplo, o EXISTS ao invés do IN? Iremos clarificar essa e outras questões no decorrer do artigo.
Preparando o ambiente
Antes de entrarmos diretamente no assunto, vamos executar o código abaixo para a criação do nosso ambiente de estudo. Estou utilizando o banco AdventureWorks2012 como referência.
— Criar o ambiente
use tempdb
go
CREATE TABLE dbo.Clientes
( ClienteID INT NOT NULL PRIMARY KEY,
Nome VARCHAR(50) NOT NULL,
Sobrenome VARCHAR(50) NOT NULL,
);
INSERT dbo.Clientes
SELECT
c.CustomerID
, p.FirstName
, p.LastName
FROM AdventureWorks2012.Sales.Customer c
JOIN AdventureWorks2012.Person.Person p
ON c.PersonID = p.BusinessEntityID
GO
CREATE TABLE dbo.Vendas
( VendasID INT NOT NULL PRIMARY KEY,
Data DATETIME NOT NULL,
ClienteID INT NOT NULL FOREIGN KEY REFERENCES dbo.Clientes (ClienteID),
TotalDaVenda DECIMAL(15,4) NOT NULL
);
INSERT dbo.Vendas
SELECT TOP 40 PERCENT
h.SalesOrderID
, h.OrderDate
, h.CustomerID
, h.TotalDue
FROM AdventureWorks2012.Sales.SalesOrderHeader h;
CREATE NONCLUSTERED INDEX AK_Vendas_ClienteID
on dbo.Vendas (ClienteID)
GO
INNER JOIN versus IN versus EXISTS
Imagine a seguinte sentença: recuperar todos os clientes que já realizaram compras na empresa?
— 1) Usando INNER JOIN com DISTINCT
SELECT DISTINCT c.ClienteID, c.Nome, c.Sobrenome
FROM dbo.Clientes c
JOIN dbo.Vendas v ON v.ClienteID = c.ClienteID
— 2) Usando IN
SELECT c.ClienteID, c.Nome, c.Sobrenome
FROM dbo.Clientes c
WHERE c.ClienteID IN
( SELECT v.ClienteID FROM dbo.Vendas v )
— 3) Usando EXISTS
SELECT c.ClienteID, c.Nome, c.Sobrenome
FROM dbo.Clientes c
WHERE EXISTS
( SELECT 1 FROM dbo.Vendas v
WHERE v.ClienteID = c.ClienteID )
Veja que:
1. Os resultados gerados são os mesmos;
2. Os planos de execução são idênticos.
Nos exemplos expostos anteriormente, para cada linha da tabela A o otimizador necessita checar se existe pelo menos uma linha correspondente na tabela B (lado oposto). Isso significa que o otimizador não precisa percorrer todas as linhas da outra tabela. Existe por aí a ideia equivocada de que quando usamos o IN o otimizador obrigatoriamente percorre todas as linhas da tabela B (o que não é verdade). O IN segue basicamente o mesmo princípio do EXISTS: fazer teste de existência. Atente que para alcançarmos o LEFT SEMI JOIN a partir do INNER JOIN, foi necessário restringir a lista de colunas da cláusula SELECT para recuperar apenas as colunas da tabela A e usarmos o DISTINCT para suprimir as linhas redundantes (este é o segredo). Apesar de não ter sido demonstrado anteriormente, também é possível alcançar um semi-join através do operador INTERSECT.
Anti-Semi Join (OUTER JOIN versus NOT IN versus NOT EXISTS)
O inverso de um semi join é um anti-semi join, que ocorre quando linhas em uma tabela A não possuem correspondentes em uma tabela B (é baseado na não existência). É possível alcançar um anti-semi join através de subconsultas utilizando NOT IN ou EXISTS, ou ainda através do operador EXCEPT. Também é possível alcançar um anti-semi join utilizando outer join, filtrando somente as outer rows.
Imagine a seguinte sentença: recuperar todos os clientes que nunca realizaram compras na empresa?
— 1) Usando NOT IN
SELECT c.ClienteID, c.Nome, c.Sobrenome
FROM dbo.Clientes c
WHERE c.ClienteID NOT IN
( SELECT v.ClienteID FROM dbo.Vendas v )
— 2) Usando NOT EXISTS
SELECT c.ClienteID, c.Nome, c.Sobrenome
FROM dbo.Clientes c
WHERE NOT EXISTS
( SELECT 1 FROM dbo.Vendas v
WHERE v.ClienteID = c.ClienteID )
Vejamos o que acontece quando utilizamos um LEFT OUTER JOIN.
— 3) Usando LEFT OUTER JOIN
SELECT c.ClienteID, c.Nome, c.Sobrenome
FROM dbo.Clientes c
LEFT JOIN dbo.Vendas v ON v.ClienteID = c.ClienteID
WHERE v.ClienteID IS NULL
O resultado da consulta é igual, mas o plano de execução gerado não é o mesmo. Veja que apesar de alcançarmos os mesmos resultados, explicitamente não aparece um Anti Semi Join quando usamos o LEFT OUTER JOIN. Se compararmos a performance das consultas veremos que o NOT EXISTS é mais eficiente. Por conta disso, deixo a seguinte dica: se você precisar recuperar linhas baseadas na não existência em outra tabela, prefira NOT EXISTS ou NOT IN; evite usar OUTER JOIN como foi demonstrado anteriormente.
Mito T-SQL: COUNT(1) é mais rápido que COUNT(*)
Publicado; 08/06/2012 Arquivado em: SQL T-SQL Mitos 2 ComentáriosSegue um novo artigo tratando de mais um dos mitos do mundo T-SQL. Para facilitar a leitura estou disponibilizando o PDF do artigo para download.
Mito T-SQL: Cláusula GROUP BY garante ordenação do result set
Publicado; 10/05/2012 Arquivado em: SQL T-SQL Mitos | Tags: Boas Práticas T-SQL, Mitos SQL Server, Mitos T-SQL, T-SQL Deixe um comentárioDe vez em quando alguém me questiona: é realmente necessário explicitar uma cláusula ORDER BY quando utilizo um GROUP BY? O GROUP BY não força o ordenamento da query?
A resposta que costumo deixar é “Não. GROUP BY não garante ordenação”. Isso é fato e o contrário é mito.
Vejamos alguns exemplos simples que comprovam essa questão.
use Northwind
go
— Esta query não retorna resultado ordenado
select o.ShipCountry, count(*) qt
from dbo.Orders o
group by o.ShipCountry
— Result set
— Plano de execução
Observe que no plano de execução não existe o operador SORT, responsável justamente por fazer a ordenação.
No segundo exemplo, a seguir, estou agrupando por duas colunas e deixando sem cláusula ORDER BY.
— Agrupando com duas colunas sem ORDER BY
select o.ShipCountry, o.ShipCity, count(*) qt
from dbo.Orders o
group by o.ShipCountry, o.ShipCity
Vamos agora observar o plano de execução para entendermos o que o otimizador fez.
Inicialmente nota-se que foi incluído um Sort, justamente porque estamos agrupando por mais de uma coluna. Mas qual o critério de ordenação nesse caso?
Veja que o otimizador escolheu ordenar de “forma contrária”: primeiro por cidade e depois por país. Portanto, o resultado não será o esperado, já que para esse exemplo, o natural é que os registros sejam listados na ordem Páis – Cidade.
Assim feito, só nos resta aplicar uma tradicional clásula ORDER BY para garantir a ordenação esperada pelo usuário.
select o.ShipCountry, o.ShipCity, count(*) qt
from dbo.Orders o
group by o.ShipCountry, o.ShipCity
order by o.ShipCountry, o.ShipCity
O plano de execução gerado é o mesmo, exceto que agora, para o operador Sort, o otimizador seguiu a ordem explicitada em ORDER BY.
Segue o result set:
Até o próximo post.