sql >> Databasteknik >  >> RDS >> Database

Dåliga vanor :Räkna rader den hårda vägen

[Se ett index över alla inlägg om dåliga vanor/bästa metoder]

En av bilderna i min återkommande presentation om dåliga vanor och bästa praxis har titeln "Abusing COUNT(*) ." Jag ser det här övergreppet ganska mycket ute i naturen, och det tar flera former.

Hur många rader i tabellen?

Jag brukar se detta:

SELECT @count = COUNT(*) FROM dbo.tablename;

SQL Server måste köra en blockerande skanning mot hela tabellen för att härleda detta antal. Det är dyrt. Denna information lagras i katalogvyerna och DMV:er, och du kan få den utan all I/O eller blockering:

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';

(Du kan få samma information från sys.dm_db_partition_stats , men i så fall ändra p.rows till p.row_count (yay konsistens!). Faktum är att detta är samma vy som sp_spaceused använder för att härleda räkningen - och även om det är mycket lättare att skriva än ovanstående fråga, rekommenderar jag att du inte använder det bara för att härleda en räkning på grund av alla extra beräkningar det gör - om du inte vill ha den informationen också. Observera också att den använder metadatafunktioner som inte följer din yttre isoleringsnivå, så du kan sluta vänta på att blockera när du anropar den här proceduren.)

Nu är det sant att dessa åsikter inte är 100%, till mikrosekund korrekta. Om du inte använder en heap kan ett mer tillförlitligt resultat erhållas från sys.dm_db_index_physical_stats() kolumn record_count (yay konsekvens igen!), men den här funktionen kan ha en prestandapåverkan, kan fortfarande blockera och kan vara till och med dyrare än en SELECT COUNT(*) – den måste göra samma fysiska operationer, men måste beräkna ytterligare information beroende på mode (som fragmentering, som du inte bryr dig om i det här fallet). Varningen i dokumentationen berättar en del av historien, relevant om du använder tillgänglighetsgrupper (och troligen påverkar databasspegling på liknande sätt):

Om du frågar sys.dm_db_index_physical_stats på en serverinstans som är värd för en AlwaysOn-läsbar sekundär replika, kan du stöta på ett REDO-blockeringsproblem. Detta beror på att denna dynamiska hanteringsvy får ett IS-lås på den angivna användartabellen eller vyn som kan blockera förfrågningar från en REDO-tråd för ett X-lås på den användartabellen eller vyn.

Dokumentationen förklarar också varför det här numret kanske inte är tillförlitligt för en hög (och ger dem också en kvasi-pass för raderna kontra inkonsekvenser av poster):

För en hög kanske antalet poster som returneras från den här funktionen inte matchar antalet rader som returneras genom att köra en SELECT COUNT(*) mot högen. Detta beror på att en rad kan innehålla flera poster. Till exempel, under vissa uppdateringssituationer, kan en enstaka heap-rad ha en vidarebefordranpost och en vidarebefordrad post som ett resultat av uppdateringsoperationen. Dessutom är de flesta stora LOB-rader uppdelade i flera poster i LOB_DATA-lagring.

Så jag skulle luta mig mot sys.partitions som ett sätt att optimera detta och offra en viss marginell noggrannhet.

    "Men jag kan inte använda DMV:erna; min räkning måste vara superkorrekt!"

    En "superexakt" räkning är faktiskt ganska meningslös. Låt oss tänka på att ditt enda alternativ för en "superexakt" räkning är att låsa hela tabellen och förbjuda någon att lägga till eller ta bort några rader (men utan att förhindra delade läsningar), t.ex.:

    SELECT @count = COUNT(*) FROM dbo.table_name WITH (TABLOCK); -- not TABLOCKX!

    Så din fråga nynnar med, skannar all data och arbetar mot det "perfekta" antalet. Under tiden blockeras skrivförfrågningar och väntar. Plötsligt, när din exakta räkning returneras, släpps dina lås på bordet, och alla de skrivförfrågningar som stod i kö och väntade, börjar avfyra alla typer av infogningar, uppdateringar och borttagningar mot ditt bord. Hur "superprecis" är din räkning nu? Var det värt att få en "exakt" räkning som redan är fruktansvärt föråldrad? Om systemet inte är upptaget är detta inte så mycket av ett problem – men om systemet inte är upptaget, skulle jag hävda ganska starkt att DMV:erna kommer att vara ganska jäkla korrekta.

    Du kunde ha använt NOLOCK istället, men det betyder bara att författare kan ändra data medan du läser den, och det leder till andra problem också (jag pratade om detta nyligen). Det är okej för många bollplank, men inte om ditt mål är noggrannhet. DMV:erna kommer att vara rätt på (eller åtminstone mycket närmare) i många scenarier, och längre bort i väldigt få (faktiskt ingen som jag kan komma på).

    Slutligen kan du använda Read Committed Snapshot Isolation. Kendra Little har ett fantastiskt inlägg om isoleringsnivåerna för ögonblicksbilder, men jag upprepar listan över varningar jag nämnde i min NOLOCK artikel:

    • Sch-S-lås måste fortfarande tas även under RCSI.
    • Isoleringsnivåer för ögonblicksbilder använder radversioner i tempdb, så du måste verkligen testa effekten där.
    • RCSI kan inte använda effektiva genomsökningar av allokeringsorder; du kommer att se räckviddssökningar istället.
    • Paul White (@SQL_Kiwi) har några bra inlägg som du bör läsa om dessa isoleringsnivåer:
      • Läs isolering av engagerad ögonblicksbild
      • Dataändringar under Läs bestyrkt ögonblicksbildsisolering
      • Snapshot-isoleringsnivån

    Dessutom, även med RCSI, tar det tid att få det "korrekta" antalet (och ytterligare resurser i tempdb). När operationen är klar, är räkningen fortfarande korrekt? Bara om ingen har rört bordet under tiden. Så en av fördelarna med RCSI (läsare blockerar inte skribenter) är bortkastad.

Hur många rader matchar en WHERE-sats?

Detta är ett lite annorlunda scenario - du måste veta hur många rader som finns för en viss delmängd av tabellen. Du kan inte använda DMV för detta, om inte WHERE klausul matchar ett filtrerat index eller täcker helt en exakt partition (eller multipel).

Om din WHERE satsen är dynamisk, kan du använda RCSI, som beskrivs ovan.

Om din WHERE klausulen är inte dynamisk, du kan också använda RCSI, men du kan också överväga ett av dessa alternativ:

  • Filtrerat index – till exempel om du har ett enkelt filter som is_active = 1 eller status < 5 , då kan du bygga ett index så här:
    CREATE INDEX ix_f ON dbo.table_name(leading_pk_column) WHERE is_active = 1;

    Nu kan du få ganska exakta räkningar från DMV:erna, eftersom det kommer att finnas poster som representerar detta index (du behöver bara identifiera index_id istället för att förlita dig på heap(0)/clustered index(1)). Du måste dock överväga några av svagheterna med filtrerade index.

  • Indexerad vy - om du till exempel ofta räknar beställningar efter kund, kan en indexerad vy hjälpa till (men snälla ta inte detta som en allmän rekommendation att "indexerade vyer förbättrar alla frågor!"):
    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);

    Nu kommer data i vyn att materialiseras, och räkningen kommer garanterat att synkroniseras med tabelldata (det finns ett par oklara buggar där detta inte är sant, som den här med MERGE , men i allmänhet är detta tillförlitligt). Så nu kan du få dina räkningar per kund (eller för en uppsättning kunder) genom att fråga i vyn, till en mycket lägre sökkostnad (1 eller 2 läsningar):

    SELECT customer_count FROM dbo.view_name WHERE customer_id = <x>;

    Det finns dock ingen gratis lunch . Du måste överväga omkostnaderna för att upprätthålla en indexerad vy och vilken inverkan det kommer att ha på skrivdelen av din arbetsbelastning. Om du inte kör den här typen av fråga så ofta är det osannolikt värt besväret.

Stämmer minst en rad med en WHERE-sats?

Detta är också en lite annorlunda fråga. Men jag ser ofta det här:

IF (SELECT COUNT(*) FROM dbo.table_name WHERE <some clause>) > 0 -- or = 0 for not exists

Eftersom du uppenbarligen inte bryr dig om den faktiska räkningen bryr du dig bara om det finns minst en rad, jag tycker verkligen att du ska ändra den till följande:

IF EXISTS (SELECT 1 FROM dbo.table_name WHERE <some clause>)

Detta har åtminstone en chans att kortsluta innan slutet av tabellen nås, och kommer nästan alltid att överträffa COUNT variation (även om det finns vissa fall där SQL Server är smart nog att konvertera IF (SELECT COUNT...) > 0 till en enklare IF EXISTS() ). I det absolut värsta scenariot, där ingen rad hittas (eller den första raden finns på den allra sista sidan i skanningen), kommer prestandan att vara densamma.

[Se ett index över alla inlägg om dåliga vanor/bästa metoder]


  1. Uppdatera värden för flera tabellkolumner med en enda fråga

  2. Hur du snabbar upp din SQL-server med hjälp av databasprestandaövervakning

  3. Installera exempelscheman för Oracle 12c med hjälp av Database Configuration Assistant

  4. hur man kontrollerar alla begränsningar på ett bord i oracle