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

Dårlige vaner:At tælle rækker på den hårde måde

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

Hvis du forespørger på sys.dm_db_index_physical_stats på en serverinstans, der er vært for en AlwaysOn-læsbar sekundær replika, kan du støde på et REDO-blokeringsproblem. Dette skyldes, at denne dynamiske administrationsvisning erhverver en IS-lås på den angivne brugertabel eller visning, der kan blokere anmodninger fra en REDO-tråd om en X-lås på den pågældende brugertabel eller visning.

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


  1. Opret et Excel-regneark fra en Oracle-database

  2. Hvordan overføres tabelværdiparametre fra Java til sql server lagret procedure?

  3. 3 måder at kontrollere en kolonnes datatype i PostgreSQL

  4. Brug COLUMNPROPERTY() til at returnere kolonne- eller parameteroplysninger i SQL Server