[Vedi un indice di tutti i post sulle cattive abitudini/migliori pratiche]
Una delle diapositive della mia presentazione ricorrente su Cattive abitudini e buone pratiche è intitolata "Abuso di COUNT(*)
." Vedo questo abuso un po' in natura e assume diverse forme.
Quante righe nella tabella?
Di solito vedo questo:
SELECT @count = COUNT(*) FROM dbo.tablename;
SQL ServerSQL Server deve eseguire un'analisi di blocco sull'intera tabella per ricavare questo conteggio. Questo è costoso. Queste informazioni sono memorizzate nelle viste del catalogo e nei DMV e puoi ottenerle senza tutti quegli I/O o blocchi:
SELECT @count = SUM(p.rows) FROM sys.partitions AS p INNER JOIN sys.tables AS t ON p.[object_id] = t.[object_id] INNER JOIN sys.schemas AS s ON t.[schema_id] = s.[schema_id] WHERE p.index_id IN (0,1) -- heap or clustered index AND t.name = N'tablename' AND s.name = N'dbo';
(Puoi ottenere le stesse informazioni da sys.dm_db_partition_stats
, ma in tal caso cambia p.rows
a p.row_count
(Sì, coerenza!). In effetti, questa è la stessa vista utilizzata da sp_spaceused
utilizza per derivare il conteggio e, sebbene sia molto più semplice da digitare rispetto alla query precedente, sconsiglio di utilizzarlo solo per derivare un conteggio a causa di tutti i calcoli extra che fa, a meno che tu non voglia anche quelle informazioni. Nota inoltre che utilizza funzioni di metadati che non obbediscono al tuo livello di isolamento esterno, quindi potresti finire per aspettare il blocco quando chiami questa procedura.)
Ora, è vero che queste visualizzazioni non sono accurate al 100%, al microsecondo. A meno che tu non stia utilizzando un heap, è possibile ottenere un risultato più affidabile da sys.dm_db_index_physical_stats()
colonna record_count
(ehi, di nuovo coerenza!), tuttavia questa funzione può avere un impatto sulle prestazioni, può comunque bloccarsi e può essere anche più costosa di un SELECT COUNT(*)
– deve eseguire le stesse operazioni fisiche, ma deve calcolare informazioni aggiuntive a seconda della mode
(come la frammentazione, che in questo caso non ti interessa). L'avviso nella documentazione racconta parte della storia, rilevante se stai utilizzando i gruppi di disponibilità (e probabilmente influisce in modo simile sul mirroring del database):
La documentazione spiega anche perché questo numero potrebbe non essere affidabile per un heap (e fornisce anche un quasi passaggio per le righe rispetto all'incoerenza dei record):
Per un heap, il numero di record restituiti da questa funzione potrebbe non corrispondere al numero di righe restituite eseguendo un SELECT COUNT(*) sull'heap. Ciò è dovuto al fatto che una riga può contenere più record. Ad esempio, in alcune situazioni di aggiornamento, una singola riga dell'heap potrebbe avere un record di inoltro e un record inoltrato come risultato dell'operazione di aggiornamento. Inoltre, le righe LOB più grandi sono suddivise in più record nell'archiviazione LOB_DATA.
Quindi mi orienterei verso sys.partitions
come modo per ottimizzarlo, sacrificando un po' di precisione marginale.
- "Ma non posso usare i DMV; il mio conteggio deve essere super preciso!"
Un conteggio "super accurato" è in realtà piuttosto privo di significato. Consideriamo che la tua unica opzione per un conteggio "super accurato" è bloccare l'intera tabella e impedire a chiunque di aggiungere o eliminare righe (ma senza impedire letture condivise), ad esempio:
SELECT @count = COUNT(*) FROM dbo.table_name WITH (TABLOCK); -- not TABLOCKX!
Quindi, la tua query sta ronzando, scansionando tutti i dati, lavorando per quel conteggio "perfetto". Nel frattempo, le richieste di scrittura vengono bloccate e in attesa. Improvvisamente, quando viene restituito il conteggio accurato, i blocchi sul tavolo vengono rilasciati e tutte quelle richieste di scrittura che erano in coda e in attesa, iniziano a sparare tutti i tipi di inserimenti, aggiornamenti ed eliminazioni sul tavolo. Quanto è "super preciso" il tuo conteggio ora? Valeva la pena ottenere un conteggio "accurato" che è già orribilmente obsoleto? Se il sistema non è occupato, questo non è un grosso problema, ma se il sistema non è occupato, direi abbastanza fermamente che i DMV saranno dannatamente accurati.
Avresti potuto usare NOLOCK
invece, ma ciò significa solo che gli scrittori possono modificare i dati mentre li stai leggendo e porta anche ad altri problemi (ne ho parlato di recente). Va bene per molti campi da baseball, ma non se il tuo obiettivo è la precisione. I DMV saranno attivi (o almeno molto più vicini) in molti scenari e più lontani in pochissimi (in effetti nessuno a cui riesco a pensare).
Infine, puoi utilizzare Read Committed Snapshot Isolation. Kendra Little ha un fantastico post sui livelli di isolamento delle istantanee, ma ripeterò l'elenco delle avvertenze che ho menzionato nel mio NOLOCK
articolo:
- Le serrature Sch-S devono ancora essere prese anche sotto RCSI.
- I livelli di isolamento delle istantanee utilizzano il controllo delle versioni delle righe in tempdb, quindi è necessario testare l'impatto lì.
- RCSI non può utilizzare scansioni efficienti degli ordini di allocazione; vedrai invece le scansioni dell'intervallo.
- Paul White (@SQL_Kiwi) ha degli ottimi post che dovresti leggere su questi livelli di isolamento:
- Leggi l'isolamento dello snapshot impegnato
- Modifiche dei dati in isolamento di istantanee confermate in lettura
- Il livello di isolamento SNAPSHOT
Inoltre, anche con RCSI, ottenere il conteggio "accurato" richiede tempo (e risorse aggiuntive in tempdb). Al termine dell'operazione, il conteggio è ancora accurato? Solo se nel frattempo nessuno ha toccato il tavolo. Quindi uno dei vantaggi di RCSI (i lettori non bloccano gli scrittori) è sprecato.
Quante righe corrispondono a una clausola WHERE?
Questo è uno scenario leggermente diverso:devi sapere quante righe esistono per un determinato sottoinsieme della tabella. Non puoi usare i DMV per questo, a meno che il WHERE
la clausola corrisponde a un indice filtrato o copre completamente una partizione esatta (o multipla).
Se il tuo WHERE
è dinamica, potresti usare RCSI, come descritto sopra.
Se il tuo WHERE
La clausola non è dinamica, potresti usare anche RCSI, ma potresti anche considerare una di queste opzioni:
- Indice filtrato – per esempio se hai un filtro semplice come
is_active = 1
ostatus < 5
, allora potresti creare un indice come questo:CREATE INDEX ix_f ON dbo.table_name(leading_pk_column) WHERE is_active = 1;
Ora puoi ottenere conteggi piuttosto accurati dai DMV, poiché ci saranno voci che rappresentano questo indice (devi solo identificare index_id invece di fare affidamento su heap(0)/clustered index(1)). Tuttavia, devi considerare alcuni dei punti deboli degli indici filtrati.
- Vista indicizzata - ad esempio, se conteggi spesso gli ordini per cliente, una vista indicizzata potrebbe essere d'aiuto (anche se per favore non prenderla come un'affermazione generica che "le viste indicizzate migliorano tutte le query!"):
CREATE VIEW dbo.view_name WITH SCHEMABINDING AS SELECT customer_id, customer_count = COUNT_BIG(*) FROM dbo.table_name GROUP BY customer_id; GO CREATE UNIQUE CLUSTERED INDEX ix_v ON dbo.view_name(customer_id);
Ora, i dati nella vista verranno materializzati e il conteggio è garantito per essere sincronizzato con i dati della tabella (ci sono un paio di oscuri bug in cui ciò non è vero, come questo con
MERGE
, ma generalmente questo è affidabile). Quindi ora puoi ottenere i tuoi conteggi per cliente (o per un insieme di clienti) interrogando la vista, a un costo di query molto più basso (1 o 2 letture):SELECT customer_count FROM dbo.view_name WHERE customer_id = <x>;
Non c'è il pranzo gratis, però . È necessario considerare il sovraccarico della gestione di una vista indicizzata e l'impatto che avrà sulla parte di scrittura del carico di lavoro. Se non esegui questo tipo di query molto spesso, è improbabile che ne valga la pena.
Almeno una riga corrisponde a una clausola WHERE?
Anche questa è una domanda leggermente diversa. Ma vedo spesso questo:
IF (SELECT COUNT(*) FROM dbo.table_name WHERE <some clause>) > 0 -- or = 0 for not exists
Dal momento che ovviamente non ti interessa il conteggio effettivo, ti interessa solo se esiste almeno una riga, penso davvero che dovresti cambiarlo come segue:
IF EXISTS (SELECT 1 FROM dbo.table_name WHERE <some clause>)
Questo almeno ha una possibilità di cortocircuito prima che venga raggiunta la fine del tavolo e quasi sempre supererà il COUNT
variazione (sebbene ci siano alcuni casi in cui SQL Server è abbastanza intelligente da convertire IF (SELECT COUNT...) > 0
a un IF EXISTS()
più semplice ). Nello scenario peggiore in assoluto, in cui non viene trovata alcuna riga (o la prima riga viene trovata nell'ultima pagina della scansione), le prestazioni saranno le stesse.
[Vedi un indice di tutti i post sulle cattive abitudini/migliori pratiche]