Quais são as diferentes maneiras de substituir ISNULL () em uma cláusula WHERE que usa apenas valores literais?

55

O que não é isso:

Esta não é uma pergunta sobre consultas abrangentes que aceitam entrada do usuário ou usam variáveis.

Trata-se estritamente de consultas ISNULL()usadas na WHEREcláusula para substituir NULLvalores por um valor canário para comparação com um predicado e de diferentes maneiras de reescrever essas consultas como SARGable no SQL Server.

Por que você não senta aqui?

Nossa consulta de exemplo é contra uma cópia local do banco de dados Stack Overflow no SQL Server 2016 e procura usuários com NULLidade ou idade <18.

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE ISNULL(u.Age, 17) < 18;

O plano de consulta mostra uma verificação de um índice não clusterizado bastante ponderado.

Nozes

O operador de verificação mostra (graças às adições ao XML do plano de execução real nas versões mais recentes do SQL Server) que lemos todas as linhas fedorentas.

Nozes

No geral, fazemos 9157 leituras e usamos cerca de meio segundo de tempo da CPU:

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 = 485 ms,  elapsed time = 483 ms.

A pergunta: Quais são as maneiras de reescrever essa consulta para torná-la mais eficiente e talvez até SARGable?

Sinta-se livre para oferecer outras sugestões. Não acho que minha resposta seja necessariamente a resposta, e há pessoas inteligentes o bastante para encontrar alternativas que podem ser melhores.

Se você quiser jogar junto no seu próprio computador, vá até aqui para baixar o banco de dados SO .

Obrigado!

Erik Darling
fonte

Respostas:

57

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 ORnos 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 malarkeyao plano de consulta.

Nozes

Observe também que o Seek é executado duas vezes aqui, o que realmente deve ser mais óbvio para o operador gráfico:

Nozes

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?).

Nozes

Faz a mesma quantidade de leituras (8233) que a ORconsulta, 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 COUNToperaçõ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);

Nozes

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.

Nozes

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.

Nozes

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.

Nozes

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 COUNTcoisas 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.

Nozes

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 );

Nozes

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);

Nozes

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 EXCEPTe com INTERSECTmais frequência do que eu.

Se você realmente precisa apenas de uma contagem, eu uso COUNTnas 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 CASEexpressã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.

Nozes

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!

Erik Darling
fonte
11
As NOT EXISTS ( INTERSECT / EXCEPT )consultas podem funcionar sem as INTERSECT / EXCEPTpartes: WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18 );Outra maneira - que usa EXCEPT: 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).
ypercubeᵀᴹ
Isso foi testado? 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!
ypercubeᵀᴹ
@ ypercubeᵀᴹ aqui está o plano para isso. É um pouco diferente, mas tem características semelhantes aos UNION ALLplanos (CPU de 360ms, 11k de leitura).
Erik Darling
Hey Erik, estava apenas vagando pelo mundo do sql e apareceu para dizer "coluna computada" apenas para incomodá-lo. <3
cadinho
17

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:

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);

Obtive os seguintes resultados de sys.dm_exec_sessions em 10 tentativas (a consulta naturalmente ficou paralela para mim):

╔══════════╦════════════════════╦═══════════════╗
 cpu_time  total_elapsed_time  logical_reads 
╠══════════╬════════════════════╬═══════════════╣
     3532                 975          60830 
╚══════════╩════════════════════╩═══════════════╝

A consulta que funcionou melhor para Erik realmente teve um desempenho pior na minha máquina:

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);

Resultados de 10 ensaios:

╔══════════╦════════════════════╦═══════════════╗
 cpu_time  total_elapsed_time  logical_reads 
╠══════════╬════════════════════╬═══════════════╣
     5704                1636          60850 
╚══════════╩════════════════════╩═══════════════╝

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 com AGE IS NULLmas 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 NULLpara Ageporque 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:

fio preguiçoso

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 NULLlinhas da tabela em quatro blocos. Uma maneira de fazer isso é criar vários índices em colunas calculadas:

ALTER TABLE dbo.Users
ADD Compute_bucket_0 AS (CASE WHEN Age IS NULL AND Id % 4 = 0 THEN 1 ELSE NULL END),
Compute_bucket_1 AS (CASE WHEN Age IS NULL AND Id % 4 = 1 THEN 1 ELSE NULL END),
Compute_bucket_2 AS (CASE WHEN Age IS NULL AND Id % 4 = 2 THEN 1 ELSE NULL END),
Compute_bucket_3 AS (CASE WHEN Age IS NULL AND Id % 4 = 3 THEN 1 ELSE NULL END);

CREATE INDEX IX_Compute_bucket_0 ON dbo.Users (Compute_bucket_0) WITH (DATA_COMPRESSION = PAGE);
CREATE INDEX IX_Compute_bucket_1 ON dbo.Users (Compute_bucket_1) WITH (DATA_COMPRESSION = PAGE);
CREATE INDEX IX_Compute_bucket_2 ON dbo.Users (Compute_bucket_2) WITH (DATA_COMPRESSION = PAGE);
CREATE INDEX IX_Compute_bucket_3 ON dbo.Users (Compute_bucket_3) WITH (DATA_COMPRESSION = PAGE);

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:

SELECT SUM(t.cnt) + (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age < 18)
FROM 
(VALUES (0), (1), (2), (3)) v(x)
CROSS APPLY 
(
    SELECT COUNT(*) cnt 
    FROM dbo.Users 
    WHERE Compute_bucket_0 = CASE WHEN v.x = 0 THEN 1 ELSE NULL END

    UNION ALL

    SELECT COUNT(*) cnt 
    FROM dbo.Users 
    WHERE Compute_bucket_1 = CASE WHEN v.x = 1 THEN 1 ELSE NULL END

    UNION ALL

    SELECT COUNT(*) cnt 
    FROM dbo.Users 
    WHERE Compute_bucket_2 = CASE WHEN v.x = 2 THEN 1 ELSE NULL END

    UNION ALL

    SELECT COUNT(*) cnt 
    FROM dbo.Users 
    WHERE Compute_bucket_3 = CASE WHEN v.x = 3 THEN 1 ELSE NULL END
) t
OPTION (QUERYTRACEON 8649);

Os resultados de dez ensaios:

╔══════════╦════════════════════╦═══════════════╗
 cpu_time  total_elapsed_time  logical_reads 
╠══════════╬════════════════════╬═══════════════╣
     3093                 803          62008 
╚══════════╩════════════════════╩═══════════════╝

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:

trabalho bem dividido

Em uma nota final, também podemos clicar no botão easy e adicionar um CCI não clusterizado à Agecoluna:

CREATE NONCLUSTERED COLUMNSTORE INDEX X_NCCI ON dbo.Users (Age);

A seguinte consulta termina em 3 ms na minha máquina:

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18 OR u.Age IS NULL;

Vai ser difícil de vencer.

Joe Obbish
fonte
7

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;e SET 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

--Erik's query From initial question.
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE ISNULL(u.Age, 17) < 18;

Tempos de execução do SQL Server: tempo de CPU = 0 ms, tempo decorrido = 0 ms. (1 linha (s) retornada)

Tabela 'Usuários'. Contagem de varreduras 17, leituras lógicas 201567, leituras físicas 0, leituras antecipadas 2740, leituras lógicas lob 0, leituras físicas lob 0, leituras antecipadas lob 0.

Tempos de execução do SQL Server: tempo de CPU = 1829 ms, tempo decorrido = 296 ms.

CONSULTA 2

--Erik's "OR" query.
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18
OR u.Age IS NULL;

Tempos de execução do SQL Server: tempo de CPU = 0 ms, tempo decorrido = 0 ms. (1 linha (s) retornada)

Tabela 'Usuários'. Contagem de varreduras 17, leituras lógicas 201567, leituras físicas 0, leituras antecipadas 0, leituras lógicas lob 0, leituras lógicas lob 0, leituras físicas lob 0, leituras antecipadas lob.

Tempos de execução do SQL Server: tempo de CPU = 2500 ms, tempo decorrido = 147 ms.

CONSULTA 3

--Erik's derived tables/UNION ALL query.
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);

Tempos de execução do SQL Server: tempo de CPU = 0 ms, tempo decorrido = 0 ms. (1 linha (s) retornada)

Tabela 'Usuários'. Contagem de varredura 34, leituras lógicas 403134, leituras físicas 0, leituras de read-ahead 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de read-ahead de lob 0.

Tempos de execução do SQL Server: tempo de CPU = 3156 ms, tempo decorrido = 215 ms.

1ª tentativa

Isso foi mais lento que todas as perguntas de Erik que listei aqui ... pelo menos em termos de tempo decorrido.

SELECT SUM(p.Rows)  -
  (
    SELECT COUNT(*)
    FROM dbo.Users AS u
    WHERE u.Age >= 18
  ) 
FROM sys.objects o
JOIN sys.partitions p
    ON p.object_id = o.object_id
WHERE p.index_id < 2
AND o.name = 'Users'
AND SCHEMA_NAME(o.schema_id) = 'dbo'
GROUP BY o.schema_id, o.name

Tempos de execução do SQL Server: tempo de CPU = 0 ms, tempo decorrido = 0 ms. (1 linha (s) retornada)

Tabela 'Mesa de trabalho'. Contagem de varreduras 0, leituras lógicas 0, leituras físicas 0, leituras de read-ahead 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de read-ahead de 0. Tabela 'sysrowsets'. Contagem de varredura 2, leituras lógicas 10, leituras físicas 0, leituras de read-ahead 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras de lob de leitura antecipada 0. Tabela 'sysschobjs'. Contagem de varreduras 1, leituras lógicas 4, leituras físicas 0, leituras de leitura antecipada 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de leitura antecipada de 0. Tabelas 'Usuários'. Contagem de varredura 1, leituras lógicas 201567, leituras físicas 0, leituras de read-ahead 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de read-ahead de lob 0.

Tempos de execução do SQL Server: tempo de CPU = 593 ms, tempo decorrido = 598 ms.

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.

DECLARE @Total INT;

SELECT @Total = SUM(p.Rows)
FROM sys.objects o
JOIN sys.partitions p
    ON p.object_id = o.object_id
WHERE p.index_id < 2
AND o.name = 'Users'
AND SCHEMA_NAME(o.schema_id) = 'dbo'
GROUP BY o.schema_id, o.name

SELECT @Total - COUNT(*)
FROM dbo.Users AS u
WHERE u.Age >= 18

Tempos de execução do SQL Server: tempo de CPU = 0 ms, tempo decorrido = 0 ms. Tabela 'Mesa de trabalho'. Contagem de varreduras 0, leituras lógicas 0, leituras físicas 0, leituras de read-ahead 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de read-ahead de 0. Tabela 'sysrowsets'. Contagem de varredura 2, leituras lógicas 10, leituras físicas 0, leituras de read-ahead 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras de lob de leitura antecipada 0. Tabela 'sysschobjs'. Contagem de varredura 1, leituras lógicas 4, leituras físicas 0, leituras de read-ahead 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de read-ahead de lob 0.

Tempos de execução do SQL Server: tempo de CPU = 0 ms, tempo decorrido = 1 ms. (1 linha (s) retornada)

Tabela 'Usuários'. Contagem de varreduras 17, leituras lógicas 201567, leituras físicas 0, leituras antecipadas 0, leituras lógicas lob 0, leituras lógicas lob 0, leituras físicas lob 0, leituras antecipadas lob.

Tempos de execução do SQL Server: tempo de CPU = 1471 ms, tempo decorrido = 98 ms.

Outras notas: DBCC TRACEON não é permitido no Stack Exchange Data Explorer, conforme observado abaixo:

O usuário 'STACKEXCHANGE \ svc_sede' não tem permissão para executar o DBCC TRACEON.

Dave Mason
fonte
11
Eles provavelmente não têm os mesmos índices que eu, daí as diferenças. E quem sabe? Talvez meu servidor doméstico esteja com um hardware melhor;) Ótima resposta!
Erik Darling
você deveria ter usado a seguinte consulta para sua primeira tentativa (será muito mais rápida, pois livra grande parte do sistema sys.objects-overhead): 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')
Thomas Franz
PS: estar ciente de que in-memory-índices (HASH NONCLUSTERED) não tem um ID de index = 0/1 como uma pilha comum / índice agrupado teria)
Thomas Franz
1

Usar variáveis?

declare @int1 int = ( select count(*) from table_1 where bb <= 1 )
declare @int2 int = ( select count(*) from table_1 where bb is null )
select @int1 + @int2;

Pelo comentário pode pular as variáveis

SELECT (select count(*) from table_1 where bb <= 1) 
     + (select count(*) from table_1 where bb is null);
paparazzo
fonte
3
Também:SELECT (select count(*) from table_1 where bb <= 1) + (select count(*) from table_1 where bb is null);
ypercubeᵀᴹ
3
Talvez queira tentar isso enquanto verifica a CPU e o IO. Dica: é o mesmo que uma das respostas de Erik.
Brent Ozar 27/03
0

Bem usando SET ANSI_NULLS OFF;

SET ANSI_NULLS OFF; 
SET STATISTICS TIME ON;
SET STATISTICS IO ON;

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE age=NULL or age<18

Table 'Users'. Scan count 17, logical reads 201567

 SQL Server Execution Times:
 CPU time = 2344 ms,  elapsed time = 166 ms.

Isso é algo que surgiu na minha mente. Apenas executei isso em https://data.stackexchange.com

Mas não tão eficiente quanto @blitz_erik

Biju jose
fonte
0

Uma solução trivial é calcular contagem (*) - contagem (idade> = 18):

SELECT
    (SELECT COUNT(*) FROM Users) -
    (SELECT COUNT(*) FROM Users WHERE Age >= 18);

Ou:

SELECT COUNT(*)
     - COUNT(CASE WHEN Age >= 18)
FROM Users;

Resultados aqui

Salman A
fonte