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

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

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

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 / EXCEPTpartes: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).