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 OR
ci 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 malarkey
al 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 OR
query, 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 COUNT
operazioni 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 COUNT
cose 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 EXCEPT
e INTERSECT
più spesso di me.
Se hai davvero bisogno di un conteggio
che uso COUNT
nelle 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 CASE
un'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 / EXCEPT
parti: 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).