[Se et indeks over alle dårlige vaner/indlæg om bedste praksis]
Et af slides i min tilbagevendende præsentation om dårlige vaner og bedste praksis har titlen "Abusing COUNT(*)
." Jeg ser dette misbrug en del ude i naturen, og det tager flere former.
Hvor mange rækker i tabellen?
Jeg ser normalt dette:
SELECT @count = COUNT(*) FROM dbo.tablename;
SQL Server skal køre en blokerende scanning mod hele tabellen for at udlede denne optælling. Det er dyrt. Disse oplysninger er gemt i katalogvisningerne og DMV'erne, og du kan få dem uden al den I/O eller blokering:
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å de samme oplysninger fra sys.dm_db_partition_stats
, men i så fald skal du ændre p.rows
til p.row_count
(yay konsistens!). Faktisk er dette den samme visning, som sp_spaceused
bruger til at udlede optællingen - og selvom det er meget nemmere at skrive end ovenstående forespørgsel, anbefaler jeg, at du ikke bruger det bare til at udlede en optælling på grund af alle de ekstra beregninger, det gør - medmindre du også vil have disse oplysninger. Bemærk også, at den bruger metadatafunktioner, der ikke adlyder dit ydre isolationsniveau, så du kan ende med at vente på at blokere, når du kalder denne procedure.)
Nu er det rigtigt, at disse synspunkter ikke er 100%, til mikrosekund nøjagtige. Medmindre du bruger en heap, kan et mere pålideligt resultat opnås fra sys.dm_db_index_physical_stats()
kolonne record_count
(yay konsistens igen!), men denne funktion kan have en ydeevnepåvirkning, kan stadig blokere og kan være endnu dyrere end en SELECT COUNT(*)
– den skal udføre de samme fysiske operationer, men skal beregne yderligere information afhængigt af mode
(såsom fragmentering, som du er ligeglad med i dette tilfælde). Advarslen i dokumentationen fortæller en del af historien, relevant, hvis du bruger tilgængelighedsgrupper (og sandsynligvis påvirker Database Mirroring på lignende måde):
Dokumentationen forklarer også, hvorfor dette tal muligvis ikke er pålideligt for en heap (og giver dem også en quasi-pass for rækkerne vs. records inkonsistens):
For en heap svarer antallet af poster, der returneres fra denne funktion, muligvis ikke til antallet af rækker, der returneres ved at køre en SELECT COUNT(*) mod heapen. Dette skyldes, at en række kan indeholde flere poster. For eksempel, under nogle opdateringssituationer, kan en enkelt heap-række have en videresendelsespost og en videresendt post som et resultat af opdateringsoperationen. De fleste store LOB-rækker er også opdelt i flere poster i LOB_DATA-lageret.
Så jeg ville læne mig mod sys.partitions
som måden at optimere dette på og ofre en marginal smule nøjagtighed.
- "Men jeg kan ikke bruge DMV'erne; min optælling skal være super nøjagtig!"
En "superpræcis" optælling er faktisk ret meningsløs. Lad os overveje, at din eneste mulighed for en "superpræcis" optælling er at låse hele tabellen og forbyde nogen at tilføje eller slette nogen rækker (men uden at forhindre delte læsninger), f.eks.:
SELECT @count = COUNT(*) FROM dbo.table_name WITH (TABLOCK); -- not TABLOCKX!
Så din forespørgsel nynner med, scanner alle data og arbejder hen imod det "perfekte" antal. I mellemtiden bliver skriveanmodninger blokeret og venter. Pludselig, når din nøjagtige optælling er returneret, frigives dine låse på bordet, og alle de skriveforespørgsler, der stod i kø og ventede, begynder at skyde alle slags indsættelser, opdateringer og sletninger af mod dit bord. Hvor "superpræcis" er din optælling nu? Var det værd at få en "præcis" optælling, der allerede er frygtelig forældet? Hvis systemet ikke er optaget, så er det ikke så meget af et problem – men hvis systemet ikke er optaget, vil jeg argumentere ret kraftigt for, at DMV'erne vil være ret så nøjagtige.
Du kunne have brugt NOLOCK
i stedet, men det betyder bare, at forfattere kan ændre dataene, mens du læser dem, og det fører også til andre problemer (jeg talte om dette for nylig). Det er okay for mange boldbaner, men ikke hvis dit mål er nøjagtighed. DMV'erne vil være lige på (eller i det mindste meget tættere på) i mange scenarier og længere væk i meget få (faktisk ingen, jeg kan komme i tanke om).
Endelig kan du bruge Read Committed Snapshot Isolation. Kendra Little har et fantastisk indlæg om snapshot-isolationsniveauerne, men jeg vil gentage listen over forbehold, jeg nævnte i min NOLOCK
artikel:
- Sch-S-låse skal stadig tages, selv under RCSI.
- Snapshot-isolationsniveauer bruger rækkeversionering i tempdb, så du er virkelig nødt til at teste effekten der.
- RCSI kan ikke bruge effektive allokeringsordrescanninger; du vil se rækkeviddescanninger i stedet.
- Paul White (@SQL_Kiwi) har nogle gode indlæg, du bør læse om disse isolationsniveauer:
- Læs Committed Snapshot Isolation
- Dataændringer under Læs Committed Snapshot Isolation
- SNAPSHOT-isolationsniveauet
Derudover, selv med RCSI, tager det tid at få den "nøjagtige" optælling (og yderligere ressourcer i tempdb). Når operationen er færdig, er optællingen så stadig nøjagtig? Kun hvis ingen har rørt bordet i mellemtiden. Så en af fordelene ved RCSI (læsere blokerer ikke forfattere) er spildt.
Hvor mange rækker matcher en WHERE-sætning?
Dette er et lidt anderledes scenarie - du skal vide, hvor mange rækker der findes for en bestemt delmængde af tabellen. Du kan ikke bruge DMV'erne til dette, medmindre WHERE
klausul matcher et filtreret indeks eller dækker fuldstændigt en nøjagtig partition (eller multiple).
Hvis din WHERE
klausulen er dynamisk, kan du bruge RCSI, som beskrevet ovenfor.
Hvis din WHERE
klausulen er ikke dynamisk, du kan også bruge RCSI, men du kan også overveje en af disse muligheder:
- Filtreret indeks – for eksempel hvis du har et simpelt filter som
is_active = 1
ellerstatus < 5
, så kunne du bygge et indeks som dette:CREATE INDEX ix_f ON dbo.table_name(leading_pk_column) WHERE is_active = 1;
Nu kan du få ret nøjagtige optællinger fra DMV'erne, da der vil være poster, der repræsenterer dette indeks (du skal bare identificere index_id i stedet for at stole på heap(0)/clustered index(1)). Du skal dog overveje nogle af svaghederne ved filtrerede indekser.
- Indekseret visning - hvis du for eksempel ofte tæller ordrer efter kunde, kan en indekseret visning hjælpe (dog venligst ikke tage dette som en generisk påtegning om, at "indekserede visninger forbedrer alle forespørgsler!"):
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 vil dataene i visningen blive materialiseret, og optællingen er garanteret synkroniseret med tabeldataene (der er et par obskure fejl, hvor dette ikke er sandt, såsom denne med
MERGE
, men generelt er dette pålideligt). Så nu kan du få dine tal pr. kunde (eller for et sæt kunder) ved at forespørge på visningen til en meget lavere forespørgselspris (1 eller 2 læsninger):SELECT customer_count FROM dbo.view_name WHERE customer_id = <x>;
Der er dog ingen gratis frokost . Du skal overveje omkostningerne ved at opretholde en indekseret visning og den indvirkning, det vil have på skrivedelen af din arbejdsbyrde. Hvis du ikke kører denne type forespørgsel meget ofte, er det usandsynligt, at det er besværet værd.
Samler mindst én række med en WHERE-sætning?
Dette er også et lidt andet spørgsmål. Men jeg ser ofte dette:
IF (SELECT COUNT(*) FROM dbo.table_name WHERE <some clause>) > 0 -- or = 0 for not exists
Da du åbenbart er ligeglad med den faktiske optælling, er du kun ligeglad, hvis der findes mindst én række, jeg synes virkelig, du skal ændre den til følgende:
IF EXISTS (SELECT 1 FROM dbo.table_name WHERE <some clause>)
Dette har i det mindste en chance for at kortslutte før slutningen af tabellen er nået, og vil næsten altid udkonkurrere COUNT
variation (selvom der er nogle tilfælde, hvor SQL Server er smart nok til at konvertere IF (SELECT COUNT...) > 0
til en enklere IF EXISTS()
). I det absolut værste tilfælde, hvor ingen række findes (eller den første række findes på den allersidste side i scanningen), vil ydelsen være den samme.
[Se et indeks over alle dårlige vaner/indlæg om bedste praksis]