sql >> Database >  >> RDS >> Database

Slechte gewoonten:rijen tellen op de harde manier

[Bekijk een index van alle berichten over slechte gewoonten/best practices]

Een van de dia's in mijn terugkerende Bad Habits &Best Practices-presentatie is getiteld "Abusing COUNT(*) ." Ik zie dit misbruik nogal eens in het wild, en het neemt verschillende vormen aan.

Hoeveel rijen in de tabel?

Meestal zie ik dit:

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

SQL Server moet een blokkeringsscan uitvoeren op de hele tabel om deze telling af te leiden. Dat is duur. Deze informatie wordt opgeslagen in de catalogusweergaven en DMV's, en u kunt deze verkrijgen zonder al die I/O of blokkering:

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

(U kunt dezelfde informatie krijgen van sys.dm_db_partition_stats , maar verander in dat geval p.rows naar p.row_count (yay consistentie!). In feite is dit dezelfde weergave die sp_spaceused gebruikt om de telling af te leiden - en hoewel het veel gemakkelijker is om te typen dan de bovenstaande zoekopdracht, raad ik aan om het niet alleen te gebruiken om een ​​telling af te leiden vanwege alle extra berekeningen die het doet - tenzij u die informatie ook wilt. Houd er ook rekening mee dat het metadatafuncties gebruikt die niet voldoen aan uw buitenste isolatieniveau, dus u zou kunnen wachten op blokkering wanneer u deze procedure aanroept.)

Het is waar dat deze weergaven niet 100% tot op de microseconde nauwkeurig zijn. Tenzij u een heap gebruikt, kan een betrouwbaarder resultaat worden verkregen uit de sys.dm_db_index_physical_stats() kolom record_count (yay consistentie weer!), maar deze functie kan een prestatie-impact hebben, kan nog steeds blokkeren en kan zelfs duurder zijn dan een SELECT COUNT(*) – het moet dezelfde fysieke bewerkingen uitvoeren, maar moet aanvullende informatie berekenen afhankelijk van de mode (zoals fragmentatie, waar je in dit geval niet om geeft). De waarschuwing in de documentatie vertelt een deel van het verhaal, relevant als u Beschikbaarheidsgroepen gebruikt (en waarschijnlijk op een vergelijkbare manier van invloed is op Database Mirroring):

Als u een query uitvoert op sys.dm_db_index_physical_stats op een serverinstantie die als host fungeert voor een AlwaysOn leesbare secundaire replica, kunt u een REDO-blokkeringsprobleem tegenkomen. Dit komt omdat deze dynamische beheerweergave een IS-vergrendeling verkrijgt op de opgegeven gebruikerstabel of weergave die verzoeken kan blokkeren door een REDO-thread voor een X-vergrendeling op die gebruikerstabel of weergave.

De documentatie legt ook uit waarom dit nummer misschien niet betrouwbaar is voor een hoop (en geeft ze ook een quasi-pass voor de rijen versus de inconsistentie van records):

Voor een heap komt het aantal records dat door deze functie wordt geretourneerd mogelijk niet overeen met het aantal rijen dat wordt geretourneerd door een SELECT COUNT(*) tegen de heap uit te voeren. Dit komt omdat een rij meerdere records kan bevatten. In sommige bijwerksituaties kan een enkele heaprij bijvoorbeeld een doorstuurrecord en een doorgestuurd record hebben als resultaat van de bijwerkbewerking. Ook worden de meeste grote LOB-rijen opgesplitst in meerdere records in LOB_DATA-opslag.

Dus ik neig naar sys.partitions als de manier om dit te optimaliseren, waarbij een klein beetje nauwkeurigheid wordt opgeofferd.

    "Maar ik kan de DMV's niet gebruiken; mijn telling moet supernauwkeurig zijn!"

    Een "supernauwkeurige" telling is eigenlijk vrij zinloos. Laten we bedenken dat uw enige optie voor een "supernauwkeurige" telling is om de hele tabel te vergrendelen en te verbieden dat iemand rijen toevoegt of verwijdert (maar zonder gedeelde leesbewerkingen te voorkomen), bijvoorbeeld:

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

    Dus uw vraag gonst maar door, scant alle gegevens en werkt naar dat "perfecte" aantal toe. Ondertussen worden schrijfverzoeken geblokkeerd en wachten ze. Plotseling, wanneer je nauwkeurige telling wordt geretourneerd, worden je sloten op de tafel vrijgegeven, en al die schrijfverzoeken die in de wachtrij stonden en wachten, beginnen allerlei soorten invoegingen, updates en verwijderingen tegen je tafel af te vuren. Hoe "super nauwkeurig" is je telling nu? Was het de moeite waard om een ​​"nauwkeurige" telling te krijgen die al vreselijk achterhaald is? Als het systeem niet druk is, is dit niet zo'n probleem - maar als het systeem niet druk is, zou ik vrij sterk beweren dat de DMV's behoorlijk nauwkeurig zullen zijn.

    Je had NOLOCK kunnen gebruiken in plaats daarvan, maar dat betekent alleen maar dat schrijvers de gegevens kunnen veranderen terwijl je het leest, en leidt ook tot andere problemen (ik heb hier onlangs over gesproken). Het is oké voor veel honkbalvelden, maar niet als je doel nauwkeurigheid is. De DMV's zullen in veel scenario's gelijk hebben (of in ieder geval veel dichterbij) en in heel weinig scenario's verder weg (eigenlijk geen enkele die ik kan bedenken).

    Ten slotte kunt u Read Committed Snapshot Isolation gebruiken. Kendra Little heeft een fantastische post over de isolatieniveaus van snapshots, maar ik zal de lijst met voorbehouden herhalen die ik in mijn NOLOCK heb genoemd. artikel:

    • Sch-S-sloten moeten nog steeds worden ingenomen, zelfs onder RCSI.
    • Snapshot-isolatieniveaus gebruiken rijversiebeheer in tempdb, dus je moet de impact daar echt testen.
    • RCSI kan geen efficiënte scans van toewijzingsorders gebruiken; je zult in plaats daarvan bereikscans zien.
    • Paul White (@SQL_Kiwi) heeft een aantal geweldige berichten die je moet lezen over deze isolatieniveaus:
      • Gecommitteerde momentopname-isolatie lezen
      • Gegevenswijzigingen onder Read Committed Snapshot Isolation
      • Het SNAPSHOT-isolatieniveau

    Bovendien kost het, zelfs met RCSI, tijd om de "nauwkeurige" telling te krijgen (en extra middelen in tempdb). Is de telling nog steeds nauwkeurig tegen de tijd dat de operatie is voltooid? Alleen als niemand in de tussentijd de tafel heeft aangeraakt. Dus een van de voordelen van RCSI (lezers blokkeren geen schrijvers) is verspild.

Hoeveel rijen komen overeen met een WHERE-clausule?

Dit is een iets ander scenario - u moet weten hoeveel rijen er zijn voor een bepaalde subset van de tabel. U kunt de DMV's hiervoor niet gebruiken, tenzij de WHERE clausule komt overeen met een gefilterde index of dekt volledig een exacte partitie (of veelvoud).

Als uw WHERE clausule dynamisch is, kunt u RCSI gebruiken, zoals hierboven beschreven.

Als uw WHERE clausule niet dynamisch is, kunt u ook RCSI gebruiken, maar u kunt ook een van deze opties overwegen:

  • Gefilterde index – bijvoorbeeld als je een eenvoudig filter hebt zoals is_active = 1 of status < 5 , dan zou je een index als deze kunnen bouwen:
    CREATE INDEX ix_f ON dbo.table_name(leading_pk_column) WHERE is_active = 1;

    Nu kunt u vrij nauwkeurige tellingen krijgen van de DMV's, aangezien er items zijn die deze index vertegenwoordigen (u hoeft alleen de index_id te identificeren in plaats van te vertrouwen op heap(0)/clustered index(1)). U moet echter rekening houden met enkele zwakke punten van gefilterde indexen.

  • Geïndexeerde weergave - als u bijvoorbeeld vaak bestellingen per klant telt, kan een geïndexeerde weergave helpen (maar vat dit alstublieft niet op als een algemene goedkeuring dat "geïndexeerde weergaven alle zoekopdrachten verbeteren!"):
    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 worden de gegevens in de weergave gematerialiseerd en wordt de telling gegarandeerd gesynchroniseerd met de tabelgegevens (er zijn een paar obscure bugs waar dit niet waar is, zoals deze met MERGE , maar over het algemeen is dit betrouwbaar). Dus nu kunt u uw tellingen per klant (of voor een reeks klanten) krijgen door de weergave te bevragen, tegen veel lagere zoekkosten (1 of 2 metingen):

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

    Er is echter geen gratis lunch . U moet rekening houden met de overhead van het onderhouden van een geïndexeerde weergave en de impact die dit zal hebben op het schrijfgedeelte van uw werkbelasting. Als u dit type zoekopdracht niet vaak uitvoert, is het waarschijnlijk niet de moeite waard.

Komt minstens één rij overeen met een WHERE-clausule?

Ook dit is een iets andere vraag. Maar ik zie dit vaak:

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

Aangezien het je duidelijk niet uitmaakt wat het daadwerkelijke aantal is, je alleen geeft als er tenminste één rij bestaat, denk ik echt dat je het als volgt moet veranderen:

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

Dit heeft in ieder geval een kans op kortsluiting voordat het einde van de tafel is bereikt, en zal bijna altijd beter presteren dan de COUNT variatie (hoewel er enkele gevallen zijn waarin SQL Server slim genoeg is om IF (SELECT COUNT...) > 0 te converteren naar een eenvoudiger IF EXISTS() ). In het ergste geval, als er geen rij wordt gevonden (of de eerste rij wordt gevonden op de allerlaatste pagina in de scan), zullen de prestaties hetzelfde zijn.

[Bekijk een index van alle berichten over slechte gewoonten/best practices]


  1. Service Broker-verbeteringen in SQL Server 2016

  2. pas pager aan in psql

  3. SQLcl-opmaakopties (Oracle)

  4. Meerdere tellingen krijgen met één query in MySQL