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).SELECT result = (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age < 18) + (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age IS NULL) ;
Desculpe se eu perdi nas milhões de versões que você testou!UNION ALL
planos (CPU de 360ms, 11k de leitura).Eu não estava disposto a restaurar um banco de dados de 110 GB para apenas uma tabela, então criei meus próprios dados . As distribuições de idade devem corresponder ao que está no Stack Overflow, mas obviamente a tabela em si não corresponde. Não acho que seja um problema demais, porque as consultas atingem os índices de qualquer maneira. Estou testando em um computador com 4 CPUs com o SQL Server 2016 SP1. Uma coisa a observar é que, para consultas que terminam isso rapidamente, é importante não incluir o plano de execução real. Isso pode atrasar bastante as coisas.
Comecei analisando algumas das soluções na excelente resposta de Erik. Para este:
Obtive os seguintes resultados de sys.dm_exec_sessions em 10 tentativas (a consulta naturalmente ficou paralela para mim):
A consulta que funcionou melhor para Erik realmente teve um desempenho pior na minha máquina:
Resultados de 10 ensaios:
Não sou capaz de explicar imediatamente por que isso é ruim, mas não está claro por que queremos forçar quase todos os operadores no plano de consulta a ficarem paralelos. No plano original, temos uma zona serial que encontra todas as linhas com
AGE < 18
. Existem apenas alguns milhares de linhas. Na minha máquina, recebo 9 leituras lógicas para essa parte da consulta e 9 ms do tempo de CPU e tempo decorrido relatados. Há também uma zona serial para o agregado global para as linhas comAGE IS NULL
mas que processa apenas uma linha por DOP. Na minha máquina, são apenas quatro linhas.Meu takeaway é que é mais importante para otimizar a parte da consulta que localiza linhas com um
NULL
paraAge
porque há milhões dessas linhas. Não consegui criar um índice com menos páginas que cobrissem os dados do que um simples comprimido na página. Suponho que exista um tamanho mínimo de índice por linha ou que muito do espaço do índice não possa ser evitado com os truques que tentei. Portanto, se estamos presos ao mesmo número de leituras lógicas para obter os dados, a única maneira de torná-lo mais rápido é tornar a consulta mais paralela, mas isso precisa ser feito de uma maneira diferente da consulta de Erik que usava TF 8649. Na consulta acima, temos uma proporção de 3,62 entre o tempo da CPU e o tempo decorrido, o que é bastante bom. O ideal seria uma proporção de 4,0 na minha máquina.Uma possível área de aprimoramento é dividir o trabalho de maneira mais uniforme entre os threads. Na captura de tela abaixo, podemos ver que uma das minhas CPUs decidiu fazer uma pequena pausa:
A varredura de índice é um dos poucos operadores que podem ser implementados em paralelo e não podemos fazer nada sobre como as linhas são distribuídas em threads. Há um elemento de chance para isso também, mas de maneira bastante consistente, eu vi um tópico mal trabalhado. Uma maneira de contornar isso é fazer o paralelismo da maneira mais difícil: na parte interna de uma junção de loop aninhada. Qualquer coisa na parte interna de um loop aninhado será implementada de maneira serial, mas muitos threads seriais podem ser executados simultaneamente. Desde que tenhamos um método de distribuição paralela favorável (como round robin), podemos controlar exatamente quantas linhas são enviadas para cada thread.
Como estou executando consultas com o DOP 4, preciso dividir igualmente as
NULL
linhas da tabela em quatro blocos. Uma maneira de fazer isso é criar vários índices em colunas calculadas:Não sei ao certo por que quatro índices separados são um pouco mais rápidos que um índice, mas foi o que encontrei nos meus testes.
Para obter um plano de loop aninhado paralelo, usarei o sinalizador de rastreamento não documentado 8649 . Também vou escrever o código um pouco estranhamente para incentivar o otimizador a não processar mais linhas do que o necessário. Abaixo está uma implementação que parece funcionar bem:
Os resultados de dez ensaios:
Com essa consulta, temos uma taxa de CPU / tempo decorrido de 3,85! Retiramos 17 ms do tempo de execução e foram necessárias apenas 4 colunas e índices computados para isso! Cada encadeamento processa muito próximo ao mesmo número de linhas no geral, porque cada índice tem muito próximo ao mesmo número de linhas e cada encadeamento verifica apenas um índice:
Em uma nota final, também podemos clicar no botão easy e adicionar um CCI não clusterizado à
Age
coluna:A seguinte consulta termina em 3 ms na minha máquina:
Vai ser difícil de vencer.
fonte
Embora eu não tenha uma cópia local do banco de dados Stack Overflow, consegui fazer algumas consultas. Meu pensamento era obter uma contagem de usuários em uma exibição do catálogo do sistema (em vez de obter diretamente uma contagem de linhas da tabela subjacente). Em seguida, obtenha uma contagem de linhas que correspondem (ou talvez não) aos critérios de Erik e faça algumas contas simples.
Eu usei o Stack Exchange Data Explorer (junto com
SET STATISTICS TIME ON;
eSET STATISTICS IO ON;
) para testar as consultas. Para um ponto de referência, aqui estão algumas consultas e as estatísticas de CPU / IO:QUERY 1
CONSULTA 2
CONSULTA 3
1ª tentativa
Isso foi mais lento que todas as perguntas de Erik que listei aqui ... pelo menos em termos de tempo decorrido.
2ª tentativa
Aqui optei por uma variável para armazenar o número total de usuários (em vez de uma subconsulta). A contagem de varredura aumentou de 1 para 17 em comparação com a 1ª tentativa. As leituras lógicas permaneceram as mesmas. No entanto, o tempo decorrido caiu consideravelmente.
Outras notas: DBCC TRACEON não é permitido no Stack Exchange Data Explorer, conforme observado abaixo:
fonte
SELECT SUM(p.Rows) - (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age >= 18 ) FROM sys.partitions p WHERE p.index_id < 2 AND p.object_id = OBJECT_ID('dbo.Users')
Usar variáveis?
Pelo comentário pode pular as variáveis
fonte
SELECT (select count(*) from table_1 where bb <= 1) + (select count(*) from table_1 where bb is null);
Bem usando
SET ANSI_NULLS OFF;
Isso é algo que surgiu na minha mente. Apenas executei isso em https://data.stackexchange.com
Mas não tão eficiente quanto @blitz_erik
fonte
Uma solução trivial é calcular contagem (*) - contagem (idade> = 18):
Ou:
Resultados aqui
fonte