Database
 sql >> Database >  >> RDS >> Database

Cattive abitudini:contare le righe nel modo più duro

[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):

Se esegui una query su sys.dm_db_index_physical_stats su un'istanza del server che ospita una replica secondaria leggibile AlwaysOn, potresti riscontrare un problema di blocco REDO. Questo perché questa vista a gestione dinamica acquisisce un blocco IS sulla tabella o vista utente specificata che può bloccare le richieste da un thread REDO per un blocco X su quella tabella o vista utente.

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 o status < 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]