Seção de resposta
Existem várias maneiras de reescrever isso usando diferentes construções T-SQL. Analisaremos os prós e contras e faremos uma comparação geral abaixo.
Primeiro : UsandoOR
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18
OR u.Age IS NULL;
Usar OR
nos fornece um plano de busca mais eficiente, que lê o número exato de linhas necessárias, no entanto, adiciona o que o mundo técnico chama a whole mess of malarkey
ao plano de consulta.
Observe também que o Seek é executado duas vezes aqui, o que realmente deve ser mais óbvio para o operador gráfico:
Table 'Users'. Scan count 2, logical reads 8233, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 469 ms, elapsed time = 473 ms.
Segundo : usar tabelas derivadas com UNION ALL
Nossa consulta também pode ser reescrita dessa maneira
SELECT SUM(Records)
FROM
(
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records);
Isso gera o mesmo tipo de plano, com muito menos malarkey e um grau mais aparente de honestidade sobre quantas vezes o índice foi procurado (procurado?).
Faz a mesma quantidade de leituras (8233) que a OR
consulta, mas reduz cerca de 100ms do tempo da CPU.
CPU time = 313 ms, elapsed time = 315 ms.
No entanto, é preciso ter muito cuidado aqui, porque se esse plano tentar ficar paralelo, as duas COUNT
operações separadas serão serializadas, porque cada uma é considerada um agregado escalar global. Se forçarmos um plano paralelo usando o Trace Flag 8649, o problema se tornará óbvio.
SELECT SUM(Records)
FROM
(
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records)
OPTION(QUERYTRACEON 8649);
Isso pode ser evitado alterando ligeiramente nossa consulta.
SELECT SUM(Records)
FROM
(
SELECT 1
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT 1
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records)
OPTION(QUERYTRACEON 8649);
Agora, os dois nós que executam uma busca são totalmente paralelizados até atingirmos o operador de concatenação.
Pelo que vale a pena, a versão totalmente paralela tem alguns bons benefícios. Ao custo de mais 100 leituras e cerca de 90ms de tempo adicional da CPU, o tempo decorrido diminui para 93ms.
Table 'Users'. Scan count 12, logical reads 8317, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 500 ms, elapsed time = 93 ms.
E o CROSS APPLY?
Nenhuma resposta está completa sem a mágica de CROSS APPLY
!
Infelizmente, temos mais problemas com COUNT
.
SELECT SUM(Records)
FROM dbo.Users AS u
CROSS APPLY
(
SELECT COUNT(Id)
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age IS NULL
) x (Records);
Esse plano é horrível. Esse é o tipo de plano com o qual você termina quando aparece pela última vez no dia de São Patrício. Embora bem paralelo, por algum motivo está digitalizando o PK / CX. Ai credo. O plano tem um custo de 2198 dólares de consulta.
Table 'Users'. Scan count 7, logical reads 31676233, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 29532 ms, elapsed time = 5828 ms.
O que é uma escolha estranha, porque se forçarmos o uso do índice não clusterizado, o custo cairá significativamente para 1798 dólares de consulta.
SELECT SUM(Records)
FROM dbo.Users AS u
CROSS APPLY
(
SELECT COUNT(Id)
FROM dbo.Users AS u2 WITH (INDEX(ix_Id_Age))
WHERE u2.Id = u.Id
AND u2.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u2 WITH (INDEX(ix_Id_Age))
WHERE u2.Id = u.Id
AND u2.Age IS NULL
) x (Records);
Ei, procura! Vejo você por lá. Observe também que, com a mágica de CROSS APPLY
, não precisamos fazer nada bobo para ter um plano quase totalmente paralelo.
Table 'Users'. Scan count 5277838, logical reads 31685303, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 27625 ms, elapsed time = 4909 ms.
A aplicação cruzada acaba se saindo melhor sem as COUNT
coisas lá.
SELECT SUM(Records)
FROM dbo.Users AS u
CROSS APPLY
(
SELECT 1
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age < 18
UNION ALL
SELECT 1
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age IS NULL
) x (Records);
O plano parece bom, mas as leituras e a CPU não são uma melhoria.
Table 'Users'. Scan count 20, logical reads 17564, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 4844 ms, elapsed time = 863 ms.
Reescrever a cruz aplica-se a uma junção derivada resulta exatamente no mesmo tudo. Não vou publicar novamente o plano de consulta e as informações estatísticas - elas realmente não mudaram.
SELECT COUNT(u.Id)
FROM dbo.Users AS u
JOIN
(
SELECT u.Id
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT u.Id
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x ON x.Id = u.Id;
Álgebra relacional : Para ser completo, e para impedir Joe Celko de assombrar meus sonhos, precisamos pelo menos tentar algumas coisas relacionais estranhas. Aqui não vai nada!
Uma tentativa com INTERSECT
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18
INTERSECT
SELECT u.Age WHERE u.Age IS NOT NULL );
Table 'Users'. Scan count 1, logical reads 9157, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 1094 ms, elapsed time = 1090 ms.
E aqui está uma tentativa com EXCEPT
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18
EXCEPT
SELECT u.Age WHERE u.Age IS NULL);
Table 'Users'. Scan count 7, logical reads 9247, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 2126 ms, elapsed time = 376 ms.
Pode haver outras maneiras de escrever isso, mas deixarei isso para as pessoas que talvez usem EXCEPT
e com INTERSECT
mais frequência do que eu.
Se você realmente precisa apenas de uma contagem,
eu uso COUNT
nas minhas consultas um pouco de abreviação (leia-se: estou com preguiça de encontrar cenários mais envolvidos às vezes). Se você precisar apenas de uma contagem, poderá usar uma CASE
expressão para fazer exatamente a mesma coisa.
SELECT SUM(CASE WHEN u.Age < 18 THEN 1
WHEN u.Age IS NULL THEN 1
ELSE 0 END)
FROM dbo.Users AS u
SELECT SUM(CASE WHEN u.Age < 18 OR u.Age IS NULL THEN 1
ELSE 0 END)
FROM dbo.Users AS u
Ambos têm o mesmo plano e têm a mesma CPU e características de leitura.
Table 'Users'. Scan count 1, logical reads 9157, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 719 ms, elapsed time = 719 ms.
O vencedor?
Nos meus testes, o plano paralelo forçado com SUM sobre uma tabela derivada teve o melhor desempenho. E sim, muitas dessas consultas poderiam ter sido ajudadas adicionando alguns índices filtrados para explicar os dois predicados, mas eu queria deixar algumas experiências para outras.
SELECT SUM(Records)
FROM
(
SELECT 1
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT 1
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records)
OPTION(QUERYTRACEON 8649);
Obrigado!
NOT EXISTS ( INTERSECT / EXCEPT )
consultas podem funcionar sem asINTERSECT / EXCEPT
partes:WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18 );
Outra maneira - que usaEXCEPT
:SELECT COUNT(*) FROM (SELECT UserID FROM dbo.Users EXCEPT SELECT UserID FROM dbo.Users WHERE u.Age >= 18) AS u ;
(onde UserID é a PK ou qualquer coluna não nula exclusiva).