Sezione risposta
Esistono vari modi per riscriverlo usando diversi costrutti T-SQL. Esamineremo i pro e i contro e faremo un confronto generale di seguito.
Primo : utilizzoOR
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18
OR u.Age IS NULL;
L'utilizzo ORci fornisce un piano di ricerca più efficiente, che legge il numero esatto di righe di cui abbiamo bisogno, tuttavia aggiunge ciò che il mondo tecnico chiama a whole mess of malarkeyal piano di query.
Si noti inoltre che la ricerca viene eseguita due volte qui, il che dovrebbe essere più evidente dall'operatore grafico:

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.
Secondo : utilizzare anche le tabelle derivate con UNION ALL
La nostra query può essere riscritto in questo modo
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);
Ciò produce lo stesso tipo di piano, con molto meno malarkey e un grado più evidente di onestà su quante volte l'indice è stato cercato (cercato?).

Fa la stessa quantità di letture (8233) della ORquery, ma riduce circa 100 ms di tempo libero della CPU.
CPU time = 313 ms, elapsed time = 315 ms.
Tuttavia, devi stare molto attento qui, perché se questo piano tenta di andare in parallelo, le due COUNToperazioni separate saranno serializzate, perché ognuna è considerata un aggregato scalare globale. Se forziamo un piano parallelo usando Trace Flag 8649, il problema diventa ovvio.
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);

Questo può essere evitato modificando leggermente la nostra query.
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);
Ora entrambi i nodi che eseguono una ricerca sono completamente parallelizzati fino a quando non colpiamo l'operatore di concatenazione.

Per quello che vale, la versione completamente parallela ha dei buoni benefici. Al costo di circa 100 letture in più e circa 90 ms di tempo di CPU aggiuntivo, il tempo trascorso si riduce a 93 ms.
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.
Che dire di CROSS APPLY?
Nessuna risposta è completa senza la magia di CROSS APPLY!
Sfortunatamente, incontriamo più problemi con 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);
Questo piano è orribile. Questo è il tipo di piano che finisci quando ti presenti per l'ultima volta al giorno di San Patrizio. Sebbene ben parallelo, per qualche motivo sta eseguendo la scansione del PK / CX. Ew. Il piano ha un costo di 2198 dollari di query.

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.
È una scelta strana, perché se lo forziamo a utilizzare l'indice non cluster, il costo scende in modo significativo a 1798 query buck.
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);
Ehi, cerca! Dai un'occhiata laggiù. Nota anche che con la magia di CROSS APPLY, non abbiamo bisogno di fare qualcosa di sciocco per avere un piano per lo più completamente parallelo.

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.
L'applicazione incrociata finisce per andare meglio senza le COUNTcose lì dentro.
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);
Il piano sembra buono, ma le letture e la CPU non sono un miglioramento.

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.
La riscrittura della croce si applica per essere un derivato derivato nello stesso identico tutto. Non ho intenzione di ripubblicare il piano di query e le informazioni sulle statistiche, in realtà non sono cambiate.
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;
Algebra relazionale : per essere accurati e per impedire a Joe Celko di perseguitare i miei sogni, dobbiamo almeno provare alcune strane cose relazionali. Qui non succede niente!
Un tentativo con 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.
Ed ecco un tentativo con 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.
Potrebbero esserci altri modi per scriverli, ma lo lascerò alle persone che forse usano EXCEPTe INTERSECTpiù spesso di me.
Se hai davvero bisogno di un conteggio
che uso COUNTnelle mie query come un po 'di stenografia (leggi: sono troppo pigro per inventare scenari più coinvolti a volte). Se hai solo bisogno di un conteggio, puoi usare CASEun'espressione per fare quasi la stessa cosa.
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
Entrambi hanno lo stesso piano e hanno la stessa CPU e caratteristiche di lettura.

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.
Il vincitore?
Nei miei test, il piano parallelo forzato con SUM su una tabella derivata ha dato i risultati migliori. E sì, molte di queste query avrebbero potuto essere aiutate aggiungendo un paio di indici filtrati per tenere conto di entrambi i predicati, ma volevo lasciare qualche sperimentazione ad altri.
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);
Grazie!
NOT EXISTS ( INTERSECT / EXCEPT )query possono funzionare senza leINTERSECT / EXCEPTparti: unWHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18 );altro modo - che utilizzaEXCEPT:SELECT COUNT(*) FROM (SELECT UserID FROM dbo.Users EXCEPT SELECT UserID FROM dbo.Users WHERE u.Age >= 18) AS u ;(dove UserID è il PK o qualsiasi colonna non nulla univoca).